From 894a2ea7dec5871e30971958e02fac94036a02e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 02:08:44 +0000 Subject: [PATCH 1/6] Initial plan From 56c03d73c272b455afa4932615970adba4d5e0df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 02:12:12 +0000 Subject: [PATCH 2/6] feat: switch notifications from email to Teams webhook Agent-Logs-Url: https://github.com/mitsuiJao/webc-asgmt-notify/sessions/d452a683-efe2-4cd8-ae7b-180563a82178 Co-authored-by: mitsuiJao <88090304+mitsuiJao@users.noreply.github.com> --- .github/workflows/cron.disable.yml | 4 +- README.md | 78 ++++++------------------------ debug_sender.js | 48 +++++++----------- package-lock.json | 69 +------------------------- package.json | 3 +- src/config.js | 4 +- src/sender.js | 75 +++++++++++++++------------- 7 files changed, 77 insertions(+), 204 deletions(-) diff --git a/.github/workflows/cron.disable.yml b/.github/workflows/cron.disable.yml index bb5e86e..d6f4fc8 100644 --- a/.github/workflows/cron.disable.yml +++ b/.github/workflows/cron.disable.yml @@ -42,9 +42,7 @@ jobs: env: USER_ID: ${{ secrets.USER_ID }} PASSWORD: ${{ secrets.PASSWORD }} - APIKEY: ${{ secrets.APIKEY }} - SENDFROM: ${{ secrets.SENDFROM }} - SENDTO: ${{ secrets.SENDTO }} + TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} MFA_SECRET: ${{ secrets.MFA_SECRET }} - name: Upload state diff --git a/README.md b/README.md index 310493d..bee75df 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ WebClassから取得した課題を任意のTeamsチャネルで通知します 主に高専の人向け 1. WebClassスクレイピングで課題を取得 & Microsoftアカウントログイン -2. 整形してResendからメールを送信 +2. 整形してTeams Webhookへ送信 3. teamsに通知が行く 簡単にこんな流れです。 @@ -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は組織が強いのでもしかしたら登録できない可能性は高いです。高専機構ではいけるかも @@ -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を使ってます。 @@ -120,9 +88,7 @@ $ npm install cat << EOF > .env USER_ID='{your_ID}' PASSWORD='{your_password}' -APIKEY='{Resend_APIKEY}' -SENDFROM='notification@{your_domain}' -SENDTO='{channel_mailaddress}' +TEAMS_WEBHOOK_URL='{teams_webhook_url}' MFA_SECRET='{MFA_SECRET}' EOF ``` @@ -131,9 +97,7 @@ EOF ``` USER_ID='{your_ID}' PASSWORD='{your_password}' -APIKEY="{Resend_APIKEY}" -SENDFROM="notification@{your_domain}" -SENDTO="{channel_mailaddress}" +TEAMS_WEBHOOK_URL='{teams_webhook_url}' MFA_SECRET='{MFA_SECRET}' ``` @@ -141,16 +105,11 @@ MFA_SECRET='{MFA_SECRET}' | -------- | ---------------------------------------------------------- | | USER_ID | Microsoftアカウントのメールアドレス | | PASSWORD | Microsoftアカウントのパスワード | -| APIKEY | ResendのAPIKEY | -| SENDFROM | ドメインのメールアドレス | -| SENDTO | チャネルのメールアドレス | +| TEAMS_WEBHOOK_URL | Teamsで発行したWebhook URL | | MFA_SECRET | 後述するシークレット | -SENDFROMのnotificationの部分はなんでもいいです - - -### 5. OTP認証 +### 3. OTP認証 Microsoft のOTP認証を通します @@ -170,7 +129,7 @@ Google Authenticator などの認証アプリを使用してこのQRコードを ここで注意するのが、Microsoft Authenticator はこのプログラム内での認証とは違います。必ず**別の認証アプリを設定する**をクリックしてください! -### 6. 確認 +### 4. 確認 `$ node --env-file=.env index.js` @@ -180,14 +139,7 @@ cookieが切れる or `cookies.json`が存在しない(初回実行) の場合 ### 補足 -teamsのフックに直接メールを送信する方法について、 -独自ドメインからのメールはスパム対策に厳しく、簡単にブロックされる可能性があります(実際に僕は一度ブロックされました) - -また受取失敗や、スパム判定で蹴られた際に状況が分からないなどいろいろ不便なことがあります。 - -回避策としてoutlookやGmailに一度送信し、そこから転送設定でチャネルメールアドレスに転送する方法があります - -必要に応じて対応してみてください +Teams Webhook経由で通知するため、メール転送の設定は不要です。 #### 追記 - 結局ローカルサーバでcrontabする方法で落ち着きました。半月ほど動かしましたが、正常に機能しています。 diff --git a/debug_sender.js b/debug_sender.js index 7e320e8..0d331e9 100644 --- a/debug_sender.js +++ b/debug_sender.js @@ -1,45 +1,31 @@ -import { Resend } from "resend"; - async function main() { - const subject = "Test Email"; - const body = "

