From 724e18ba3beae7cb37df4ebab1ea36c07ac28051 Mon Sep 17 00:00:00 2001 From: Last2014 Date: Sat, 26 Jul 2025 21:28:07 +0900 Subject: [PATCH] noticeUwuzu v6.0@uwuzu1.5.4 --- .gitignore | 2 - README.md | 15 ------ checks/api.ts | 32 +++++++++++ checks/config.ts | 9 ++++ checks/main.ts | 13 +++++ checks/packages.ts | 59 +++++++++++++++++++++ checks/packagesExist.ts | 16 ++++++ checks/version.ts | 38 +++++++++++++ examples/config.ts | 15 +++--- logs/.gitignore | 2 + main.ts | 16 +++--- package.json | 10 ++-- scripts/earthquakeNotice.ts | 57 +++----------------- scripts/{followBack.ts => follow/follow.ts} | 8 +-- scripts/follow/main.ts | 7 +++ scripts/follow/unfollow.ts | 27 ++++++++++ scripts/successExit.ts | 39 +++++++------- scripts/timeNotice.ts | 4 +- scripts/weatherNotice.ts | 8 +-- types/config.d.ts | 11 ++-- 20 files changed, 268 insertions(+), 120 deletions(-) create mode 100644 checks/api.ts create mode 100644 checks/config.ts create mode 100644 checks/main.ts create mode 100644 checks/packages.ts create mode 100644 checks/packagesExist.ts create mode 100644 checks/version.ts create mode 100644 logs/.gitignore rename scripts/{followBack.ts => follow/follow.ts} (80%) create mode 100644 scripts/follow/main.ts create mode 100644 scripts/follow/unfollow.ts diff --git a/.gitignore b/.gitignore index b029ab7..251675d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,3 @@ /node_modules/ /package-lock.json /config.ts - -*log* diff --git a/README.md b/README.md index 02a5381..9f6c4cb 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,6 @@ uwuzuで動作するお知らせBOTです。 # 設定 examples/config.tsをプロジェクトルートへ移動し各設定を更新してください。 -## 設定項目 -time.stopTimes.start: 時報休止期間の開始時刻(HH) -time.stopTimes.stop: 時報休止期間の停止時刻(HH) - -earthquake.reconnectTimes:地震情報のWebSocketが切断されたときに自動再接続する時間(ミリ秒) -earthquake.websocketUrl:地震情報のWebSocket接続先URL -earthquake.areasCsvUrl:地域情報のデータベース(CSV)ファイルのURL -earthquake.maxScaleMin: 地震発生投稿の最低震度(10-70) -earthquake.rateLimit: 地域情報更新のレート制限(分) - -weather.splitCount:天気お知らせの返信の分割数(4分割を推奨) - -apiToken:BOTアカウントのAPIキー -uwuzuServer:使用するuwuzuサーバーのホスト名(uwuzu.netなど) - # サーバー起動 ``` diff --git a/checks/api.ts b/checks/api.ts new file mode 100644 index 0000000..554d71e --- /dev/null +++ b/checks/api.ts @@ -0,0 +1,32 @@ +import { styleText } from "util"; +import config from "../config.js"; + +export default async function APICheck() { + try { + const req = await fetch(`https://${config.uwuzu.host}/api/me`, { + method: "POST", + body: JSON.stringify({ + token: config.uwuzu.apiToken, + }) + }); + + const res = await req.json(); + + if ( + res.isBot === undefined || + res.isBot === null + ) { + console.log(styleText("red", "APIトークンあるいはuwuzuサーバーホストが無効です")); + process.exit(); + } + + if (!res.isBot) { + setTimeout(() => { + console.log(styleText("yellow", "使用するアカウントでBOTフラグが設定されていません")); + }, 1500); + } + } catch (err) { + console.log(styleText("red", `uwuzuサーバーへ接続できませんでした: ${err}`)); + process.exit(); + } +} diff --git a/checks/config.ts b/checks/config.ts new file mode 100644 index 0000000..f2dfa9f --- /dev/null +++ b/checks/config.ts @@ -0,0 +1,9 @@ +import * as fs from "fs"; +import { styleText } from "util"; + +export default function ConfigCheck() { + if (!fs.existsSync("config.ts")) { + console.log(styleText("red", "config.tsがありません")); + process.exit(); + } +} diff --git a/checks/main.ts b/checks/main.ts new file mode 100644 index 0000000..a78155d --- /dev/null +++ b/checks/main.ts @@ -0,0 +1,13 @@ +import PackagesCheck from "./packages.js"; +import PackagesIsExist from "./packagesExist.js"; +import ConfigCheck from "./config.js"; +import APICheck from "./api.js"; +import VersionCheck from "./version.js"; + +export default async function Check() { + PackagesCheck(); + PackagesIsExist(); + ConfigCheck(); + await APICheck(); + await VersionCheck(); +} diff --git a/checks/packages.ts b/checks/packages.ts new file mode 100644 index 0000000..7f20718 --- /dev/null +++ b/checks/packages.ts @@ -0,0 +1,59 @@ +import * as fs from "fs"; +import { styleText } from "util"; + +export default function PackagesCheck() { + try { + if (!fs.existsSync("package.json")) { + console.log(styleText("red", "package.jsonがありません。正規のリポジトリでgit pullを実行してください。")); + process.exit(); + } + + // package.json取得 + const packages = JSON.parse(fs.readFileSync("package.json", "utf-8")); + const dependencies = packages.dependencies; + + const packageNames: Array = []; + + Object.keys(dependencies).forEach((packageName) => { + let version: string; + + if (dependencies[packageName].charAt(0) === "^") { + version = dependencies[packageName].replace('^', ''); + } else { + version = dependencies[packageName] + } + + dependencies[packageName] = version; + packageNames.push(packageName); + }); + + // パッケージのバージョン取得 + const mismatchPackages: Array = []; + + packageNames.forEach((packageName) => { + const packagePath = `node_modules/${packageName}/package.json`; + + if (fs.existsSync(packagePath)) { + const modulePackage = JSON.parse(fs.readFileSync(packagePath, "utf-8")); + + if (modulePackage.version !== dependencies[packageName]) { + mismatchPackages.push(packageName); + } + } else { + console.log(styleText("red", `パッケージ「${packageName}」が見つかりません。`)); + process.exit(); + } + }); + + if (mismatchPackages.length !== 0) { + console.log(styleText("red", "以下のパッケージのバージョンが異なります:")); + mismatchPackages.forEach((mismatch) => { + console.log(styleText("red", mismatch)); + console.log(styleText("red", ` 要求バージョン: ${dependencies[mismatch]}`)); + }); + process.exit(); + } + } catch (err) { + console.log("パッケージの存在確認でエラーが発生しました: ", err); + } +} diff --git a/checks/packagesExist.ts b/checks/packagesExist.ts new file mode 100644 index 0000000..32bd6d0 --- /dev/null +++ b/checks/packagesExist.ts @@ -0,0 +1,16 @@ +import * as fs from "fs"; +import { styleText } from "util"; + +export default function PackagesIsExist() { + try { + if (!fs.existsSync("node_modules/.package-lock.json")) { + console.log(styleText("red", ` + node_modules/.package-lock.jsonがありません。 + プロジェクト直下でnpm installを実行してください。 + `)); + process.exit(); + } + } catch (err) { + console.log("node_modules/.package-lock.jsonの存在確認でエラーが発生しました: ", err); + } +} diff --git a/checks/version.ts b/checks/version.ts new file mode 100644 index 0000000..24f690b --- /dev/null +++ b/checks/version.ts @@ -0,0 +1,38 @@ +import * as fs from "fs"; +import config from "../config.js"; + +export default async function VersionCheck() { + const nowVersion: string = JSON.parse(fs.readFileSync("package.json", "utf-8")).version; + + // 初期化 + if (!fs.existsSync("logs/version.txt")) { + fs.writeFileSync( + "logs/version.txt", + nowVersion, + "utf-8", + ); + } + + // 最終起動バージョン取得 + const oldVersion = fs.readFileSync("logs/version.txt", "utf-8"); + + if (oldVersion !== nowVersion) { + try { + fs.writeFileSync( + "logs/version.txt", + nowVersion, + "utf-8", + ); + + await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, { + method: "POST", + body: JSON.stringify({ + token: config.uwuzu.apiToken, + text: `${nowVersion}にBOTがアップデートされました!`, + }), + }); + } catch (err) { + console.log("アップデート通知にエラーが発生しました: ", err); + } + } +} diff --git a/examples/config.ts b/examples/config.ts index 9967bf6..d8d07bd 100644 --- a/examples/config.ts +++ b/examples/config.ts @@ -8,7 +8,7 @@ const config: configTypes = { stopTimes: { start: 23, // 開始 stop: 6, // 停止 - } + }, }, // 地震速報設定 earthquake: { @@ -16,7 +16,6 @@ const config: configTypes = { websocketUrl: "wss://api.p2pquake.net/v2/ws", // WebSocketのURL areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", // 対象地域CSVファイルのURL maxScaleMin: 30, // 地震発生の際の最低震度(10-70) - rateLimit: 30, // 地域情報更新のレート制限(分) }, weather: { splitCount: 4, // 返信の分割数 @@ -32,12 +31,14 @@ const config: configTypes = { user: "mailUser@example.com", // BOTメール送信元 password: "mailPassword", // SMTPパスワード secure: false, // SMTPsecure設定 - to: "admin@noticeuwuzu.example.com" // 緊急時メール送信先(配列可) - } + to: "admin@noticeuwuzu.example.com", // 緊急時メール送信先(配列可) + }, + }, + uwuzu: { + apiToken: "TOKEN_EXAMPLE", + clientToken: "TOKEN_EXAMPLE", + host: "uwuzu.example.com", }, - - apiToken: "TOKEN_EXAMPLE", // BOTアカウントのAPIトークン - uwuzuServer: "uwuzu.example.com", // uwuzuのサーバー }; export default config; diff --git a/logs/.gitignore b/logs/.gitignore new file mode 100644 index 0000000..005717e --- /dev/null +++ b/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/main.ts b/main.ts index 8136fa8..44d0530 100644 --- a/main.ts +++ b/main.ts @@ -1,3 +1,9 @@ +// 起動チェック +import Check from "./checks/main.js"; +(async () => { + await Check(); +})(); + // 定期実行読み込み import * as cron from "node-cron"; @@ -10,8 +16,8 @@ import earthquakeNotice from "./scripts/earthquakeNotice.js"; import asciiArt from "./scripts/asciiart.js"; asciiArt(); -// フォローバック機能読み込み -import followBack from "./scripts/followBack.js"; +// フォロー機能読み込み +import follows from "./scripts/follow/main.js"; // 正常終了確認読み込み import successExit from "./scripts/successExit.js"; @@ -23,14 +29,12 @@ earthquakeNotice(); // 時報・フォローバック(毎時) cron.schedule("0 * * * *", () => { timeNotice(); - followBack(); + follows(); }); // 天気お知らせ(毎日7:01) cron.schedule("1 7 * * *", () => { - setTimeout(() => { - weatherNotice(); - }, 100); + weatherNotice(); }); // コンソールで表示 diff --git a/package.json b/package.json index 14e5347..fa7771a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "noticeuwuzu", - "version": "v5.1.1@uwuzu1.5.4", + "name": "notice-uwuzu", + "version": "v6.0@uwuzu1.5.4", "description": "uwuzu Notice Bot", "main": "dist/main.js", "scripts": { @@ -30,16 +30,16 @@ "@types/dotenv": "^6.1.1", "@types/node-cron": "^3.0.11", "@types/nodemailer": "^6.4.17", + "@types/node": "^24.0.7", + "@types/ws": "^8.18.1", "date-fns": "^4.1.0", "fs": "^0.0.1-security", "node-cron": "^4.1.1", "nodemailer": "^7.0.4", - "tsx": "^4.20.3", "typescript": "^5.8.3", "ws": "^8.18.3" }, "devDependencies": { - "@types/node": "^24.0.7", - "@types/ws": "^8.18.1" + "tsx": "^4.20.3" } } diff --git a/scripts/earthquakeNotice.ts b/scripts/earthquakeNotice.ts index bcb9084..1a7d4ce 100644 --- a/scripts/earthquakeNotice.ts +++ b/scripts/earthquakeNotice.ts @@ -1,11 +1,8 @@ import WebSocket from "ws"; -import { differenceInMinutes, subMinutes } from "date-fns"; import sendMail from "../src/mailer.js"; import config from "../config.js"; -let rateLimit: Date | null = null; - class P2PEarthquakeClient { private ws: WebSocket | null = null; private reconnectInterval: number = config.earthquake.reconnectTimes; @@ -75,10 +72,6 @@ class P2PEarthquakeClient { console.log("緊急地震速報を受信しました"); this.executeEventFunc(message); break; - case 555: // 地域情報更新情報 - console.log("地域情報更新を受信しました"); - this.executeEventFunc(message); - break; default: console.log(`未対応の情報を受信しました(コード: ${message.code})`); break; @@ -208,7 +201,7 @@ async function event(earthquakeInfo: any): Promise { let magnitude: string = "マグニチュード:"; if ( - earthquakeInfo.earthquake.hypocenter.magnitude !== -1 || + earthquakeInfo.earthquake.hypocenter.magnitude != -1 || earthquakeInfo.earthquake.hypocenter.magnitude === undefined ) { magnitude += "マグニチュードの情報はありません"; @@ -262,7 +255,7 @@ async function event(earthquakeInfo: any): Promise { } if ( - earthquakeInfo.earthquake.maxScale === -1 && + earthquakeInfo.earthquake.maxScale == -1 && earthquakeInfo.earthquake.maxScale === undefined ) { maxScale = "最大震度情報なし"; @@ -346,7 +339,7 @@ async function event(earthquakeInfo: any): Promise { if ( earthquakeInfo.earthquake.hypocenter.depth !== null || earthquakeInfo.earthquake.hypocenter.depth !== undefined || - earthquakeInfo.earthquake.hypocenter.depth !== -1 + earthquakeInfo.earthquake.hypocenter.depth != -1 ) { if (earthquakeInfo.earthquake.hypocenter.depth === 0) { depth = "深さ:ごく浅い"; @@ -361,9 +354,9 @@ async function event(earthquakeInfo: any): Promise { if( earthquakeInfo.earthquake.hypocenter.magnitude !== null || earthquakeInfo.earthquake.hypocenter.magnitude !== undefined || - earthquakeInfo.earthquake.hypocenter.magnitude !== -1 + earthquakeInfo.earthquake.hypocenter.magnitude != -1 ) { - magnitude = `マグニチュード:${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`; + magnitude = `マグニチュード:M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`; } ueuse(` @@ -378,49 +371,13 @@ async function event(earthquakeInfo: any): Promise { 国内の津波:${domesticTsunami} `); } - - // 地域情報更新の場合 - else if (earthquakeInfo.code === 555) { - if (rateLimit === null) { - rateLimit = subMinutes(new Date(), config.earthquake.rateLimit + 15); - } - - // 対象地域マッピング - const areaMaps: any = await areaMap(); - - const areaNames: Array = Array.from( - new Set( - earthquakeInfo.areas - .map((i: any) => { - return areaMaps[i.id]; - }) - .filter(Boolean), - ), - ); - - const areas = areaNames.join("・"); - - if (Math.abs(differenceInMinutes(rateLimit, new Date())) >= config.earthquake.rateLimit) { - ueuse(` - ==地震情報== - 【地域情報更新】 - 時刻:${earthquakeInfo.time} - 対象地域:${areas} - `); - - rateLimit = new Date(); - } else { - console.log("レート制限に満たしていないため投稿されませんでした"); - return; - } - } } async function ueuse(text: string) { - const res = await fetch(`https://${config.uwuzuServer}/api/ueuse/create`, { + const res = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, { method: "POST", body: JSON.stringify({ - token: config.apiToken, + token: config.uwuzu.apiToken, text: text, }), }); diff --git a/scripts/followBack.ts b/scripts/follow/follow.ts similarity index 80% rename from scripts/followBack.ts rename to scripts/follow/follow.ts index 87e6ae0..4b214e0 100644 --- a/scripts/followBack.ts +++ b/scripts/follow/follow.ts @@ -1,13 +1,13 @@ import type * as types from "types/types"; -import config from "../config.js"; +import config from "../../config.js"; export default async function followBack() { console.log("----------------"); // フォロワーを取得 const resMe = await fetch( - `https://${config.uwuzuServer}/api/me?token=${config.apiToken}`, + `https://${config.uwuzu.host}/api/me?token=${config.uwuzu.apiToken}`, { method: "GET", // uwuzu v1.5.4で/api/meのPOSTが死んでいるため簡易的にGET @@ -25,11 +25,11 @@ export default async function followBack() { const followerItem = followers[i]; const resFollow = await fetch( - `https://${config.uwuzuServer}/api/users/follow`, + `https://${config.uwuzu.host}/api/users/follow`, { method: "POST", body: JSON.stringify({ - token: config.apiToken, + token: config.uwuzu.apiToken, userid: followerItem, }), }, diff --git a/scripts/follow/main.ts b/scripts/follow/main.ts new file mode 100644 index 0000000..5edd24d --- /dev/null +++ b/scripts/follow/main.ts @@ -0,0 +1,7 @@ +import followBack from "./follow.js"; +import unFollowBack from "./unfollow.js"; + +export default function follows() { + unFollowBack(); + followBack(); +} diff --git a/scripts/follow/unfollow.ts b/scripts/follow/unfollow.ts new file mode 100644 index 0000000..1cf7eae --- /dev/null +++ b/scripts/follow/unfollow.ts @@ -0,0 +1,27 @@ +import config from "../../config.js"; +import { meApi } from "types/types.js"; + +export default async function unFollowBack() { + const profile: meApi = await + (await fetch(`https://${config.uwuzu.host}/api/me`, { + method: "POST", + body: JSON.stringify({ + token: config.uwuzu.apiToken, + }) + })).json(); + + profile.followee.forEach(async (followUser: string) => { + if ( + profile.follower[followUser] === undefined || + profile.follower[followUser] === null + ) { + await fetch(`https://${config.uwuzu.host}/api/users/unfollow`, { + method: "POST", + body: JSON.stringify({ + token: config.uwuzu.apiToken, + userId: followUser, + }), + }) + } + }); +} diff --git a/scripts/successExit.ts b/scripts/successExit.ts index 6662212..eacd277 100644 --- a/scripts/successExit.ts +++ b/scripts/successExit.ts @@ -1,29 +1,26 @@ import * as fs from "fs"; -import { format, isAfter } from "date-fns"; -import { parse } from "date-fns/fp"; +import { isBefore } from "date-fns/fp"; import config from "../config.js"; import sendMail from "../src/mailer.js"; -const formatParse = parse(new Date(), "yyyy-MM-dd HH:mm:ss.SSS") - -// 初期化 -if (fs.existsSync("iolog.json") === false) { - fs.writeFileSync("iolog.json", JSON.stringify({ - start: format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"), - stop: "", - }), "utf-8"); -} - export default function successExit() { - const iolog = JSON.parse(fs.readFileSync("iolog.json", "utf-8")); + // 初期化 + if (!fs.existsSync("logs/boot.json")) { + fs.writeFileSync("logs/boot.json", JSON.stringify({ + start: new Date(), + stop: "", + }), "utf-8"); + } + + const iolog = JSON.parse(fs.readFileSync("logs/boot.json", "utf-8")); if (config.emergency.function) { // 前回の終了確認 - const start = formatParse(iolog.start); - const stop = formatParse(iolog.stop); + const start = iolog.start; + const stop = iolog.stop; - if (isAfter(start, stop)) { + if (isBefore(start, stop)) { console.log("前回の終了が適切でない可能性があります"); if (config.emergency.mail.function) { @@ -48,15 +45,15 @@ export default function successExit() { } // 起動時に起動時刻を保存 - iolog.start = format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"); - fs.writeFileSync("iolog.json", JSON.stringify(iolog), "utf-8"); + iolog.start = new Date(); + fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8"); // 終了時に終了時刻を保存 process.on("exit", () => { - const iolog = JSON.parse(fs.readFileSync("iolog.json", "utf-8")); - iolog.stop = format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"); + const iolog = JSON.parse(fs.readFileSync("logs/boot.json", "utf-8")); + iolog.stop = new Date(); - fs.writeFileSync("iolog.json", JSON.stringify(iolog), "utf-8"); + fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8"); }); } diff --git a/scripts/timeNotice.ts b/scripts/timeNotice.ts index b469249..eac5745 100644 --- a/scripts/timeNotice.ts +++ b/scripts/timeNotice.ts @@ -28,11 +28,11 @@ export default async function timeNotice() { } else { // 投稿 const resUeuse = await fetch( - `https://${config.uwuzuServer}/api/ueuse/create`, + `https://${config.uwuzu.host}/api/ueuse/create`, { method: "POST", body: JSON.stringify({ - token: config.apiToken, + token: config.uwuzu.apiToken, text: `${format(new Date(), "HH:mm")}になりました`, }), }, diff --git a/scripts/weatherNotice.ts b/scripts/weatherNotice.ts index ba99c20..1dc5d6c 100644 --- a/scripts/weatherNotice.ts +++ b/scripts/weatherNotice.ts @@ -9,11 +9,11 @@ export default async function weatherNotice() { // 仮投稿 const resUeuse = await fetch( - `https://${config.uwuzuServer}/api/ueuse/create`, + `https://${config.uwuzu.host}/api/ueuse/create`, { method: "POST", body: JSON.stringify({ - token: config.apiToken, + token: config.uwuzu.apiToken, text: ` 本日の天気 ※タイムラインが埋まるため返信に記載しています @@ -103,11 +103,11 @@ export default async function weatherNotice() { // 分割投稿 for (let i = 0; i < splitCount; i++) { const resReply = await fetch( - `https://${config.uwuzuServer}/api/ueuse/create`, + `https://${config.uwuzu.host}/api/ueuse/create`, { method: "POST", body: JSON.stringify({ - token: config.apiToken, + token: config.uwuzu.apiToken, text: weatherResults[i], replyid: ueuseData.uniqid }), diff --git a/types/config.d.ts b/types/config.d.ts index 6e6dd58..a463307 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -3,7 +3,6 @@ interface earthquakeTypes { websocketUrl: string; areasCsvUrl: string; maxScaleMin: number; - rateLimit: number; } interface weatherTypes { @@ -34,13 +33,17 @@ interface emergencyTypes { mail: emergencyMailTypes; } +interface uwuzuTypes { + apiToken: string; + clientToken: string; + host: string; +} + export interface configTypes { time: timeTypes, earthquake: earthquakeTypes; weather: weatherTypes; emergency: emergencyTypes; - - apiToken: string; - uwuzuServer: string; + uwuzu: uwuzuTypes; }