diff --git a/CHANGELOG.md b/CHANGELOG.md index 759d015..733a69e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# 2026.4.0-beta.0 +- Feat: 地震発生情報の地域を都道府県でグループ化する機能 +- Feat: ユーズの再試行 +- Feat: ユーズの文字数制限回避 +- Feat: ユーズ送信関数 +- Feat: コマンドを並列処理 +- Feat: 地震情報のWebSocket再接続 +- Feat: 天気予報の分割数を自動的に計算する機能 +- Feat: 最大震度の要求を設定できる機能 +- Feat: 最大震度が不明な場合に投稿するかどうかを設定できる機能 +- Chg: 地震発生情報の時刻に頃を追加 +- Chg: weatherNotice.tsのマジックナンバーに命名 +- Fix: 震度0の地震情報に対応していない問題 +- Fix: コマンド処理済みのユーズのid除外が正しく動作しない問題 +- Fix: miqコマンドのコンソール出力に値が存在しない問題 + # 2026.4.0-alpha.2 - Feat: 地震情報のid除外 - Feat: 新年迎春 diff --git a/config/example.yaml b/config/example.yaml index d33f0c6..ab1c040 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -1,10 +1,37 @@ command: + # コマンド処理の間隔(分) number + # 自然数のみが有効です。 interval: 10 -weather: - splits: 4 + # コマンドの最大並列処理数 number + # 例: 2を指定すると、コマンドを並列に2つずつ処理します。 + # 並列処理の数であるため、最終的なコマンドの処理数は変わりません。 + # 自然数のみが有効です。 + maxParallels: 3 earthquake: + # 地震発生情報を投稿することに必要な最大震度 number + # 例: 30を指定すると、最大震度が震度3以上の地震発生情報のみを投稿します。 + # 10(震度1), 20(震度2), 30(震度3), 40(震度4), 45(震度5弱), 50(震度5強), 55(震度6弱), 60(震度6強), 70(震度7)が有効です。 + requireMaxScale: 30 + # 最大震度が不明な地震発生情報を投稿するかどうか boolean + canUnknownMaxScale: true + # 過去のデバッグ用データを使用して地震情報を配信するかどうか boolean + # デバッグ用途のみで使用してください。 useHistoryData: false + # 再接続の間隔(ミリ秒) number + # 正数のみが有効です。 + reconnectInterval: 3000 uwuzu: + # APIトークン string + # とくに理由がない限り、全権限を与えてください。 token: API_TOKEN + # uwuzuサーバーのorigin string origin: https://uwuzu.example.com +ueuse: + # 最大再試行数 number + # 自然数のみが有効です。 + maxRetries: 3 + # 再試行の間隔(ミリ秒) number + # 正数のみが有効です。 + retryInterval: 1000 +# デバッグモードにするかどうか boolean debug: false \ No newline at end of file diff --git a/locales/ja.yaml b/locales/ja.yaml index 813145d..89ab965 100644 --- a/locales/ja.yaml +++ b/locales/ja.yaml @@ -10,7 +10,7 @@ weatherReply: | 降水確率: {{ chanceOfRain }} earthquakeNotice: | ### ==地震発生== - ⏰時刻: {{ occuredTime }} + ⏰時刻: {{ occuredTime }}頃 🫨最大震度: {{ maxScale }} 📍震源地: {{ epicenter }} 💪マグニチュード: {{ magnitude }} @@ -50,10 +50,14 @@ eewNotice: | 📍震源地: {{ epicenter }} 💪マグニチュード: {{ magnitude }} 🪨深さ: {{ depth }}{{ areas }} + + 🔬情報源: P2P地震速報 eewCancelNotice: | ### ==緊急地震速報(警報)**解除**== {{ isTest }} ⏰発表時刻: {{ announceTime }} + + 🔬情報源: P2P地震速報 hnyNotice: | あけましておめでとうございます。今年は、{{ year }}年です。 commandNotFound: | diff --git a/package.json b/package.json index b30f999..aa3946b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notice-uwuzu", - "version": "26.4.0-alpha.2", + "version": "26.4.0-beta.0", "type": "module", "main": "dist/index.js", "scripts": { diff --git a/src/feature/command/follow.ts b/src/feature/command/follow.ts index cb71eda..ff33b58 100644 --- a/src/feature/command/follow.ts +++ b/src/feature/command/follow.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import client, { createUeuse } from "@/lib/client"; import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse"; import i18next from "i18next"; @@ -14,15 +14,8 @@ export default async function followCommand(ueuse: ueuseModule) { console.log("フォロー:", follow.userid); - const notice = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("followedNotification", { username: ueuse.account.username }), replyid: ueuse.uniqid, - }); - - if (!notice.success) { - console.warn("フォロー通知に失敗:", notice.error_code); - return; - } - - console.log("フォロー通知:", notice.uniqid); + }, "フォロー通知"); } \ No newline at end of file diff --git a/src/feature/command/help.ts b/src/feature/command/help.ts index 552cde8..649aeff 100644 --- a/src/feature/command/help.ts +++ b/src/feature/command/help.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import { createUeuse } from "@/lib/client"; import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse"; import i18next from "i18next"; import { EOL } from "node:os"; @@ -14,31 +14,19 @@ const helps = [ export default async function helpCommand(ueuse: ueuseModule, args: string[]) { if (args[1] !== undefined) { if (!(helps.includes(args[1]))) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("invalidOption", { option: args[1], command: "help" }), replyid: ueuse.uniqid, - }); + }, "無効なオプションである旨"); - if (!response.success) { - console.warn("コマンド詳細の返信に失敗:", response.error_code); - return; - } - - console.warn("コマンド詳細:", response.uniqid); return; } - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t(`fullHelp${args[1].charAt(0).toUpperCase()}${args[1].slice(1)}`), replyid: ueuse.uniqid, - }); + }, "コマンド詳細"); - if (!response.success) { - console.warn("コマンド詳細の返信に失敗:", response.error_code); - return; - } - - console.warn("コマンド詳細:", response.uniqid); return; } @@ -52,15 +40,8 @@ export default async function helpCommand(ueuse: ueuseModule, args: string[]) { summarys += `${i18next.t(`help${help.charAt(0).toUpperCase()}${help.slice(1)}`)}${EOL}`; } - const response = await client.request("ueuse/create", { + await createUeuse({ text: summarys.trim(), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("コマンド概要の返信に失敗:", response.error_code); - return; - } - - console.warn("コマンド概要:", response.uniqid); + }, "コマンド概要"); } diff --git a/src/feature/command/index.ts b/src/feature/command/index.ts index 0d04155..0f70e07 100644 --- a/src/feature/command/index.ts +++ b/src/feature/command/index.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import client, { createUeuse } from "@/lib/client"; import Memory from "@/lib/memory"; import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse"; import i18next from "i18next"; @@ -8,6 +8,7 @@ import followCommand from "@/feature/command/follow"; import unfollowCommand from "@/feature/command/unfollow"; import miqCommand from "@/feature/command/miq"; import initI18n from "@/lib/i18n"; +import config from "@/lib/config"; await initI18n(); console.log("コマンドの処理を行います"); @@ -22,19 +23,17 @@ try { }); if (response.success) { - for (const [index, notification] of response.data.entries()) { - if (notification.category !== "reply") + const notifications = response.data.filter(notification => notification.category === "reply" && typeof notification.valueid === "string"); + + const mem = Memory.memory; + const lastReadReply = mem.lastReadReply; + + for (const [index, notification] of notifications.entries()) { + if (notification.category !== "reply" || typeof notification.valueid !== "string") continue; - if (!notification.valueid) { - console.warn("返信通知にvalueidが存在しないため、スキップします"); - continue; - } - - const mem = Memory.memory; - if (mem.lastReadReply === notification.valueid) { + if (lastReadReply === notification.valueid) break; - } const ueuseResponse = await client.request("ueuse/get", { uniqid: notification.valueid, @@ -48,6 +47,7 @@ try { if (index === 0) { const mem = Memory.memory; mem.lastReadReply = ueuseResponse.data[0].uniqid; + Memory.memory = mem; } ueuses.push(ueuseResponse.data[0]); @@ -64,15 +64,16 @@ try { }); if (response.success) { - for (const [index, mention] of response.data.entries()) { - const mem = Memory.memory; - if (mem.lastReadMention === mention.uniqid) { - break; - } + const mentions = response.data; - if (index === 0) { - const mem = Memory.memory; - mem.lastReadMention = mention.uniqid; + const mem = Memory.memory; + const lastReadMention = mem.lastReadMention; + mem.lastReadMention = mentions[0]?.uniqid ?? lastReadMention; + Memory.memory = mem; + + for (const mention of mentions) { + if (lastReadMention === mention.uniqid) { + break; } ueuses.push(mention); @@ -84,58 +85,58 @@ try { ueuses = [...new Set(ueuses)]; - for (const ueuse of ueuses) { - const mem = Memory.memory; - let text = ueuse.text; - text = text.replace(`@${mem.userid}`, ""); - text = text.trim(); - - const rows = text.split(/\r\n|\r|\n/).map(row => row.trim()); - const commandRow = rows.filter(row => row.startsWith("/"))[0]; - - if (!commandRow || commandRow === "") { - console.warn("コマンドが本文から参照できません"); - - const response = await client.request("ueuse/create", { - text: i18next.t("commandNotFound"), - replyid: ueuse.uniqid, - }); - - if (!response.success) - console.warn("ユーズの作成に失敗しました:", response.error_code); - break; - } - - const args = commandRow.replace("/", "").split(" "); + for (let i = 0; i < ueuses.length; i += config.command.maxParallels) { + const chunk = ueuses.slice(i, i + config.command.maxParallels); - switch (args[0]) { - case "help": - await helpCommand(ueuse, args); - break; - case "weather": - await weatherCommand(ueuse); - break; - case "follow": - await followCommand(ueuse); - break; - case "unfollow": - await unfollowCommand(ueuse); - break; - case "miq": - await miqCommand(ueuse, args); - break; - default: - console.warn("不明なコマンドが入力されました:", args[0]); + await Promise.all(chunk.map(async (ueuse) => { + const mem = Memory.memory; + let text = ueuse.text; + text = text.replace(`@${mem.userid}`, ""); + text = text.trim(); - const response = await client.request("ueuse/create", { - text: i18next.t("unknownCommand", { command: args[0] }), + const rows = text.split(/\r\n|\r|\n/).map(row => row.trim()); + const commandRow = rows.filter(row => row.startsWith("/"))[0]; + + if (!commandRow || commandRow === "") { + console.warn("コマンドが本文から参照できません"); + + await createUeuse({ + text: i18next.t("commandNotFound"), replyid: ueuse.uniqid, - }); + }, "コマンドが見つからない旨"); - if (!response.success) - console.warn("ユーズの作成に失敗しました:", response.error_code); - break; - } + return; + } + + const args = commandRow.replace("/", "").split(" "); + + switch (args[0]) { + case "help": + await helpCommand(ueuse, args); + break; + case "weather": + await weatherCommand(ueuse); + break; + case "follow": + await followCommand(ueuse); + break; + case "unfollow": + await unfollowCommand(ueuse); + break; + case "miq": + await miqCommand(ueuse, args); + break; + default: + console.warn("不明なコマンドが入力されました:", args[0]); + + await createUeuse({ + text: i18next.t("unknownCommand", { command: args[0] }), + replyid: ueuse.uniqid, + }, "コマンドが不明である旨"); + + break; + } + })); } process.exit(0); diff --git a/src/feature/command/miq.ts b/src/feature/command/miq.ts index eb19602..8618f99 100644 --- a/src/feature/command/miq.ts +++ b/src/feature/command/miq.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import client, { createUeuse } from "@/lib/client"; import Memory from "@/lib/memory"; import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse"; import i18next from "i18next"; @@ -7,14 +7,10 @@ import { EOL } from "node:os"; export default async function miqCommand(ueuse: ueuseModule, args: string[]) { if (!args[1]) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("lackOption", { command: "miq" }), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - } + }, "オプションが不足している旨"); return; } @@ -32,14 +28,10 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { ? itUeuse.error_code : "データなし"); - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("replySourceFailed"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.log("返信に失敗:", response.error_code); - } + }, "ソースが見つからない旨"); return; } @@ -49,27 +41,19 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { switch (permission) { case "me": if (itUeuse.data[0].account.userid !== ueuse.account.userid) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("miqPermissionMe"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - } + }, "権限が自分自身のみである旨"); return; } case "consent": if (itUeuse.data[0].account.userid !== ueuse.account.userid) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("miqPermissionConsent", { userid: itUeuse.data[0].account.userid }), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - } + }, "権限が許可制である旨"); return; } @@ -85,19 +69,15 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { }); if (!(typeof result === "string")) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("miqGenerateFailed"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - } + }, "Make it a Quoteの生成に失敗した旨"); return; } - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("miqSuccess", { message: (args[2] ?? "") === "color" ? "カラーモードで生成しました。" : "モノクロモードで生成しました。" @@ -106,14 +86,7 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { photo: [result], }, replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("MiQを生成:", response.uniqid); + }, "Make it a Quote"); break; case "permission": @@ -121,33 +94,21 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { mem = Memory.memory; const permission = mem["permissions"][ueuse.account.userid] ?? "consent"; - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("permissionResponse", { permission }), replyid: ueuse.uniqid, - }); + }, "権限"); - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); return; } const availablePermission = ["me", "everyone", "consent"]; if (!(availablePermission.includes(args[2]))) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("invalidOption", { option: args[2], command: "miq" }), replyid: ueuse.uniqid, - }); + }, "無効なオプションである旨"); - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); return; } @@ -156,33 +117,20 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { mem["permissions"][ueuse.account.userid] = args[2]; Memory.memory = mem; - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("permissionChangeSuccess", { username: ueuse.account.username, permission: args[2] }), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); + }, "権限の変更に成功した旨"); } break; case "allow": if (ueuse.replyid === "") { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("injusticeFormat"), replyid: ueuse.uniqid, - }); + }, "形式が異なる旨"); - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); return; } @@ -191,17 +139,11 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { const permission = mem["permissions"][ueuse.account.userid] ?? "consent"; if (permission !== "consent") { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("permisionIsNotConsent"), replyid: ueuse.uniqid, - }); + }, "権限が許可制ではない旨"); - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); return; } } @@ -215,30 +157,20 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { ? confirmUeuse.error_code : "データなし"); - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("replySourceFailed"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.log("返信に失敗:", response.error_code); - } + }, "ソースが見つからない旨"); return; } if (confirmUeuse.data[0].replyid === "") { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("injusticeFormat"), replyid: ueuse.uniqid, - }); + }, "形式が異なる旨"); - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); return; } @@ -248,14 +180,10 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { ? confirmUeuse.error_code : "データなし"); - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("replySourceIsNotThis"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.log("返信に失敗:", response.error_code); - } + }, "返信元がこのBotではない旨"); return; } @@ -269,30 +197,19 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { ? requestUeuse.error_code : "データなし"); - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("replySourceFailed"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.log("返信に失敗:", response.error_code); - } + }, "ソースが見つからない旨"); return; } if (requestUeuse.data[0].replyid === "") { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("injusticeFormat"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); + }, "形式が異なる旨"); return; } @@ -305,29 +222,19 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { ? sourceUeuse.error_code : "データなし"); - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("replySourceFailed"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.log("返信に失敗:", response.error_code); - } + }, "ソースが見つからない旨"); return; } if (sourceUeuse.data[0].account.userid !== ueuse.account.userid) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("replySourceIsNotSourceUser"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.log("返信に失敗:", response.error_code); - } - - console.warn("ソースのユーズと/miq allowのユーザーが一致しない:", ); + }, "ソースのユーズと/miq allowのユーザーが一致しない旨"); return; } @@ -344,27 +251,15 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { if (!commandRow || commandRow === "") { console.warn("コマンドが本文から参照できません"); - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("commandNotFound"), replyid: requestUeuse.data[0].uniqid, - }); + }, "コマンドが見つからない旨"); - if (!response.success) - console.warn("ユーズの作成に失敗しました:", response.error_code); - - { - const response = await client.request("ueuse/create", { - text: i18next.t("injusticeFormat"), - replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("返信:", response.uniqid); - } + await createUeuse({ + text: i18next.t("injusticeFormat"), + replyid: ueuse.uniqid, + }, "形式が異なる旨"); break; } @@ -381,19 +276,15 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { }); if (!(typeof result === "string")) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("miqGenerateFailed"), replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - } + }, "Make it a Quoteの生成に失敗した旨"); return; } - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("miqSuccess", { message: ((requestUeuseArgs[2] ?? "") === "color" ? "カラーモードで生成しました。" : "モノクロモードで生成しました。") @@ -403,14 +294,7 @@ export default async function miqCommand(ueuse: ueuseModule, args: string[]) { photo: [result], }, replyid: ueuse.uniqid, - }); - - if (!response.success) { - console.warn("返信に失敗:", response.error_code); - return; - } - - console.log("MiQを生成:", response.uniqid); + }, "Make it a Quote"); } break; diff --git a/src/feature/command/unfollow.ts b/src/feature/command/unfollow.ts index d9f7ed8..2ff6916 100644 --- a/src/feature/command/unfollow.ts +++ b/src/feature/command/unfollow.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import client, { createUeuse } from "@/lib/client"; import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse"; import i18next from "i18next"; @@ -14,15 +14,8 @@ export default async function unfollowCommand(ueuse: ueuseModule) { console.log("フォロー解除:", unfollow.userid); - const notice = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("unfollowedNotification", { username: ueuse.account.username }), replyid: ueuse.uniqid, - }); - - if (!notice.success) { - console.warn("フォロー解除通知に失敗:", notice.error_code); - return; - } - - console.log("フォロー解除通知:", notice.uniqid); + }, "フォロー解除通知"); } \ No newline at end of file diff --git a/src/feature/earthquakeNotice.ts b/src/feature/earthquakeNotice.ts index 3e43130..e9572a4 100644 --- a/src/feature/earthquakeNotice.ts +++ b/src/feature/earthquakeNotice.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import { createUeuse } from "@/lib/client"; import config from "@/lib/config"; import initI18n from "@/lib/i18n"; import Memory from "@/lib/memory"; @@ -21,49 +21,67 @@ if (config.earthquake?.useHistoryData) { i++; }, 10 * 1000); } else { - const WEBSOCKET_URL = config.debug - ? "wss://api-realtime-sandbox.p2pquake.net/v2/ws" - : "wss://api.p2pquake.net/v2/ws"; + const connect = () => { + const WEBSOCKET_URL = config.debug + ? "wss://api-realtime-sandbox.p2pquake.net/v2/ws" + : "wss://api.p2pquake.net/v2/ws"; - console.log("P2P地震情報のWebSocketに接続します"); - const socket = new WebSocket(WEBSOCKET_URL); + console.log("P2P地震情報のWebSocketに接続します"); + const socket = new WebSocket(WEBSOCKET_URL); - socket.addEventListener("open", () => { - console.log("P2P地震情報のWebSocketに接続しました"); - }); + socket.addEventListener("open", () => { + console.log("P2P地震情報のWebSocketに接続しました"); + }); - socket.addEventListener("message", async (event) => { - let message; + socket.addEventListener("close", (err) => { + const interval = config.earthquake.reconnectInterval; + console.log(`WebSocketが切断されました。${interval / 1000}秒後に再接続します:`, err.reason); + setTimeout(() => { + connect(); + }, interval); + }); - try { - message = typeof event.data === "string" - ? JSON.parse(event.data) - : event.data; - } catch (err) { - console.error(`地震情報メッセージのパースでエラーが発生: ${err}`); - return; - } + socket.addEventListener("error", (err) => { + console.error("WebSocketでエラーが発生しました:", err); + socket.close(); + }); - const id = message.id ?? message._id; - const mem = Memory.memory; - if (mem.processedInfo.includes(id) && !config.debug) { - console.log("重複した地震情報: ", message.id); - return; - } + socket.addEventListener("message", async (event) => { + let message; - processMessage(message); + try { + message = typeof event.data === "string" + ? JSON.parse(event.data) + : event.data; + } catch (err) { + console.error("地震情報メッセージのパースでエラーが発生:", err); + return; + } - if (!config.debug) { - mem.processedInfo.concat([id]); - Memory.memory = mem; - } - }); + const id = message.id ?? message._id; + const mem = Memory.memory; + if (mem.processedInfo.includes(id) && !config.debug) { + console.log("重複した地震情報:", message.id); + return; + } + + processMessage(message); + + if (!config.debug) { + mem.processedInfo.concat([id]); + Memory.memory = mem; + } + }); + } + + connect(); } const processMessage = async (message: any) => { try { const scaleMessages: Record = { "-1": "不明", + "0": "震度0", "10": "震度1", "20": "震度2", "30": "震度3", @@ -85,6 +103,23 @@ const processMessage = async (message: any) => { { console.log("地震発生情報を受信しました"); + if ( + (message.earthquake.maxScale === -1 || + message.earthquake.maxScale === undefined) && + !config.earthquake.canUnknownMaxScale + ) { + console.log("最大震度が不明であり、最大震度が不明な場合の投稿が許可されていないため、スキップします"); + break; + } + + if ( + message.earthquake.maxScale !== -1 && + message.earthquake.maxScale < config.earthquake.requireMaxScale + ) { + console.log("投稿に必要な最大震度に満たないため、スキップします"); + break; + } + const domesticTsunamiMessages: Record = { "None": "😌この地震による**国内**の津波の心配はありません。", "Unknown": "😕この地震による**国内**の***津波情報は***不明です。", @@ -107,34 +142,36 @@ const processMessage = async (message: any) => { "Potential": "🚨この地震によって**一般的に**この規模では津波の可能性があると考えられています。", } - let points: Record = {}; + const grouped: Record }> = {}; + for (const point of message.points) { - const scaleMsg = scaleMessages[String(point.scale)]; - if (!scaleMsg) - continue; - - points[scaleMsg]?.push(point); - } - - const grouped: Record = {}; - for (const point of message.points) { - const { addr, scale } = point; + const { addr, scale, pref } = point; const label = scaleMessages[String(scale)] ?? "不明"; if (!grouped[label]) { - grouped[label] = { scale, addrs: [] }; + grouped[label] = { scale, prefs: {} }; } - grouped[label].addrs.push(addr); + if (!grouped[label].prefs[pref]) { + grouped[label].prefs[pref] = []; + } + + grouped[label].prefs[pref].push(addr); } const pointsMsg = Object.entries(grouped) .sort((a, b) => b[1].scale - a[1].scale) - .map(([label, { addrs }]) => - `【${label}】${EOL}${addrs.join("・")}`) - .join(EOL.repeat(2)).trim(); + .map(([label, { prefs }]) => { + const prefLines = Object.entries(prefs) + .map(([pref, addrs]) => `${pref}: ${addrs.join("・")}`) + .join(EOL); - const response = await client.request("ueuse/create", { + return `【${label}】${EOL}${prefLines}`; + }) + .join(EOL.repeat(2)) + .trim(); + + await createUeuse({ text: i18next.t("earthquakeNotice", { occuredTime: format(new Date(message.earthquake.time), "yyyy年M月d日 H:mm"), maxScale: scaleMessages[String(message.earthquake.maxScale)], @@ -159,14 +196,7 @@ const processMessage = async (message: any) => { ? "" : EOL + message.comments.freeFormComment + EOL, }), - }); - - if (!response.success) { - console.warn("ユーズの作成に失敗しました:", response.error_code); - break; - } - - console.log("地震発生情報を投稿:", response.uniqid); + }, "地震発生情報"); } break; case 552: @@ -174,19 +204,12 @@ const processMessage = async (message: any) => { console.log("津波予報情報を受信しました"); if (message.cancelled) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("tsunamiCancelNotice", { announceTime: format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss"), source: message.issue.source ?? "不明", }), - }); - - if (!response.success) { - console.warn("ユーズの作成に失敗しました:", response.error_code); - break; - } - - console.log("津波予報解除情報を投稿:", response.uniqid); + }, "津波予報解除情報"); break; } @@ -217,20 +240,13 @@ const processMessage = async (message: any) => { }) + EOL.repeat(2); } - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("tsunamiForecastNotice", { announceTime: format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss"), areasMsg: areasMsg.trim(), source: message.issue.source ?? "不明", }), - }); - - if (!response.success) { - console.warn("ユーズの作成に失敗しました:", response.error_code); - break; - } - - console.log("津波予報情報を投稿:", response.uniqid); + }, "津波予報情報"); } break; case 556: @@ -238,21 +254,14 @@ const processMessage = async (message: any) => { console.log("緊急地震速報(警報)を受信しました"); if (message.cancelled) { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("eewCancelNotice", { isTest: message.test ? "⚒️これは**テストです。**" : "🚨これは**テストではありません。**", announceTime: format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss"), }), - }); - - if (!response.success) { - console.warn("ユーズの作成に失敗しました:", response.error_code); - break; - } - - console.log("緊急地震速報(警報)解除情報を投稿:", response.uniqid); + }, "緊急地震速報(警報)解除情報"); } const kindMessages: Record = { @@ -278,7 +287,7 @@ const processMessage = async (message: any) => { }) + EOL.repeat(2); } - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("eewNotice", { isTest: message.test ? "⚒️これは**テストです。**" @@ -302,14 +311,7 @@ const processMessage = async (message: any) => { ? EOL.repeat(2) + areasMsg.trim() : "", }), - }); - - if (!response.success) { - console.warn("ユーズの作成に失敗しました:", response.error_code); - break; - } - - console.log("緊急地震速報(警報)情報を投稿:", response.uniqid); + }, "緊急地震速報(警報)情報"); } break; default: diff --git a/src/feature/hnyNotice.ts b/src/feature/hnyNotice.ts index e9036a5..8868313 100644 --- a/src/feature/hnyNotice.ts +++ b/src/feature/hnyNotice.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import { createUeuse } from "@/lib/client"; import initI18n from "@/lib/i18n"; import { format } from "date-fns"; import i18next from "i18next"; @@ -10,16 +10,11 @@ parentPort?.on("message", async () => { console.log("新年迎春の投稿を行います"); try { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("hnyNotice", { year: String(new Date().getFullYear()) }), - }); + }, "新年迎春"); - if (!response.success) { - console.warn("新年迎春投稿に失敗しました:", response.error_code); - process.exit(1); - } - - console.log("新年迎春投稿:", `${response.uniqid} (${format(new Date(), "yyyy/M/d H:mm:ss:SSS")})`); + console.log("新年迎春投稿時刻:", format(new Date(), "yyyy/M/d H:mm:ss:SSS")); process.exit(0); } catch (err: any) { console.error("message" in err diff --git a/src/feature/timeNotice.ts b/src/feature/timeNotice.ts index f3141cf..4ad0bfe 100644 --- a/src/feature/timeNotice.ts +++ b/src/feature/timeNotice.ts @@ -1,4 +1,4 @@ -import client from "@/lib/client"; +import { createUeuse } from "@/lib/client"; import initI18n from "@/lib/i18n"; import { format } from "date-fns"; import i18next from "i18next"; @@ -7,16 +7,10 @@ await initI18n(); console.log("時報の投稿を行います"); try { - const response = await client.request("ueuse/create", { + await createUeuse({ text: i18next.t("timeNotice", { time: format(new Date(), "H:mm") }), - }); + }, "時報"); - if (!response.success) { - console.warn("時報投稿に失敗しました:", response.error_code); - process.exit(1); - } - - console.log("時報投稿:", response.uniqid); process.exit(0); } catch (err: any) { console.error("message" in err diff --git a/src/feature/weatherNotice.ts b/src/feature/weatherNotice.ts index ef55938..2d69206 100644 --- a/src/feature/weatherNotice.ts +++ b/src/feature/weatherNotice.ts @@ -1,6 +1,7 @@ import client from "@/lib/client"; import config from "@/lib/config"; import initI18n from "@/lib/i18n"; +import Memory from "@/lib/memory"; import i18next from "i18next"; import { readFileSync } from "node:fs"; import { EOL } from "node:os"; @@ -61,12 +62,27 @@ if (!isMainThread && workerData === "scheduledWeatherNotice") { console.log("天気予報の投稿を行います"); try { - const provisionalUeuse = await client.request("ueuse/create", { - text: i18next.t("weatherProvisional"), - }); + let provisionalUeuse; + let success = false; - if (!provisionalUeuse.success) { - console.error("天気仮投稿に失敗しました:", provisionalUeuse.error_code); + for (let attempt = 1; attempt <= config.ueuse.maxRetries; attempt++) { + provisionalUeuse = await client.request("ueuse/create", { + text: i18next.t("weatherProvisional"), + }); + + if (provisionalUeuse.success) { + success = true; + break; + } + + console.warn(`天気仮投稿に失敗しました (試行 ${attempt}/${config.ueuse.maxRetries}):`, provisionalUeuse.error_code); + if (attempt < config.ueuse.maxRetries) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + if (!success || !provisionalUeuse?.success) { + console.error("天気仮投稿の全試行に失敗したため、終了します。"); process.exit(1); } @@ -84,7 +100,9 @@ if (!isMainThread && workerData === "scheduledWeatherNotice") { export async function weatherReply(uniqid: string) { // インデックス - const splitCount = config.weather.splits; + const aboutFullLength = 3100; + const mem = Memory.memory; + const splitCount = Math.round(aboutFullLength / mem.max_length); const total = cityList.length; const chunkSizes = Array(splitCount).fill(0).map((_, i) => Math.floor((total + i) / splitCount) @@ -148,13 +166,29 @@ export async function weatherReply(uniqid: string) { // 分割投稿 for (let i = 0; i < splitCount; i++) { - const replyUeuse = await client.request("ueuse/create", { - text: weatherResults[i].trim(), - replyid: uniqid, - }); + let replyUeuse; + let success = false; - if (!replyUeuse.success) { - console.error("天気返信に失敗しました:", replyUeuse.error_code); + for (let attempt = 1; attempt <= config.ueuse.maxRetries; attempt++) { + replyUeuse = await client.request("ueuse/create", { + text: weatherResults[i].trim(), + replyid: uniqid, + }); + + if (replyUeuse.success) { + success = true; + break; + } + + console.warn(`天気返信に失敗しました (試行 ${attempt}/${config.ueuse.maxRetries}):`, replyUeuse.error_code); + if (attempt < config.ueuse.maxRetries) { + await new Promise(resolve => setTimeout(resolve, config.ueuse.retryInterval)); + } + } + + if (!success || !replyUeuse?.success) { + console.error("天気返信の全試行に失敗したため、終了します。"); + process.exit(1); } console.log("天気返信:", replyUeuse.uniqid); diff --git a/src/index.ts b/src/index.ts index 18be265..dc70c41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { schedule } from "node-cron"; import { readFileSync } from "node:fs"; import config from "@/lib/config"; -import { initUserID } from "@/lib/memory"; +import { initData } from "@/lib/memory"; import { styleText } from "node:util"; import { Worker } from "node:worker_threads"; @@ -18,7 +18,7 @@ try { } console.log(); - await initUserID(); + await initData(); new Worker(`${import.meta.dirname}/feature/earthquakeNotice.js`); diff --git a/src/lib/client.ts b/src/lib/client.ts index d5bc847..0024124 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -2,6 +2,8 @@ import uwuzu from "better-uwuzu-sdk"; import config from "@/lib/config"; import Parser from "better-uwuzu-sdk/1.6.8/parser"; import ApiMap from "better-uwuzu-sdk/types/1.6.8/map"; +import Memory from "@/lib/memory"; +import { EOL } from "node:os"; const client = new uwuzu({ origin: config.uwuzu.origin, @@ -9,5 +11,81 @@ const client = new uwuzu({ }); client.token = config.uwuzu.token; +export default client; -export default client; \ No newline at end of file +export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: string) => { + const mem = Memory.memory; + const maxLength = mem.max_length; + const excessedMessage = "👉返信に続きがあります。"; + + let lines = data.text.split(EOL); + let firstUniqid = ""; + let count = 0; + + while (lines.length > 0) { + count++; + let currentText = ""; + + const limit = maxLength - (excessedMessage.length + EOL.length * 2); + + while (lines.length > 0) { + const nextLine = lines[0]; + if (nextLine === undefined) + break; + + if (nextLine.length > limit && currentText === "") { + const targetLine = lines.shift() || ""; + currentText = targetLine.slice(0, limit); + + lines.unshift(targetLine.slice(limit)); + break; + } + + const potentialText = currentText ? currentText + EOL + nextLine : nextLine; + if (potentialText.length <= limit) { + currentText = potentialText; + lines.shift(); + } else { + break; + } + } + + if (lines.length > 0) { + currentText += EOL.repeat(2) + excessedMessage; + } + + let postedUniqid = ""; + let success = false; + + for (let attempt = 1; attempt <= config.ueuse.maxRetries; attempt++) { + const response = await client.request("ueuse/create", { + ...data, + text: currentText, + replyid: data.replyid === undefined && firstUniqid !== "" + ? firstUniqid + : data.replyid, + }); + + if (response.success) { + success = true; + postedUniqid = response.uniqid; + break; + } + + console.warn(`${title}の投稿に失敗しました (試行 ${attempt}/${config.ueuse.maxRetries}):`, response.error_code); + if (attempt < config.ueuse.maxRetries) { + await new Promise(resolve => setTimeout(resolve, config.ueuse.retryInterval)); + } + } + + if (!success) { + console.error(`${title}の全試行が失敗したため、処理を中断します。`); + break; + } + + if (firstUniqid === "") + firstUniqid = postedUniqid; + + console.log(`${title}を投稿(${count}):`, postedUniqid); + } +} \ No newline at end of file diff --git a/src/lib/config.ts b/src/lib/config.ts index d85056b..76c9c64 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -6,13 +6,24 @@ import { EOL } from "node:os"; const schema = z.object({ command: z.object({ interval: z.number().int().positive(), - }), - weather: z.object({ - splits: z.number().int().positive(), + maxParallels: z.number().int().positive(), }), earthquake: z.object({ + requireMaxScale: z.union([ + z.literal(10), + z.literal(20), + z.literal(30), + z.literal(40), + z.literal(45), + z.literal(50), + z.literal(55), + z.literal(60), + z.literal(70), + ]), + canUnknownMaxScale: z.boolean(), useHistoryData: z.boolean(), - }).optional(), + reconnectInterval: z.number().positive(), + }), uwuzu: z.object({ token: z.string().length(64), origin: z.string().refine(data => { @@ -23,6 +34,10 @@ const schema = z.object({ } }), }), + ueuse: z.object({ + maxRetries: z.number().int().positive(), + retryInterval: z.number().positive(), + }), debug: z.boolean().optional(), }); diff --git a/src/lib/memory.ts b/src/lib/memory.ts index 0e41425..f0c6962 100644 --- a/src/lib/memory.ts +++ b/src/lib/memory.ts @@ -14,6 +14,7 @@ class MemoryClass { lastReadMention: "", lastReadReply: "", userid: "", + max_length: 0, })); } @@ -32,15 +33,26 @@ class MemoryClass { const Memory = new MemoryClass(); -export const initUserID = async () => { - const response = await client.request("me/"); +export const initData = async () => { + await Promise.all([ + (async () => { + const response = await client.request("me/"); - if (!response.success) - throw new Error("meの取得に失敗しました"); + if (!response.success) + throw new Error("meの取得に失敗しました"); - const mem = Memory.memory; - mem.userid = response.userid; - Memory.memory = mem; + const mem = Memory.memory; + mem.userid = response.userid; + Memory.memory = mem; + })(), + (async () => { + const response = await client.request("serverinfo-api"); + + const mem = Memory.memory; + mem.max_length = response.server_info.max_ueuse_length; + Memory.memory = mem; + })(), + ]); } export default Memory; \ No newline at end of file