This is a test email from the notifier.

"; + 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); } } diff --git a/package-lock.json b/package-lock.json index f9a2266..6f58b0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,12 +4,10 @@ "requires": true, "packages": { "": { - "name": "webc-asgmt-notify", "dependencies": { "bottleneck": "^2.19.5", "dotenv": "^17.2.3", - "puppeteer": "^24.34.0", - "resend": "^6.7.0" + "puppeteer": "^24.34.0" } }, "node_modules/@babel/code-frame": { @@ -56,12 +54,6 @@ "node": ">=18" } }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -535,12 +527,6 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -889,26 +875,6 @@ "node": ">=0.10.0" } }, - "node_modules/resend": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-6.7.0.tgz", - "integrity": "sha512-2ZV0NDZsh4Gh+Nd1hvluZIitmGJ59O4+OxMufymG6Y8uz1Jgt2uS1seSENnkIUlmwg7/dwmfIJC9rAufByz7wA==", - "license": "MIT", - "dependencies": { - "svix": "1.84.1" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@react-email/render": "*" - }, - "peerDependenciesMeta": { - "@react-email/render": { - "optional": true - } - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -978,16 +944,6 @@ "node": ">=0.10.0" } }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, "node_modules/streamx": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", @@ -1025,16 +981,6 @@ "node": ">=8" } }, - "node_modules/svix": { - "version": "1.84.1", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", - "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==", - "license": "MIT", - "dependencies": { - "standardwebhooks": "1.0.0", - "uuid": "^10.0.0" - } - }, "node_modules/tar-fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", @@ -1088,19 +1034,6 @@ "license": "MIT", "optional": true }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/webdriver-bidi-protocol": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.10.tgz", diff --git a/package.json b/package.json index cee4ede..3b3c101 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "dependencies": { "bottleneck": "^2.19.5", "dotenv": "^17.2.3", - "puppeteer": "^24.34.0", - "resend": "^6.7.0" + "puppeteer": "^24.34.0" } } diff --git a/src/config.js b/src/config.js index 1b9b522..b48d217 100644 --- a/src/config.js +++ b/src/config.js @@ -25,9 +25,7 @@ export default { username: process.env.USER_ID, password: process.env.PASSWORD, mfaSecret: process.env.MFA_SECRET, - 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', }; diff --git a/src/sender.js b/src/sender.js index 536b070..3a7ef0d 100644 --- a/src/sender.js +++ b/src/sender.js @@ -1,46 +1,51 @@ -import { Resend } from "resend"; import config from "./config.js"; +function htmlToPlainText(html) { + return html + .replace(//gi, "\n") + .replace(/<\/(p|div|li|h1|h2|h3|ul|ol)>/gi, "\n") + .replace(/<[^>]+>/g, "") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + export async function sendNotification(subject, body) { - const { apikey, sendto, sendfrom } = config; - if (!apikey) { - console.warn("APIKEY not set. Skipping email notification."); - return; - } - if (!sendto) { - console.warn("SENDTO not set. Skipping email notification."); + const { teamsWebhookUrl } = config; + if (!teamsWebhookUrl) { + console.warn("TEAMS_WEBHOOK_URL not set. Skipping Teams notification."); return; } + const text = htmlToPlainText(body); - const resend = new Resend(apikey); - - 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(teamsWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: `**${subject}**\n\n${text}`, + }), }); - if (error) { - console.error("Failed to send email:", { error }); - throw error; // Re-throw the error to be caught by the caller + if (!response.ok) { + const responseText = await response.text(); + const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); + console.error("Failed to send Teams webhook notification:", error.message); + throw error; } - console.log("Email sent successfully:", { data }); + console.log("Teams webhook notification sent successfully."); } export async function sendLoginRequiredNotification() { - const { apikey, username } = config; + const { teamsWebhookUrl, username } = config; - if (!apikey || !username) { + if (!teamsWebhookUrl || !username) { console.warn( - "APIKEY or USER_ID not set. Skipping login required notification." + "TEAMS_WEBHOOK_URL or USER_ID not set. Skipping login required notification." ); return; } - const resend = new Resend(apikey); const subject = "WebClass Scraper: Authentication Required"; const body = `

Authentication Required

@@ -50,19 +55,21 @@ export async function sendLoginRequiredNotification() {

This is a notification for the user: ${username}

`; - console.log(`Sending authentication required email to ${username}...`); - const { data, error } = await resend.emails.send({ - from: "Scraper Alert ", - to: username, - subject: subject, - html: body, + console.log(`Sending authentication required notification for ${username}...`); + const response = await fetch(teamsWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: `**${subject}**\n\n${htmlToPlainText(body)}`, + }), }); - if (error) { - console.error("Failed to send login required email:", { error }); + if (!response.ok) { + const responseText = await response.text(); + const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); + console.error("Failed to send login required notification:", error.message); throw error; } - console.log("Login required email sent successfully:", { data }); + console.log("Login required notification sent successfully."); } - From e67cd3cf43901698bc338e4e59caa930c1810892 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 02:13:48 +0000 Subject: [PATCH 3/6] fix: address codeql findings in webhook sender Agent-Logs-Url: https://github.com/mitsuiJao/webc-asgmt-notify/sessions/d452a683-efe2-4cd8-ae7b-180563a82178 Co-authored-by: mitsuiJao <88090304+mitsuiJao@users.noreply.github.com> --- src/sender.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/sender.js b/src/sender.js index 3a7ef0d..3a290e3 100644 --- a/src/sender.js +++ b/src/sender.js @@ -1,28 +1,18 @@ import config from "./config.js"; -function htmlToPlainText(html) { - return html - .replace(//gi, "\n") - .replace(/<\/(p|div|li|h1|h2|h3|ul|ol)>/gi, "\n") - .replace(/<[^>]+>/g, "") - .replace(/\n{3,}/g, "\n\n") - .trim(); -} - export async function sendNotification(subject, body) { const { teamsWebhookUrl } = config; if (!teamsWebhookUrl) { console.warn("TEAMS_WEBHOOK_URL not set. Skipping Teams notification."); return; } - const text = htmlToPlainText(body); console.log(`Sending Teams webhook notification with subject: "${subject}"`); const response = await fetch(teamsWebhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - text: `**${subject}**\n\n${text}`, + text: `**${subject}**\n\n${body}`, }), }); @@ -55,12 +45,12 @@ export async function sendLoginRequiredNotification() {

This is a notification for the user: ${username}

`; - console.log(`Sending authentication required notification for ${username}...`); + console.log("Sending authentication required notification..."); const response = await fetch(teamsWebhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - text: `**${subject}**\n\n${htmlToPlainText(body)}`, + text: `**${subject}**\n\n${body}`, }), }); From beaaba0b13c0139595171a050a521885fb2d0445 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 05:05:27 +0000 Subject: [PATCH 4/6] fix: restore email notifications alongside Teams webhook Agent-Logs-Url: https://github.com/mitsuiJao/webc-asgmt-notify/sessions/016259d8-a7ff-4e9e-bc0a-e1385df81527 Co-authored-by: mitsuiJao <88090304+mitsuiJao@users.noreply.github.com> --- .github/workflows/cron.disable.yml | 3 + README.md | 9 ++ package-lock.json | 52 +++++++++++- package.json | 3 +- src/config.js | 3 + src/sender.js | 129 +++++++++++++++++++---------- 6 files changed, 155 insertions(+), 44 deletions(-) diff --git a/.github/workflows/cron.disable.yml b/.github/workflows/cron.disable.yml index d6f4fc8..80bd089 100644 --- a/.github/workflows/cron.disable.yml +++ b/.github/workflows/cron.disable.yml @@ -42,6 +42,9 @@ jobs: env: USER_ID: ${{ secrets.USER_ID }} PASSWORD: ${{ secrets.PASSWORD }} + APIKEY: ${{ secrets.APIKEY }} + SENDFROM: ${{ secrets.SENDFROM }} + SENDTO: ${{ secrets.SENDTO }} TEAMS_WEBHOOK_URL: ${{ secrets.TEAMS_WEBHOOK_URL }} MFA_SECRET: ${{ secrets.MFA_SECRET }} diff --git a/README.md b/README.md index bee75df..a6e49c2 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,9 @@ 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}' MFA_SECRET='{MFA_SECRET}' EOF ``` @@ -98,6 +101,9 @@ EOF USER_ID='{your_ID}' PASSWORD='{your_password}' TEAMS_WEBHOOK_URL='{teams_webhook_url}' +APIKEY='{Resend_APIKEY}' +SENDFROM='notification@{your_domain}' +SENDTO='{channel_mailaddress}' MFA_SECRET='{MFA_SECRET}' ``` @@ -106,6 +112,9 @@ MFA_SECRET='{MFA_SECRET}' | USER_ID | Microsoftアカウントのメールアドレス | | PASSWORD | Microsoftアカウントのパスワード | | TEAMS_WEBHOOK_URL | Teamsで発行したWebhook URL | +| APIKEY | ResendのAPIKEY(メール通知も使う場合) | +| SENDFROM | 送信元メールアドレス(メール通知も使う場合) | +| SENDTO | 通知先メールアドレス(メール通知も使う場合) | | MFA_SECRET | 後述するシークレット | diff --git a/package-lock.json b/package-lock.json index 6f58b0c..1a572ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,8 @@ "dependencies": { "bottleneck": "^2.19.5", "dotenv": "^17.2.3", - "puppeteer": "^24.34.0" + "puppeteer": "^24.34.0", + "resend": "^6.12.4" } }, "node_modules/@babel/code-frame": { @@ -54,6 +55,12 @@ "node": ">=18" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -527,6 +534,12 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -783,6 +796,12 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -875,6 +894,27 @@ "node": ">=0.10.0" } }, + "node_modules/resend": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.4.tgz", + "integrity": "sha512-lRpJ2Hxd+ht+JPDm97juRcUp9HOMuZyxaRFRFmc9Tx8iNWiei94Dx9v6SWufgKk2667C/uCeKKspMotOHSpCSg==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "standardwebhooks": "1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -944,6 +984,16 @@ "node": ">=0.10.0" } }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/streamx": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", diff --git a/package.json b/package.json index 3b3c101..7dd34b6 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "dependencies": { "bottleneck": "^2.19.5", "dotenv": "^17.2.3", - "puppeteer": "^24.34.0" + "puppeteer": "^24.34.0", + "resend": "^6.12.4" } } diff --git a/src/config.js b/src/config.js index b48d217..3de1542 100644 --- a/src/config.js +++ b/src/config.js @@ -25,6 +25,9 @@ export default { username: process.env.USER_ID, password: process.env.PASSWORD, mfaSecret: process.env.MFA_SECRET, + 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', diff --git a/src/sender.js b/src/sender.js index 3a290e3..67efb13 100644 --- a/src/sender.js +++ b/src/sender.js @@ -1,40 +1,57 @@ +import { Resend } from "resend"; import config from "./config.js"; export async function sendNotification(subject, body) { - const { teamsWebhookUrl } = config; - if (!teamsWebhookUrl) { - console.warn("TEAMS_WEBHOOK_URL not set. Skipping Teams notification."); - return; + const { teamsWebhookUrl, apikey, sendto, sendfrom } = config; + let sent = false; + + if (teamsWebhookUrl) { + console.log(`Sending Teams webhook notification with subject: "${subject}"`); + const response = await fetch(teamsWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: `**${subject}**\n\n${body}`, + }), + }); + + if (!response.ok) { + const responseText = await response.text(); + const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); + console.error("Failed to send Teams webhook notification:", error.message); + throw error; + } + + console.log("Teams webhook notification sent successfully."); + sent = true; } - console.log(`Sending Teams webhook notification with subject: "${subject}"`); - const response = await fetch(teamsWebhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - text: `**${subject}**\n\n${body}`, - }), - }); - - if (!response.ok) { - const responseText = await response.text(); - const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); - console.error("Failed to send Teams webhook notification:", error.message); - throw error; + if (apikey && sendto && sendfrom) { + const resend = new Resend(apikey); + console.log(`Sending email notification with subject: "${subject}"`); + const { data, error } = await resend.emails.send({ + from: `WebClass Notifier <${sendfrom}>`, + to: sendto, + subject: subject, + html: body, + }); + + if (error) { + console.error("Failed to send email notification:", { error }); + throw error; + } + + console.log("Email notification sent successfully:", { data }); + sent = true; } - console.log("Teams webhook notification sent successfully."); + if (!sent) { + console.warn("No notification target configured. Set TEAMS_WEBHOOK_URL or APIKEY/SENDFROM/SENDTO."); + } } export async function sendLoginRequiredNotification() { - const { teamsWebhookUrl, username } = config; - - if (!teamsWebhookUrl || !username) { - console.warn( - "TEAMS_WEBHOOK_URL or USER_ID not set. Skipping login required notification." - ); - return; - } + const { teamsWebhookUrl, username, apikey } = config; const subject = "WebClass Scraper: Authentication Required"; const body = ` @@ -45,21 +62,49 @@ export async function sendLoginRequiredNotification() {

