Compare commits

..

19 Commits

Author SHA1 Message Date
last2014 f6f7030d8c APIパス修正・無駄import削除 2025-08-01 22:03:36 +09:00
last2014 a2576f961b APIパスの問題・デバッグ用コメントアウトを修正 2025-08-01 19:53:41 +09:00
last2014 844a9cd058 APIの500問題を修正 2025-08-01 19:39:21 +09:00
last2014 2a30fdf1e9 v7.0.1(LTS)をリリース 2025-08-01 19:09:18 +09:00
last2014 dc67845bc8 v7.0(unconfirmed)をリリース 2025-08-01 18:57:15 +09:00
last2014 50db83cc8c バージョン表記を変更 2025-07-31 22:03:52 +09:00
last2014 d7a5d8a43e v6.5(LTS)をリリース 2025-07-31 22:02:54 +09:00
last2014 1c10aeb9e4 API確認の複数の問題を修正 2025-07-26 21:46:19 +09:00
last2014 081d1eca6e クライアントトークンの?の位置がずれていた問題を修正 2025-07-26 21:38:07 +09:00
last2014 0e9c8f8225 クライアントトークンの収集を任意に・起動時確認の順序を修正 2025-07-26 21:33:55 +09:00
last2014 724e18ba3b noticeUwuzu v6.0@uwuzu1.5.4 2025-07-26 21:28:07 +09:00
last2014 3955d91978 深さ判定に.earthquake.をスキップしてhypocenterを参照していた問題を修正(v5.1.1) 2025-07-08 13:47:10 +09:00
last2014 d28ca6ce4c メールに改行を追加・起動時にバージョンを表示・v5.1へ 2025-07-07 15:24:36 +09:00
last2014 ad0d975a12 fromをメールアドレスに統一(v5.0.3) 2025-07-07 15:11:49 +09:00
last2014 7f07710d23 地震情報でnullではなくundefinedによる存在確認を使用(v5.0.2) 2025-07-07 14:50:42 +09:00
last2014 3b58e7ee2d tsc後用に.jsを追加 2025-07-07 03:19:00 +09:00
last2014 22ec582e0a v5.0 2025-07-06 22:11:59 +09:00
last2014 05194ad7b8 停止時間のBooleanを使いまわしている問題を修正(v4.3.3) 2025-07-06 08:53:02 +09:00
last2014 ef374b1639 レート制限が聞かないのを修正(v4.3.2) 2025-07-05 16:24:59 +09:00
32 changed files with 914 additions and 253 deletions
-2
View File
@@ -3,5 +3,3 @@
/node_modules/
/package-lock.json
/config.ts
log*
+1 -1
View File
@@ -4,7 +4,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+25 -24
View File
@@ -1,36 +1,37 @@
# uwuzuお知らせBOT
# noticeUwuzu
# uwuzuお知らせBOTについて
# Overview
uwuzuで動作するお知らせBOTです。
Automatic notification bot for uwuzu
# 設定
examples/config.tsをプロジェクトルートへ移動し各設定を更新してください。
# Functions
## 設定項目
time.stopTimes.start: 時報休止期間の開始時刻(HH)
time.stopTimes.stop: 時報休止期間の停止時刻(HH)
- Time notification
- Earthquake information notification
- Earthquake Early Warning
- Earthquake occurs
- Tsunami forecast
- Weather notification
- Startup requirements check
- Check package existence
- Required package version check
- Check uwuzu API
- Check the configuration file
- Version change notification
- ASCII art at startup
- Confirm normal completion
earthquake.reconnectTimes:地震情報のWebSocketが切断されたときに自動再接続する時間(ミリ秒)
earthquake.websocketUrl:地震情報のWebSocket接続先URL
earthquake.areasCsvUrl:地域情報のデータベース(CSV)ファイルのURL
earthquake.maxScaleMin: 地震発生投稿の最低震度(10-70)
earthquake.rateLimit: 地域情報更新のレート制限(分)
# Config
An example configuration file is available in `examples/config.ts`.
weather.splitCount:天気お知らせの返信の分割数(4分割を推奨)
# Start the server
apiTokenBOTアカウントのAPIキー
uwuzuServer:使用するuwuzuサーバーのホスト名(uwuzu.netなど)
# サーバー起動
```
```bash
npm install
npm run build
npm run start
```
Recommended Node.js version: v22.16.0
※Node.js・npmがインストールされている必要があります。
# ライセンス
Apache 2.0 License
# License
[Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0)
+5 -5
View File
@@ -1,5 +1,5 @@
# # ##### ####### ### ##### ####### # # # # # # ###### # #
## # # # # # ## # # # # # # # # # # #
# ## # # # # # # ###### # # # # # # # # ## # #
# ## # # # # ## # # # # # # # # # #
# # ##### # ### ##### ####### ###### # # ###### ###### #######
# # ###### ##### ### ###### ####### # # # # # # ###### # #
## # # # # # # # # # # # # # # # # #
# ## # # # # # # ####### # # # # # # # # ## # #
# ## # # # # # # # # # # # # # # #
# # ###### # ### ###### ####### ###### # # ###### ###### ######
+29
View File
@@ -0,0 +1,29 @@
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.error_code !== undefined) {
console.log(styleText("red", "APIトークンあるいはuwuzuサーバーホストが無効です"));
process.exit();
}
if (res.isBot === false) {
setTimeout(() => {
console.log(styleText("yellow", "使用するアカウントでBOTフラグが設定されていません"));
}, 1500);
}
} catch (err) {
console.log(styleText("red", `uwuzuサーバーへ接続できませんでした: ${err}`));
process.exit();
}
}
+9
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
View File
@@ -0,0 +1,13 @@
import PackagesIsExist from "./packagesExist.js";
import PackagesCheck from "./packages.js";
import ConfigCheck from "./config.js";
import APICheck from "./api.js";
import VersionCheck from "./version.js";
export default async function Check() {
PackagesIsExist();
PackagesCheck();
ConfigCheck();
await APICheck();
await VersionCheck();
}
+59
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
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
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);
}
}
}
+19 -4
View File
@@ -8,7 +8,7 @@ const config: configTypes = {
stopTimes: {
start: 23, // 開始
stop: 6, // 停止
}
},
},
// 地震速報設定
earthquake: {
@@ -16,14 +16,29 @@ 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, // 返信の分割数
},
apiToken: "TOKEN_EXAMPLE", // BOTアカウントのAPIトークン
uwuzuServer: "uwuzu.example.com", // uwuzuのサーバー
// 緊急時設定
emergency: {
function: true, // 緊急時のコンソール表示
mail: {
function: true, // 緊急時のメール送信
host: "smtp.example.com", // SMTPサーバー
port: 465, // SMTPポート
user: "mailUser@example.com", // BOTメール送信元
password: "mailPassword", // SMTPパスワード
secure: false, // SMTPsecure設定
to: "admin@noticeuwuzu.example.com", // 緊急時メール送信先(配列可)
},
},
uwuzu: {
apiToken: "TOKEN_EXAMPLE",
clientToken: "TOKEN_EXAMPLE",
host: "uwuzu.example.com",
},
};
export default config;
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+20 -12
View File
@@ -1,33 +1,41 @@
// 起動チェック
import Check from "./checks/main.js";
(async () => {
await Check();
})();
// 定期実行読み込み
import * as cron from "node-cron";
// 機能読み込み
import timeNotice from "./scripts/timeNotice.js";
import weatherNotice from "./scripts/weatherNotice.js";
import { weatherNotice } from "./scripts/weatherNotice.js";
import earthquakeNotice from "./scripts/earthquakeNotice.js";
import Commands from "./scripts/commands/main.js";
// アスキーアート読み込み
// その他機能
import asciiArt from "./scripts/asciiart.js";
asciiArt();
// フォローバック機能読み込み
import followBack from "./scripts/followBack.js";
import successExit from "./scripts/successExit.js";
successExit();
// 地震情報観測開始
earthquakeNotice();
// 時報・フォローバック(毎時)
// 時報(1時間/1回)
cron.schedule("0 * * * *", () => {
timeNotice();
followBack();
});
// 天気お知らせ(毎日7:01)
cron.schedule("1 7 * * *", () => {
setTimeout(() => {
// コマンド(10分/1回)
cron.schedule('*/10 * * * *', () => {
Commands();
});
// 天気お知らせ(毎日7:00)
cron.schedule("0 7 * * *", () => {
weatherNotice();
}, 100);
});
// コンソールで表示
// 起動表示
console.log("BOTサーバーが起動しました");
+12 -7
View File
@@ -1,12 +1,14 @@
{
"name": "noticeuwuzu",
"version": "v4.3.1@uwuzu1.5.4",
"description": "uwuzu Notice Bot",
"name": "notice-uwuzu",
"version": "v7.1.1(LTS)@uwuzu1.5.4",
"description": "Notice Bot for uwuzu",
"main": "dist/main.js",
"scripts": {
"start": "node .",
"build": "tsc",
"dev": "tsx main.ts"
"main": "tsc && node .",
"dev": "tsx main.ts",
"clean": "tsc && node dist/scripts/clean/main.js"
},
"keywords": [
"uwuzu",
@@ -27,16 +29,19 @@
"dependencies": {
"@types/date-fns": "^2.5.3",
"@types/dotenv": "^6.1.1",
"@types/node": "^24.0.7",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17",
"@types/ws": "^8.18.1",
"child_process": "^1.0.2",
"date-fns": "^4.1.0",
"fs": "^0.0.1-security",
"node-cron": "^4.1.1",
"tsx": "^4.20.3",
"nodemailer": "^7.0.4",
"typescript": "^5.8.3",
"ws": "^8.18.3"
},
"devDependencies": {
"@types/node": "^24.0.7",
"@types/ws": "^8.18.1"
"tsx": "^4.20.3"
}
}
+4 -1
View File
@@ -1,5 +1,8 @@
import * as fs from "fs";
const version = JSON.parse(fs.readFileSync("package.json", "utf-8")).version;
export default function asciiArt() {
console.log(fs.readFileSync("asciiart.txt", "utf-8"));
console.log(fs.readFileSync("asciiart.txt", "utf-8").replace(/(\r?\n)$/, ''));
console.log(`${version}\n`);
}
+16
View File
@@ -0,0 +1,16 @@
import { unlinkSync } from "fs";
export default function logsDelete() {
console.log("ログを削除中します");
try {
unlinkSync("logs/boot.json");
unlinkSync("logs/version.txt");
console.log("ログファイルを削除しました");
console.log("----------------");
} catch (err) {
console.log("ログファイルの削除中にエラーが発生しました:\n", err);
process.exit();
}
}
+7
View File
@@ -0,0 +1,7 @@
import logsDelete from "./logsDel.js";
import packageLockJsonDelete from "./packageLockDel.js";
import npmInstall from "./npmInstall.js";
logsDelete();
packageLockJsonDelete();
npmInstall();
+23
View File
@@ -0,0 +1,23 @@
import { execSync } from "child_process";
export default function npmInstall() {
// npm install実行
console.log("npm installを実行します");
try {
const npmInstall = execSync("npm install");
console.log("----");
console.log("$ npm install");
console.log(npmInstall.toString());
console.log("----");
console.log("npm installを実行しました");
} catch (err) {
console.log("npm installの実行中にエラーが発生しました:\n", err);
process.exit();
}
console.log("初期化が完了しました");
process.exit();
}
+15
View File
@@ -0,0 +1,15 @@
import { unlinkSync } from "fs";
export default function packageLockJsonDelete() {
console.log("package-lock.jsonを削除します");
try {
unlinkSync("package-lock.json");
console.log("package-lock.jsonを削除しました");
console.log("----------------");
} catch (err) {
console.log("package-lock.jsonの削除中にエラーが発生しました:\n", err);
process.exit();
}
}
+32
View File
@@ -0,0 +1,32 @@
import { ueuse } from "types/types.js";
import config from "../../config.js";
export default async function Follow(data: ueuse) {
const followReq = await fetch(`https://${config.uwuzu.host}/api/users/follow`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
userid: data.account.userid,
}),
});
const followRes = await followReq.json();
console.log("フォロー: ", followRes);
const noticeReq = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: `
${data.account.username}さんをフォローしました
`,
replyid: data.uniqid,
}),
});
const noticeRes = await noticeReq.json();
console.log("フォロー通知: ", noticeRes);
}
+116
View File
@@ -0,0 +1,116 @@
import * as fs from "fs";
import config from "../../config.js";
import type { ueuse } from "types/types.js";
const initialFile: Array<string> = [];
// コマンド読み込み
import Follow from "./follow.js";
import UnFollow from "./unfollow.js";
import Weather from "./weather.js";
// 初期化
if (!fs.existsSync("logs/alreadyCommands.json")) {
fs.writeFileSync(
"logs/alreadyCommands.json",
JSON.stringify(initialFile),
"utf-8",
);
}
// 対応済みユーズ一覧
const alreadyCommands: Array<string> = JSON.parse(fs.readFileSync("logs/alreadyCommands.json", "utf-8"));
function cutAfterChar(str: string, char: string) {
const index = str.indexOf(char);
if (index === -1) {
return "";
}
return str.substring(index + 1);
}
async function Reply(text: string, reply: string) {
const req = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: text,
replyid: reply,
}),
});
const res = await req.json();
return res;
}
function alreadyAdd(data: string) {
alreadyCommands[alreadyCommands.length] = data;
fs.writeFileSync(
"logs/alreadyCommands.json",
JSON.stringify(alreadyCommands),
"utf-8",
);
}
export default async function Commands() {
const mentionsReq = await fetch(
`https://${config.uwuzu.host}/api/ueuse/mentions`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
}
);
const mentions: Array<ueuse> = await mentionsReq.json();
console.log("----------------");
console.log("コマンド処理");
for (let i = 0; i < mentions.length; i++) {
const data = mentions[i];
// 除外ユーズ
if (alreadyCommands.indexOf(data.uniqid) !== -1) {
break;
}
if (data.text.charAt(0) === "!") {
break;
}
// コマンド処理
console.log("--------");
const commandName = cutAfterChar(data.text, "/");
switch (commandName) {
case "follow":
alreadyAdd(data.uniqid);
Follow(data);
break;
case "unfollow":
alreadyAdd(data.uniqid);
UnFollow(data);
break;
case "weather":
alreadyAdd(data.uniqid);
Weather(data);
break;
default:
alreadyAdd(data.uniqid);
const reply = await Reply(`
不明なコマンドです。
コマンド実行を除外する場合は1文字目に\`!\`を入れてください。
`, data.uniqid);
console.log("未対応コマンド: ", reply);
break;
}
}
}
+31
View File
@@ -0,0 +1,31 @@
import { ueuse } from "types/types.js";
import config from "../../config.js";
export default async function UnFollow(data: ueuse) {
const unfollowReq = await fetch(`https://${config.uwuzu.host}/api/users/unfollow`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
userid: data.account.userid,
}),
});
const unfollowRes = await unfollowReq.json();
console.log("フォロー解除: ", unfollowRes);
const noticeReq = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: `
${data.account.username}さんをフォロー解除しました
`,
replyid: data.uniqid,
}),
});
const noticeRes = await noticeReq.json();
console.log("フォロー解除通知: ", noticeRes);
}
+6
View File
@@ -0,0 +1,6 @@
import { weatherReply } from "../weatherNotice.js";
import { ueuse } from "types/types.js";
export default function Weather(data: ueuse) {
weatherReply(data.uniqid);
}
+221 -116
View File
@@ -1,10 +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;
@@ -39,8 +37,8 @@ class P2PEarthquakeClient {
try {
const message = JSON.parse(data.toString());
this.handleMessage(message);
} catch (error) {
console.error("メッセージのパースに失敗:", error);
} catch (err) {
console.error(`メッセージのパースでエラーが発生: ${err}`);
}
});
@@ -65,27 +63,18 @@ class P2PEarthquakeClient {
private handleMessage(message: any): void {
console.log("----------------");
switch (message.code) {
case 551: // 地震情報
console.log("地震情報を受信しました");
this.executeEventFunc(message);
break;
case 554: // 緊急地震速報
console.log("緊急地震速報を受信しました");
this.executeEventFunc(message);
break;
case 555: // 地域情報更新情報
console.log("地域情報更新を受信しました");
this.executeEventFunc(message);
break;
default:
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
break;
}
}
const supportCode: Array<number> = [
551,
552,
556,
];
private executeEventFunc(earthquakeInfo: any): void {
event(earthquakeInfo);
if (supportCode.indexOf(message.code) !== -1) {
event(message);
} else {
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
console.log(`受信メッセージ:${message}`);
}
}
private scheduleReconnect(): void {
@@ -93,6 +82,7 @@ class P2PEarthquakeClient {
console.log("地震情報サーバーから切断されました");
console.log(`${this.reconnectInterval / 1000}秒後に再接続を試みます`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
@@ -102,7 +92,7 @@ class P2PEarthquakeClient {
private setupCleanup(): void {
const cleanup = () => {
this.stop();
process.exit(0);
process.exit();
};
process.on("SIGINT", cleanup);
@@ -122,7 +112,7 @@ class P2PEarthquakeClient {
}
}
// 地名オブジェクトマッピング
// 地名マッピング
async function areaMap(): Promise<Record<number, string>> {
const res = await fetch(config.earthquake.areasCsvUrl);
@@ -147,41 +137,48 @@ async function areaMap(): Promise<Record<number, string>> {
// 情報受信
async function event(earthquakeInfo: any): Promise<void> {
console.log(`受信データ${JSON.stringify(earthquakeInfo)}`);
console.log(`受信メッセージ${earthquakeInfo}`);
// ----処理----
// 緊急地震速報の場合
if (earthquakeInfo.code === 554) {
if (earthquakeInfo.code === 556) {
console.log("緊急地震速報を受信しました");
// 地震詳細
let descriptionEarthquake: string = "";
if (earthquakeInfo.earthquake.description !== "") {
if (
earthquakeInfo.earthquake.description !== "" ||
earthquakeInfo.earthquake.description !== undefined
) {
descriptionEarthquake = `この地震について:${earthquakeInfo.earthquake.description}`;
}
// 発令詳細
let description: string = "";
if (earthquakeInfo.comments.freeFormComment !== "") {
if (
earthquakeInfo.comments.freeFormComment !== "" ||
earthquakeInfo.comments.freeFormComment !== undefined
) {
description = `この発令について:${earthquakeInfo.comments.freeFormComment}`;
}
// テスト・訓練
let test: string = "";
if (earthquakeInfo.test) {
if (earthquakeInfo.test !== undefined) {
test = "この情報にテスト・訓練かの情報はありません";
} else if (earthquakeInfo.test) {
test = "これはテスト、あるいは訓練です";
} else if (earthquakeInfo.test === false) {
test = "これはテスト・訓練ではありません";
} else {
test = "この情報にテスト・訓練かの情報はありません";
}
// 対象地域
let areas: string = "";
if (earthquakeInfo.areas !== null) {
if (earthquakeInfo.areas !== undefined) {
const areaNames: Array<string> = Array.from(
new Set(
earthquakeInfo.areas.map((point: any) => point.name).filter(Boolean),
@@ -201,8 +198,8 @@ async function event(earthquakeInfo: any): Promise<void> {
let magnitude: string = "マグニチュード:";
if (
earthquakeInfo.earthquake.hypocenter.magnitude !== -1 ||
earthquakeInfo.earthquake.hypocenter.magnitude === null
earthquakeInfo.earthquake.hypocenter.magnitude != -1 ||
earthquakeInfo.earthquake.hypocenter.magnitude === undefined
) {
magnitude += "マグニチュードの情報はありません";
} else {
@@ -223,63 +220,94 @@ async function event(earthquakeInfo: any): Promise<void> {
// 地震情報
else if (earthquakeInfo.code === 551) {
console.log("地震発生情報を受信しました");
if (
earthquakeInfo.earthquake.maxScale !== undefined &&
earthquakeInfo.earthquake.maxScale < config.earthquake.maxScaleMin
) {
console.log("最低震度に満たしていないため投稿されませんでした");
return;
}
// 国内津波
let domesticTsunami;
if (earthquakeInfo.earthquake.domesticTsunami === "None") {
domesticTsunami = "この地震による国内の津波の心配はありません";
} else if (earthquakeInfo.earthquake.domesticTsunami === "Unknown") {
const TsunamiMessages = {
"None": "この地震による国内の津波の心配はありません",
"Unknown": "この地震による国内の津波情報はありません",
"Checking": "この地震による国内の津波情報を調査中です",
"NonEffective": "この地震による国内の津波影響は若干の海面変動が予想されますが被害の心配はありません",
"Watch": "この地震により国内で津波注意報が発令しています",
"Warning": "この地震による国内の津波予報があります",
} as { [key: string]: string };
if (earthquakeInfo.earthquake.domesticTsunami === undefined) {
domesticTsunami = "この地震による国内の津波情報はありません";
} else if (earthquakeInfo.earthquake.domesticTsunami === "Checking") {
domesticTsunami = "この地震による国内の津波情報を調査中です";
} else if (earthquakeInfo.earthquake.domesticTsunami === "NonEffective") {
domesticTsunami =
"この地震による国内の津波影響は若干の海面変動が予想されますが被害の心配はありません";
} else if (earthquakeInfo.earthquake.domesticTsunami === "Watch") {
domesticTsunami = "この地震により国内で津波注意報が発令しています";
} else if (earthquakeInfo.earthquake.domesticTsunami === "Warning") {
domesticTsunami = "この地震による国内の津波予報があります";
} else {
domesticTsunami = "この地震による国内の津波情報はありません";
domesticTsunami = TsunamiMessages[earthquakeInfo.earthquake.domesticTsunami];
}
// 最大震度
let maxScale: string = "最大深度:";
if (earthquakeInfo.earthquake.maxScale < config.earthquake.maxScaleMin) {
console.log("最低震度に満たしていないため投稿されませんでした");
return;
}
const maxScales = {
10: "震度1",
20: "震度2",
30: "震度3",
40: "震度4",
45: "震度5弱",
50: "震度5強",
55: "震度6弱",
60: "震度6強",
70: "震度7",
} as { [key: number]: string };
if (
earthquakeInfo.earthquake.maxScale === -1 ||
earthquakeInfo.earthquake.maxScale === null
earthquakeInfo.earthquake.maxScale == -1 ||
earthquakeInfo.earthquake.maxScale === undefined
) {
maxScale = "最大震度情報なし";
} else if (earthquakeInfo.earthquake.maxScale === 10) {
maxScale += "震度1";
} else if (earthquakeInfo.earthquake.maxScale === 20) {
maxScale += "震度2";
} else if (earthquakeInfo.earthquake.maxScale === 30) {
maxScale += "震度3";
} else if (earthquakeInfo.earthquake.maxScale === 40) {
maxScale += "震度4";
} else if (earthquakeInfo.earthquake.maxScale === 45) {
maxScale += "震度5弱";
} else if (earthquakeInfo.earthquake.maxScale === 50) {
maxScale += "震度5強";
} else if (earthquakeInfo.earthquake.maxScale === 55) {
maxScale += "震度6弱";
} else if (earthquakeInfo.earthquake.maxScale === 60) {
maxScale += "震度6強";
} else if (earthquakeInfo.earthquake.maxScale === 70) {
maxScale += "震度7";
maxScale = "最大震度:不明";
} else {
maxScale = `最大震度:${maxScales[earthquakeInfo.earthquake.maxScale]}`;
}
// 警告
if (
earthquakeInfo.earthquake.maxScale !== undefined &&
earthquakeInfo.earthquake.maxScale >= 60 &&
config.emergency.function
) {
console.log("----------------");
console.log("震度6強以上の地震を受信しました");
console.log("サーバーがダウンする可能性があります");
// メール送信
if (config.emergency.function) {
sendMail({
to: config.emergency.mail.to,
subject: "【警告】震度6強以上の地震を受信しました",
text: `
※noticeUwuzu自動送信によるメールです
【警告】
BOT管理者さん、noticeUwuzu自動送信メールです。
震度6強以上の地震を受信したため警告メールが送信されました。
物理、システム的にサーバーがダウンする可能性があります。
ご自身の身をお守りください。
`
});
console.log("管理者へ警告メールを送信しました");
}
console.log("----------------");
}
// 対象地域
let areas: string = "";
let areas;
if (earthquakeInfo.points !== null) {
if (earthquakeInfo.points !== undefined) {
const areaNames: Array<string> = Array.from(
new Set(
earthquakeInfo.points.map((point: any) => point.addr).filter(Boolean),
@@ -289,34 +317,43 @@ async function event(earthquakeInfo: any): Promise<void> {
}
// 詳細
let description: string = "";
let description;
if (earthquakeInfo.comments.freeFormComment !== "") {
if (
earthquakeInfo.comments.freeFormComment !== "" &&
earthquakeInfo.comments.freeFormComment !== undefined
) {
description = `この地震について:${earthquakeInfo.comments.freeFormComment}`;
}
// 深さ
let depth: string = "";
let depth;
if (
earthquakeInfo.earthquake.hypocenter.depth !== null ||
earthquakeInfo.earthquake.hypocenter.depth !== -1
earthquakeInfo.earthquake.hypocenter.depth !== undefined
) {
if (earthquakeInfo.earthquake.hypocenter.depth === 0) {
depth = "深さ:ごく浅い";
} else if (earthquakeInfo.earthquake.hypocenter.depth === -1) {
depth = "深さ:不明";
} else {
depth = `深さ:${earthquakeInfo.hypocenter.depth}km`;
depth = `深さ:${String(earthquakeInfo.earthquake.hypocenter.depth)}km`;
}
}
// マグニチュード
let magnitude: string = "";
let magnitude;
if(
earthquakeInfo.earthquake.hypocenter.magnitude !== null ||
earthquakeInfo.earthquake.hypocenter.magnitude !== -1
earthquakeInfo.earthquake.hypocenter.magnitude !== undefined
) {
magnitude = `マグニチュード:${earthquakeInfo.earthquake.hypocenter.magnitude}`;
if (earthquakeInfo.earthquake.hypocenter.magnitude === -1) {
depth = "マグニチュード:不明";
} else {
magnitude = `マグニチュード:M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
}
}
ueuse(`
@@ -330,50 +367,118 @@ async function event(earthquakeInfo: any): Promise<void> {
${areas}
国内の津波:${domesticTsunami}
`);
}
} else if (earthquakeInfo.code === 552) {
console.log("津波予報情報を受信しました");
// 地域情報更新の場合
else if (earthquakeInfo.code === 555) {
if (rateLimit === null) {
rateLimit = subMinutes(new Date(), config.earthquake.rateLimit + 5);
}
// 対象地域マッピング
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) {
// 予報取り消し
if (earthquakeInfo.cancelled) {
ueuse(`
==地震情報==
地域情報更新
津波予報
※津波予報が取り消されました※
時刻:${earthquakeInfo.time}
対象地域:${areas}
`);
rateLimit = new Date();
} else {
console.log("レート制限に満たしていないため投稿されませんでした");
return;
}
let result: string = `
==地震情報==
【津波予報】
時刻:${earthquakeInfo.time}
\n
`;
for (let i = 0; i < earthquakeInfo.areas.length; i++) {
const data = earthquakeInfo.areas[i];
// 種類
const gradeMessages = {
"MajorWarning": "大津波警報",
"Warning": "津波警報",
"Watch": "津波注意報",
"Unknown": "不明",
} as { [key: string]: string };
let grade;
if (data.grade === undefined) {
grade = "予報種類:不明";
} else {
grade = `予報種類:${gradeMessages[data.grade]}`;
}
// 直後襲来
let immediate;
if (data.immediate === undefined) {
immediate = "津波の襲来が直後かの情報がありません";
} else if (data.immediate) {
immediate = "### 津波が直ちに襲来します";
} else if (!data.immediate) {
immediate = "津波は直ちには襲来しません";
}
// 第1波
let firstHeight;
if (data.firstHeight === undefined) {
firstHeight = "第1波の情報がありません";
} else if (
data.firstHeight.arrivalTime === undefined &&
data.firstHeight.condition === undefined
) {
firstHeight = "第1波の情報がありません";
} else {
let arrivalTime;
if (data.arrivalTime === undefined) {
arrivalTime = "不明";
} else {
arrivalTime = data.arrivalTime;
}
let condition;
if (data.condition === undefined) {
condition = "不明";
} else {
condition = data.condition;
}
firstHeight = `
第1波到達予想時刻:${arrivalTime}
第1波の状態:${condition}
`
}
// 予想高さ
let maxHeight;
if (data.maxHeight.description === undefined) {
maxHeight = "津波の高さ(予想):不明";
} else {
maxHeight = `津波の高さ(予想)${data.maxHeight.description}`;
}
result = `
${data.name}
${grade}
${immediate}
${firstHeight}
${maxHeight}\n\n
`;
}
ueuse(result);
}
}
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,
}),
});
-42
View File
@@ -1,42 +0,0 @@
import type * as types from "types/types";
import config from "../config.js";
export default async function followBack() {
console.log("----------------");
// フォロワーを取得
const resMe = await fetch(
`https://${config.uwuzuServer}/api/me?token=${config.apiToken}`,
{
method: "GET",
// uwuzu v1.5.4で/api/meのPOSTが死んでいるため簡易的にGET
},
);
const meData: types.meApi = await resMe.json();
console.log(`BOTプロフィール:${meData}`);
const followers: Array<string> = meData.follower;
// フォロー
for (let i = 0; i < followers.length; i++) {
const followerItem = followers[i];
const resFollow = await fetch(
`https://${config.uwuzuServer}/api/users/follow`,
{
method: "POST",
body: JSON.stringify({
token: config.apiToken,
userid: followerItem,
}),
},
);
const followData: types.followApi = await resFollow.json();
console.log(`フォロー:${JSON.stringify(followData)}`);
}
}
+60
View File
@@ -0,0 +1,60 @@
import * as fs from "fs";
import { isBefore } from "date-fns/fp";
import config from "../config.js";
import sendMail from "../src/mailer.js";
export default function successExit() {
// 初期化
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 = iolog.start;
const stop = iolog.stop;
if (isBefore(start, stop)) {
console.log("前回の終了が適切でない可能性があります");
if (config.emergency.mail.function) {
sendMail({
to: config.emergency.mail.to,
subject: "【警告】前回終了が不適切な可能性",
text: `
※noticeUwuzu自動送信によるメールです。
【警告】
BOT管理者さん、noticeUwuzu自動送信メールです。
BOTの前回終了で不適切なデータを検出しました。
これは適切な終了時にはデータを残しデータがない場合に送信されます。
電源を強制的に遮断するなどの行為による可能性があります。
その場合は今後やめ、OSからのシャットダウンを使用してください。
BOTのプログラムが破損していないかご確認ください。
`
});
}
console.log("----------------");
}
}
// 起動時に起動時刻を保存
iolog.start = new Date();
fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8");
// 終了時に終了時刻を保存
process.on("exit", () => {
const iolog = JSON.parse(fs.readFileSync("logs/boot.json", "utf-8"));
iolog.stop = new Date();
fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8");
});
}
successExit();
+20 -22
View File
@@ -3,39 +3,36 @@ import { format } from "date-fns";
import type * as types from "types/types";
import config from "../config.js";
// 停止時間
// 時刻取得
const start = config.time.stopTimes.start;
const stop = config.time.stopTimes.stop;
// 現在の時間を取得
const nowHour = new Date().getHours();
// 停止時刻内かどうかの判定
let inRange: boolean = false;
if (start < stop) {
inRange = nowHour >= start && nowHour < stop;
} else {
inRange = nowHour >= start || nowHour < stop;
}
export default async function timeNotice() {
console.log("----------------");
// 停止時間
// 時刻取得
const start = config.time.stopTimes.start;
const stop = config.time.stopTimes.stop;
// 現在の時間を取得
const nowHour = new Date().getHours();
// 停止時刻内かどうかの判定
let inRange: boolean = false;
if (start < stop) {
inRange = nowHour >= start && nowHour < stop;
} else {
inRange = nowHour >= start || nowHour < stop;
}
if (inRange) {
console.log("----------------");
console.log("時報休止期間のため投稿されませんでした");
return;
} 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")}になりました`,
}),
},
@@ -43,6 +40,7 @@ export default async function timeNotice() {
const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log("----------------");
console.log(`時報投稿:${JSON.stringify(ueuseData)}`);
}
}
+10 -7
View File
@@ -4,16 +4,16 @@ import type * as types from "types/types.js";
import config from "../config.js";
export default async function weatherNotice() {
export async function weatherNotice() {
console.log("----------------");
// 仮投稿
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: `
本日の天気
※タイムラインが埋まるため返信に記載しています
@@ -26,7 +26,10 @@ export default async function weatherNotice() {
console.log(`天気仮投稿:${JSON.stringify(ueuseData)}`);
weatherReply(ueuseData.uniqid);
}
export async function weatherReply(uniqid: string) {
// インデックス
const splitCount = config.weather.splitCount;
const total = cityList.length;
@@ -103,19 +106,19 @@ 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
replyid: uniqid,
}),
},
);
const replyData: types.ueuseCreateApi = await resReply.json();
console.log(`天気投稿${JSON.stringify(replyData)}`);
console.log(`天気返信${JSON.stringify(replyData)}`);
}
}
+52
View File
@@ -0,0 +1,52 @@
import config from "../config.js";
import * as nodemailer from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport";
export interface EmailMessage {
to: string | string[];
subject: string;
text?: string;
html?: string;
}
async function createTransporter() {
const transporter = nodemailer.createTransport({
host: config.emergency.mail.host,
port: config.emergency.mail.port,
secure: config.emergency.mail.secure,
auth: {
user: config.emergency.mail.user,
pass: config.emergency.mail.password,
},
} as SMTPTransport.Options);
// 接続テスト
try {
await transporter.verify();
console.log("SMTPサーバーに接続できました");
} catch (error) {
console.error("SMTP接続テストに失敗:", error);
throw error;
}
return transporter;
}
export default async function sendMail(message: EmailMessage): Promise<void> {
try {
const transporter = await createTransporter();
await transporter.sendMail({
from: config.emergency.mail.user,
to: Array.isArray(message.to) ? message.to.join(",") : message.to,
subject: message.subject,
text: message.text,
html: message.html,
});
console.log("メール送信成功");
} catch (error) {
console.error("メール送信に失敗しました:", error);
throw error;
}
}
+5 -4
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2022",
"module": "ES2022",
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"esModuleInterop": true,
@@ -17,6 +17,7 @@
"paths": {
"ws": ["./node_modules/ws/index.js"],
"@types/ws": ["./node_modules/@types/ws/index.d.ts"]
}
}
},
"removeComments": true,
},
}
+23 -3
View File
@@ -3,7 +3,6 @@ interface earthquakeTypes {
websocketUrl: string;
areasCsvUrl: string;
maxScaleMin: number;
rateLimit: number;
}
interface weatherTypes {
@@ -19,11 +18,32 @@ interface timeTypes {
stopTimes: stopsTypes;
}
interface emergencyMailTypes {
function: Boolean;
host: string | undefined;
port: number;
user: string;
password: string;
secure: Boolean;
to: string;
}
interface emergencyTypes {
function: Boolean;
mail: emergencyMailTypes;
}
interface uwuzuTypes {
apiToken: string;
clientToken?: string;
host: string;
}
export interface configTypes {
time: timeTypes,
earthquake: earthquakeTypes;
weather: weatherTypes;
apiToken: string;
uwuzuServer: string;
emergency: emergencyTypes;
uwuzu: uwuzuTypes;
}
+24 -2
View File
@@ -12,9 +12,9 @@ export interface meApi {
user_icon: string;
user_header: string;
registered_date: string;
followee: Array;
followee: Array<string>;
followee_cnt: number;
follower: Array;
follower: Array<string>;
follower_cnt: number;
ueuse_cnt: number;
isBot: Boolean;
@@ -23,6 +23,28 @@ export interface meApi {
language: String;
}
export interface ueuse {
uniqid: string;
text: string;
account: {
username: string;
userid: string;
user_icon: string;
user_header: string;
};
photo1: Base64URLString;
photo2: Base64URLString;
photo3: Base64URLString;
photo4: Base64URLString;
video1: Base64URLString;
favorite: Array<string>;
favorite_cnt: string;
datetime: string;
abi: string;
abidatetime: string;
nsfw: boolean;
}
export interface ueuseCreateApi {
uniqid: string;
userid: string;