Compare commits

..

10 Commits

34 changed files with 1009 additions and 217 deletions
+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 not use this file except in compliance with the License.
You may obtain a copy of the License at 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 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
+34 -11
View File
@@ -1,21 +1,44 @@
# uwuzuお知らせBOT # noticeUwuzu
# uwuzuお知らせBOTについて # Overview
uwuzuで動作するお知らせBOTです。 Automatic notification bot for uwuzu
# 設定 # Functions
examples/config.tsをプロジェクトルートへ移動し各設定を更新してください。
# サーバー起動 - Time notification
- Earthquake information notification
- Earthquake Early Warning
- Earthquake occurs
- Tsunami forecast
- Weather notification
- Commands that users can use freely
- About BOT
- Command Help
- Report to the operator
- Follow back
- Unfollow
- Weather Repost
- 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
``` # Config
An example configuration file is available in `examples/config.ts`.
# Start the server
```bash
npm install npm install
npm run build npm run build
npm run start npm run start
``` ```
Recommended Node.js version: v22.16.0
※Node.js・npmがインストールされている必要があります。 # License
[Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0)
# ライセンス
Apache 2.0 License
+2 -2
View File
@@ -3,7 +3,7 @@ import config from "../config.js";
export default async function APICheck() { export default async function APICheck() {
try { try {
const req = await fetch(`https://${config.uwuzu.host}/api/me`, { const req = await fetch(`https://${config.uwuzu.host}/api/me/`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
@@ -12,7 +12,7 @@ export default async function APICheck() {
const res = await req.json(); const res = await req.json();
if (!res.userid) { if (res.error_code !== undefined) {
console.log(styleText("red", "APIトークンあるいはuwuzuサーバーホストが無効です")); console.log(styleText("red", "APIトークンあるいはuwuzuサーバーホストが無効です"));
process.exit(); process.exit();
} }
+16 -10
View File
@@ -1,35 +1,41 @@
import * as fs from "fs";
import config from "../config.js"; import config from "../config.js";
import { readFileSync, writeFileSync, existsSync } from "fs";
export default async function VersionCheck() { export default async function VersionCheck() {
const nowVersion: string = JSON.parse(fs.readFileSync("package.json", "utf-8")).version; const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
// 初期化 // 初期化
if (!fs.existsSync("logs/version.txt")) { if (!existsSync("logs/version.txt")) {
fs.writeFileSync( writeFileSync(
"logs/version.txt", "logs/version.txt",
nowVersion, packageJson.version,
"utf-8", "utf-8",
); );
} }
// 最終起動バージョン取得 // 最終起動バージョン取得
const oldVersion = fs.readFileSync("logs/version.txt", "utf-8"); const oldVersion = readFileSync("logs/version.txt", "utf-8");
if (oldVersion !== nowVersion) { if (oldVersion !== packageJson.version) {
try { try {
fs.writeFileSync( writeFileSync(
"logs/version.txt", "logs/version.txt",
nowVersion, packageJson.version,
"utf-8", "utf-8",
); );
const releaseUrl = `${packageJson.repository.url}/releases/tag/${packageJson.tag}`;
await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, { await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
text: `${nowVersion}にBOTがアップデートされました!`, text: `
${packageJson.version}にBOTがアップデートされました!
リリース内容:${releaseUrl}
`,
}), }),
cache: "no-store",
}); });
} catch (err) { } catch (err) {
console.log("アップデート通知にエラーが発生しました: ", err); console.log("アップデート通知にエラーが発生しました: ", err);
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+18 -3
View File
@@ -17,6 +17,7 @@ const config: configTypes = {
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)
}, },
// 天気お知らせ設定
weather: { weather: {
splitCount: 4, // 返信の分割数 splitCount: 4, // 返信の分割数
}, },
@@ -24,6 +25,7 @@ const config: configTypes = {
// 緊急時設定 // 緊急時設定
emergency: { emergency: {
function: true, // 緊急時のコンソール表示 function: true, // 緊急時のコンソール表示
report: false, // reportコマンド
mail: { mail: {
function: true, // 緊急時のメール送信 function: true, // 緊急時のメール送信
host: "smtp.example.com", // SMTPサーバー host: "smtp.example.com", // SMTPサーバー
@@ -34,10 +36,23 @@ const config: configTypes = {
to: "admin@noticeuwuzu.example.com", // 緊急時メール送信先(配列可) to: "admin@noticeuwuzu.example.com", // 緊急時メール送信先(配列可)
}, },
}, },
// 規約等
legal: {
terms: `
`, // 利用規約
privacy: `
`, // プライバシーポリシー
},
// 管理者情報設定
admin: {
name: "あどみん", // BOT管理者名
showMail: true, // メールアドレスを公開するか(emergency.mail.toが使用されます)
},
// uwuzuサーバー設定
uwuzu: { uwuzu: {
apiToken: "TOKEN_EXAMPLE", apiToken: "TOKEN_EXAMPLE", // APIトークン
clientToken: "TOKEN_EXAMPLE", clientToken: "TOKEN_EXAMPLE", // クライアントトークン(任意)
host: "uwuzu.example.com", host: "uwuzu.example.com", // サーバーホスト(HTTPSである必要があります)
}, },
}; };
+19 -13
View File
@@ -9,33 +9,39 @@ import * as cron from "node-cron";
// 機能読み込み // 機能読み込み
import timeNotice from "./scripts/timeNotice.js"; 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 earthquakeNotice from "./scripts/earthquakeNotice.js";
import birthdayNotice from "./scripts/birthdayNotice.js";
import Commands from "./scripts/commands/main.js";
import BirthdayDataSet from "./scripts/birthdayDataSet.js";
// アスキーアート読み込み // その他機能
import asciiArt from "./scripts/asciiart.js"; import asciiArt from "./scripts/asciiart.js";
asciiArt(); asciiArt();
// フォロー機能読み込み
import follows from "./scripts/follow/main.js";
// 正常終了確認読み込み
import successExit from "./scripts/successExit.js"; import successExit from "./scripts/successExit.js";
successExit(); successExit();
BirthdayDataSet();
// 地震情報観測開始 // 地震情報観測開始
earthquakeNotice(); earthquakeNotice();
// 時報・フォローバック(毎時) // 時報(1時間/1回)
cron.schedule("0 * * * *", () => { cron.schedule("0 * * * *", () => {
timeNotice(); timeNotice();
follows();
}); });
// 天気お知らせ(毎日7:01) // コマンド(10分/1回)
cron.schedule("1 7 * * *", () => { cron.schedule('*/10 * * * *', () => {
weatherNotice(); Commands();
}); });
// コンソールで表示 // 天気お知らせ(毎日7:00)
cron.schedule("0 7 * * *", () => {
setTimeout(() => {
weatherNotice();
birthdayNotice();
}, 100);
});
// 起動表示
console.log("BOTサーバーが起動しました"); console.log("BOTサーバーが起動しました");
+16 -5
View File
@@ -1,37 +1,48 @@
{ {
"name": "notice-uwuzu", "name": "notice-uwuzu",
"version": "v6.0.3@uwuzu1.5.4", "version": "v7.3@uwuzu1.5.4",
"description": "uwuzu Notice Bot", "tag": "v7.3",
"description": "Notice Bot for uwuzu",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"start": "node .", "start": "node .",
"build": "tsc", "build": "tsc",
"main": "tsc && node .", "main": "tsc && node .",
"dev": "tsx main.ts" "dev": "tsx main.ts",
"clean": "tsc && node dist/scripts/clean/main.js"
},
"repository": {
"type": "git",
"url": "https://gitea.last2014.com/last2014/noticeUwuzu"
}, },
"keywords": [ "keywords": [
"uwuzu", "uwuzu",
"bot", "bot",
"cron", "cron",
"notice", "notice",
"mail",
"weather", "weather",
"time", "time",
"earthquake" "earthquake",
"command",
"commands"
], ],
"author": { "author": {
"name": "Last2014", "name": "Last2014",
"url": "https://last2014.com", "url": "https://last2014.com",
"email": "info@last2014.com" "email": "info@last2014.com"
}, },
"contributors": [],
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@types/date-fns": "^2.5.3", "@types/date-fns": "^2.5.3",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/node": "^24.0.7",
"@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", "@types/ws": "^8.18.1",
"child_process": "^1.0.2",
"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",
+14
View File
@@ -0,0 +1,14 @@
import { existsSync, writeFileSync } from "fs";
const initialData = {} as
{ [key: string]: string | undefined };
export default function BirthdayDataSet() {
if (!existsSync("data/birthdays.json")) {
writeFileSync(
"data/birthdays.json",
JSON.stringify(initialData),
"utf-8",
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import { readFileSync } from "fs";
import { isSameDay, format, differenceInYears } from "date-fns/fp";
import config from "../config.js";
export default async function birthdayNotice() {
// 読み込み
const birthdays: { [key: string]: string | undefined } =
JSON.parse(readFileSync("data/birthdays.json", "utf-8"));
// 配列化
const birthdaysIndex: string[] = Object.entries(birthdays)
.map(([key, value]) => value)
.filter(value => value !== undefined) as string[];
// 初期値
const resultInitial: string = `
【今日誕生日の人】\n`;
let result = resultInitial;
for (let i = 0; i < Object.keys(birthdays).length; i++) {
const birthday = format(birthdaysIndex[i], "yyyy/MM/dd")
if (isSameDay(birthday, new Date())) {
const age = differenceInYears(new Date(), birthday);
const req = await fetch(`https://${config.uwuzu.host}/api/users/`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
userid: Object.keys(birthdays)[i],
}),
});
const res = await req.json();
result+= `${res.username}さん(${age}歳)\n`
}
}
if (result === resultInitial) {
return;
}
const req = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: result,
}),
});
const res = await req.json();
console.log("誕生日お知らせ:", res);
}
+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();
}
}
+88
View File
@@ -0,0 +1,88 @@
import { ueuse } from "types/types.js";
import { readFileSync, writeFileSync, existsSync } from "fs";
import { parse, isValid } from 'date-fns/fp';
import { Reply } from "./main.js";
function normalizedString(Str: string) {
return Str.replace(/[\u3000-\u303F]+/g, ' ')
.replace(/[\uFF01-\uFF0F]+/g, '0-9')
.replace(/[\u3001-\u3002]+/g, '/');
}
function isValidDateString(dateString: string) {
const normalizedStr = normalizedString(dateString);
const regex = /^\d{4}\/\d{2}\/\d{2}$/;
if (!regex.test(normalizedStr)) {
return false;
}
const parseString = parse(new Date(), 'yyyy/MM/dd');
return isValid(parseString(normalizedStr));
}
export default function Birthday(data: ueuse) {
// 読み込み
const birthdays: { [key: string]: string | undefined } =
JSON.parse(readFileSync("data/birthdays.json", "utf-8"));
if (
(data.abi === "none" ||
data.abi === "") &&
birthdays[data.account.userid] === undefined
) {
Reply(`
追記に誕生日を入力してください
(このユーズはもう利用できません。他のユーズで\`/birthday\`をまたご利用ください。)
`, data.uniqid);
return;
}
if (
data.abi === "delete" &&
birthdays[data.account.userid] !== undefined
) {
birthdays[data.account.userid] = undefined;
writeFileSync(
"data/birthdays.json",
JSON.stringify(birthdays),
"utf-8",
);
Reply(`
誕生日のデータを削除しました
`, data.uniqid);
return;
}
if (
data.abi === "delete" &&
birthdays[data.account.userid] === undefined
) {
Reply(`
誕生日のデータが存在しないため削除できません
`, data.uniqid);
return;
}
if (!isValidDateString(data.abi)) {
Reply(`
誕生日の形式が違います。
yyyy/MM/ddの形式で入力してください。
スラッシュ・数字は全角での使用が可能です。
(このユーズはもう利用できません。他のユーズで\`/birthday\`をまたご利用ください。)
`, data.uniqid);
return;
}
birthdays[data.account.userid] = normalizedString(data.abi);
writeFileSync(
"data/birthdays.json",
JSON.stringify(birthdays),
"utf-8",
);
Reply(`
${data.account.username}さんの誕生日を${normalizedString(data.abi)}に設定しました
`, data.uniqid);
}
+25
View File
@@ -0,0 +1,25 @@
import { ueuse } from "types/types.js";
import config from "../../config.js";
import { Reply } from "./main.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,
}),
cache: "no-store",
});
const followRes = await followReq.json();
console.log("フォロー: ", followRes);
const notice = await Reply(`
${data.account.username}さんをフォローしました
`, data.uniqid);
console.log("フォロー通知: ", notice);
}
+82
View File
@@ -0,0 +1,82 @@
import { ueuse } from "types/types.js";
import { readFileSync } from "fs";
import { Reply } from "./main.js";
const helpsMin = {
"info": "このBOTについての概要を返信するコマンドです。",
"help": "コマンドの概要を返信します。追記に\`/\`抜きのコマンド名を入力することでそのコマンドの詳細(フル)を返信します。",
"follow": "コマンド送信者をフォローします。",
"unfollow": "コマンド送信者をフォロー解除します。",
"weather": "天気を返信します。",
"report": "運営者に不具合などを報告します。",
"birthday": "誕生日を設定・削除できます。",
"legal privacy": "プライバシーポリシーを返信します。",
} as { [key: string]: string };
const helpsFull = {
"info": `
このBOTについての概要を返信するコマンドです。
バージョン、開発者などが確認できます。
`,
"help": `
このコマンドです。コマンドの概要を返信します。
追記に\`/\`抜きのコマンド名を入力することでそのコマンドの詳細(フル)を返信します。
`,
"follow": `
コマンドを送信したユーザーをフォローします。
既にフォローされているユーザーも使用できます。
`,
"unfollow": `
コマンドを送信したユーザーをフォロー解除します。
既にフォローされていないユーザーも使用できます。
`,
"weather": `
天気を返信します。
毎日7:00の天気を再投稿するわけではなく、
再取得して返信します。
`,
"report": `
不具合などを運営者にメールで報告できます。
運営者によって有効化されていないと使用できません。
\`/report\`を使用してそのユーズの追記に内容を入力することで使用できます。
`,
"birthday": `
誕生日を設定できます。
設定された誕生日の7:00に祝われます。
追記にyyyy/MM/ddの形式で誕生日を入力することで誕生日を設定できます。
また、追記に\`delete\`と入力することで誕生日のデータを削除できます。
`,
"legal privacy": `
プライバシーポリシーを返信します。
`,
} as { [key: string]: string };
export default async function Help(data: ueuse) {
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
if (
data.abi === "none" ||
data.abi === ""
) {
const helpMsg =
Object.entries(helpsMin)
.map(([command, message]) =>
`\`/${command}\`${message}`
).join('\n');
const ueuse = await Reply(`
${helpMsg}
BOTの概要は\`/info\`をご利用ください。
Wikiを見る:${packageJson.repository.url}/wiki
`, data.uniqid);
console.log("ヘルプ:", ueuse);
} else {
const ueuse = await Reply(`
${helpsFull[data.abi]}
機能を見る:${packageJson.repository.url}/wiki
`, data.uniqid);
console.log("ヘルプ:", ueuse);
}
}
+70
View File
@@ -0,0 +1,70 @@
import { ueuse } from "types/types.js";
import { readFileSync } from "fs";
import { Reply } from "./main.js";
import config from "../../config.js";
export default async function Info(data: ueuse) {
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
const releaseUrl = `${packageJson.repository.url}/releases/tag/${packageJson.tag}`;
let editor = "";
if (packageJson.author.name !== "Last2014") {
editor = `\nEdited by ${packageJson.author.name}`;
}
let adminMail;
if (
config.admin.showMail &&
config.emergency.mail.to !== undefined
) {
adminMail = config.emergency.mail.to;
} else if (
config.admin.showMail &&
config.emergency.mail.to === undefined
) {
adminMail = "未設定";
} else {
adminMail = "非公開";
}
let isReport;
if (config.emergency.report) {
isReport = "有効";
} else {
isReport = "無効";
}
const ueuse = await Reply(`
【BOTについて】
このBOTはオープンソースソフトウェアであるnoticeUwuzuを利用して運営されています。
noticeUwuzuはApache License 2.0によって保護されています。
ライセンスに違反して使用した場合は著作権法違反となります。
バージョン:${packageJson.version}
リリース詳細:${releaseUrl}
【運営者情報】
運営者名:${config.admin.name}
メールアドレス:${adminMail}
報告機能(\`/report\`)${isReport}
【関連コマンド】
コマンドのヘルプをお探しですか?
\`/help\`をご利用ください。
運営者へ報告が必要ですか?
\`/report\`をご利用ください。
プライバシーポリシーをお探しですか?
\`/legal privacy\`をご利用ください。
【クレジット】
Created by Last2014${editor}
`, data.uniqid);
console.log("概要:", ueuse);
}
+7
View File
@@ -0,0 +1,7 @@
import { ueuse } from "types/types.js";
import { Reply } from "../main.js";
import config from "../../../config.js";
export default function PrivacyPolicy(data: ueuse) {
Reply(config.legal.privacy, data.uniqid);
}
+7
View File
@@ -0,0 +1,7 @@
import { ueuse } from "types/types.js";
import { Reply } from "../main.js";
import config from "../../../config.js";
export default function Terms(data: ueuse) {
Reply(config.legal.terms, data.uniqid);
}
+145
View File
@@ -0,0 +1,145 @@
import * as fs from "fs";
import config from "../../config.js";
import type { ueuse } from "types/types.js";
const initialFile: Array<string> = [];
// コマンド読み込み
import Info from "./info.js";
import Follow from "./follow.js";
import UnFollow from "./unfollow.js";
import Weather from "./weather.js";
import Help from "./help.js";
import Report from "./report.js";
import Birthday from "./birthday.js";
import Terms from "./legal/terms.js"
import PrivacyPolicy from "./legal/privacy.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);
}
export 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,
}),
cache: "no-store",
});
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,
}),
cache: "no-store",
}
);
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) === "!" ||
data.text.charAt(0) === "" ||
data.abi === "ignore"
) {
break;
}
// コマンド処理
console.log("--------");
const commandName = cutAfterChar(data.text, "/");
alreadyAdd(data.uniqid);
switch (commandName) {
case "":
break;
case "info":
Info(data);
break;
case "help":
Help(data);
break;
case "legal terms":
Terms(data);
break;
case "legal privacy":
PrivacyPolicy(data);
break;
case "report":
Report(data);
break;
case "follow":
Follow(data);
break;
case "unfollow":
UnFollow(data);
break;
case "weather":
Weather(data);
break;
case "birthday":
Birthday(data);
break;
default:
const reply = await Reply(`
不明なコマンドです。
コマンド実行を除外する場合は1文字目に\`!\`を入れてください。
`, data.uniqid);
console.log("未対応コマンド: ", reply);
break;
}
}
}
+65
View File
@@ -0,0 +1,65 @@
import { ueuse } from "types/types.js";
import { Reply } from "./main.js";
import config from "../../config.js";
import sendMail from "../../src/mailer.js";
export default async function Report(data: ueuse) {
if (
data.abi === "none" ||
data.abi === ""
) {
console.log("報告(内容なし)", await Reply(`
追記に内容を記入してください。
(このユーズはもう利用できません。他のユーズで\`/report\`をまたご利用ください。)
`, data.uniqid));
return;
}
if (!config.emergency.mail.function) {
console.log("報告(メールオフ)", await Reply(`
BOTの運営者によってメール送信機能が無効化されています。
そのため報告機能はご利用いただけません。
`, data.uniqid));
return;
}
if (!config.emergency.report) {
console.log("報告(機能オフ)", await Reply(`
BOTの運営者によって報告機能が無効化されています。
そのため報告機能はご利用いただけません。
`, data.uniqid));
return;
}
try {
sendMail({
to: config.emergency.mail.to,
subject: "【報告】BOT利用者からの報告",
text: `
※noticeUwuzu自動送信によるメールです
【報告】
BOT管理者さん、noticeUwuzu自動送信メールです。
@${data.account.userid}@${config.uwuzu.host}から/reportコマンドを利用した報告がありました。
報告元ユーズ:https://${config.uwuzu.host}/!${data.uniqid}
下記が内容となります。
${data.abi}
`,
});
console.log("報告(完了)", await Reply(`
報告が完了しました。
運営者は報告者、ユーズのURL、内容を確認できます。
場合によっては運営者、BOTからブロックされる可能性があります。
`, data.uniqid));
return;
} catch (err) {
console.log("/reportエラー:", err);
console.log("報告(エラー)", await Reply(`
報告に失敗しました。
`, data.uniqid));
return;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { ueuse } from "types/types.js";
import config from "../../config.js";
import { Reply } from "./main.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,
}),
cache: "no-store",
});
const unfollowRes = await unfollowReq.json();
console.log("フォロー解除: ", unfollowRes);
const notice = await Reply(`
${data.account.username}さんをフォロー解除しました
`, data.uniqid);
console.log("フォロー解除通知: ", notice);
}
+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);
}
+192 -80
View File
@@ -37,8 +37,8 @@ class P2PEarthquakeClient {
try { try {
const message = JSON.parse(data.toString()); const message = JSON.parse(data.toString());
this.handleMessage(message); this.handleMessage(message);
} catch (error) { } catch (err) {
console.error("メッセージのパースに失敗:", error); console.error(`メッセージのパースでエラーが発生: ${err}`);
} }
}); });
@@ -63,23 +63,18 @@ class P2PEarthquakeClient {
private handleMessage(message: any): void { private handleMessage(message: any): void {
console.log("----------------"); console.log("----------------");
switch (message.code) { const supportCode: Array<number> = [
case 551: // 地震情報 551,
console.log("地震情報を受信しました"); 552,
this.executeEventFunc(message); 556,
break; ];
case 554: // 緊急地震速報
console.log("緊急地震速報を受信しました");
this.executeEventFunc(message);
break;
default:
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
break;
}
}
private executeEventFunc(earthquakeInfo: any): void { if (supportCode.indexOf(message.code) !== -1) {
event(earthquakeInfo); event(message);
} else {
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
console.log("受信メッセージ:", message);
}
} }
private scheduleReconnect(): void { private scheduleReconnect(): void {
@@ -87,6 +82,7 @@ class P2PEarthquakeClient {
console.log("地震情報サーバーから切断されました"); console.log("地震情報サーバーから切断されました");
console.log(`${this.reconnectInterval / 1000}秒後に再接続を試みます`); console.log(`${this.reconnectInterval / 1000}秒後に再接続を試みます`);
this.reconnectTimer = setTimeout(() => { this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null; this.reconnectTimer = null;
this.connect(); this.connect();
@@ -96,7 +92,7 @@ class P2PEarthquakeClient {
private setupCleanup(): void { private setupCleanup(): void {
const cleanup = () => { const cleanup = () => {
this.stop(); this.stop();
process.exit(0); process.exit();
}; };
process.on("SIGINT", cleanup); process.on("SIGINT", cleanup);
@@ -116,7 +112,7 @@ class P2PEarthquakeClient {
} }
} }
// 地名オブジェクトマッピング // 地名マッピング
async function areaMap(): Promise<Record<number, string>> { async function areaMap(): Promise<Record<number, string>> {
const res = await fetch(config.earthquake.areasCsvUrl); const res = await fetch(config.earthquake.areasCsvUrl);
@@ -141,12 +137,13 @@ async function areaMap(): Promise<Record<number, string>> {
// 情報受信 // 情報受信
async function event(earthquakeInfo: any): Promise<void> { 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 = ""; let descriptionEarthquake: string = "";
@@ -223,28 +220,7 @@ async function event(earthquakeInfo: any): Promise<void> {
// 地震情報 // 地震情報
else if (earthquakeInfo.code === 551) { else if (earthquakeInfo.code === 551) {
// 国内津波 console.log("地震発生情報を受信しました");
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 ( if (
earthquakeInfo.earthquake.maxScale !== undefined && earthquakeInfo.earthquake.maxScale !== undefined &&
@@ -254,29 +230,46 @@ async function event(earthquakeInfo: any): Promise<void> {
return; return;
} }
// 国内津波
let domesticTsunami;
const TsunamiMessages = {
"None": "この地震による国内の津波の心配はありません",
"Unknown": "この地震による国内の津波情報はありません",
"Checking": "この地震による国内の津波情報を調査中です",
"NonEffective": "この地震による国内の津波影響は若干の海面変動が予想されますが被害の心配はありません",
"Watch": "この地震により国内で津波注意報が発令しています",
"Warning": "この地震による国内の津波予報があります",
} as { [key: string]: string };
if (earthquakeInfo.earthquake.domesticTsunami === undefined) {
domesticTsunami = "この地震による国内の津波情報はありません";
} else {
domesticTsunami = TsunamiMessages[earthquakeInfo.earthquake.domesticTsunami];
}
// 最大震度
let maxScale: string = "最大深度:";
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 ( if (
earthquakeInfo.earthquake.maxScale == -1 && earthquakeInfo.earthquake.maxScale == -1 ||
earthquakeInfo.earthquake.maxScale === undefined earthquakeInfo.earthquake.maxScale === undefined
) { ) {
maxScale = "最大震度情報なし"; maxScale = "最大震度:不明";
} else if (earthquakeInfo.earthquake.maxScale === 10) { } else {
maxScale += "震度1"; maxScale = `最大震度:${maxScales[earthquakeInfo.earthquake.maxScale]}`;
} 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";
} }
// 警告 // 警告
@@ -296,12 +289,12 @@ async function event(earthquakeInfo: any): Promise<void> {
to: config.emergency.mail.to, to: config.emergency.mail.to,
subject: "【警告】震度6強以上の地震を受信しました", subject: "【警告】震度6強以上の地震を受信しました",
text: ` text: `
※noticeUwuzu自動送信によるメールです ※noticeUwuzu自動送信によるメールです
【警告】 【警告】
BOT管理者さん、noticeUwuzu自動送信メールです。 BOT管理者さん、noticeUwuzu自動送信メールです。
震度6強以上の地震を受信したため警告メールが送信されました。 震度6強以上の地震を受信したため警告メールが送信されました。
物理、システム的にサーバーがダウンする可能性があります。 物理、システム的にサーバーがダウンする可能性があります。
ご自身の身をお守りください。 ご自身の身をお守りください。
` `
}); });
@@ -312,7 +305,7 @@ async function event(earthquakeInfo: any): Promise<void> {
} }
// 対象地域 // 対象地域
let areas: string = ""; let areas;
if (earthquakeInfo.points !== undefined) { if (earthquakeInfo.points !== undefined) {
const areaNames: Array<string> = Array.from( const areaNames: Array<string> = Array.from(
@@ -321,42 +314,54 @@ async function event(earthquakeInfo: any): Promise<void> {
), ),
); );
areas = `対象地域:${areaNames.join("・")}`; areas = `対象地域:${areaNames.join("・")}`;
} else {
areas = "対象地域:不明";
} }
// 詳細 // 詳細
let description: string = ""; let description;
if ( if (
earthquakeInfo.comments.freeFormComment !== "" && earthquakeInfo.comments.freeFormComment !== "" &&
earthquakeInfo.comments.freeFormComment !== undefined earthquakeInfo.comments.freeFormComment !== undefined
) { ) {
description = `この地震について:${earthquakeInfo.comments.freeFormComment}`; description = `この地震について:${earthquakeInfo.comments.freeFormComment}`;
} else {
description = "";
} }
// 深さ // 深さ
let depth: string = ""; let depth;
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
) { ) {
if (earthquakeInfo.earthquake.hypocenter.depth === 0) { if (earthquakeInfo.earthquake.hypocenter.depth === 0) {
depth = "深さ:ごく浅い"; depth = "深さ:ごく浅い";
} else if (earthquakeInfo.earthquake.hypocenter.depth === -1) {
depth = "深さ:不明";
} else { } else {
depth = `深さ:${String(earthquakeInfo.earthquake.hypocenter.depth)}km`; depth = `深さ:${String(earthquakeInfo.earthquake.hypocenter.depth)}km`;
} }
} else {
depth = "深さ:不明";
} }
// マグニチュード // マグニチュード
let magnitude: string = ""; let magnitude;
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
) { ) {
magnitude = `マグニチュード:M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`; if (earthquakeInfo.earthquake.hypocenter.magnitude === -1) {
magnitude = "マグニチュード:不明";
} else {
magnitude = `マグニチュード:M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
}
} else {
magnitude = "マグニチュード:不明";
} }
ueuse(` ueuse(`
@@ -370,6 +375,112 @@ async function event(earthquakeInfo: any): Promise<void> {
${areas} ${areas}
国内の津波:${domesticTsunami} 国内の津波:${domesticTsunami}
`); `);
} else if (earthquakeInfo.code === 552) {
console.log("津波予報情報を受信しました");
// 予報取り消し
if (earthquakeInfo.cancelled) {
ueuse(`
==地震情報==
【津波予報】
※津波予報が取り消されました※
時刻:${earthquakeInfo.time}
`);
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 = "津波は直ちには襲来しません";
} else {
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);
} }
} }
@@ -380,6 +491,7 @@ async function ueuse(text: string) {
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
text: text, text: text,
}), }),
cache: "no-store",
}); });
const resData = await res.json(); const resData = await res.json();
-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.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
@@ -1,7 +0,0 @@
import followBack from "./follow.js";
import unFollowBack from "./unfollow.js";
export default function follows() {
unFollowBack();
followBack();
}
-27
View File
@@ -1,27 +0,0 @@
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,
}),
})
}
});
}
+2 -2
View File
@@ -1,5 +1,5 @@
import * as fs from "fs"; import * as fs from "fs";
import { isBefore } from "date-fns/fp"; import { isAfter } from "date-fns";
import config from "../config.js"; import config from "../config.js";
import sendMail from "../src/mailer.js"; import sendMail from "../src/mailer.js";
@@ -20,7 +20,7 @@ export default function successExit() {
const start = iolog.start; const start = iolog.start;
const stop = iolog.stop; const stop = iolog.stop;
if (isBefore(start, stop)) { if (isAfter(start, stop)) {
console.log("前回の終了が適切でない可能性があります"); console.log("前回の終了が適切でない可能性があります");
if (config.emergency.mail.function) { if (config.emergency.mail.function) {
+1
View File
@@ -35,6 +35,7 @@ export default async function timeNotice() {
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
text: `${format(new Date(), "HH:mm")}になりました`, text: `${format(new Date(), "HH:mm")}になりました`,
}), }),
cache: "no-store",
}, },
); );
+8 -3
View File
@@ -4,7 +4,7 @@ import type * as types from "types/types.js";
import config from "../config.js"; import config from "../config.js";
export default async function weatherNotice() { export async function weatherNotice() {
console.log("----------------"); console.log("----------------");
// 仮投稿 // 仮投稿
@@ -19,6 +19,7 @@ export default async function weatherNotice() {
※タイムラインが埋まるため返信に記載しています ※タイムラインが埋まるため返信に記載しています
`, `,
}), }),
cache: "no-store",
}, },
); );
@@ -26,7 +27,10 @@ export default async function weatherNotice() {
console.log(`天気仮投稿:${JSON.stringify(ueuseData)}`); console.log(`天気仮投稿:${JSON.stringify(ueuseData)}`);
weatherReply(ueuseData.uniqid);
}
export async function weatherReply(uniqid: string) {
// インデックス // インデックス
const splitCount = config.weather.splitCount; const splitCount = config.weather.splitCount;
const total = cityList.length; const total = cityList.length;
@@ -109,13 +113,14 @@ export default async function weatherNotice() {
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
text: weatherResults[i], text: weatherResults[i],
replyid: ueuseData.uniqid replyid: uniqid,
}), }),
cache: "no-store",
}, },
); );
const replyData: types.ueuseCreateApi = await resReply.json(); const replyData: types.ueuseCreateApi = await resReply.json();
console.log(`天気投稿${JSON.stringify(replyData)}`); console.log(`天気返信${JSON.stringify(replyData)}`);
} }
} }
+5 -4
View File
@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2022", "target": "ES2024",
"module": "ES2022", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"outDir": "./dist", "outDir": "./dist",
"esModuleInterop": true, "esModuleInterop": true,
@@ -17,6 +17,7 @@
"paths": { "paths": {
"ws": ["./node_modules/ws/index.js"], "ws": ["./node_modules/ws/index.js"],
"@types/ws": ["./node_modules/@types/ws/index.d.ts"] "@types/ws": ["./node_modules/@types/ws/index.d.ts"]
} },
} "removeComments": true,
},
} }
+18 -5
View File
@@ -19,18 +19,29 @@ interface timeTypes {
} }
interface emergencyMailTypes { interface emergencyMailTypes {
function: Boolean; function: boolean;
host: string | undefined; host: string;
port: number; port: number;
user: string; user: string;
password: string; password: string;
secure: Boolean; secure: boolean;
to: string; to: string | string[];
} }
interface emergencyTypes { interface emergencyTypes {
function: Boolean; function: boolean;
mail: emergencyMailTypes; mail: emergencyMailTypes;
report: boolean;
}
interface legalTypes {
terms: string;
privacy: string;
}
interface adminTypes {
name: string;
showMail: boolean;
} }
interface uwuzuTypes { interface uwuzuTypes {
@@ -45,5 +56,7 @@ export interface configTypes {
weather: weatherTypes; weather: weatherTypes;
emergency: emergencyTypes; emergency: emergencyTypes;
legal: legalTypes;
admin: adminTypes;
uwuzu: uwuzuTypes; uwuzu: uwuzuTypes;
} }
+24 -2
View File
@@ -12,9 +12,9 @@ export interface meApi {
user_icon: string; user_icon: string;
user_header: string; user_header: string;
registered_date: string; registered_date: string;
followee: Array; followee: Array<string>;
followee_cnt: number; followee_cnt: number;
follower: Array; follower: Array<string>;
follower_cnt: number; follower_cnt: number;
ueuse_cnt: number; ueuse_cnt: number;
isBot: Boolean; isBot: Boolean;
@@ -23,6 +23,28 @@ export interface meApi {
language: String; 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 { export interface ueuseCreateApi {
uniqid: string; uniqid: string;
userid: string; userid: string;