This is a notification for the user: ${username}

`; - console.log("Sending authentication required notification..."); - const response = await fetch(teamsWebhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - text: `**${subject}**\n\n${body}`, - }), - }); - - if (!response.ok) { - const responseText = await response.text(); - const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); - console.error("Failed to send login required notification:", error.message); - throw error; + let sent = false; + + if (teamsWebhookUrl) { + console.log("Sending authentication required notification to Teams..."); + const response = await fetch(teamsWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: `**${subject}**\n\n${body}`, + }), + }); + + if (!response.ok) { + const responseText = await response.text(); + const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); + console.error("Failed to send login required notification to Teams:", error.message); + throw error; + } + + console.log("Login required notification sent to Teams successfully."); + sent = true; + } + + if (apikey && username) { + const resend = new Resend(apikey); + console.log("Sending authentication required email notification..."); + const { data, error } = await resend.emails.send({ + from: "Scraper Alert ", + to: username, + subject: subject, + html: body, + }); + + if (error) { + console.error("Failed to send login required email notification:", { error }); + throw error; + } + + console.log("Login required email notification sent successfully:", { data }); + sent = true; } - console.log("Login required notification sent successfully."); + if (!sent) { + console.warn("No login-required notification target configured."); + } } From 7c664c8899df8efdf0d890669c2d51765e180820 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 05:20:14 +0000 Subject: [PATCH 5/6] fix: route normal notifications to webhook and keep email for error alerts Agent-Logs-Url: https://github.com/mitsuiJao/webc-asgmt-notify/sessions/eee5199c-8c36-487d-a3e5-f0d5fc4e4b35 Co-authored-by: mitsuiJao <88090304+mitsuiJao@users.noreply.github.com> --- README.md | 6 +- index.js | 10 ++- src/sender.js | 174 +++++++++++++++++++++++++++----------------------- 3 files changed, 106 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index a6e49c2..ddb65d0 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,9 @@ MFA_SECRET='{MFA_SECRET}' | USER_ID | Microsoftアカウントのメールアドレス | | PASSWORD | Microsoftアカウントのパスワード | | TEAMS_WEBHOOK_URL | Teamsで発行したWebhook URL | -| APIKEY | ResendのAPIKEY(メール通知も使う場合) | -| SENDFROM | 送信元メールアドレス(メール通知も使う場合) | -| SENDTO | 通知先メールアドレス(メール通知も使う場合) | +| APIKEY | ResendのAPIKEY(エラー時の管理者メール通知も使う場合) | +| SENDFROM | 送信元メールアドレス(エラー時の管理者メール通知も使う場合)| +| SENDTO | 管理者通知先メールアドレス(エラー時の管理者メール通知) | | MFA_SECRET | 後述するシークレット | diff --git a/index.js b/index.js index 8cfc0d8..85982a6 100644 --- a/index.js +++ b/index.js @@ -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"; @@ -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; } @@ -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); } } diff --git a/src/sender.js b/src/sender.js index 67efb13..de1c3b4 100644 --- a/src/sender.js +++ b/src/sender.js @@ -1,57 +1,62 @@ import { Resend } from "resend"; import config from "./config.js"; -export async function sendNotification(subject, body) { - const { teamsWebhookUrl, apikey, sendto, sendfrom } = config; - let sent = false; +async function sendTeamsWebhook(subject, body) { + const { teamsWebhookUrl } = config; + if (!teamsWebhookUrl) { + throw new Error("TEAMS_WEBHOOK_URL not set."); + } - if (teamsWebhookUrl) { - console.log(`Sending Teams webhook notification with subject: "${subject}"`); - const response = await fetch(teamsWebhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - text: `**${subject}**\n\n${body}`, - }), - }); - - if (!response.ok) { - const responseText = await response.text(); - const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); - console.error("Failed to send Teams webhook notification:", error.message); - throw error; - } - - console.log("Teams webhook notification sent successfully."); - sent = true; + console.log(`Sending Teams webhook notification with subject: "${subject}"`); + const response = await fetch(teamsWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: `**${subject}**\n\n${body}`, + }), + }); + + if (!response.ok) { + const responseText = await response.text(); + throw new Error(`Webhook request failed with status ${response.status}: ${responseText}`); } - if (apikey && sendto && sendfrom) { - const resend = new Resend(apikey); - console.log(`Sending email notification with subject: "${subject}"`); - const { data, error } = await resend.emails.send({ - from: `WebClass Notifier <${sendfrom}>`, - to: sendto, - subject: subject, - html: body, - }); - - if (error) { - console.error("Failed to send email notification:", { error }); - throw error; - } - - console.log("Email notification sent successfully:", { data }); - sent = true; + console.log("Teams webhook notification sent successfully."); +} + +async function sendAdminEmail(subject, body) { + const { apikey, sendto, sendfrom } = config; + if (!apikey || !sendto || !sendfrom) { + throw new Error("APIKEY/SENDFROM/SENDTO not set."); } - if (!sent) { - console.warn("No notification target configured. Set TEAMS_WEBHOOK_URL or APIKEY/SENDFROM/SENDTO."); + const resend = new Resend(apikey); + console.log(`Sending admin email notification with subject: "${subject}"`); + const { data, error } = await resend.emails.send({ + from: `WebClass Notifier <${sendfrom}>`, + to: sendto, + subject, + html: body, + }); + + if (error) { + throw error; + } + + console.log("Admin email notification sent successfully:", { data }); +} + +export async function sendNotification(subject, body) { + try { + await sendTeamsWebhook(subject, body); + } catch (error) { + console.error("Failed to send Teams webhook notification:", error.message); + throw error; } } -export async function sendLoginRequiredNotification() { - const { teamsWebhookUrl, username, apikey } = config; +export async function sendLoginRequiredNotification(errorMessage = "") { + const { username } = config; const subject = "WebClass Scraper: Authentication Required"; const body = ` @@ -60,51 +65,62 @@ export async function sendLoginRequiredNotification() {

Please run the script manually in your terminal to enter the MFA code:

node --env-file=.env src/scraper.js

This is a notification for the user: ${username}

+ ${errorMessage ? `

Error detail: ${errorMessage}

` : ""} + `; + + let sent = false; + const errors = []; + + try { + await sendTeamsWebhook(subject, body); + sent = true; + } catch (error) { + console.error("Failed to send login required notification to Teams:", error.message); + errors.push(error); + } + + try { + await sendAdminEmail(subject, body); + sent = true; + } catch (error) { + console.error("Failed to send login required admin email notification:", error.message); + errors.push(error); + } + + if (!sent && errors.length > 0) { + throw errors[0]; + } +} + +export async function sendErrorNotification(error) { + const errorText = error instanceof Error ? `${error.message}\n\n${error.stack || ""}` : String(error); + const subject = "WebClass Scraper: Runtime Error"; + const body = ` +

Runtime Error

+

An error occurred in the main process.

+
${errorText}
`; let sent = false; + const errors = []; - if (teamsWebhookUrl) { - console.log("Sending authentication required notification to Teams..."); - const response = await fetch(teamsWebhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - text: `**${subject}**\n\n${body}`, - }), - }); - - if (!response.ok) { - const responseText = await response.text(); - const error = new Error(`Webhook request failed with status ${response.status}: ${responseText}`); - console.error("Failed to send login required notification to Teams:", error.message); - throw error; - } - - console.log("Login required notification sent to Teams successfully."); + try { + await sendTeamsWebhook(subject, body); sent = true; + } catch (sendError) { + console.error("Failed to send runtime error notification to Teams:", sendError.message); + errors.push(sendError); } - if (apikey && username) { - const resend = new Resend(apikey); - console.log("Sending authentication required email notification..."); - const { data, error } = await resend.emails.send({ - from: "Scraper Alert ", - to: username, - subject: subject, - html: body, - }); - - if (error) { - console.error("Failed to send login required email notification:", { error }); - throw error; - } - - console.log("Login required email notification sent successfully:", { data }); + try { + await sendAdminEmail(subject, body); sent = true; + } catch (sendError) { + console.error("Failed to send runtime error admin email notification:", sendError.message); + errors.push(sendError); } - if (!sent) { - console.warn("No login-required notification target configured."); + if (!sent && errors.length > 0) { + throw errors[0]; } } From 0432f6a6642b454942464664faeecdce29044c5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 05:26:52 +0000 Subject: [PATCH 6/6] fix: runtime error notifications should be email-only Agent-Logs-Url: https://github.com/mitsuiJao/webc-asgmt-notify/sessions/c2312826-ec47-497b-921e-98ab2942c425 Co-authored-by: mitsuiJao <88090304+mitsuiJao@users.noreply.github.com> --- src/sender.js | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/sender.js b/src/sender.js index de1c3b4..92ac810 100644 --- a/src/sender.js +++ b/src/sender.js @@ -101,26 +101,10 @@ export async function sendErrorNotification(error) {
${errorText}
`; - let sent = false; - const errors = []; - - try { - await sendTeamsWebhook(subject, body); - sent = true; - } catch (sendError) { - console.error("Failed to send runtime error notification to Teams:", sendError.message); - errors.push(sendError); - } - try { await sendAdminEmail(subject, body); - sent = true; } catch (sendError) { console.error("Failed to send runtime error admin email notification:", sendError.message); - errors.push(sendError); - } - - if (!sent && errors.length > 0) { - throw errors[0]; + throw sendError; } }