Compare commits

...

42 Commits

Author SHA1 Message Date
last2014 fb3e233244 v25.8.7 2025-08-26 22:29:05 +09:00
last2014 ca96f83a44 Git LFS廃止 2025-08-25 16:51:05 +09:00
last2014 97af8da4dd v25.8.6 2025-08-25 16:30:46 +09:00
last2014 a83054f5ce Git LFS 2025-08-25 06:15:14 +09:00
last2014 bbf2d66b57 v25.8.5@uwuzu1.6.4 2025-08-25 06:11:36 +09:00
last2014 db719b8312 v8.1.2 2025-08-08 21:34:30 +09:00
last2014 2966eee889 v8.1.1 2025-08-08 21:31:43 +09:00
last2014 db5e174dd4 v8.1 2025-08-08 21:29:19 +09:00
last2014 2b53b8cd3d v8.0.2 2025-08-07 08:56:49 +09:00
last2014 d20f630e1b v8.0.1 2025-08-07 08:47:11 +09:00
last2014 644ba912b1 v8.0 2025-08-06 21:27:29 +09:00
last2014 1ff4b2c429 Merge branch 'main' of https://gitea.last2014.com/last2014/noticeUwuzu 2025-08-05 20:01:36 +09:00
last2014 0a3bb241a6 v7.5 2025-08-05 19:58:09 +09:00
last2014 f14999be13 デバッグ用コード削除 2025-08-04 23:45:52 +09:00
last2014 6ec9831ed4 v7.3@uwuzu1.5.4をリリース 2025-08-04 20:08:23 +09:00
last2014 718e97ed45 v7.2@uwuzu1.5.4をリリース 2025-08-02 19:39:50 +09:00
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
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
59 changed files with 2598 additions and 253 deletions
-2
View File
@@ -3,5 +3,3 @@
/node_modules/ /node_modules/
/package-lock.json /package-lock.json
/config.ts /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 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,
+33 -19
View File
@@ -1,31 +1,45 @@
# uwuzuお知らせBOT # noticeUwuzu
# uwuzuお知らせBOTについて # Overview
uwuzuで動作するお知らせBOTです。 Automatic notification bot for uwuzu
# 設定 # Functions
examples/config.tsをプロジェクトルートへ移動し各設定を更新してください。
## 設定項目 - Time notification
earthquake.reconnectTimes:地震情報のWebSocketが切断されたときに自動再接続する時間(ミリ秒) - Earthquake information notification
earthquake.websocketUrl:地震情報のWebSocket接続先URL - Earthquake Early Warning
earthquake.areasCsvUrl:地域情報のデータベース(CSV)ファイルのURL - 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
- Make it a quote
- 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
weather.splitCount:天気お知らせの返信の分割数(4分割を推奨) # Config
An example configuration file is available in `examples/config.ts`.
apiTokenBOTアカウントのAPIキー # Start the server
uwuzuServer:使用するuwuzuサーバーのホスト名(uwuzu.netなど)
# サーバー起動 ```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
+5
View File
@@ -0,0 +1,5 @@
# # ###### ##### ### ###### ####### # # # # # # ###### # #
## # # # # # # # # # # # # # # # # #
# ## # # # # # # ####### # # # # # # # # ## # #
# ## # # # # # # # # # # # # # # #
# # ###### # ### ###### ####### ###### # # ###### ###### ######
+30
View File
@@ -0,0 +1,30 @@
import { styleText } from "util";
import config from "../config.js";
export default async function APICheck() {
try {
const req = await fetch(`${config.uwuzu.host}/api/me/`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
cache: "no-store",
});
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();
}
}
+12
View File
@@ -0,0 +1,12 @@
import config from "../config.js";
import { styleText } from "util";
export default function LegalCheck() {
if (
config.legal.terms.length <= 50 ||
config.legal.terms.length <= 50
) {
console.log(styleText("red", "利用規約とプライバシーポリシーは50文字以上にしてください。"));
process.exit();
}
}
+18
View File
@@ -0,0 +1,18 @@
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";
import LegalCheck from "./legal.js";
import config from "../config.js";
export default async function Check() {
PackagesIsExist();
PackagesCheck();
ConfigCheck();
if (config.debug === undefined) {
LegalCheck()
}
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);
}
}
+44
View File
@@ -0,0 +1,44 @@
import config from "../config.js";
import { readFileSync, writeFileSync, existsSync } from "fs";
export default async function VersionCheck() {
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
// 初期化
if (!existsSync("logs/version.txt")) {
writeFileSync(
"logs/version.txt",
packageJson.version,
"utf-8",
);
}
// 最終起動バージョン取得
const oldVersion = readFileSync("logs/version.txt", "utf-8");
if (oldVersion !== packageJson.version) {
try {
writeFileSync(
"logs/version.txt",
packageJson.version,
"utf-8",
);
const releaseUrl = `${packageJson.repository.url}/releases/tag/${packageJson.tag}`;
await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: `
${packageJson.version}にBOTがアップデートされました!
リリース内容:${releaseUrl}
`,
}),
cache: "no-store",
});
} catch (err) {
console.log("アップデート通知にエラーが発生しました: ", err);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+53 -11
View File
@@ -2,25 +2,67 @@ import type { configTypes } from "types/config";
// READMEの設定項目を参照 // READMEの設定項目を参照
const config: configTypes = { const config: configTypes = {
// 時報設定
time: { time: {
// 時報休止期間
stopTimes: { stopTimes: {
start: new Date("23:00"), start: 23, // 開始
stop: new Date("6:00"), stop: 6, // 停止
} },
}, },
// 地震速報設定
earthquake: { earthquake: {
reconnectTimes: 5000, reconnectTimes: 5000, // 再接続時間(ミリ秒)
websocketUrl: "wss://api.p2pquake.net/v2/ws", websocketUrl: "wss://api.p2pquake.net/v2/ws", // WebSocketのURL
areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", // 対象地域CSVファイルのURL
maxScaleMax: 30, maxScaleMin: 30, // 地震発生の際の最低震度(10-70)
rateLimit: 30,
}, },
// 天気お知らせ設定
weather: { weather: {
splitCount: 4, splitCount: 4, // 返信の分割数
}, },
// Make it a quote設定
miq: true, // 有効/無効
apiToken: "TOKEN_EXAMPLE", // 緊急時設定
uwuzuServer: "uwuzu.example.com", emergency: {
isEnabled: true, // 緊急時のコンソール表示
mail: {
isEnabled: 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", // 緊急時メール送信先(配列可)
},
},
// /report設定
report: {
isEnabled: true, // 有効/無効
message: "", // 報告者へのメッセージ
},
// 規約等
legal: {
terms: `
`, // 利用規約
privacy: `
`, // プライバシーポリシー
},
// 管理者情報設定
admin: {
name: "あどみん", // BOT管理者名
showMail: false, // メールアドレスを公開するか(false:非公開/文字列:メールアドレス)
panel: { // 管理パネル
isEnabled: true, // 有効/無効
port: 74919, // 配信ポート
},
},
// uwuzuサーバー設定
uwuzu: {
apiToken: "TOKEN_EXAMPLE", // APIトークン
host: "https://uwuzu.example.com", // サーバーホスト
},
}; };
export default config; export default config;
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+43 -9
View File
@@ -1,29 +1,63 @@
// 起動チェック
import Check from "./checks/main.js";
(async () => {
await Check();
})();
// 定期実行読み込み // 定期実行読み込み
import * as cron from "node-cron"; 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 EventDays from "./scripts/eventday.js";
import Commands from "./scripts/commands/main.js";
// フォローバック機能読み込み // その他機能
import followBack from "./scripts/followBack.js"; import asciiArt from "./scripts/asciiart.js";
asciiArt();
import successExit from "./scripts/successExit.js";
successExit();
// 地震情報観測開始 // 地震情報観測開始
earthquakeNotice(); earthquakeNotice();
// 時報・フォローバック(毎時) // 時報(1時間/1回)
cron.schedule("0 * * * *", () => { cron.schedule("0 * * * *", () => {
timeNotice(); timeNotice();
followBack();
}); });
// 天気お知らせ(毎日7:01) // コマンド(10分/1回)
cron.schedule("1 7 * * *", () => { cron.schedule("*/10 * * * *", () => {
Commands();
});
// 祝日などお知らせ(毎日0:00)
cron.schedule("0 0 * * *", () => {
EventDays();
});
// 天気お知らせ(毎日7:00)
import { setTimeout } from "timers";
cron.schedule("0 7 * * *", () => {
setTimeout(() => { setTimeout(() => {
weatherNotice(); weatherNotice();
}, 100); }, 100);
}); });
// コンソールで表示 // 管理パネル
console.log("サーバーが起動しました"); import AdminPanel from "./panel/main.js";
import { styleText } from "util";
(async () => {
await AdminPanel();
})();
// 起動表示
console.log("BOTサーバーが起動しました");
import config from "./config.js";
if (config.debug !== undefined) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
console.log(styleText(["bgRed", "cyan", "bold"], "デバッグモードで起動中"));
}
+91
View File
@@ -0,0 +1,91 @@
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+174
View File
@@ -0,0 +1,174 @@
import { createCanvas, loadImage, registerFont } from "canvas";
import sharp from "sharp";
import { MiQOptions } from "./miq";
// フォント読み込み
registerFont("miq/fonts/MPLUS.ttf", {
family: "M PLUS Rounded 1c",
weight: "400",
});
function maxLengthCut(
text: string,
maxLength: number,
) {
if (text.length > maxLength) {
text = text.substring(0, maxLength)
+ "...";
}
return text;
}
function autoLineBreak(
text: string,
maxWidth: number,
font: string = '48px "M PLUS Rounded 1c"'
): string {
const ctx = createCanvas(maxWidth, 100).getContext("2d");
ctx.font = font;
const lines: string[] = [];
let currentLine = "";
for (let i = 0; i < text.length; i++) {
const char = text[i];
const testLine = currentLine + char;
const width = ctx.measureText(testLine).width;
if (width > maxWidth) {
lines.push(currentLine);
currentLine = char;
while (ctx.measureText(currentLine).width > maxWidth && currentLine.length > 1) {
const cutPoint = currentLine.length - 1;
lines.push(currentLine.slice(0, cutPoint));
currentLine = currentLine.slice(cutPoint);
}
} else {
currentLine = testLine;
}
}
if (currentLine) lines.push(currentLine);
return lines.join("\n");
}
function textReplace(
text: string,
maxWidth: number,
font?: string
) {
// 改行削除
text = text.replaceAll("\n", "");
text = text.replaceAll("\r", "");
// 自動改行
text = autoLineBreak(text, maxWidth, font);
// 100文字以上を省略
text = maxLengthCut(text, 100);
return text;
}
async function iconReplace(
color: boolean,
iconURL: string
) {
let result = "";
const buffer = await(await fetch(iconURL, {
method: "GET",
cache: "no-store",
})).arrayBuffer();
if (color) {
const img = await sharp(Buffer.from(buffer))
.png()
.toBuffer();
result = `data:image/png;base64,${img.toString("base64")}`;
} else {
const img = await sharp(Buffer.from(buffer))
.png()
.grayscale()
.toBuffer();
result = `data:image/png;base64,${img.toString("base64")}`;
}
return result;
}
/**
* A function to generate
* Make it a quote on Node.js.
*/
export default async function MiQ({
type,
color,
text,
iconURL,
userName,
userID,
}: MiQOptions) {
// 初期化
const canvas = createCanvas(1200, 630);
const ctx = canvas.getContext("2d");
// 背景描画
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// アイコン描画
const iconImg = await loadImage(await iconReplace(
color,
iconURL,
));
const iconSize = canvas.height;
ctx.drawImage(iconImg, 0, 0, iconSize, iconSize);
// ユーザー名描画
ctx.font = '38px "M PLUS Rounded 1c"';
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let x = 910;
let y = 480;
ctx.fillText(`- ${userName}`, x, y, canvas.width-iconSize);
// ユーザーID描画
ctx.font = '28px "M PLUS Rounded 1c"';
ctx.fillStyle = "#b4b4b4";
ctx.fillText(`@${userID}`, x, y+50, canvas.width-iconSize);
// 本文描画
const maxWidth = canvas.width - iconSize;
ctx.font = '48px "M PLUS Rounded 1c"';
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "white";
text = textReplace(text, maxWidth, ctx.font);
ctx.fillText(text, x, 80);
// フェード描画
const fadeColor = "black";
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
gradient.addColorStop(0.5, fadeColor);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, iconSize, canvas.height);
// 返答
switch (type) {
case "Buffer":
return canvas.toBuffer();
case "Base64Data":
return canvas.toDataURL()
.replace("data:image/png;base64,", "");
case "Base64URL":
return canvas.toDataURL();
default:
return "Error: The type property is invalid.";
}
}
+13
View File
@@ -0,0 +1,13 @@
type MiQType =
"Buffer" |
"Base64Data" |
"Base64URL";
export interface MiQOptions {
type: MiQType;
color: boolean;
text: string;
iconURL: string;
userName: string;
userID: string;
}
+33 -11
View File
@@ -1,40 +1,62 @@
{ {
"name": "noticeuwuzu", "name": "notice-uwuzu",
"version": "v4.0@uwuzu1.5.4", "version": "v25.8.7@uwuzu1.6.4",
"description": "uwuzu Notice Bot", "tag": "v25.8.7",
"description": "Notice Bot for uwuzu",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"start": "node .", "start": "node .",
"build": "tsc", "build": "tsc",
"dev": "tsx main.ts" "main": "tsc && node .",
"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",
"mail",
"weather",
"time", "time",
"timenotice", "earthquake",
"notice" "command",
"commands"
], ],
"author": { "author": {
"name": "Last2014", "name": "Last2014",
"url": "https://last2014.com", "url": "https://last2014.com",
"email": "info@last2014.com" "email": "info@last2014.com"
}, },
"license": "ISC", "contributors": [],
"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/express": "^5.0.3",
"@types/node": "^24.0.7",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17",
"@types/sharp": "^0.31.1",
"@types/ws": "^8.18.1",
"canvas": "^3.2.0",
"child_process": "^1.0.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"express": "^5.1.0",
"fs": "^0.0.1-security",
"node-cron": "^4.1.1", "node-cron": "^4.1.1",
"tsx": "^4.20.3", "nodemailer": "^7.0.4",
"typescript": "^5.8.3", "sharp": "^0.34.3",
"timers": "^0.1.1",
"typescript": "^5.9.2",
"ws": "^8.18.3" "ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.7", "tsx": "^4.20.3"
"@types/ws": "^8.18.1"
} }
} }
+54
View File
@@ -0,0 +1,54 @@
import express from "express";
import * as os from "os";
import config from "../config.js";
import { NetworkInterfaceDetails } from "types/types";
// バックエンドルーティング
import CommandExecute from "./route/command.js";
import ueusePost from "./route/ueuse.js";
import WeatherUeuse from "./route/weather.js";
import API from "./route/api.js";
import Token from "./route/token.js";
import Debug from "./route/debug.js";
export default async function AdminPanel() {
// 無効
if (!config.admin.panel.isEnabled) {
return;
}
// 管理パネル
const app = express();
const port = config.admin.panel.port;
// ルーティング
app.use(ueusePost);
app.use(CommandExecute);
app.use(WeatherUeuse);
app.use(API);
app.use(Token);
app.use(Debug);
app.use(express.static("panel/public"));
app.listen(port, () => {
console.log(`http://${LocalIP()}:${port} で管理パネルを起動しました`);
});
}
function LocalIP() {
const interfaces = os.networkInterfaces();
for (const name in interfaces) {
const iface: any = interfaces[name];
for (const i of iface) {
const details: NetworkInterfaceDetails = i;
if (details.family === 'IPv4' && details.internal !== true) {
return details.address;
}
}
}
return "localhost";
}
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理パネル</title>
<!-- パッケージ -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://code.iconify.design/iconify-icon/3.0.0/iconify-icon.min.js"></script>
<script>
import "iconify-icon";
</script>
</head>
<body class="dark:bg-gray-950 dark:text-white text-center">
<h1 class="text-4xl font-bold">noticeUwuzu管理パネル</h1>
<button id="commandExec" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
コマンド実行
</button>
<button id="weatherUeuse" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
天気お知らせ
</button>
<button id="eventdayUeuse" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
祝日等お知らせ
</button>
<button id="ueuse" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
ユーズ投稿
</button>
<button id="api" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
API使用
</button>
<script src="/script.js"></script>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
document.getElementById("commandExec").addEventListener("click", async () => {
const req = await fetch("/actions/command-execute", {
method: "POST",
});
const res = await req.text();
if (res === "Accepted") {
alert("コマンド実行を受け付けました");
} else {
alert(`コマンド実行の要求にエラーが発生しました:${res}`);
}
});
document.getElementById("weatherUeuse").addEventListener("click", async () => {
const req = await fetch("/actions/weather", {
method: "POST",
});
const res = await req.text();
if (res === "Accepted") {
alert("天気お知らせを受け付けました");
} else {
alert(`天気お知らせの要求にエラーが発生しました:${res}`);
}
});
document.getElementById("eventdayUeuse").addEventListener("click", async () => {
const req = await fetch("/actions/eventday", {
method: "POST",
});
const res = await req.text();
if (res === "Accepted") {
alert("祝日等お知らせを受け付けました");
} else {
alert(`祝日等お知らせの要求にエラーが発生しました:${res}`);
}
});
document.getElementById("ueuse").addEventListener("click", async () => {
const text = prompt("ユーズ内容").toLowerCase();
if (text === "") {
alert("ユーズ内容がありません。");
return;
}
const nsfw = confirm("NSFWにしますか?");
const req = await fetch("/actions/ueuse", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text, text,
nsfw: nsfw,
}),
});
const res = await req.text();
if (res === "Success") {
alert("ユーズ投稿を受け付けました");
} else {
alert(`ユーズ投稿の要求にエラーが発生しました:${res}`);
}
});
document.getElementById("api").addEventListener("click", async () => {
const token = await (await fetch("/actions/token", {
method: "GET",
})).text();
const endpoint = prompt("エンドポイント", "/serverinfo-api").toLowerCase();
if (endpoint === "") {
alert("エンドポイントが設定されていません。");
return;
}
const body = prompt("body(JSON)", `{"token": "${token}"}`).toLowerCase();
if (body === "") {
alert("bodyが設定されていません。");
return;
}
const req = await fetch("/actions/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
endpoint: endpoint,
body: JSON.parse(body),
}),
});
const res = await req.text();
alert(res);
});
+34
View File
@@ -0,0 +1,34 @@
import express from "express";
const API = express.Router();
import config from "../../config.js";
API.use(express.json());
API.use(express.urlencoded({ extended: true }));
API.post("/actions/api", async (req, res, next) => {
const endpoint = req.body.endpoint;
const body = req.body.body;
try {
const apiReq = await fetch(`${config.uwuzu.host}/api${endpoint}`, {
method: "POST",
body: JSON.stringify(body),
});
const apiRes = await apiReq.json();
res.status(200)
.send(apiRes);
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
API.get("/actions/api", (req, res) => {
res.status(501)
.send("POST Only");
});
export default API;
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
const CommandExecute = express.Router();
import Commands from "../../scripts/commands/main.js";
CommandExecute.post("/actions/command-execute", (req, res) => {
try {
(async () => {
await Commands();
})();
res.status(202)
.send("Accepted");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
CommandExecute.get("/actions/command-execute", (req, res) => {
res.status(501)
.send("POST Only");
});
export default CommandExecute;
+24
View File
@@ -0,0 +1,24 @@
import express from "express";
const Debug = express.Router();
import config from "../../config.js";
Debug.post("/actions/debug", (req, res, next) => {
res.status(501)
.send("GET Only");
});
Debug.get("/actions/debug", (req, res) => {
let debug;
if (config.debug === undefined) {
debug = false;
} else {
debug = true;
}
res.status(200)
.send(debug);
});
export default Debug;
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
const EventdayUeuse = express.Router();
import EventDays from "../../scripts/eventday.js";
EventdayUeuse.post("/actions/eventday", (req, res) => {
try {
(async () => {
await EventDays();
})();
res.status(202)
.send("Accepted");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
EventdayUeuse.get("/actions/eventday", (req, res) => {
res.status(501)
.send("POST Only");
});
export default EventdayUeuse;
+16
View File
@@ -0,0 +1,16 @@
import express from "express";
const Token = express.Router();
import config from "../../config.js";
Token.post("/actions/token", (req, res, next) => {
res.status(501)
.send("GET Only");
});
Token.get("/actions/token", (req, res) => {
res.status(200)
.send(config.uwuzu.apiToken);
});
export default Token;
+44
View File
@@ -0,0 +1,44 @@
import express from "express";
const ueusePost = express.Router();
import config from "../../config.js";
ueusePost.use(express.json());
ueusePost.use(express.urlencoded({ extended: true }));
ueusePost.post("/actions/ueuse", async (req, res, next) => {
const text = req.body.text;
const nsfw = req.body.nsfw;
try {
const ueuseReq = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: `
${text}
このユーズはnoticeUwuzuの管理パネルから投稿されました
`,
nsfw: nsfw,
}),
});
const ueuseRes = await ueuseReq.json();
console.log(`ユーズ(管理パネル)${JSON.stringify(ueuseRes)}`);
res.status(200)
.send("Success");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
ueusePost.get("/actions/ueuse", (req, res) => {
res.status(501)
.send("POST Only");
});
export default ueusePost;
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
const WeatherUeuse = express.Router();
import { weatherNotice } from "../../scripts/weatherNotice.js";
WeatherUeuse.post("/actions/weather", (req, res) => {
try {
(async () => {
await weatherNotice();
})();
res.status(202)
.send("Accepted");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
WeatherUeuse.get("/actions/weather", (req, res) => {
res.status(501)
.send("POST Only");
});
export default WeatherUeuse;
+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`);
}
+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();
}
}
+52
View File
@@ -0,0 +1,52 @@
import { meApi, ueuse } from "../../types/types";
import config from "../../config.js";
import { Reply } from "./main.js";
export default async function Delete(data: ueuse) {
const me: meApi = await (await fetch(`${config.uwuzu.host}/api/me/`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
})).json()
const replyUeuse: ueuse = (await (await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
})).json())["0"];
if (me.userid === replyUeuse.account.userid) {
const ueuseDelete = await (await fetch(`${config.uwuzu.host}/api/ueuse/delete`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
})).json();
console.log("削除:", ueuseDelete);
if (ueuseDelete.success === true) {
console.log("削除通知:", await Reply(`
対象のユーズを削除しました。
`, data.uniqid));
} else {
console.log("削除失敗通知:", await Reply(`
対象のユーズを削除できませんでした。
`, data.uniqid));
}
return;
} else {
console.log("削除失敗通知(他人)", await Reply(`
削除するユーズが${me.username}のものではありません。
そのため削除できませんでした。
`, data.uniqid));
return;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { ueuse } from "types/types";
import config from "../../config.js";
import { Reply } from "./main.js";
export default async function Follow(data: ueuse) {
const followReq = await fetch(`${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);
}
+112
View File
@@ -0,0 +1,112 @@
import { ueuse } from "types/types";
import { readFileSync } from "fs";
import { Reply } from "./main.js";
const helpsMin = {
"info": "このBOTについての概要を返信するコマンドです。",
"help": "コマンドの概要を返信します。追記に\`/\`抜きのコマンド名を入力することでそのコマンドの詳細(フル)を返信します。",
"follow": "コマンド送信者をフォローします。",
"unfollow": "コマンド送信者をフォロー解除します。",
"weather": "天気を返信します。",
"miq": "Make it a Quoteを生成します。",
"miq permission": "あなたに対してのMake it a Quoteを生成する要求者を制限できます。",
"miq allow": "Make it a Quoteの許可制にて生成を許可します。",
"delete": "ユーズを削除します。",
"report": "運営者に不具合などを報告します。",
"legal terms": "利用規約を返信します。",
"legal privacy": "プライバシーポリシーを返信します。",
} as { [key: string]: string };
const helpsFull = {
"info": `
このBOTについての概要を返信するコマンドです。
バージョン、開発者などが確認できます。
`,
"help": `
このコマンドです。コマンドの概要を返信します。
追記に\`/\`抜きのコマンド名を入力することでそのコマンドの詳細(フル)を返信します。
`,
"follow": `
コマンドを送信したユーザーをフォローします。
既にフォローされているユーザーも使用できます。
`,
"unfollow": `
コマンドを送信したユーザーをフォロー解除します。
既にフォローされていないユーザーも使用できます。
`,
"weather": `
天気を返信します。
毎日7:00の天気を再投稿するわけではなく、
再取得して返信します。
`,
"miq": `
Make it a Quoteを生成します。
追記に\`color: true\`と入力することでカラーモードに変更可能です。
\`/delete\`コマンドを使用して削除できます。
`,
"miq permission": `
あなたに対してのMake it a Quoteを生成する要求者を制限できます。
確認するには追記なしで実行してください。
変更するには以下のいずれかを追記に入力して実行してください。
- \`me\`: 自分自身のみになります。あなたのみが生成でき、あなた以外が生成を要求すると拒否されます。
- \`everyone\`: 全体公開になります。全てのユーザーが許可なしにあなたのユーズでMake it a Quoteを生成できます。
- \`consent\`: 許可制になります。生成を要求されるとあなたにメンションが届き許可するかを選択できます。
`,
"miq allow": `
Make it a Quoteの許可制にて生成を許可します。
形式が異なる場合生成されません。
`,
"delete": `
BOTのユーズにて返信で使用すると返信先のユーズを削除します。
`,
"report": `
不具合などを運営者にメールで報告できます。
運営者によって有効化されていないと使用できません。
\`/report\`を使用してそのユーズの追記に内容を入力することで使用できます。
`,
"legal terms": `
利用規約を返信します。
`,
"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 {
if (Object.keys(helpsFull).indexOf(data.abi) === -1) {
const ueuse = await Reply(`
不明なコマンドです。
機能を見る:${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);
}
}
}
+65
View File
@@ -0,0 +1,65 @@
import { ueuse } from "types/types";
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 === false) {
adminMail = "非公開";
} else {
adminMail = config.admin.showMail;
}
let isReport;
if (config.report.isEnabled) {
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 terms\`をご利用ください。
プライバシーポリシーをお探しですか?
\`/legal privacy\`をご利用ください。
【クレジット】
Created by Last2014${editor}
`, data.uniqid);
console.log("概要:", ueuse);
}
+7
View File
@@ -0,0 +1,7 @@
import { ueuse } from "types/types";
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";
import { Reply } from "../main.js";
import config from "../../../config.js";
export default function Terms(data: ueuse) {
Reply(config.legal.terms, data.uniqid);
}
+202
View File
@@ -0,0 +1,202 @@
import * as fs from "fs";
import config from "../../config.js";
import type { ueuse } from "types/types";
const initialFile: Array<string> = [];
// コマンド読み込み
import Info from "./info.js";
import Help from "./help.js";
import Follow from "./follow.js";
import UnFollow from "./unfollow.js";
import Weather from "./weather.js";
import Report from "./report.js";
import Terms from "./legal/terms.js"
import PrivacyPolicy from "./legal/privacy.js";
import Delete from "./delete.js";
import MakeItAQuote from "./miq/main.js";
import MiQPermission from "./miq/permission.js";
import MiQAllow from "./miq/allow.js";
// 初期化
if (!fs.existsSync("data/alreadyCommands.json")) {
fs.writeFileSync(
"data/alreadyCommands.json",
JSON.stringify(initialFile),
"utf-8",
);
}
// 対応済みユーズ一覧
const alreadyCommands: Array<string> = JSON.parse(fs.readFileSync("data/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 commandSearch(text: string) {
// /のある行を特定
const lines = text.split(/\n/);
let slashLine: number = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].indexOf("/") !== -1) {
slashLine = i;
}
}
// /がない場合は無を返答
if (slashLine === -1) {
return "";
}
// BOTのユーザーIDを取得
const userid: string = (await (await fetch(`${config.uwuzu.host}/api/me/`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
})).json()).userid;
// BOTへのメンションを削除
let slashLineText = lines[slashLine];
slashLineText = slashLineText.replace(`@${userid}`, "");
// /以降の文字を取得
slashLineText = cutAfterChar(slashLineText, "/");
// 前後の空白を削除
slashLineText = slashLineText.trimStart().trimEnd();
return slashLineText;
}
export async function Reply(text: string, reply: string) {
const req = await fetch(`${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(
"data/alreadyCommands.json",
JSON.stringify(alreadyCommands),
"utf-8",
);
}
export default async function Commands() {
const mentionsReq = await fetch(
`${config.uwuzu.host}/api/ueuse/mentions`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
cache: "no-store",
}
);
const mentions: { [key: string]: ueuse } = await mentionsReq.json();
console.log("----------------");
console.log("コマンド処理");
for (const key in mentions) {
if (mentions.hasOwnProperty(key)) {
const data = mentions[key];
// 除外ユーズ
if (data.text === undefined) {
break;
}
if (alreadyCommands.indexOf(data.uniqid) !== -1) {
break;
}
if (
data.text.charAt(0) === "!" ||
data.text.charAt(0) === "" ||
data.abi === "ignore"
) {
break;
}
if (data.text.indexOf("/") === -1) {
break;
}
// コマンド処理
console.log("--------");
const commandName = await commandSearch(data.text);
alreadyAdd(data.uniqid);
switch (commandName) {
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 "miq":
MakeItAQuote(data);
break;
case "miq permission":
MiQPermission(data);
break;
case "miq allow":
MiQAllow(data);
break;
case "delete":
Delete(data);
break;
default:
const reply = await Reply(`
不明なコマンドです。
コマンド実行を除外する場合は1文字目に\`!\`を入れてください。
`, data.uniqid);
console.log("未対応コマンド: ", reply);
break;
}
}
}
}
+130
View File
@@ -0,0 +1,130 @@
import { meApi, ueuse } from "../../../types/types";
import { readFileSync } from "fs";
import config from "../../../config.js";
import { Reply } from "../main.js";
import { Permission } from "./permission";
import MiQ from "../../../miq/main.js";
export default async function MiQAllow(data: ueuse) {
if (!config.miq) {
console.log("MiQ(管理者無効)", await Reply(`
BOT管理者によってMake it a quoteが無効化されています。
そのため\`/miq\`はご利用いただけません。
`, data.uniqid));
return;
}
// 権限一覧取得
const permissions: { [user: string]: Permission } =
JSON.parse(readFileSync("data/miqPermissions.json", "utf-8"));
if (permissions[data.account.userid] !== "consent") {
console.log("MiQ許可制(許可制以外)", await Reply(`
あなたに対してのMake it a Quoteの生成要求者が許可制に設定されていません。
そのため、\`/miq allow\`はご利用いただけません。
`, data.uniqid));
return;
}
const confirmUeuse: ueuse = (await (
await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
})
).json())["0"];
const me: meApi = await (
await fetch(`${config.uwuzu.host}/api/me`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken
}),
})
).json()
if (confirmUeuse.account.userid !== me.userid) {
console.log("MiQ許可制(誤アカウント)", await Reply(`
返信先がこのBOTではありません。
`, data.uniqid));
return;
}
if (confirmUeuse.replyid === "") {
console.log("MiQ許可制(誤ユーズ)", await Reply(`
形式が正規ではありません。
`, data.uniqid));
return;
}
const requestUeuse: ueuse = (await (
await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: confirmUeuse.replyid,
}),
})
).json())["0"];
if (requestUeuse.replyid === "") {
console.log("MiQ許可制(誤ユーズ)", await Reply(`
形式が正規ではありません。
`, data.uniqid));
return;
}
const miqUeuse: ueuse = (await (
await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: requestUeuse.replyid,
}),
})
).json())["0"];
let color: boolean;
let msg: string;
if (requestUeuse.abi === "color: true") {
msg = "カラーモードでMake it a Quoteを生成しました。";
color = true;
} else if (requestUeuse.abi === "color: false") {
msg = "モノクロモードでMake it a Quoteを生成しました。";
color = false;
} else {
msg = "ご指定がないためモノクロモードでMake it a Quoteを生成しました。";
color = false;
}
const img = await MiQ({
type: "Base64Data",
color: color,
text: miqUeuse.text,
iconURL: miqUeuse.account.user_icon,
userName: miqUeuse.account.username,
userID: miqUeuse.account.userid,
});
const req = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: msg,
image1: img,
nsfw: miqUeuse.nsfw,
replyid: data.uniqid,
}),
cache: "no-store",
});
const res = await req.json();
console.log("MiQ(許可制)", res);
}
+102
View File
@@ -0,0 +1,102 @@
import { ueuse } from "../../../types/types";
import MiQ from "../../../miq/main.js";
import config from "../../../config.js";
import { Reply } from "../main.js";
import { readFileSync, writeFileSync } from "fs";
import { Permission } from "./permission";
export default async function MakeItAQuote(data: ueuse) {
if (!config.miq) {
console.log("MiQ(管理者無効)", await Reply(`
BOT管理者によってMake it a quoteが無効化されています。
そのため\`/miq\`はご利用いただけません。
`, data.uniqid));
return;
}
let color: boolean;
let msg: string;
if (data.abi === "color: true") {
msg = "カラーモードでMake it a Quoteを生成しました。";
color = true;
} else if (data.abi === "color: false") {
msg = "モノクロモードでMake it a Quoteを生成しました。";
color = false;
} else {
msg = "ご指定がないためモノクロモードでMake it a Quoteを生成しました。";
color = false;
}
const ueuseData: ueuse = (await (
await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
})
).json())["0"];
console.log(ueuseData);
// 権限一覧取得
const permissions: { [user: string]: Permission } =
JSON.parse(readFileSync("data/miqPermissions.json", "utf-8"));
// 初期化
if (permissions[ueuseData.account.userid] === undefined) {
permissions[ueuseData.account.userid] = "consent";
writeFileSync(
"data/miqPermissions.json",
JSON.stringify(permissions),
"utf-8"
);
}
if (
permissions[ueuseData.account.userid] === "me" &&
ueuseData.account.userid !== data.account.userid
) {
console.log("MiQ(自分自身専用)", await Reply(`
生成元ユーズの投稿者が生成要求者を自分自身のみに設定しています。
しかし、あなたは投稿者自身ではないためこのユーズにMake it a Quoteを使用することはできません。
`, data.uniqid));
return;
}
if (permissions[ueuseData.account.userid] === "consent") {
console.log("MiQ(許可制)", await Reply(`
生成元ユーズの投稿者が生成要求者を許可制に設定しています。
このユーズにMake it a Quoteを使用するには、
@${ueuseData.account.userid}さんがこのユーズに返信で\`/miq allow\`を使用する必要があります。
`, data.uniqid));
return;
}
const img = await MiQ({
type: "Base64Data",
color: color,
text: ueuseData.text,
iconURL: ueuseData.account.user_icon,
userName: ueuseData.account.username,
userID: ueuseData.account.userid,
});
const req = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: msg,
image1: img,
nsfw: data.nsfw,
replyid: data.uniqid,
}),
cache: "no-store",
});
const res = await req.json();
console.log("MiQ", res);
}
+79
View File
@@ -0,0 +1,79 @@
import { ueuse } from "../../../types/types";
import { Reply } from "../main.js";
import config from "../../../config";
import { readFileSync, writeFileSync, existsSync } from "fs";
import { request } from "express";
// 初期化
const initialFile = {};
if (!existsSync("data/miqPermissions.json")) {
writeFileSync(
"data/miqPermissions.json",
JSON.stringify(initialFile),
"utf-8"
);
}
export type Permission =
"consent" |
"everyone" |
"me";
const PermissionsNames: { [name: string]: string } = {
"consent": "許可制",
"everyone": "全体公開",
"me": "自身のみ",
}
export default async function MiQPermission(data: ueuse) {
if (!config.miq) {
await Reply(`
BOT管理者によってMake it a quoteが無効化されています。
そのため\`/miq\`はご利用いただけません。
`, data.uniqid);
return;
}
const permissions: { [user: string]: string } =
JSON.parse(readFileSync("data/miqPermissions.json", "utf-8"));
// 初期化
if (permissions[data.account.userid] === undefined) {
permissions[data.account.userid] = "consent";
writeFileSync(
"data/miqPermissions.json",
JSON.stringify(permissions),
"utf-8"
);
}
if (
data.abi === "" ||
data.abi === "none"
) {
await Reply(`
あなたに対してのMake it a Quoteを生成するための権限は「${PermissionsNames[permissions[data.account.userid]]}」です。
`, data.uniqid);
return;
}
const requestPermission = data.abi.trimEnd();
if (PermissionsNames[requestPermission] === undefined) {
await Reply(`
変更希望の権限「${requestPermission}」はご指定できません。
\`consent\`\`everyone\`\`me\`のいずれかからご選択ください。
`, data.uniqid);
return;
}
permissions[data.account.userid] = requestPermission;
writeFileSync(
"data/miqPermissions.json",
JSON.stringify(permissions),
"utf-8"
);
await Reply(`
あなたに対してのMake it a Quoteを生成するための権限を「${PermissionsNames[requestPermission]}」に変更しました。
`, data.uniqid);
}
+74
View File
@@ -0,0 +1,74 @@
import { ueuse } from "types/types";
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.isEnabled) {
console.log("報告(重要通知オフ)", await Reply(`
BOTの運営者によって重要通知が無効化されています。
そのため報告機能はご利用いただけません。
`, data.uniqid));
return;
}
if (!config.emergency.mail.isEnabled) {
console.log("報告(メールオフ)", await Reply(`
BOTの運営者によってメール送信機能が無効化されています。
そのため報告機能はご利用いただけません。
`, data.uniqid));
return;
}
if (!config.report.isEnabled) {
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コマンドを利用した報告がありました。
報告元ユーズ:${config.uwuzu.host}/!${data.uniqid}
下記が内容となります。
${data.abi}
`,
});
console.log("報告(完了)", await Reply(`
報告が完了しました。
運営者は報告者、ユーズのURL、内容を確認できます。
--運営者からのメッセージ--
${config.report.message}
`, 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";
import config from "../../config.js";
import { Reply } from "./main.js";
export default async function UnFollow(data: ueuse) {
const unfollowReq = await fetch(`${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";
export default function Weather(data: ueuse) {
weatherReply(data.uniqid);
}
+240 -117
View File
@@ -1,10 +1,8 @@
import WebSocket from "ws"; import WebSocket from "ws";
import { differenceInMinutes } from "date-fns"; import sendMail from "../src/mailer.js";
import config from "../config.js"; import config from "../config.js";
let rateLimit: Date;
class P2PEarthquakeClient { class P2PEarthquakeClient {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
private reconnectInterval: number = config.earthquake.reconnectTimes; private reconnectInterval: number = config.earthquake.reconnectTimes;
@@ -20,13 +18,13 @@ class P2PEarthquakeClient {
if (this.isConnecting) return; if (this.isConnecting) return;
this.isConnecting = true; this.isConnecting = true;
console.log("P2P地震情報に接続中"); console.log("地震情報サーバーに接続中");
try { try {
this.ws = new WebSocket(config.earthquake.websocketUrl); this.ws = new WebSocket(config.earthquake.websocketUrl);
this.ws.on("open", () => { this.ws.on("open", () => {
console.log("P2P地震情報に接続しました"); console.log("地震情報サーバーに接続しました");
this.isConnecting = false; this.isConnecting = false;
if (this.reconnectTimer) { if (this.reconnectTimer) {
@@ -39,13 +37,13 @@ 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}`);
} }
}); });
this.ws.on("close", (code: number, reason: Buffer) => { this.ws.on("close", (code: number, reason: Buffer) => {
console.log(`接続が閉じられました: ${code} - ${reason.toString()}`); console.log(`切断されました: ${code} - ${reason.toString()}`);
this.isConnecting = false; this.isConnecting = false;
this.scheduleReconnect(); this.scheduleReconnect();
}); });
@@ -63,33 +61,28 @@ class P2PEarthquakeClient {
} }
private handleMessage(message: any): void { private handleMessage(message: any): void {
switch (message.code) { console.log("----------------");
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;
}
}
private executeEventFunc(earthquakeInfo: any): void { const supportCode: Array<number> = [
event(earthquakeInfo); 551,
552,
556,
];
if (supportCode.indexOf(message.code) !== -1) {
event(message);
} else {
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
console.log("受信メッセージ:", message);
}
} }
private scheduleReconnect(): void { private scheduleReconnect(): void {
if (this.reconnectTimer) return; if (this.reconnectTimer) return;
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();
@@ -99,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);
@@ -119,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);
@@ -144,41 +137,48 @@ 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 = "";
if (earthquakeInfo.earthquake.description !== "") { if (
earthquakeInfo.earthquake.description !== "" ||
earthquakeInfo.earthquake.description !== undefined
) {
descriptionEarthquake = `この地震について:${earthquakeInfo.earthquake.description}`; descriptionEarthquake = `この地震について:${earthquakeInfo.earthquake.description}`;
} }
// 発令詳細 // 発令詳細
let description: string = ""; let description: string = "";
if (earthquakeInfo.comments.freeFormComment !== "") { if (
earthquakeInfo.comments.freeFormComment !== "" ||
earthquakeInfo.comments.freeFormComment !== undefined
) {
description = `この発令について:${earthquakeInfo.comments.freeFormComment}`; description = `この発令について:${earthquakeInfo.comments.freeFormComment}`;
} }
// テスト・訓練 // テスト・訓練
let test: string = ""; let test: string = "";
if (earthquakeInfo.test) { if (earthquakeInfo.test !== undefined) {
test = "この情報にテスト・訓練かの情報はありません";
} else if (earthquakeInfo.test) {
test = "これはテスト、あるいは訓練です"; test = "これはテスト、あるいは訓練です";
} else if (earthquakeInfo.test === false) { } else if (earthquakeInfo.test === false) {
test = "これはテスト・訓練ではありません"; test = "これはテスト・訓練ではありません";
} else {
test = "この情報にテスト・訓練かの情報はありません";
} }
// 対象地域 // 対象地域
let areas: string = ""; let areas: string = "";
if (earthquakeInfo.areas !== null) { if (earthquakeInfo.areas !== undefined) {
const areaNames: Array<string> = Array.from( const areaNames: Array<string> = Array.from(
new Set( new Set(
earthquakeInfo.areas.map((point: any) => point.name).filter(Boolean), earthquakeInfo.areas.map((point: any) => point.name).filter(Boolean),
@@ -198,8 +198,8 @@ async function event(earthquakeInfo: any): Promise<void> {
let magnitude: string = "マグニチュード:"; let magnitude: string = "マグニチュード:";
if ( if (
earthquakeInfo.earthquake.hypocenter.magnitude !== -1 || earthquakeInfo.earthquake.hypocenter.magnitude != -1 ||
earthquakeInfo.earthquake.hypocenter.magnitude === null earthquakeInfo.earthquake.hypocenter.magnitude === undefined
) { ) {
magnitude += "マグニチュードの情報はありません"; magnitude += "マグニチュードの情報はありません";
} else { } else {
@@ -220,99 +220,148 @@ async function event(earthquakeInfo: any): Promise<void> {
// 地震情報 // 地震情報
else if (earthquakeInfo.code === 551) { else if (earthquakeInfo.code === 551) {
console.log("地震発生情報を受信しました");
if (
earthquakeInfo.earthquake.maxScale !== undefined &&
earthquakeInfo.earthquake.maxScale < config.earthquake.maxScaleMin
) {
console.log("最低震度に満たしていないため投稿されませんでした");
return;
}
// 国内津波 // 国内津波
let domesticTsunami; let domesticTsunami;
if (earthquakeInfo.earthquake.domesticTsunami === "None") { const TsunamiMessages = {
domesticTsunami = "この地震による国内の津波の心配はありません"; "None": "この地震による国内の津波の心配はありません",
} else if (earthquakeInfo.earthquake.domesticTsunami === "Unknown") { "Unknown": "この地震による国内の津波情報はありません",
"Checking": "この地震による国内の津波情報を調査中です",
"NonEffective": "この地震による国内の津波影響は若干の海面変動が予想されますが被害の心配はありません",
"Watch": "この地震により国内で津波注意報が発令しています",
"Warning": "この地震による国内の津波予報があります",
} as { [key: string]: string };
if (earthquakeInfo.earthquake.domesticTsunami === undefined) {
domesticTsunami = "この地震による国内の津波情報はありません"; 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 { } else {
domesticTsunami = "この地震による国内の津波情報はありません"; domesticTsunami = TsunamiMessages[earthquakeInfo.earthquake.domesticTsunami];
} }
// 最大震度 // 最大震度
let maxScale: string = "最大深度:"; let maxScale: string = "最大深度:";
if (earthquakeInfo.earthquake.maxScale < config.earthquake.maxScaleMax) { const maxScales = {
return; 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 === null 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"; if (
} else if (earthquakeInfo.earthquake.maxScale === 40) { earthquakeInfo.earthquake.maxScale !== undefined &&
maxScale += "震度4"; earthquakeInfo.earthquake.maxScale >= 60 &&
} else if (earthquakeInfo.earthquake.maxScale === 45) { config.emergency.isEnabled
maxScale += "震度5弱"; ) {
} else if (earthquakeInfo.earthquake.maxScale === 50) { console.log("----------------");
maxScale += "震度5強";
} else if (earthquakeInfo.earthquake.maxScale === 55) { console.log("震度6強以上の地震を受信しました");
maxScale += "震度6弱"; console.log("サーバーがダウンする可能性があります");
} else if (earthquakeInfo.earthquake.maxScale === 60) {
maxScale += "震度6強"; // メール送信
} else if (earthquakeInfo.earthquake.maxScale === 70) { if (config.emergency.isEnabled) {
maxScale += "震度7"; 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( const areaNames: Array<string> = Array.from(
new Set( new Set(
earthquakeInfo.points.map((point: any) => point.addr).filter(Boolean), earthquakeInfo.points.map((point: any) => point.addr).filter(Boolean),
), ),
); );
areas = `対象地域:${areaNames.join("・")}`; areas = `対象地域:${areaNames.join("・")}`;
} else {
areas = "対象地域:不明";
} }
// 詳細 // 詳細
let description: string = ""; let description;
if (earthquakeInfo.comments.freeFormComment !== "") { if (
earthquakeInfo.comments.freeFormComment !== "" &&
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 !== -1 earthquakeInfo.earthquake.hypocenter.depth !== undefined
) { ) {
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 = `深さ:${earthquakeInfo.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 !== -1 earthquakeInfo.earthquake.hypocenter.magnitude !== undefined
) { ) {
magnitude = `マグニチュード:${earthquakeInfo.earthquake.hypocenter.magnitude}`; if (earthquakeInfo.earthquake.hypocenter.magnitude === -1) {
magnitude = "マグニチュード:不明";
} else {
magnitude = `マグニチュード:M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
}
} else {
magnitude = "マグニチュード:不明";
} }
ueuse(` ueuse(`
@@ -326,54 +375,128 @@ async function event(earthquakeInfo: any): Promise<void> {
${areas} ${areas}
国内の津波:${domesticTsunami} 国内の津波:${domesticTsunami}
`); `);
} } else if (earthquakeInfo.code === 552) {
console.log("津波予報情報を受信しました");
// 地域情報更新の場合 // 予報取り消し
else if (earthquakeInfo.code === 555) { if (earthquakeInfo.cancelled) {
if (rateLimit === null) {
rateLimit = new Date();
}
// 対象地域マッピング
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 (differenceInMinutes(rateLimit, new Date()) >= config.earthquake.rateLimit) {
ueuse(` ueuse(`
==地震情報== ==地震情報==
地域情報更新 津波予報
※津波予報が取り消されました※
時刻:${earthquakeInfo.time} 時刻:${earthquakeInfo.time}
対象地域:${areas}
`); `);
rateLimit = new Date(); 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);
} }
} }
async function ueuse(text: string) { async function ueuse(text: string) {
const res = await fetch(`https://${config.uwuzuServer}/api/ueuse/create`, { const res = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: text, text: text,
}), }),
cache: "no-store",
}); });
const resData = await res.json(); const resData = await res.json();
console.log(JSON.stringify(resData)); console.log(`地震情報投稿:${JSON.stringify(resData)}`);
} }
export default function earthquakeNotice(): void { export default function earthquakeNotice(): void {
+30
View File
@@ -0,0 +1,30 @@
import { format } from "date-fns";
import eventdays from "./eventdayData.js";
import config from "../config.js";
export default async function EventDays() {
const now = format(new Date(), "MM/dd");
for (let i = 0; i < Object.keys(eventdays).length; i++) {
const day = Object.keys(eventdays)[i];
const value = Object.values(eventdays)[i];
const name = value.name;
const message = value.message;
if (day === now) {
const req = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text:
`今日は${name}です
${message}`,
}),
});
const res = await req.json();
console.log("祝日等ユーズ:", res);
}
}
}
+70
View File
@@ -0,0 +1,70 @@
interface eventdaysValue {
name: string;
message: string;
}
const eventdays = {
"01/01": {
name: "元日",
message: "はい年越した瞬間地球にいなかった~",
},
"02/11": {
name: "建国記念日",
message: "建国記念日とかいう強制休暇",
},
"04/29": {
name: "昭和の日",
message: "平成の日は???",
},
"05/03": {
name: "憲法記念日",
message: "憲法決めた日とか祝日じゃなくていいだろ",
},
"05/04": {
name: "みどりの日",
message:
`なんだよみどりの日って
`,
},
"05/05": {
name: "こどもの日",
message: "こどもの日あるならおとなの日もあっていいだろ",
},
"07/07": {
name: "七夕",
message:
`祭りでも行っとけ
Last2014は家でサーバーいじってるから`,
},
"08/11": {
name: "山の日",
message:
`空の日と山の日も作れよ
3`,
},
"11/03": {
name: "文化の日",
message: "ネットミームできるたびに休みになればいいのになぁ...",
},
"11/23": {
name: "勤労感謝の日",
message: "学生は神!!",
},
"12/24": {
name: "クリスマスイブ",
message:
`リア充爆破します
by `,
},
"12/25": {
name: "クリスマス",
message: `リア充爆破します(2回目)
by `,
},
"12/31": {
name: "大晦日",
message: "大掃除!!大掃除!!",
},
} as { [key: string]: eventdaysValue };
export default eventdays;
-40
View File
@@ -1,40 +0,0 @@
import type * as types from "types/types";
import config from "../config.js";
export default async function followBack() {
// フォロワーを取得
const resMe = await fetch(
`https://${config.uwuzuServer}/api/me?token=${config.apiToken}`,
{
method: "GET",
// uwuzu.netで/api/meのPOSTが死んでいるため簡易的にGET
},
);
const meData: types.meApi = await resMe.json();
console.log(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 { isAfter } from "date-fns";
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.isEnabled) {
// 前回の終了確認
const start = iolog.start;
const stop = iolog.stop;
if (isAfter(start, stop)) {
console.log("前回の終了が適切でない可能性があります");
if (config.emergency.mail.isEnabled) {
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();
+29 -21
View File
@@ -1,39 +1,47 @@
import { format, isAfter, isBefore } from "date-fns"; import { format } from "date-fns";
import type * as types from "types/types"; import type * as types from "types/types";
import config from "../config.js"; import config from "../config.js";
// 時刻取得
const now = new Date();
const start = config.time.stopTimes.start;
const stop = config.time.stopTimes.stop;
// 停止時刻外
let inRange: Boolean = false;
if (isBefore(start, stop)) {
inRange = isAfter(now, start) && isBefore(now, stop);
} else {
inRange = isAfter(now, start) || isBefore(now, stop);
}
export default async function timeNotice() { export default async function timeNotice() {
if (inRange === false) { // 停止時間
// 時刻取得
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( const resUeuse = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`, `${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: `${now}になりました`, text: `${format(new Date(), "HH:mm")}になりました`,
}), }),
cache: "no-store",
}, },
); );
const ueuseData: types.ueuseCreateApi = await resUeuse.json(); const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log(JSON.stringify(ueuseData)); console.log("----------------");
console.log(`時報投稿:${JSON.stringify(ueuseData)}`);
} }
} }
+16 -9
View File
@@ -1,30 +1,36 @@
import { cityList } from "../src/weatherId.js"; import { cityList } from "../src/weatherId.js";
import type * as types from "types/types.js"; import type * as types from "types/types";
import config from "../config.js"; import config from "../config.js";
export default async function weatherNotice() { export async function weatherNotice() {
console.log("----------------");
// 仮投稿 // 仮投稿
const resUeuse = await fetch( const resUeuse = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`, `${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: ` text: `
`, `,
}), }),
cache: "no-store",
}, },
); );
const ueuseData: types.ueuseCreateApi = await resUeuse.json(); const ueuseData: types.ueuseCreateApi = await resUeuse.json();
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;
@@ -101,19 +107,20 @@ export default async function weatherNotice() {
// 分割投稿 // 分割投稿
for (let i = 0; i < splitCount; i++) { for (let i = 0; i < splitCount; i++) {
const resReply = await fetch( const resReply = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`, `${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.apiToken, token: config.uwuzu.apiToken,
text: weatherResults[i], text: weatherResults[i],
replyid: ueuseData.uniqid replyid: 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)}`);
} }
} }
+64
View File
@@ -0,0 +1,64 @@
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() {
if (
config.emergency.isEnabled &&
config.emergency.mail.isEnabled
) {
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> {
if (
config.emergency.isEnabled &&
config.emergency.mail.isEnabled
) {
try {
const transporter: any = 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;
}
} else {
return;
}
}
+6 -5
View File
@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2022", "target": "ES2024",
"module": "ES2022", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "node",
"outDir": "./dist", "outDir": "./dist",
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": 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,
},
} }
+64 -6
View File
@@ -2,8 +2,7 @@ interface earthquakeTypes {
reconnectTimes: number; reconnectTimes: number;
websocketUrl: string; websocketUrl: string;
areasCsvUrl: string; areasCsvUrl: string;
maxScaleMax: number; maxScaleMin: number;
rateLimit: number;
} }
interface weatherTypes { interface weatherTypes {
@@ -11,19 +10,78 @@ interface weatherTypes {
} }
interface stopsTypes { interface stopsTypes {
start: Date; start: number;
stop: Date; stop: number;
} }
interface timeTypes { interface timeTypes {
stopTimes: stopsTypes; stopTimes: stopsTypes;
} }
interface emergencyMailFullTypes {
isEnabled: true;
host: string;
port: number;
user: string;
password: string;
secure: boolean;
to: string | string[];
}
interface emergencyMailMinTypes {
isEnabled: false;
mail: undefined;
}
interface emergencyFullTypes {
isEnabled: true;
mail: emergencyMailFullTypes | emergemcyMailMinTypes;
}
interface emergencyMinTypes {
isEnabled: false;
}
interface reportTypes {
isEnabled: boolean;
message: string;
}
interface legalTypes {
terms: string;
privacy: string;
}
interface PanelFullTypes {
isEnabled: true;
port: number;
}
interface PanelMinTypes {
isEnabled: false;
}
interface adminTypes {
name: string;
showMail: string | false;
panel: PanelFullTypes | PanelMinTypes;
}
interface uwuzuTypes {
apiToken: string;
host: string;
}
export interface configTypes { export interface configTypes {
time: timeTypes, time: timeTypes,
earthquake: earthquakeTypes; earthquake: earthquakeTypes;
weather: weatherTypes; weather: weatherTypes;
miq: boolean;
apiToken: string; emergency: emergencyFullTypes | emergencyMinTypes;
uwuzuServer: string; report: reportTypes;
legal: legalTypes;
admin: adminTypes;
uwuzu: uwuzuTypes;
debug?: true;
} }
+33 -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,31 @@ export interface meApi {
language: String; language: String;
} }
export interface ueuse {
uniqid: string;
replyid: string;
reuseid: string;
text: string;
account: {
username: string;
userid: string;
user_icon: string;
user_header: string;
is_bot: boolean;
};
photo1: string;
photo2: string;
photo3: string;
photo4: string;
video1: string;
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;
@@ -31,3 +56,9 @@ export interface ueuseCreateApi {
export interface followApi { export interface followApi {
userid: string; userid: string;
} }
export interface NetworkInterfaceDetails {
family: string;
internal: boolean;
address: string;
}