noticeUwuzu v6.0@uwuzu1.5.4

This commit is contained in:
Last2014 2025-07-26 21:28:07 +09:00
parent 3955d91978
commit 724e18ba3b
20 changed files with 268 additions and 120 deletions

2
.gitignore vendored
View File

@ -3,5 +3,3 @@
/node_modules/ /node_modules/
/package-lock.json /package-lock.json
/config.ts /config.ts
*log*

View File

@ -7,21 +7,6 @@ uwuzuで動作するお知らせBOTです。
# 設定 # 設定
examples/config.tsをプロジェクトルートへ移動し各設定を更新してください。 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分割を推奨)
apiTokenBOTアカウントのAPIキー
uwuzuServer使用するuwuzuサーバーのホスト名(uwuzu.netなど)
# サーバー起動 # サーバー起動
``` ```

32
checks/api.ts Normal file
View File

@ -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();
}
}

9
checks/config.ts Normal file
View File

@ -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();
}
}

13
checks/main.ts Normal file
View File

@ -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();
}

59
checks/packages.ts Normal file
View File

@ -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<string> = [];
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<string> = [];
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);
}
}

16
checks/packagesExist.ts Normal file
View File

@ -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);
}
}

38
checks/version.ts Normal file
View File

@ -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);
}
}
}

View File

