Compare commits

...

37 Commits

Author SHA1 Message Date
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
last2014 563d70aacd 地域情報更新のレート時刻初期化を修正(v4.3.1) 2025-07-05 16:18:02 +09:00
last2014 568ae7abf6 デバッグ表示を分かりやすく2(v4.3) 2025-07-05 14:06:37 +09:00
last2014 c3ab3a8456 時報休止期間を修正・package.jsonの情報を修正・v4.2 2025-07-05 11:46:43 +09:00
last2014 678f4aa1b1 誤字を修正・デバッグ表示の--線の位置を変更・v4.1.2 2025-07-04 21:55:13 +09:00
last2014 a96c3f0e4f サーバー起動時の時刻を使用した上HH:mm以外のフォーマットで時報をしている問題を修正(v4.1.1) 2025-07-04 21:44:54 +09:00
last2014 53ba26f417 デバッグ用の情報を分かりやすく・v4.1 2025-07-04 20:07:34 +09:00
last2014 6b68b6e1b5 地域情報更新のレート制限がnullで初期化されていない(v4.0.1) 2025-07-04 19:17:31 +09:00
last2014 e3451323f3 時報
の停止期間設定機能を追加・地震発生お知らせにマグニチュード/深さを追加・天気お知らせに%が2つ付く問題を修正・地域情報更新が正常に発信されない問題を修正・地域情報更新のレート制限のデフォルト設定を30分に一回に・v4.0
2025-07-04 19:07:05 +09:00
last2014 8be3684a78 <>のミスを修正(v3.5.6) 2025-07-03 15:02:00 +09:00
last2014 600626d071 v3.5.5 2025-07-03 07:57:08 +09:00
last2014 5c0c34bbb4 ゆずねっと向け早急対処 2025-07-03 07:56:57 +09:00
last2014 88bd2e88a5 緊急地震速報にも反映→v3.5.3 2025-07-02 16:49:06 +09:00
last2014 7c7dfdcf3c examples/config.tsにREADMEを参照する旨をコメント・v3.5.2へ 2025-07-02 16:44:18 +09:00
last2014 dd70fe5494 文字列を参照していた問題を修正 2025-07-02 16:42:06 +09:00
last2014 73091bb92f .filterによるクラッシュを修正 2025-07-01 18:54:02 +09:00
last2014 6c69ce80ef v3.5.1へ・ライセンス追加・.env.exampleを削除 2025-07-01 17:10:52 +09:00
last2014 7e3c4840d3 dotenvを廃止しconfig.tsへ・天気の分割数をconfigで変更できるように・デバッグ用のサンプルログを.gitignoreに追加・地震情報解析の修正 2025-07-01 16:59:45 +09:00
last2014 007efd85a8 v3.5 2025-07-01 07:46:38 +09:00
last2014 2689dfd777 地震速報の情報を増量 2025-07-01 07:44:44 +09:00
last2014 909258f421 v3.0へ 2025-07-01 00:02:06 +09:00
last2014 de3c970712 地震情報を追加(最低限情報) 2025-07-01 00:01:35 +09:00
last2014 c5bd22600a 天気機能を最適化 2025-06-30 12:24:03 +09:00
last2014 c4d2990118 天気お知らせを修正 2025-06-30 10:47:56 +09:00
last2014 5bd251f6eb v2.0 2025-06-29 18:56:27 +09:00
last2014 761af6eafc console.log()削除 2025-06-28 12:59:30 +09:00
29 changed files with 1230 additions and 185 deletions
-2
View File
@@ -1,2 +0,0 @@
TOKEN=
SERVER=
+2 -2
View File
@@ -1,5 +1,5 @@
/dist/
/.env
/.env.local
/.env*
/node_modules/
/package-lock.json
/config.ts
+13
View File
@@ -0,0 +1,13 @@
Copyright 2025 Last2014
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+10 -8
View File
@@ -1,14 +1,11 @@
# uwuzu時報BOT
# uwuzuお知らせBOT
# なにこれ
# uwuzuお知らせBOTについて
uwuzuの時報BOT。
1時間おきに投稿する。
タイムゾーンはサーバー依存。
uwuzuで動作するお知らせBOTです
# .envは?
.env.example見て
# 設定
examples/config.tsをプロジェクトルートへ移動し各設定を更新してください。
# サーバー起動
@@ -17,3 +14,8 @@ npm install
npm run build
npm run start
```
※Node.js・npmがインストールされている必要があります。
# ライセンス
Apache 2.0 License
+5
View File
@@ -0,0 +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.userid) {
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);
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import type { configTypes } from "types/config";
// READMEの設定項目を参照
const config: configTypes = {
// 時報設定
time: {
// 時報休止期間
stopTimes: {
start: 23, // 開始
stop: 6, // 停止
},
},
// 地震速報設定
earthquake: {
reconnectTimes: 5000, // 再接続時間(ミリ秒)
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)
},
weather: {
splitCount: 4, // 返信の分割数
},
// 緊急時設定
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
+31 -55
View File
@@ -1,65 +1,41 @@
// 起動チェック
import Check from "./checks/main.js";
(async () => {
await Check();
})();
// 定期実行読み込み
import * as cron from "node-cron";
import * as dotenv from "dotenv";
import { format } from "date-fns";
import type * as type from "./types";
// 機能読み込み
import timeNotice from "./scripts/timeNotice.js";
import weatherNotice from "./scripts/weatherNotice.js";
import earthquakeNotice from "./scripts/earthquakeNotice.js";
dotenv.config();
// アスキーアート読み込み
import asciiArt from "./scripts/asciiart.js";
asciiArt();
async function ueuse() {
const now = format(new Date(), "HH:mm");
console.log(now);
// フォロー機能読み込み
import follows from "./scripts/follow/main.js";
const resUeuse = await fetch(
`https://${process.env.SERVER}/api/ueuse/create`,
{
method: "POST",
body: JSON.stringify({
token: process.env.TOKEN,
text: `${now}になりました`,
}),
},
);
// 正常終了確認読み込み
import successExit from "./scripts/successExit.js";
successExit();
const ueuseData: type.ueuseCreateApi = await resUeuse.json();
console.log(JSON.stringify(ueuseData));
}
async function follow() {
const resMe = await fetch(`https://${process.env.SERVER}/api/me`, {
method: "POST",
body: JSON.stringify({
token: process.env.TOKEN,
}),
});
const meData: type.meApi = await resMe.json();
const followers: Array<string> = meData.follower;
for (let i = 0; i < followers.length; i++) {
const followerItem = followers[i];
const resFollow = await fetch(
`https://${process.env.SERVER}/api/users/follow`,
{
method: "POST",
body: JSON.stringify({
token: process.env.TOKEN,
userid: followerItem,
}),
},
);
const followData: type.followApi = await resFollow.json();
console.log(JSON.stringify(followData));
}
}
// 地震情報観測開始
earthquakeNotice();
// 時報・フォローバック(毎時)
cron.schedule("0 * * * *", () => {
ueuse();
timeNotice();
follows();
});
console.log("サーバーが起動しました");
// 天気お知らせ(毎日7:01)
cron.schedule("1 7 * * *", () => {
weatherNotice();
});
// コンソールで表示
console.log("BOTサーバーが起動しました");
+18 -9
View File
@@ -1,36 +1,45 @@
{
"name": "timenoticeuwuzu",
"version": "v1.1@uwuzu1.5.4",
"description": "@TimeNotice@uwuzu.netの中身",
"name": "notice-uwuzu",
"version": "v6.0.3@uwuzu1.5.4",
"description": "uwuzu Notice Bot",
"main": "dist/main.js",
"scripts": {
"start": "node .",
"build": "tsc",
"main": "tsc && node .",
"dev": "tsx main.ts"
},
"keywords": [
"uwuzu",
"bot",
"cron",
"notice",
"weather",
"time",
"timenotice",
"notice"
"earthquake"
],
"author": {
"name": "Last2014",
"url": "https://last2014.com",
"email": "info@last2014.com"
},
"license": "ISC",
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@types/date-fns": "^2.5.3",
"@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",
"dotenv": "^17.0.0",
"fs": "^0.0.1-security",
"node-cron": "^4.1.1",
"tsx": "^4.20.3",
"typescript": "^5.8.3"
"nodemailer": "^7.0.4",
"typescript": "^5.8.3",
"ws": "^8.18.3"
},
"devDependencies": {
"tsx": "^4.20.3"
}
}
+8
View File
@@ -0,0 +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").replace(/(\r?\n)$/, ''));
console.log(`${version}\n`);
}
+394
View File
@@ -0,0 +1,394 @@
import WebSocket from "ws";
import sendMail from "../src/mailer.js";
import config from "../config.js";
class P2PEarthquakeClient {
private ws: WebSocket | null = null;
private reconnectInterval: number = config.earthquake.reconnectTimes;
private reconnectTimer: NodeJS.Timeout | null = null;
private isConnecting: boolean = false;
public start(): void {
this.connect();
this.setupCleanup();
}
private connect(): void {
if (this.isConnecting) return;
this.isConnecting = true;
console.log("地震情報サーバーに接続中");
try {
this.ws = new WebSocket(config.earthquake.websocketUrl);
this.ws.on("open", () => {
console.log("地震情報サーバーに接続しました");
this.isConnecting = false;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
});
this.ws.on("message", (data: WebSocket) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(message);
} catch (error) {
console.error("メッセージのパースに失敗:", error);
}
});
this.ws.on("close", (code: number, reason: Buffer) => {
console.log(`切断されました: ${code} - ${reason.toString()}`);
this.isConnecting = false;
this.scheduleReconnect();
});
this.ws.on("error", (error: Error) => {
console.error("WebSocketエラー:", error);
this.isConnecting = false;
this.scheduleReconnect();
});
} catch (error) {
console.error("接続エラー:", error);
this.isConnecting = false;
this.scheduleReconnect();
}
}
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;
default:
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
break;
}
}
private executeEventFunc(earthquakeInfo: any): void {
event(earthquakeInfo);
}
private scheduleReconnect(): void {
if (this.reconnectTimer) return;
console.log("地震情報サーバーから切断されました");
console.log(`${this.reconnectInterval / 1000}秒後に再接続を試みます`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, this.reconnectInterval);
}
private setupCleanup(): void {
const cleanup = () => {
this.stop();
process.exit(0);
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
}
public stop(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
// 地名オブジェクトマッピング
async function areaMap(): Promise<Record<number, string>> {
const res = await fetch(config.earthquake.areasCsvUrl);
const text = await res.text();
const lines = text.split("\n");
const map: Record<number, string> = {};
for (const line of lines) {
const cols = line.split(",");
if (cols.length >= 3 && /^\d+$/.test(cols[0])) {
const id = Number(cols[0]);
const name = cols[2].trim();
map[id] = name;
}
}
return map;
}
// 情報受信
async function event(earthquakeInfo: any): Promise<void> {
console.log(`受信データ:${JSON.stringify(earthquakeInfo)}`);
// ----処理----
// 緊急地震速報の場合
if (earthquakeInfo.code === 554) {
// 地震詳細
let descriptionEarthquake: string = "";
if (
earthquakeInfo.earthquake.description !== "" ||
earthquakeInfo.earthquake.description !== undefined
) {
descriptionEarthquake = `この地震について:${earthquakeInfo.earthquake.description}`;
}
// 発令詳細
let description: string = "";
if (
earthquakeInfo.comments.freeFormComment !== "" ||
earthquakeInfo.comments.freeFormComment !== undefined
) {
description = `この発令について:${earthquakeInfo.comments.freeFormComment}`;
}
// テスト・訓練
let test: string = "";
if (earthquakeInfo.test !== undefined) {
test = "この情報にテスト・訓練かの情報はありません";
} else if (earthquakeInfo.test) {
test = "これはテスト、あるいは訓練です";
} else if (earthquakeInfo.test === false) {
test = "これはテスト・訓練ではありません";
}
// 対象地域
let areas: string = "";
if (earthquakeInfo.areas !== undefined) {
const areaNames: Array<string> = Array.from(
new Set(
earthquakeInfo.areas.map((point: any) => point.name).filter(Boolean),
),
);
areas = `対象地域:${areaNames.join("・")}`;
}
// 速報取り消し
let cancelled: string = "";
if (earthquakeInfo.cancelled) {
cancelled = "※以下の緊急地震速報が取り消されました※";
}
// マグニチュード
let magnitude: string = "マグニチュード:";
if (
earthquakeInfo.earthquake.hypocenter.magnitude != -1 ||
earthquakeInfo.earthquake.hypocenter.magnitude === undefined
) {
magnitude += "マグニチュードの情報はありません";
} else {
magnitude += `M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
}
ueuse(`
==地震情報==
【緊急地震速報】
${cancelled}
時刻:${earthquakeInfo.time}
${descriptionEarthquake}
${description}
${test}
${areas}
`);
}
// 地震情報
else if (earthquakeInfo.code === 551) {
// 国内津波
let domesticTsunami;
if (earthquakeInfo.earthquake.domesticTsunami === undefined) {
domesticTsunami = "この地震による国内の津波情報はありません";
} else if (earthquakeInfo.earthquake.domesticTsunami === "None") {
domesticTsunami = "この地震による国内の津波の心配はありません";
} else if (earthquakeInfo.earthquake.domesticTsunami === "Unknown") {
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 = "この地震による国内の津波予報があります";
}
// 最大震度
let maxScale: string = "最大深度:";
if (
earthquakeInfo.earthquake.maxScale !== undefined &&
earthquakeInfo.earthquake.maxScale < config.earthquake.maxScaleMin
) {
console.log("最低震度に満たしていないため投稿されませんでした");
return;
}
if (
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";
}
// 警告
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 = "";
if (earthquakeInfo.points !== undefined) {
const areaNames: Array<string> = Array.from(
new Set(
earthquakeInfo.points.map((point: any) => point.addr).filter(Boolean),
),
);
areas = `対象地域:${areaNames.join("・")}`;
}
// 詳細
let description: string = "";
if (
earthquakeInfo.comments.freeFormComment !== "" &&
earthquakeInfo.comments.freeFormComment !== undefined
) {
description = `この地震について:${earthquakeInfo.comments.freeFormComment}`;
}
// 深さ
let depth: string = "";
if (
earthquakeInfo.earthquake.hypocenter.depth !== null ||
earthquakeInfo.earthquake.hypocenter.depth !== undefined ||
earthquakeInfo.earthquake.hypocenter.depth != -1
) {
if (earthquakeInfo.earthquake.hypocenter.depth === 0) {
depth = "深さ:ごく浅い";
} else {
depth = `深さ:${String(earthquakeInfo.earthquake.hypocenter.depth)}km`;
}
}
// マグニチュード
let magnitude: string = "";
if(
earthquakeInfo.earthquake.hypocenter.magnitude !== null ||
earthquakeInfo.earthquake.hypocenter.magnitude !== undefined ||
earthquakeInfo.earthquake.hypocenter.magnitude != -1
) {
magnitude = `マグニチュード:M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
}
ueuse(`
==地震情報==
【地震発生】
時刻:${earthquakeInfo.time}
${description}
${magnitude}
${depth}
${maxScale}
${areas}
国内の津波:${domesticTsunami}
`);
}
}
async function ueuse(text: string) {
const res = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: text,
}),
});
const resData = await res.json();
console.log(`地震情報投稿:${JSON.stringify(resData)}`);
}
export default function earthquakeNotice(): void {
console.log("地震情報サーバーに接続します");
const client = new P2PEarthquakeClient();
client.start();
}
+42
View File
@@ -0,0 +1,42 @@
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.uwuzu.host}/api/me?token=${config.uwuzu.apiToken}`,
{
method: "GET",
// uwuzu v1.5.4で/api/meのPOSTが死んでいるため簡易的にGET
},
);
const meData: types.meApi = await resMe.json();
console.log(`BOTプロフィール:${JSON.stringify(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.uwuzu.host}/api/users/follow`,
{
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
userid: followerItem,
}),
},
);
const followData: types.followApi = await resFollow.json();
console.log(`フォロー:${JSON.stringify(followData)}`);
}
}
+7
View File
@@ -0,0 +1,7 @@
import followBack from "./follow.js";
import unFollowBack from "./unfollow.js";
export default function follows() {
unFollowBack();
followBack();
}
+27
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,
}),
})
}
});
}
+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();
+46
View File
@@ -0,0 +1,46 @@
import { format } from "date-fns";
import type * as types from "types/types";
import config from "../config.js";
export default async function timeNotice() {
// 停止時間
// 時刻取得
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.uwuzu.host}/api/ueuse/create`,
{
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: `${format(new Date(), "HH:mm")}になりました`,
}),
},
);
const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log("----------------");
console.log(`時報投稿:${JSON.stringify(ueuseData)}`);
}
}
+121
View File
@@ -0,0 +1,121 @@
import { cityList } from "../src/weatherId.js";
import type * as types from "types/types.js";
import config from "../config.js";
export default async function weatherNotice() {
console.log("----------------");
// 仮投稿
const resUeuse = await fetch(
`https://${config.uwuzu.host}/api/ueuse/create`,
{
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: `
本日の天気
※タイムラインが埋まるため返信に記載しています
`,
}),
},
);
const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log(`天気仮投稿:${JSON.stringify(ueuseData)}`);
// インデックス
const splitCount = config.weather.splitCount;
const total = cityList.length;
const chunkSizes = Array(splitCount).fill(0).map((_, i) =>
Math.floor((total + i) / splitCount)
);
// 分割インデックス
let start = 0;
const ranges = chunkSizes.map(size => {
const range = [start, start + size];
start += size;
return range;
});
// 配列作成
const weatherResults = Array(splitCount).fill("");
// 天気取得
for (let chunkIndex = 0; chunkIndex < splitCount; chunkIndex++) {
const [chunkStart, chunkEnd] = ranges[chunkIndex];
for (let i = chunkStart; i < chunkEnd; i++) {
const res = await fetch(
`https://weather.tsukumijima.net/api/forecast/city/${cityList[i]}`,
);
const data = await res.json();
const today = data.forecasts[0];
// 天気
const weather = today.telop ?? "取得できませんでした";
// 最高気温
let maxTemp: string;
if (today.temperature.max.celsius !== null) {
maxTemp = `${today.temperature.max.celsius}`;
} else {
maxTemp = "取得できませんでした";
}
// 最低気温
let minTemp: string;
if (today.temperature.min.celsius !== null) {
minTemp = `${today.temperature.min.celsius}`;
} else {
minTemp = "取得できませんでした";
}
// 降水確率
let chanceOfRain: string;
if (
today.chanceOfRain.T06_12 !== null ||
today.chanceOfRain.T06_12 !== "--%"
) {
chanceOfRain = today.chanceOfRain.T06_12;
} else {
chanceOfRain = "取得できませんでした";
}
weatherResults[chunkIndex] += `
${data.location.city}
天気:${weather}
最高気温:${maxTemp}
最低気温:${minTemp}
降水確率:${chanceOfRain}
`;
}
}
// 分割投稿
for (let i = 0; i < splitCount; i++) {
const resReply = await fetch(
`https://${config.uwuzu.host}/api/ueuse/create`,
{
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: weatherResults[i],
replyid: ueuseData.uniqid
}),
},
);
const replyData: types.ueuseCreateApi = await resReply.json();
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;
}
}
+49
View File
@@ -0,0 +1,49 @@
export const cityList: Array<String> = [
"016010",
"020010",
"030010",
"040010",
"050010",
"060010",
"070010",
"080010",
"090010",
"100010",
"110010",
"120010",
"130010",
"140010",
"150010",
"160010",
"170010",
"180010",
"190010",
"200010",
"210010",
"220010",
"230010",
"240010",
"250010",
"260010",
"270000",
"280010",
"290010",
"300010",
"310010",
"320010",
"330010",
"340010",
"350010",
"360010",
"370000",
"380010",
"390010",
"400010",
"410010",
"420010",
"430010",
"440010",
"450010",
"460010",
"471010",
];
+18 -109
View File
@@ -1,113 +1,22 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"target": "es2022",
"module": "ES2022",
"moduleResolution": "bundler",
"outDir": "./dist",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": "./",
"typeRoots": [
"./node_modules/@types",
"./types"
],
"paths": {
"ws": ["./node_modules/ws/index.js"],
"@types/ws": ["./node_modules/@types/ws/index.d.ts"]
}
}
}
+49
View File
@@ -0,0 +1,49 @@
interface earthquakeTypes {
reconnectTimes: number;
websocketUrl: string;
areasCsvUrl: string;
maxScaleMin: number;
}
interface weatherTypes {
splitCount: number;
}
interface stopsTypes {
start: number;
stop: number;
}
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;
emergency: emergencyTypes;
uwuzu: uwuzuTypes;
}
View File
+68
View File
@@ -0,0 +1,68 @@
declare module 'ws' {
import { EventEmitter } from 'events';
import { IncomingMessage } from 'http';
import { Socket } from 'net';
export type Data = string | Buffer | ArrayBuffer | Buffer[];
export interface WebSocketEventMap {
close: CloseEvent;
error: ErrorEvent;
message: MessageEvent;
open: Event;
}
export default class WebSocket extends EventEmitter {
static readonly CONNECTING: 0;
static readonly OPEN: 1;
static readonly CLOSING: 2;
static readonly CLOSED: 3;
readonly CONNECTING: 0;
readonly OPEN: 1;
readonly CLOSING: 2;
readonly CLOSED: 3;
readonly readyState: 0 | 1 | 2 | 3;
readonly url: string;
readonly protocol: string;
constructor(address: string | URL, protocols?: string | string[]);
close(code?: number, reason?: string): void;
send(data: Data): void;
ping(data?: Data): void;
pong(data?: Data): void;
terminate(): void;
on(event: 'close', listener: (code: number, reason: Buffer) => void): this;
on(event: 'error', listener: (error: Error) => void): this;
on(event: 'message', listener: (data: Data) => void): this;
on(event: 'open', listener: () => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this;
addEventListener(type: 'close', listener: (event: CloseEvent) => void): void;
addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void;
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
addEventListener(type: 'open', listener: (event: Event) => void): void;
}
export interface CloseEvent {
code: number;
reason: string;
wasClean: boolean;
}
export interface ErrorEvent {
error: Error;
message: string;
type: string;
}
export namespace WebSocket {
export const CONNECTING: 0;
export const OPEN: 1;
export const CLOSING: 2;
export const CLOSED: 3;
}
}