Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cron.disable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
APIKEY: ${{ secrets.APIKEY }}
SENDFROM: ${{ secrets.SENDFROM }}
SENDTO: ${{ secrets.SENDTO }}
TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }}
MFA_SECRET: ${{ secrets.MFA_SECRET }}

- name: Upload state
Expand Down
81 changes: 21 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ WebClassから取得した課題を任意のTeamsチャネルで通知します
主に高専の人向け

1. WebClassスクレイピングで課題を取得 & Microsoftアカウントログイン
2. 整形してResendからメールを送信
2. 整形してTeams Webhookへ送信
3. teamsに通知が行く

簡単にこんな流れです。
Expand All @@ -15,17 +15,14 @@ WebClassから取得した課題を任意のTeamsチャネルで通知します
- Ubuntu 22.04.5 LTS
- Node v22.15.0
- pupperteer 24.34.0
- resend 6.7.0


## setup
0. ワークフローの作成
1. チャネルのメールアドレス取得
2. ドメインの取得
3. Resendに登録
4. 環境構築
5. OTP認証
6. 確認
1. Webhook URLの取得
2. 環境構築
3. OTP認証
4. 確認

0が結構くせもので、Microsoftは組織が強いのでもしかしたら登録できない可能性は高いです。高専機構ではいけるかも

Expand Down Expand Up @@ -57,44 +54,15 @@ WebClassから取得した課題を任意のTeamsチャネルで通知します
組織で制限されてたらここまで来れない、ワークフロー作れなそうなら諦める


### 1.メールアドレスの作成
### 1.Webhook URLの取得

最初の画像のところにあるメールアドレスを取得、< >で囲われたメールアドレスをコピーしておく
最初の画像のところにあるWebhook URLを取得し、コピーしておく

詳細から一応ドメインから絞れるけど一旦「だれでも」にしとく
Webhookの受信設定は必要に応じて制限してください

![alt text](img/image6.png)

### 2.ドメインの取得

このプロジェクトではプログラムからAPIを通してメール送るので独自ドメインから送信した方が楽です

GmailやoutlookとかのドメインからはOAUTHなどでAPIが複雑になりそうです。あとドメインのほうがかっこいい

どこでもいいですが、cloudflareから取るのが安くておすすめです

### 3.Resendに登録

さっきからResend言ってますが、主に開発者のためのメール配信APIのことです。
コードを数行書くだけでメールが送信できます。

https://resend.com/

サインアップした後、ドメインも登録します。ここでは説明しません。

もしDNSをcloudflare使っているのであれば簡単にDNSに登録することができます。
右にあるSign in to Cloudflareをクリックしてログインするだけです。

**必ずDMARCも登録する必要があります!**

cloudflareログインではDMARCレコードが自動で作成されないので手動で登録します

DMARCを登録しないと普通迷惑メールに振り分けられるようで、その場合は正常に通知されません

![alt text](img/image7.png)


### 4.環境構築
### 2.環境構築

パッケージをインストールします。aptを使ってます。

Expand All @@ -120,6 +88,7 @@ $ npm install
cat << EOF > .env
USER_ID='{your_ID}'
PASSWORD='{your_password}'
TEAMS_WEBHOOK_URL='{teams_webhook_url}'
APIKEY='{Resend_APIKEY}'
SENDFROM='notification@{your_domain}'
SENDTO='{channel_mailaddress}'
Expand All @@ -131,26 +100,25 @@ EOF
```
USER_ID='{your_ID}'
PASSWORD='{your_password}'
APIKEY="{Resend_APIKEY}"
SENDFROM="notification@{your_domain}"
SENDTO="{channel_mailaddress}"
TEAMS_WEBHOOK_URL='{teams_webhook_url}'
APIKEY='{Resend_APIKEY}'
SENDFROM='notification@{your_domain}'
SENDTO='{channel_mailaddress}'
MFA_SECRET='{MFA_SECRET}'
```

| .env | 用途 |
| -------- | ---------------------------------------------------------- |
| USER_ID | Microsoftアカウントのメールアドレス |
| PASSWORD | Microsoftアカウントのパスワード |
| APIKEY | ResendのAPIKEY |
| SENDFROM | ドメインのメールアドレス |
| SENDTO | チャネルのメールアドレス |
| TEAMS_WEBHOOK_URL | Teamsで発行したWebhook URL |
| APIKEY | ResendのAPIKEY(エラー時の管理者メール通知も使う場合) |
| SENDFROM | 送信元メールアドレス(エラー時の管理者メール通知も使う場合)|
| SENDTO | 管理者通知先メールアドレス(エラー時の管理者メール通知) |
| MFA_SECRET | 後述するシークレット |


SENDFROMのnotificationの部分はなんでもいいです