@ -8,7 +8,7 @@ const config: configTypes = {
stopTimes: { stopTimes: {
start: 23, // 開始 start: 23, // 開始
stop: 6, // 停止 stop: 6, // 停止
} },
}, },
// 地震速報設定 // 地震速報設定
earthquake: { earthquake: {
@ -16,7 +16,6 @@ const config: configTypes = {
websocketUrl: "wss://api.p2pquake.net/v2/ws", // WebSocketのURL websocketUrl: "wss://api.p2pquake.net/v2/ws", // WebSocketのURL
areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", // 対象地域CSVファイルのURL areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", // 対象地域CSVファイルのURL
maxScaleMin: 30, // 地震発生の際の最低震度(10-70) maxScaleMin: 30, // 地震発生の際の最低震度(10-70)
rateLimit: 30, // 地域情報更新のレート制限(分)
}, },
weather: { weather: {
splitCount: 4, // 返信の分割数 splitCount: 4, // 返信の分割数
@ -32,12 +31,14 @@ const config: configTypes = {
user: "mailUser@example.com", // BOTメール送信元 user: "mailUser@example.com", // BOTメール送信元
password: "mailPassword", // SMTPパスワード password: "mailPassword", // SMTPパスワード
secure: false, // SMTPsecure設定 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; export default config;

2
logs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

16
main.ts
View File

@ -1,3 +1,9 @@
// 起動チェック
import Check from "./checks/main.js";
(async () => {
await Check();
})();
// 定期実行読み込み // 定期実行読み込み
import * as cron from "node-cron"; import * as cron from "node-cron";
@ -10,8 +16,8 @@ import earthquakeNotice from "./scripts/earthquakeNotice.js";
import asciiArt from "./scripts/asciiart.js"; import asciiArt from "./scripts/asciiart.js";
asciiArt(); asciiArt();
// フォローバック機能読み込み // フォロー機能読み込み
import followBack from "./scripts/followBack.js"; import follows from "./scripts/follow/main.js";
// 正常終了確認読み込み // 正常終了確認読み込み
import successExit from "./scripts/successExit.js"; import successExit from "./scripts/successExit.js";
@ -23,14 +29,12 @@ earthquakeNotice();
// 時報・フォローバック(毎時) // 時報・フォローバック(毎時)
cron.schedule("0 * * * *", () => { cron.schedule("0 * * * *", () => {
timeNotice(); timeNotice();
followBack(); follows();
}); });
// 天気お知らせ(毎日7:01) // 天気お知らせ(毎日7:01)
cron.schedule("1 7 * * *", () => { cron.schedule("1 7 * * *", () => {
setTimeout(() => { weatherNotice();
weatherNotice();
}, 100);
}); });
// コンソールで表示 // コンソールで表示

View File

@ -1,6 +1,6 @@
{ {
"name": "noticeuwuzu", "name": "notice-uwuzu",
"version": "v5.1.1@uwuzu1.5.4", "version": "v6.0@uwuzu1.5.4",
"description": "uwuzu Notice Bot", "description": "uwuzu Notice Bot",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
@ -30,16 +30,16 @@
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/node": "^24.0.7",
"@types/ws": "^8.18.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"node-cron": "^4.1.1", "node-cron": "^4.1.1",
"nodemailer": "^7.0.4", "nodemailer": "^7.0.4",
"tsx": "^4.20.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"ws": "^8.18.3" "ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.7", "tsx": "^4.20.3"
"@types/ws": "^8.18.1"
} }
} }

View File

@ -1,11 +1,8 @@
import WebSocket from "ws"; import WebSocket from "ws";
import { differenceInMinutes, subMinutes } from "date-fns";
import sendMail from "../src/mailer.js"; import sendMail from "../src/mailer.js";
import config from "../config.js"; import config from "../config.js";
let rateLimit: Date | null = null;
class P2PEarthquakeClient { class P2PEarthquakeClient {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
private reconnectInterval: number = config.earthquake.reconnectTimes; private reconnectInterval: number = config.earthquake.reconnectTimes;
@ -75,10 +72,6 @@ class P2PEarthquakeClient {
console.log("緊急地震速報を受信しました"); console.log("緊急地震速報を受信しました");
this.executeEventFunc(message); this.executeEventFunc(message);
break; break;
case 555: // 地域情報更新情報
console.log("地域情報更新を受信しました");
this.executeEventFunc(message);
break;
default: default:
console.log(`未対応の情報を受信しました(コード: ${message.code})`); console.log(`未対応の情報を受信しました(コード: ${message.code})`);
break; break;
@ -208,7 +201,7 @@ async function event(earthquakeInfo: any): Promise<void> {
let magnitude: string = "マグニチュード:"; let magnitude: string = "マグニチュード:";
if ( if (
earthquakeInfo.earthquake.hypocenter.magnitude !== -1 || earthquakeInfo.earthquake.hypocenter.magnitude != -1 ||
earthquakeInfo.earthquake.hypocenter.magnitude === undefined earthquakeInfo.earthquake.hypocenter.magnitude === undefined
) { ) {
magnitude += "マグニチュードの情報はありません"; magnitude += "マグニチュードの情報はありません";
@ -262,7 +255,7 @@ async function event(earthquakeInfo: any): Promise<void> {
} }
if ( if (
earthquakeInfo.earthquake.maxScale === -1 && earthquakeInfo.earthquake.maxScale == -1 &&
earthquakeInfo.earthquake.maxScale === undefined earthquakeInfo.earthquake.maxScale === undefined
) { ) {
maxScale = "最大震度情報なし"; maxScale = "最大震度情報なし";
@ -346,7 +339,7 @@ async function event(earthquakeInfo: any): Promise<void> {
if ( if (
earthquakeInfo.earthquake.hypocenter.depth !== null || earthquakeInfo.earthquake.hypocenter.depth !== null ||
earthquakeInfo.earthquake.hypocenter.depth !== undefined || earthquakeInfo.earthquake.hypocenter.depth !== undefined ||
earthquakeInfo.earthquake.hypocenter.depth !== -1 earthquakeInfo.earthquake.hypocenter.depth != -1
) { ) {
if (earthquakeInfo.earthquake.hypocenter.depth === 0) { if (earthquakeInfo.earthquake.hypocenter.depth === 0) {
depth = "深さ:ごく浅い"; depth = "深さ:ごく浅い";
@ -361,9 +354,9 @@ async function event(earthquakeInfo: any): Promise<void> {
if( if(
earthquakeInfo.earthquake.hypocenter.magnitude !== null || earthquakeInfo.earthquake.hypocenter.magnitude !== null ||
earthquakeInfo.earthquake.hypocenter.magnitude !== undefined || 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(` ueuse(`
@ -378,49 +371,13 @@ async function event(earthquakeInfo: any): Promise<void> {
${domesticTsunami} ${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<string> = 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) { 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", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: text, text: text,
}), }),
}); });

View File

@ -1,13 +1,13 @@
import type * as types from "types/types"; import type * as types from "types/types";
import config from "../config.js"; import config from "../../config.js";
export default async function followBack() { export default async function followBack() {
console.log("----------------"); console.log("----------------");
// フォロワーを取得 // フォロワーを取得
const resMe = await fetch( const resMe = await fetch(
`https://${config.uwuzuServer}/api/me?token=${config.apiToken}`, `https://${config.uwuzu.host}/api/me?token=${config.uwuzu.apiToken}`,
{ {
method: "GET", method: "GET",
// uwuzu v1.5.4で/api/meのPOSTが死んでいるため簡易的にGET // uwuzu v1.5.4で/api/meのPOSTが死んでいるため簡易的にGET
@ -25,11 +25,11 @@ export default async function followBack() {
const followerItem = followers[i]; const followerItem = followers[i];
const resFollow = await fetch( const resFollow = await fetch(
`https://${config.uwuzuServer}/api/users/follow`, `https://${config.uwuzu.host}/api/users/follow`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
userid: followerItem, userid: followerItem,
}), }),
}, },

7
scripts/follow/main.ts Normal file
View File

@ -0,0 +1,7 @@
import followBack from "./follow.js";
import unFollowBack from "./unfollow.js";
export default function follows() {
unFollowBack();
followBack();
}

View File

@ -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,
}),
})
}
});
}

View File

@ -1,29 +1,26 @@
import * as fs from "fs"; import * as fs from "fs";
import { format, isAfter } from "date-fns"; import { isBefore } from "date-fns/fp";
import { parse } from "date-fns/fp";
import config from "../config.js"; import config from "../config.js";
import sendMail from "../src/mailer.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() { 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) { if (config.emergency.function) {
// 前回の終了確認 // 前回の終了確認
const start = formatParse(iolog.start); const start = iolog.start;
const stop = formatParse(iolog.stop); const stop = iolog.stop;
if (isAfter(start, stop)) { if (isBefore(start, stop)) {
console.log("前回の終了が適切でない可能性があります"); console.log("前回の終了が適切でない可能性があります");
if (config.emergency.mail.function) { 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"); iolog.start = new Date();
fs.writeFileSync("iolog.json", JSON.stringify(iolog), "utf-8"); fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8");
// 終了時に終了時刻を保存 // 終了時に終了時刻を保存
process.on("exit", () => { process.on("exit", () => {
const iolog = JSON.parse(fs.readFileSync("iolog.json", "utf-8")); const iolog = JSON.parse(fs.readFileSync("logs/boot.json", "utf-8"));
iolog.stop = format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"); iolog.stop = new Date();
fs.writeFileSync("iolog.json", JSON.stringify(iolog), "utf-8"); fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8");
}); });
} }

View File

@ -28,11 +28,11 @@ export default async function timeNotice() {
} else { } else {
// 投稿 // 投稿
const resUeuse = await fetch( const resUeuse = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`, `https://${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: `${format(new Date(), "HH:mm")}になりました`, text: `${format(new Date(), "HH:mm")}になりました`,
}), }),
}, },

View File

@ -9,11 +9,11 @@ export default async function weatherNotice() {
// 仮投稿 // 仮投稿
const resUeuse = await fetch( const resUeuse = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`, `https://${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: ` text: `
@ -103,11 +103,11 @@ export default async function weatherNotice() {
// 分割投稿 // 分割投稿
for (let i = 0; i < splitCount; i++) { for (let i = 0; i < splitCount; i++) {
const resReply = await fetch( const resReply = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`, `https://${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: weatherResults[i], text: weatherResults[i],
replyid: ueuseData.uniqid replyid: ueuseData.uniqid
}), }),

11
types/config.d.ts vendored
View File

@ -3,7 +3,6 @@ interface earthquakeTypes {
websocketUrl: string; websocketUrl: string;
areasCsvUrl: string; areasCsvUrl: string;
maxScaleMin: number; maxScaleMin: number;
rateLimit: number;
} }
interface weatherTypes { interface weatherTypes {
@ -34,13 +33,17 @@ interface emergencyTypes {
mail: emergencyMailTypes; mail: emergencyMailTypes;
} }
interface uwuzuTypes {
apiToken: string;
clientToken: string;
host: string;
}
export interface configTypes { export interface configTypes {
time: timeTypes, time: timeTypes,
earthquake: earthquakeTypes; earthquake: earthquakeTypes;
weather: weatherTypes; weather: weatherTypes;
emergency: emergencyTypes; emergency: emergencyTypes;
uwuzu: uwuzuTypes;
apiToken: string;
uwuzuServer: string;
} }