### 5. OTP認証
### 3. OTP認証

Microsoft のOTP認証を通します

Expand All @@ -170,7 +138,7 @@ Google Authenticator などの認証アプリを使用してこのQRコードを

ここで注意するのが、Microsoft Authenticator はこのプログラム内での認証とは違います。必ず**別の認証アプリを設定する**をクリックしてください!

### 6. 確認
### 4. 確認

`$ node --env-file=.env index.js`

Expand All @@ -180,14 +148,7 @@ cookieが切れる or `cookies.json`が存在しない(初回実行) の場合


### 補足
teamsのフックに直接メールを送信する方法について、
独自ドメインからのメールはスパム対策に厳しく、簡単にブロックされる可能性があります(実際に僕は一度ブロックされました)

また受取失敗や、スパム判定で蹴られた際に状況が分からないなどいろいろ不便なことがあります。

回避策としてoutlookやGmailに一度送信し、そこから転送設定でチャネルメールアドレスに転送する方法があります

必要に応じて対応してみてください
Teams Webhook経由で通知するため、メール転送の設定は不要です。

#### 追記
- 結局ローカルサーバでcrontabする方法で落ち着きました。半月ほど動かしましたが、正常に機能しています。
Expand Down
48 changes: 17 additions & 31 deletions debug_sender.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,31 @@
import { Resend } from "resend";

async function main() {
const subject = "Test Email";
const body = "<p>This is a test email from the notifier.</p>";
const subject = "Test Teams Notification";
const body = "This is a test Teams webhook notification from the notifier.";

const APIKEY = process.env.APIKEY;
if (!APIKEY) {
console.warn("APIKEY not set. Skipping email notification.");
return;
}
const sendTo = process.env.SENDTO;
if (!sendTo) {
console.warn("SENDTO not set. Skipping email notification.");
const webhookUrl = process.env.TEAMS_WEBHOOK_URL;
if (!webhookUrl) {
console.warn("TEAMS_WEBHOOK_URL not set. Skipping Teams notification.");
return;
}
const sendFrom = process.env.SENDFROM;
if (!sendFrom) {
console.warn("SENDFROM not set. Skipping email notification.");
return;
}

const resend = new Resend(APIKEY);

try {
console.log(`Sending email with subject: "${subject}"`);
const { data, error } = await resend.emails.send({
from: `WebClass Notifier <${sendFrom}>`,
to: sendTo,
subject: subject,
html: body,
console.log(`Sending Teams webhook notification with subject: "${subject}"`);
const response = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `**${subject}**\n\n${body}`,
}),
});

if (error) {
console.error("Failed to send email:", { error });
throw error;
if (!response.ok) {
const responseText = await response.text();
throw new Error(`Webhook request failed with status ${response.status}: ${responseText}`);
}

console.log("Email sent successfully:", { data });
console.log("Test email sent successfully!");
console.log("Teams webhook notification sent successfully!");
} catch (error) {
console.error("Failed to send test email:", error);
console.error("Failed to send test Teams notification:", error);
}
}

Expand Down
10 changes: 8 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { scrapeAssignments } from "./src/scraper.js";
import { sendNotification } from "./src/sender.js";
import { sendErrorNotification, sendLoginRequiredNotification, sendNotification } from "./src/sender.js";
import { processNotifications } from "./src/notifier.js";
import fs from "fs/promises";
import path from "path";
Expand All @@ -22,7 +22,8 @@ async function run() {
const scrapeResult = await scrapeAssignments();

if (scrapeResult?.loginRequired) {
console.log("Scraping failed. Skipping notification.");
console.log("Scraping failed due to authentication/session issue. Sending alert notification.");
await sendLoginRequiredNotification(scrapeResult?.error || "");
return;
}

Expand All @@ -37,6 +38,11 @@ async function run() {

} catch (error) {
console.error("An error occurred in the main process:", error);
try {
await sendErrorNotification(error);
} catch (notificationError) {
console.error("Failed to send error notifications:", notificationError);
}
process.exit(1);
}
}
Expand Down
41 changes: 12 additions & 29 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"bottleneck": "^2.19.5",
"dotenv": "^17.2.3",
"puppeteer": "^24.34.0",
"resend": "^6.7.0"
"resend": "^6.12.4"
}
}
1 change: 1 addition & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default {
apikey: process.env.APIKEY,
sendfrom: process.env.SENDFROM,
sendto: process.env.SENDTO,
teamsWebhookUrl: process.env.TEAMS_WEBHOOK_URL,
baseUrl: 'https://webclass.kosen-k.go.jp/webclass/',
entryUrl: 'https://webclass.kosen-k.go.jp/webclass/index.php',
};
Loading
Loading