Compare commits

..

2 Commits

39 changed files with 27693 additions and 1137 deletions
+5 -8
View File
@@ -1,8 +1,5 @@
/dist/
/.env*
/node_modules/
/package-lock.json
/config.ts
log*
/iolog.json
/node_modules
/dist
/memory.json
/config/**
!/config/example.yaml
+24841
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
# 2026.4.0-alpha.0
- Breaking: noticeUwuzuを0から作り直しました
- Breaking: 25.12.0-alpha.1までの方針を全て廃止しました
- Chg: パッケージマネージャーをnpmからpnpmへ変更しました
- Chg: 全コードをsrcへ移動しました
- Chg: ユーズのメッセージをyamlとi18nextで管理することで先頭と末尾の改行のある問題などに対策しました
- Chg: 全ての保存データをmemory.jsonとして保管し、memory.tsを実装しました
- Note: 以下のログは全て重要な変更以外の再実装として判断しています
- Feat: ユーザーは以下のコマンドを使用することができます。
- `/weather`: 天気予報を取得できます。
- `/help`: コマンドの利用方法などを取得できます。
- `/miq`: Make it a Quoteを操作できます。
- `/follow`: Botからフォローされます。
- `/unfollow`: Botからフォロー解除されます。
- Feat: 地震情報が利用できます。以下の情報を投稿します。
- 地震発生情報
- 津波予報
- 緊急地震速報(警報)
- Feat: 時報が利用できます。毎時0分に投稿します。
- Feat: 天気予報を利用できます。毎日7:00に投稿します。
-13
View File
@@ -1,13 +0,0 @@
Copyright 2025 Last2014
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+27 -34
View File
@@ -1,36 +1,29 @@
# uwuzuお知らせBOT
# noticeUwuzu
[uwuzu](https://www.uwuzu.com)向けのお知らせBotです。
uwuzu v1.6.3以上で利用可能です。
# uwuzuお知らせBOTについて
## 機能
- 時報
毎時0分に「h:00になりました。」と投稿します。
- 天気予報
[天気予報 APIlivedoor 天気互換)](https://weather.tsukumijima.net/)を利用して、毎日7:00に天気予報を投稿します。
- 地震情報
[P2P地震情報](https://www.p2pquake.net/)のWebSocket APIを利用して、以下の情報を受信した際に投稿します。
- 地震発生
- 緊急地震速報(警報)
- 津波予報
- デバッグモード
設定で有効化することで、以下の機能が利用できます。
開発以外では使用しないでください。
- NODE_TLS_REJECT_UNAUTHORIZED=1
暗号化通信を無効化します。
- 地震情報のWebSocketの接続先をテスト用サンドボックスに変更
P2P地震情報の接続先を本番環境から[テスト用サンドボックスのエンドポイント](https://www.p2pquake.net/develop/json_api_v2/)に変更します。
そのため、最新の情報は配信されません。
- i18nextのdebugを有効化
[i18nextでのdebug](https://www.i18next.com/overview/configuration-options#logging)が有効化されます。
uwuzuで動作するお知らせBOTです。
# 設定
examples/config.tsをプロジェクトルートへ移動し各設定を更新してください。
## 設定項目
time.stopTimes.start: 時報休止期間の開始時刻(HH)
time.stopTimes.stop: 時報休止期間の停止時刻(HH)
earthquake.reconnectTimes:地震情報のWebSocketが切断されたときに自動再接続する時間(ミリ秒)
earthquake.websocketUrl:地震情報のWebSocket接続先URL
earthquake.areasCsvUrl:地域情報のデータベース(CSV)ファイルのURL
earthquake.maxScaleMin: 地震発生投稿の最低震度(10-70)
earthquake.rateLimit: 地域情報更新のレート制限(分)
weather.splitCount:天気お知らせの返信の分割数(4分割を推奨)
apiTokenBOTアカウントのAPIキー
uwuzuServer:使用するuwuzuサーバーのホスト名(uwuzu.netなど)
# サーバー起動
```
npm install
npm run build
npm run start
```
※Node.js・npmがインストールされている必要があります。
# ライセンス
Apache 2.0 License
## 260420.jsonについて
このデータは2026年4月20日に三陸で発生した地震の後21件を記録したものです。
P2P地震情報APIのhistoryで取得しました。
検証などにご利用ください。
+5 -5
View File
@@ -1,5 +1,5 @@
# # ###### ##### ### ###### ####### # # # # # # ###### # #
## # # # # # # # # # # # # # # # # #
# ## # # # # # # ####### # # # # # # # # ## # #
# ## # # # # # # # # # # # # # # #
# # ###### # ### ###### ####### ###### # # ###### ###### ######
# # ###### ##### ### ###### ####### # # # # # # ###### # #
## # # # # # # # # # # # # # # # # #
# ## # # # # # # ####### # # # # # # # # ## # #
# ## # # # # # # # # # # # # # # #
# # ###### # ### ###### ####### ###### # # ###### ###### ######
+10
View File
@@ -0,0 +1,10 @@
command:
interval: 10
weather:
splits: 4
earthquake:
useHistoryData: false
uwuzu:
token: API_TOKEN
origin: https://uwuzu.example.com
debug: false
-43
View File
@@ -1,43 +0,0 @@
import type { configTypes } from "types/config";
// READMEの設定項目を参照
const config: configTypes = {
// 時報設定
time: {
// 時報休止期間
stopTimes: {
start: 23, // 開始
stop: 6, // 停止
}
},
// 地震速報設定
earthquake: {
reconnectTimes: 5000, // 再接続時間(ミリ秒)
websocketUrl: "wss://api.p2pquake.net/v2/ws", // WebSocketのURL
areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", // 対象地域CSVファイルのURL
maxScaleMin: 30, // 地震発生の際の最低震度(10-70)
rateLimit: 30, // 地域情報更新のレート制限(分)
},
weather: {
splitCount: 4, // 返信の分割数
},
// 緊急時設定
emergency: {
function: true, // 緊急時のコンソール表示
mail: {
function: true, // 緊急時のメール送信
host: "smtp.example.com", // SMTPサーバー
port: 465, // SMTPポート
user: "mailUser@example.com", // BOTメール送信元
password: "mailPassword", // SMTPパスワード
secure: false, // SMTPsecure設定
to: "admin@noticeuwuzu.example.com" // 緊急時メール送信先(配列可)
}
},
apiToken: "TOKEN_EXAMPLE", // BOTアカウントのAPIトークン
uwuzuServer: "uwuzu.example.com", // uwuzuのサーバー
};
export default config;
+125
View File
@@ -0,0 +1,125 @@
timeNotice: "{{ time }}になりました。"
weatherProvisional: |
本日の天気
※タイムラインが埋まるため返信に記載しています。
weatherReply: |
【{{ city }}】
天気: {{ weather }}
最高気温: {{ maxTemp }}
最低気温: {{ minTemp }}
降水確率: {{ chanceOfRain }}
earthquakeNotice: |
### ==地震発生==
⏰時刻: {{ occuredTime }}
🫨最大震度: {{ maxScale }}
📍震源地: {{ epicenter }}
💪マグニチュード: {{ magnitude }}
🪨深さ: {{ depth }}
{{ domesticTsunami }}
{{ foreignTsunami }}{{ points }}
{{ comment }}
🔬情報源: P2P地震速報 - {{ source }}
tsunamiAreaMsg: |
【{{ name }}】{{ immediate }}
🏷️種別: {{ grade }}
⏳第1波到達予想時刻: {{ arrivalTime }}
🌊第1波の状態: {{ condition }}
🗼予想される津波の高さ: {{ maxHeight }}
tsunamiForecastNotice: |
### ==津波予報**発表**==
⏰発表時刻: {{ announceTime }}
{{ areasMsg }}
🔬情報源: P2P地震速報 - {{ source }}
tsunamiCancelNotice: |
### ==津波予報**解除**==
⏰発表時刻: {{ announceTime }}
🔬情報源: P2P地震速報 - {{ source }}
eewAreaMsg: |
【{{ name }}】
🫨最大予測震度: {{ maxScale }}
⏰到達予想時刻: {{ arrivalTime }}
{{ kind }}
eewNotice: |
### ==***緊急地震速報(警報)***==
{{ isTest }}{{isAssume}}
⏰発表時刻: {{ announceTime }}
⏰地震発生時刻: {{ occuredTime }}
⏰地震発現時刻: {{ arrivalTime }}
📍震源地: {{ epicenter }}
💪マグニチュード: {{ magnitude }}
🪨深さ: {{ depth }}{{ areas }}
commandNotFound: |
コマンドが本文から参照できませんでした。
Botでは、このアカウントに対してのメンション部分を取り除きます。
その後、各行の先頭と末尾のスペースを削除します。
その上で、`/`から始まる最初の行を検索します。
検索結果をコマンドとして認識します。
上記の条件に当てはまる文法で再度お試しください。
unknownCommand: |
`/{{ command }}`は不明なコマンドです。
スペルミスや、既に削除されたコマンドである可能性があります。
`/help`を使用することで、コマンドのヘルプを確認できます。
invalidOption: |
{{ option }}は無効なオプションです。
`/help {{ command }}`を確認してください。
lackOption: |
オプションが不足しています。
`/help {{ command }}`を確認してください。
helpHelp: |
コマンドの概要を返信します。
オプション1にコマンド名(`/`を除く)を入力することで、詳細を返信します。
helpFollow: コマンド送信者をフォローします。
helpUnfollow: コマンド送信者をフォロー解除します。
helpWeather: 天気を返信します。
helpMiq: |
Make it a Quoteを操作します。
`/help miq`で詳細を確認することを推奨します。
fullHelpHelp: |
コマンドの概要を返信します。
オプション1にコマンド名(`/`を除く)を入力することで、詳細を返信します。
fullHelpFollow: |
コマンドを送信したユーザーをフォローします。
既にフォローされているユーザーが使用した場合は、応答されません。
fullHelpUnfollow: |
コマンドを送信したユーザーをフォロー解除します。
既にフォローされていないユーザーが使用した場合は、応答されません。
fullHelpWeather: |
天気を返信します。
毎日7:00の天気を再投稿するわけではなく、
再取得して返信します。
fullHelpMiq: |
Make it a Quoteを操作します。
オプション1が必須です。
`/miq generate`で、コマンドを送信したユーズの返信元のユーズ内容を使用して、Make it a Quoteを生成できます。
オプション2にcolorを指定することで、(`/miq generate color`)カラーモードで生成できます。
`/miq permission`で、コマンド送信者に対してのMake it a Quoteを生成する要求者の制限を変更、確認できます。
確認する場合は、オプション2は不要です。
変更するには以下のいずれかをオプション2として指定してください。(例: `/miq permission me`)
- `me`: 自分自身のみになります。あなたのみが生成でき、あなた以外が生成を要求すると拒否されます。
- `everyone`: 全体公開になります。全てのユーザーが許可なしにあなたのユーズでMake it a Quoteを生成できます。
- `consent`: 許可制になります。生成を要求されるとあなたにメンションが届き許可するかを選択できます。
`/miq allow`で、`/miq permission`で設定されている制限が`consent`の場合にMake it a Quoteの生成を許可できます。
形式が異なる場合には生成されません。
followedNotification: "{{ username }}さんをフォローしました。"
unfollowedNotification: "{{ username }}さんをフォロー解除しました。"
replySourceFailed: 返信元のユーズの取得に失敗しました。
miqPermissionMe: |
生成を要求したユーズの投稿者が生成要求者を自分自身のみに設定しています。
しかし、あなたは投稿者自身ではないため、Make it a Quoteを使用することはできません。
miqPermissionConsent: |
生成を要求したユーズの投稿者が生成要求者を許可制に設定しています。
そのため、Make it a Quoteを使用するには、@{{ userid }}さんがこのユーズに返信で`/miq allow`を使用する必要があります。
miqGenerateFailed: |
Make it a Quoteの生成に失敗しました。
また後でお試しください。
miqSuccess: "{{ message }}"
permissionResponse: あなたのMake it a Quoteの生成要求者の制限は、{{ permission }}です。
permissionChangeSuccess: "{{ username }}さんのMake it a Quoteの生成要求者の制限を{{ permission }}に変更しました。"
injusticeFormat: 不正な形式です。
permisionIsNotConsent: |
あなたに対してのMake it a Quoteの生成要求者が許可制に設定されていません。
そのため、`/miq allow`はご利用いただけません。
replySourceIsNotThis: 返信元のユーザーがこのBotではありません。
replySourceIsNotSourceUser: ソースのユーズと`/miq allow`を使用したユーザーが一致しません。
-37
View File
@@ -1,37 +0,0 @@
// 定期実行読み込み
import * as cron from "node-cron";
// 機能読み込み
import timeNotice from "./scripts/timeNotice.js";
import weatherNotice from "./scripts/weatherNotice.js";
import earthquakeNotice from "./scripts/earthquakeNotice.js";
// アスキーアート読み込み
import asciiArt from "./scripts/asciiart.js";
asciiArt();
// フォローバック機能読み込み
import followBack from "./scripts/followBack.js";
// 正常終了確認読み込み
import successExit from "./scripts/successExit.js";
successExit();
// 地震情報観測開始
earthquakeNotice();
// 時報・フォローバック(毎時)
cron.schedule("0 * * * *", () => {
timeNotice();
followBack();
});
// 天気お知らせ(毎日7:01)
cron.schedule("1 7 * * *", () => {
setTimeout(() => {
weatherNotice();
}, 100);
});
// コンソールで表示
console.log("BOTサーバーが起動しました");
+22 -34
View File
@@ -1,45 +1,33 @@
{
"name": "noticeuwuzu",
"version": "v5.0@uwuzu1.5.4",
"description": "uwuzu Notice Bot",
"main": "dist/main.js",
"name": "notice-uwuzu",
"version": "26.4.0-alpha.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"start": "node .",
"build": "tsc",
"main": "tsc && node .",
"dev": "tsx main.ts"
"build": "tsc && tsc-alias"
},
"keywords": [
"uwuzu",
"bot",
"cron",
"notice",
"weather",
"time",
"earthquake"
],
"author": {
"name": "Last2014",
"url": "https://last2014.com",
"email": "info@last2014.com"
"email": "info@last2014.com",
"url": "https://about.last2014.com"
},
"license": "Apache-2.0",
"type": "module",
"packageManager": "pnpm@10.33.0",
"dependencies": {
"@types/date-fns": "^2.5.3",
"@types/dotenv": "^6.1.1",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17",
"@types/node": "^25.5.2",
"@types/ws": "^8.18.1",
"better-uwuzu-sdk": "git+https://gitea.last2014.com/last2014/better-uwuzu-sdk.git#1.1.7",
"date-fns": "^4.1.0",
"fs": "^0.0.1-security",
"node-cron": "^4.1.1",
"nodemailer": "^7.0.4",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"ws": "^8.18.3"
},
"devDependencies": {
"@types/node": "^24.0.7",
"@types/ws": "^8.18.1"
"fs": "0.0.1-security",
"i18next": "^26.0.3",
"miq": "git+https://gitea.last2014.com/last2014/miq.git#1.0.1",
"node-cron": "^4.2.1",
"os": "^0.1.2",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3",
"util": "^0.12.5",
"ws": "^8.20.0",
"yaml": "^2.8.3",
"zod": "^4.3.6"
}
}
+1293
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
onlyBuiltDependencies:
- "better-uwuzu-sdk"
- canvas
- esbuild
- "miq"
- sharp
-5
View File
@@ -1,5 +0,0 @@
import * as fs from "fs";
export default function asciiArt() {
console.log(fs.readFileSync("asciiart.txt", "utf-8"));
}
-425
View File
@@ -1,425 +0,0 @@
import WebSocket from "ws";
import { differenceInMinutes, subMinutes } from "date-fns";
import sendMail from "../src/mailer.js";
import config from "../config.js";
let rateLimit: Date | null = null;
class P2PEarthquakeClient {
private ws: WebSocket | null = null;
private reconnectInterval: number = config.earthquake.reconnectTimes;
private reconnectTimer: NodeJS.Timeout | null = null;
private isConnecting: boolean = false;
public start(): void {
this.connect();
this.setupCleanup();
}
private connect(): void {
if (this.isConnecting) return;
this.isConnecting = true;
console.log("地震情報サーバーに接続中");
try {
this.ws = new WebSocket(config.earthquake.websocketUrl);
this.ws.on("open", () => {
console.log("地震情報サーバーに接続しました");
this.isConnecting = false;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
});
this.ws.on("message", (data: WebSocket) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(message);
} catch (error) {
console.error("メッセージのパースに失敗:", error);
}
});
this.ws.on("close", (code: number, reason: Buffer) => {
console.log(`切断されました: ${code} - ${reason.toString()}`);
this.isConnecting = false;
this.scheduleReconnect();
});
this.ws.on("error", (error: Error) => {
console.error("WebSocketエラー:", error);
this.isConnecting = false;
this.scheduleReconnect();
});
} catch (error) {
console.error("接続エラー:", error);
this.isConnecting = false;
this.scheduleReconnect();
}
}
private handleMessage(message: any): void {
console.log("----------------");
switch (message.code) {
case 551: // 地震情報
console.log("地震情報を受信しました");
this.executeEventFunc(message);
break;
case 554: // 緊急地震速報
console.log("緊急地震速報を受信しました");
this.executeEventFunc(message);
break;
case 555: // 地域情報更新情報
console.log("地域情報更新を受信しました");
this.executeEventFunc(message);
break;
default:
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
break;
}
}
private executeEventFunc(earthquakeInfo: any): void {
event(earthquakeInfo);
}
private scheduleReconnect(): void {
if (this.reconnectTimer) return;
console.log("地震情報サーバーから切断されました");
console.log(`${this.reconnectInterval / 1000}秒後に再接続を試みます`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, this.reconnectInterval);
}
private setupCleanup(): void {
const cleanup = () => {
this.stop();
process.exit(0);
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
}
public stop(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
// 地名オブジェクトマッピング
async function areaMap(): Promise<Record<number, string>> {
const res = await fetch(config.earthquake.areasCsvUrl);
const text = await res.text();
const lines = text.split("\n");
const map: Record<number, string> = {};
for (const line of lines) {
const cols = line.split(",");
if (cols.length >= 3 && /^\d+$/.test(cols[0])) {
const id = Number(cols[0]);
const name = cols[2].trim();
map[id] = name;
}
}
return map;
}
// 情報受信
async function event(earthquakeInfo: any): Promise<void> {
console.log(`受信データ:${JSON.stringify(earthquakeInfo)}`);
// ----処理----
// 緊急地震速報の場合
if (earthquakeInfo.code === 554) {
// 地震詳細
let descriptionEarthquake: string = "";
if (earthquakeInfo.earthquake.description !== "") {
descriptionEarthquake = `この地震について:${earthquakeInfo.earthquake.description}`;
}
// 発令詳細
let description: string = "";
if (earthquakeInfo.comments.freeFormComment !== "") {
description = `この発令について:${earthquakeInfo.comments.freeFormComment}`;
}
// テスト・訓練
let test: string = "";
if (earthquakeInfo.test) {
test = "これはテスト、あるいは訓練です";
} else if (earthquakeInfo.test === false) {
test = "これはテスト・訓練ではありません";
} else {
test = "この情報にテスト・訓練かの情報はありません";
}
// 対象地域
let areas: string = "";
if (earthquakeInfo.areas !== null) {
const areaNames: Array<string> = Array.from(
new Set(
earthquakeInfo.areas.map((point: any) => point.name).filter(Boolean),
),
);
areas = `対象地域:${areaNames.join("・")}`;
}
// 速報取り消し
let cancelled: string = "";
if (earthquakeInfo.cancelled) {
cancelled = "※以下の緊急地震速報が取り消されました※";
}
// マグニチュード
let magnitude: string = "マグニチュード:";
if (
earthquakeInfo.earthquake.hypocenter.magnitude !== -1 ||
earthquakeInfo.earthquake.hypocenter.magnitude === null
) {
magnitude += "マグニチュードの情報はありません";
} else {
magnitude += `M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
}
ueuse(`
==地震情報==
【緊急地震速報】
${cancelled}
時刻:${earthquakeInfo.time}
${descriptionEarthquake}
${description}
${test}
${areas}
`);
}
// 地震情報
else if (earthquakeInfo.code === 551) {
// 国内津波
let domesticTsunami;
if (earthquakeInfo.earthquake.domesticTsunami === "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 = "この地震による国内の津波予報があります";
} else {
domesticTsunami = "この地震による国内の津波情報はありません";
}
// 最大震度
let maxScale: string = "最大深度:";
if (earthquakeInfo.earthquake.maxScale < config.earthquake.maxScaleMin) {
console.log("最低震度に満たしていないため投稿されませんでした");
return;
}
if (
earthquakeInfo.earthquake.maxScale === -1 ||
earthquakeInfo.earthquake.maxScale === null
) {
maxScale = "最大震度情報なし";
} else if (earthquakeInfo.earthquake.maxScale === 10) {
maxScale += "震度1";
} else if (earthquakeInfo.earthquake.maxScale === 20) {
maxScale += "震度2";
} else if (earthquakeInfo.earthquake.maxScale === 30) {
maxScale += "震度3";
} else if (earthquakeInfo.earthquake.maxScale === 40) {
maxScale += "震度4";
} else if (earthquakeInfo.earthquake.maxScale === 45) {
maxScale += "震度5弱";
} else if (earthquakeInfo.earthquake.maxScale === 50) {
maxScale += "震度5強";
} else if (earthquakeInfo.earthquake.maxScale === 55) {
maxScale += "震度6弱";
} else if (earthquakeInfo.earthquake.maxScale === 60) {
maxScale += "震度6強";
} else if (earthquakeInfo.earthquake.maxScale === 70) {
maxScale += "震度7";
}
// 警告
if (
earthquakeInfo.earthquake.maxScale >= 60 ||
config.emergency.function
) {
console.log("----------------");
console.log("震度6強以上の地震を受信しました");
console.log("サーバーがダウンする可能性があります");
// メール送信
if (config.emergency.function) {
sendMail({
from: "noticeUwuzu自動送信",
to: config.emergency.mail.to,
subject: "【警告】震度6強以上の地震を受信しました",
html: `
※noticeUwuzu自動送信によるメールです。
【警告】
BOT管理者さん、noticeUwuzu自動送信メールです。
震度6強以上の地震を受信したため警告メールが送信されました。
物理、システム的にサーバーがダウンする可能性があります。
ご自身の身をお守りください。
`
});
console.log("管理者へ警告メールを送信しました");
}
console.log("----------------");
}
// 対象地域
let areas: string = "";
if (earthquakeInfo.points !== null) {
const areaNames: Array<string> = Array.from(
new Set(
earthquakeInfo.points.map((point: any) => point.addr).filter(Boolean),
),
);
areas = `対象地域:${areaNames.join("・")}`;
}
// 詳細
let description: string = "";
if (earthquakeInfo.comments.freeFormComment !== "") {
description = `この地震について:${earthquakeInfo.comments.freeFormComment}`;
}
// 深さ
let depth: string = "";
if (
earthquakeInfo.earthquake.hypocenter.depth !== null ||
earthquakeInfo.earthquake.hypocenter.depth !== undefined ||
earthquakeInfo.earthquake.hypocenter.depth !== -1
) {
if (earthquakeInfo.earthquake.hypocenter.depth === 0) {
depth = "深さ:ごく浅い";
} else {
depth = `深さ:${earthquakeInfo.hypocenter.depth}km`;
}
}
// マグニチュード
let magnitude: string = "";
if(
earthquakeInfo.earthquake.hypocenter.magnitude !== null ||
earthquakeInfo.earthquake.hypocenter.magnitude !== undefined ||
earthquakeInfo.earthquake.hypocenter.magnitude !== -1
) {
magnitude = `マグニチュード:${earthquakeInfo.earthquake.hypocenter.magnitude}`;
}
ueuse(`
==地震情報==
【地震発生】
時刻:${earthquakeInfo.time}
${description}
${magnitude}
${depth}
${maxScale}
${areas}
国内の津波:${domesticTsunami}
`);
}
// 地域情報更新の場合
else if (earthquakeInfo.code === 555) {
if (rateLimit === null) {
rateLimit = subMinutes(new Date(), config.earthquake.rateLimit + 15);
}
// 対象地域マッピング
const areaMaps: any = await areaMap();
const areaNames: Array<string> = Array.from(
new Set(
earthquakeInfo.areas
.map((i: any) => {
return areaMaps[i.id];
})
.filter(Boolean),
),
);
const areas = areaNames.join("・");
if (Math.abs(differenceInMinutes(rateLimit, new Date())) >= config.earthquake.rateLimit) {
ueuse(`
==地震情報==
【地域情報更新】
時刻:${earthquakeInfo.time}
対象地域:${areas}
`);
rateLimit = new Date();
} else {
console.log("レート制限に満たしていないため投稿されませんでした");
return;
}
}
}
async function ueuse(text: string) {
const res = await fetch(`https://${config.uwuzuServer}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.apiToken,
text: text,
}),
});
const resData = await res.json();
console.log(`地震情報投稿:${JSON.stringify(resData)}`);
}
export default function earthquakeNotice(): void {
console.log("地震情報サーバーに接続します");
const client = new P2PEarthquakeClient();
client.start();
}
-42
View File
@@ -1,42 +0,0 @@
import type * as types from "types/types";
import config from "../config.js";
export default async function followBack() {
console.log("----------------");
// フォロワーを取得
const resMe = await fetch(
`https://${config.uwuzuServer}/api/me?token=${config.apiToken}`,
{
method: "GET",
// uwuzu v1.5.4で/api/meのPOSTが死んでいるため簡易的にGET
},
);
const meData: types.meApi = await resMe.json();
console.log(`BOTプロフィール:${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.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)}`);
}
}
-64
View File
@@ -1,64 +0,0 @@
import * as fs from "fs";
import { format, isAfter } from "date-fns";
import { parse } from "date-fns/fp";
import config from "../config.js";
import sendMail from "../src/mailer.js";
const formatParse = parse(new Date(), "yyyy-MM-dd HH:mm:ss.SSS")
// 初期化
if (fs.existsSync("iolog.json") === false) {
fs.writeFileSync("iolog.json", JSON.stringify({
start: format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"),
stop: "",
}), "utf-8");
}
export default function successExit() {
const iolog = JSON.parse(fs.readFileSync("iolog.json", "utf-8"));
if (config.emergency.function) {
// 前回の終了確認
const start = formatParse(iolog.start);
const stop = formatParse(iolog.stop);
if (isAfter(start, stop)) {
console.log("前回の終了が適切でない可能性があります");
if (config.emergency.mail.function) {
sendMail({
from: "noticeUwuzu自動送信",
to: config.emergency.mail.to,
subject: "【警告】前回終了が不適切な可能性",
html: `
※noticeUwuzu自動送信によるメールです。
【警告】
BOT管理者さん、noticeUwuzu自動送信メールです。
BOTの前回終了で不適切なデータを検出しました。
これは適切な終了時にはデータを残しデータがない場合に送信されます。
電源を強制的に遮断するなどの行為による可能性があります。
その場合は今後やめ、OSからのシャットダウンを使用してください。
BOTのプログラムが破損していないかご確認ください。
`
});
}
console.log("----------------");
}
}
// 起動時に起動時刻を保存
iolog.start = format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
fs.writeFileSync("iolog.json", JSON.stringify(iolog), "utf-8");
// 終了時に終了時刻を保存
process.on("exit", () => {
const iolog = JSON.parse(fs.readFileSync("iolog.json", "utf-8"));
iolog.stop = format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
fs.writeFileSync("iolog.json", JSON.stringify(iolog), "utf-8");
});
}
successExit();
-46
View File
@@ -1,46 +0,0 @@
import { format } from "date-fns";
import type * as types from "types/types";
import config from "../config.js";
export default async function timeNotice() {
// 停止時間
// 時刻取得
const start = config.time.stopTimes.start;
const stop = config.time.stopTimes.stop;
// 現在の時間を取得
const nowHour = new Date().getHours();
// 停止時刻内かどうかの判定
let inRange: boolean = false;
if (start < stop) {
inRange = nowHour >= start && nowHour < stop;
} else {
inRange = nowHour >= start || nowHour < stop;
}
if (inRange) {
console.log("----------------");
console.log("時報休止期間のため投稿されませんでした");
return;
} else {
// 投稿
const resUeuse = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`,
{
method: "POST",
body: JSON.stringify({
token: config.apiToken,
text: `${format(new Date(), "HH:mm")}になりました`,
}),
},
);
const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log("----------------");
console.log(`時報投稿:${JSON.stringify(ueuseData)}`);
}
}
-121
View File
@@ -1,121 +0,0 @@
import { cityList } from "../src/weatherId.js";
import type * as types from "types/types.js";
import config from "../config.js";
export default async function weatherNotice() {
console.log("----------------");
// 仮投稿
const resUeuse = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`,
{
method: "POST",
body: JSON.stringify({
token: config.apiToken,
text: `
本日の天気
※タイムラインが埋まるため返信に記載しています
`,
}),
},
);
const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log(`天気仮投稿:${JSON.stringify(ueuseData)}`);
// インデックス
const splitCount = config.weather.splitCount;
const total = cityList.length;
const chunkSizes = Array(splitCount).fill(0).map((_, i) =>
Math.floor((total + i) / splitCount)
);
// 分割インデックス
let start = 0;
const ranges = chunkSizes.map(size => {
const range = [start, start + size];
start += size;
return range;
});
// 配列作成
const weatherResults = Array(splitCount).fill("");
// 天気取得
for (let chunkIndex = 0; chunkIndex < splitCount; chunkIndex++) {
const [chunkStart, chunkEnd] = ranges[chunkIndex];
for (let i = chunkStart; i < chunkEnd; i++) {
const res = await fetch(
`https://weather.tsukumijima.net/api/forecast/city/${cityList[i]}`,
);
const data = await res.json();
const today = data.forecasts[0];
// 天気
const weather = today.telop ?? "取得できませんでした";
// 最高気温
let maxTemp: string;
if (today.temperature.max.celsius !== null) {
maxTemp = `${today.temperature.max.celsius}`;
} else {
maxTemp = "取得できませんでした";
}
// 最低気温
let minTemp: string;
if (today.temperature.min.celsius !== null) {
minTemp = `${today.temperature.min.celsius}`;
} else {
minTemp = "取得できませんでした";
}
// 降水確率
let chanceOfRain: string;
if (
today.chanceOfRain.T06_12 !== null ||
today.chanceOfRain.T06_12 !== "--%"
) {
chanceOfRain = today.chanceOfRain.T06_12;
} else {
chanceOfRain = "取得できませんでした";
}
weatherResults[chunkIndex] += `
${data.location.city}
天気:${weather}
最高気温:${maxTemp}
最低気温:${minTemp}
降水確率:${chanceOfRain}
`;
}
}
// 分割投稿
for (let i = 0; i < splitCount; i++) {
const resReply = await fetch(
`https://${config.uwuzuServer}/api/ueuse/create`,
{
method: "POST",
body: JSON.stringify({
token: config.apiToken,
text: weatherResults[i],
replyid: ueuseData.uniqid
}),
},
);
const replyData: types.ueuseCreateApi = await resReply.json();
console.log(`天気投稿:${JSON.stringify(replyData)}`);
}
}
+28
View File
@@ -0,0 +1,28 @@
import client from "@/lib/client";
import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse";
import i18next from "i18next";
export default async function followCommand(ueuse: ueuseModule) {
const follow = await client.request("users/follow", {
userid: ueuse.account.userid,
});
if (!follow.success) {
console.warn("フォローに失敗:", follow.error_code);
return;
}
console.log("フォロー:", follow.userid);
const notice = await client.request("ueuse/create", {
text: i18next.t("followedNotification", { username: ueuse.account.username }),
replyid: ueuse.uniqid,
});
if (!notice.success) {
console.warn("フォロー通知に失敗:", notice.error_code);
return;
}
console.log("フォロー通知:", notice.uniqid);
}
+66
View File
@@ -0,0 +1,66 @@
import client from "@/lib/client";
import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse";
import i18next from "i18next";
import { EOL } from "node:os";
const helps = [
"help",
"follow",
"unfollow",
"weather",
"miq",
];
export default async function helpCommand(ueuse: ueuseModule, args: string[]) {
if (args[1] !== undefined) {
if (!(helps.includes(args[1]))) {
const response = await client.request("ueuse/create", {
text: i18next.t("invalidOption", { option: args[1], command: "help" }),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("コマンド詳細の返信に失敗:", response.error_code);
return;
}
console.warn("コマンド詳細:", response.uniqid);
return;
}
const response = await client.request("ueuse/create", {
text: i18next.t(`fullHelp${args[1].charAt(0).toUpperCase()}${args[1].slice(1)}`),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("コマンド詳細の返信に失敗:", response.error_code);
return;
}
console.warn("コマンド詳細:", response.uniqid);
return;
}
let summarys = "";
for (let i = 0; i < helps.length; i++) {
const help = helps[i];
if (!help)
break;
summarys += `${i18next.t(`help${help.charAt(0).toUpperCase()}${help.slice(1)}`)}${EOL}`;
}
const response = await client.request("ueuse/create", {
text: summarys.trim(),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("コマンド概要の返信に失敗:", response.error_code);
return;
}
console.warn("コマンド概要:", response.uniqid);
}
+138
View File
@@ -0,0 +1,138 @@
import client from "@/lib/client";
import Memory from "@/lib/memory";
import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse";
import i18next from "i18next";
import weatherCommand from "@/feature/command/weather";
import helpCommand from "@/feature/command/help";
import followCommand from "@/feature/command/follow";
import unfollowCommand from "@/feature/command/unfollow";
import miqCommand from "@/feature/command/miq";
import initI18n from "@/lib/i18n";
await initI18n();
console.log("コマンドの処理を行います");
try {
let ueuses: ueuseModule[] = [];
{
const response = await client.request("me/notification/", {
limit: 20,
});
if (response.success) {
for (const notification of response.data) {
if (notification.category !== "reply")
break;
if (!notification.valueid) {
console.warn("返信通知にvalueidが存在しないため、スキップします");
break;
}
const ueuseResponse = await client.request("ueuse/get", {
uniqid: notification.valueid,
});
if (!ueuseResponse.success || !ueuseResponse.data[0]) {
console.warn("返信通知からユーズを参照できないため、スキップします");
break;
}
ueuses.push(ueuseResponse.data[0]);
}
} else {
console.warn("返信通知の取得に失敗しましたが、続行します");
}
}
{
const response = await client.request("ueuse/mentions", {
limit: 20,
});
if (response.success) {
ueuses.push(...response.data);
} else {
console.warn("メンションの取得に失敗しましたが、続行します");
}
}
ueuses = [...new Set(ueuses)];
for (const ueuse of ueuses) {
const repliedUeuse = (Memory.memory["repliedUeuse"] as string[]);
if (repliedUeuse.includes(ueuse.uniqid)) {
console.log("既に応答しているため、スキップします");
break;
}
const mem = Memory.memory;
let text = ueuse.text;
text = text.replace(`@${mem.userid}`, "");
text = text.trim();
const rows = text.split(/\r\n|\r|\n/).map(row => row.trim());
const commandRow = rows.filter(row => row.startsWith("/"))[0];
if (!commandRow || commandRow === "") {
console.warn("コマンドが本文から参照できません");
const response = await client.request("ueuse/create", {
text: i18next.t("commandNotFound"),
replyid: ueuse.uniqid,
});
if (!response.success)
console.warn("ユーズの作成に失敗しました:", response.error_code);
break;
}
const args = commandRow.replace("/", "").split(" ");
switch (args[0]) {
case "help":
await helpCommand(ueuse, args);
break;
case "weather":
await weatherCommand(ueuse);
break;
case "follow":
await followCommand(ueuse);
break;
case "unfollow":
await unfollowCommand(ueuse);
break;
case "miq":
await miqCommand(ueuse, args);
break;
default:
console.warn("不明なコマンドが入力されました:", args[0]);
const response = await client.request("ueuse/create", {
text: i18next.t("unknownCommand", { command: args[0] }),
replyid: ueuse.uniqid,
});
if (!response.success)
console.warn("ユーズの作成に失敗しました:", response.error_code);
break;
}
{
const repliedUeuse = (Memory.memory["repliedUeuse"] as string[]);
repliedUeuse.push(ueuse.uniqid);
const mem = Memory.memory;
mem["repliedUeuse"] = repliedUeuse;
Memory.memory = mem;
}
}
process.exit(0);
} catch (err: any) {
console.error("message" in err
? err.message
: err);
process.exit(1);
}
+418
View File
@@ -0,0 +1,418 @@
import client from "@/lib/client";
import Memory from "@/lib/memory";
import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse";
import i18next from "i18next";
import MiQ from "miq";
import { EOL } from "node:os";
export default async function miqCommand(ueuse: ueuseModule, args: string[]) {
if (!args[1]) {
const response = await client.request("ueuse/create", {
text: i18next.t("lackOption", { command: "miq" }),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
}
return;
}
let mem = Memory.memory;
switch (args[1]) {
case "generate":
const itUeuse = await client.request("ueuse/get", {
uniqid: ueuse.replyid,
});
if (!itUeuse.success || !itUeuse.data[0]) {
console.warn("返信元のユーズの取得に失敗:", "error_code" in itUeuse
? itUeuse.error_code
: "データなし");
const response = await client.request("ueuse/create", {
text: i18next.t("replySourceFailed"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.log("返信に失敗:", response.error_code);
}
return;
}
mem = Memory.memory;
const permission = mem["permissions"][itUeuse.data[0].account.userid] ?? "consent";
switch (permission) {
case "me":
if (itUeuse.data[0].account.userid !== ueuse.account.userid) {
const response = await client.request("ueuse/create", {
text: i18next.t("miqPermissionMe"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
}
return;
}
case "consent":
if (itUeuse.data[0].account.userid !== ueuse.account.userid) {
const response = await client.request("ueuse/create", {
text: i18next.t("miqPermissionConsent", { userid: itUeuse.data[0].account.userid }),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
}
return;
}
}
const result = await MiQ({
type: "Base64Data",
color: (args[2] ?? "") === "color",
text: itUeuse.data[0].text,
iconURL: itUeuse.data[0].account.user_icon,
userid: itUeuse.data[0].account.userid,
username: itUeuse.data[0].account.username,
});
if (!(typeof result === "string")) {
const response = await client.request("ueuse/create", {
text: i18next.t("miqGenerateFailed"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
}
return;
}
const response = await client.request("ueuse/create", {
text: i18next.t("miqSuccess", { message: (args[2] ?? "") === "color"
? "カラーモードで生成しました。"
: "モノクロモードで生成しました。"
}),
media: {
photo: [result],
},
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("MiQを生成:", response.uniqid);
break;
case "permission":
if (args[2] === undefined) {
mem = Memory.memory;
const permission = mem["permissions"][ueuse.account.userid] ?? "consent";
const response = await client.request("ueuse/create", {
text: i18next.t("permissionResponse", { permission }),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
return;
}
const availablePermission = ["me", "everyone", "consent"];
if (!(availablePermission.includes(args[2]))) {
const response = await client.request("ueuse/create", {
text: i18next.t("invalidOption", { option: args[2], command: "miq" }),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
return;
}
{
mem = Memory.memory;
mem["permissions"][ueuse.account.userid] = args[2];
Memory.memory = mem;
const response = await client.request("ueuse/create", {
text: i18next.t("permissionChangeSuccess", { username: ueuse.account.username, permission: args[2] }),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
}
break;
case "allow":
if (ueuse.replyid === "") {
const response = await client.request("ueuse/create", {
text: i18next.t("injusticeFormat"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
return;
}
{
mem = Memory.memory;
const permission = mem["permissions"][ueuse.account.userid] ?? "consent";
if (permission !== "consent") {
const response = await client.request("ueuse/create", {
text: i18next.t("permisionIsNotConsent"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
return;
}
}
const confirmUeuse = await client.request("ueuse/get", {
uniqid: ueuse.replyid,
});
if (!confirmUeuse.success || !confirmUeuse.data[0]) {
console.warn("返信元のユーズの取得に失敗:", "error_code" in confirmUeuse
? confirmUeuse.error_code
: "データなし");
const response = await client.request("ueuse/create", {
text: i18next.t("replySourceFailed"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.log("返信に失敗:", response.error_code);
}
return;
}
if (confirmUeuse.data[0].replyid === "") {
const response = await client.request("ueuse/create", {
text: i18next.t("injusticeFormat"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
return;
}
mem = Memory.memory;
if (confirmUeuse.data[0].account.userid !== mem["userid"]) {
console.warn("返信元のユーズがBotではない:", "error_code" in confirmUeuse
? confirmUeuse.error_code
: "データなし");
const response = await client.request("ueuse/create", {
text: i18next.t("replySourceIsNotThis"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.log("返信に失敗:", response.error_code);
}
return;
}
const requestUeuse = await client.request("ueuse/get", {
uniqid: confirmUeuse.data[0].replyid,
});
if (!requestUeuse.success || !requestUeuse.data[0]) {
console.warn("返信元のユーズの取得に失敗:", "error_code" in requestUeuse
? requestUeuse.error_code
: "データなし");
const response = await client.request("ueuse/create", {
text: i18next.t("replySourceFailed"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.log("返信に失敗:", response.error_code);
}
return;
}
if (requestUeuse.data[0].replyid === "") {
const response = await client.request("ueuse/create", {
text: i18next.t("injusticeFormat"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
return;
}
const sourceUeuse = await client.request("ueuse/get", {
uniqid: requestUeuse.data[0].replyid,
});
if (!sourceUeuse.success || !sourceUeuse.data[0]) {
console.warn("返信元のユーズの取得に失敗:", "error_code" in sourceUeuse
? sourceUeuse.error_code
: "データなし");
const response = await client.request("ueuse/create", {
text: i18next.t("replySourceFailed"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.log("返信に失敗:", response.error_code);
}
return;
}
if (sourceUeuse.data[0].account.userid !== ueuse.account.userid) {
const response = await client.request("ueuse/create", {
text: i18next.t("replySourceIsNotSourceUser"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.log("返信に失敗:", response.error_code);
}
console.warn("ソースのユーズと/miq allowのユーザーが一致しない:", );
return;
}
{
mem = Memory.memory;
let text = requestUeuse.data[0].text;
text = text.replace(`@${mem.userid}`, "");
text = text.trim();
const rows = text.split(/\r\n|\r|\n/).map(row => row.trim());
const commandRow = rows.filter(row => row.startsWith("/"))[0];
if (!commandRow || commandRow === "") {
console.warn("コマンドが本文から参照できません");
const response = await client.request("ueuse/create", {
text: i18next.t("commandNotFound"),
replyid: requestUeuse.data[0].uniqid,
});
if (!response.success)
console.warn("ユーズの作成に失敗しました:", response.error_code);
{
const response = await client.request("ueuse/create", {
text: i18next.t("injusticeFormat"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("返信:", response.uniqid);
}
break;
}
const requestUeuseArgs = commandRow.replace("/", "").split(" ");
const result = await MiQ({
type: "Base64Data",
color: (requestUeuseArgs[2] ?? "") === "color",
text: sourceUeuse.data[0].text,
iconURL: sourceUeuse.data[0].account.user_icon,
userid: sourceUeuse.data[0].account.userid,
username: sourceUeuse.data[0].account.username,
});
if (!(typeof result === "string")) {
const response = await client.request("ueuse/create", {
text: i18next.t("miqGenerateFailed"),
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
}
return;
}
const response = await client.request("ueuse/create", {
text: i18next.t("miqSuccess", { message: ((requestUeuseArgs[2] ?? "") === "color"
? "カラーモードで生成しました。"
: "モノクロモードで生成しました。")
+ EOL + `@${requestUeuse.data[0].account.userid}`,
}),
media: {
photo: [result],
},
replyid: ueuse.uniqid,
});
if (!response.success) {
console.warn("返信に失敗:", response.error_code);
return;
}
console.log("MiQを生成:", response.uniqid);
}
break;
}
}
+28
View File
@@ -0,0 +1,28 @@
import client from "@/lib/client";
import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse";
import i18next from "i18next";
export default async function unfollowCommand(ueuse: ueuseModule) {
const unfollow = await client.request("users/unfollow", {
userid: ueuse.account.userid,
});
if (!unfollow.success) {
console.warn("フォロー解除に失敗:", unfollow.error_code);
return;
}
console.log("フォロー解除:", unfollow.userid);
const notice = await client.request("ueuse/create", {
text: i18next.t("unfollowedNotification", { username: ueuse.account.username }),
replyid: ueuse.uniqid,
});
if (!notice.success) {
console.warn("フォロー解除通知に失敗:", notice.error_code);
return;
}
console.log("フォロー解除通知:", notice.uniqid);
}
+6
View File
@@ -0,0 +1,6 @@
import ueuseModule from "better-uwuzu-sdk/types/1.6.8/types/modules/ueuse";
import { weatherReply } from "@/feature/weatherNotice";
export default async function weatherCommand(ueuse: ueuseModule) {
await weatherReply(ueuse.uniqid);
}
+288
View File
@@ -0,0 +1,288 @@
import client from "@/lib/client";
import config from "@/lib/config";
import initI18n from "@/lib/i18n";
import { format } from "date-fns";
import i18next from "i18next";
import { readFileSync } from "node:fs";
import { EOL } from "node:os";
import { WebSocket } from "ws";
await initI18n();
if (config.earthquake?.useHistoryData) {
console.log("過去の地震情報を配信します");
const history = JSON.parse(readFileSync(`${import.meta.dirname}/../../260420.json`, "utf-8"));
history.reverse();
let i = 0;
setInterval(() => {
processMessage(history[i]);
i++;
}, 10 * 1000);
} else {
const WEBSOCKET_URL = config.debug
? "wss://api-realtime-sandbox.p2pquake.net/v2/ws"
: "wss://api.p2pquake.net/v2/ws";
console.log("P2P地震情報のWebSocketに接続します");
const socket = new WebSocket(WEBSOCKET_URL);
socket.addEventListener("open", () => {
console.log("P2P地震情報のWebSocketに接続しました");
});
socket.addEventListener("message", async (event) => {
let message;
try {
message = typeof event.data === "string"
? JSON.parse(event.data)
: event.data;
} catch (err) {
console.error(`メッセージのパースでエラーが発生: ${err}`);
return;
}
processMessage(message);
});
}
const processMessage = async (message: any) => {
try {
const scaleMessages: Record<string, string> = {
"-1": "不明",
"10": "震度1",
"20": "震度2",
"30": "震度3",
"40": "震度4",
"45": "震度5弱",
"46": "**推定**震度5弱以上***(正確には不明)***",
"50": "震度5強",
"55": "震度6弱",
"60": "震度6強",
"70": "震度7",
"99": "程度以上",
}
switch (message.code) {
case 551:
{
console.log("地震発生情報を受信しました");
const domesticTsunamiMessages: Record<string, string> = {
"None": "😌この地震による**国内**の津波の心配はありません。",
"Unknown": "😕この地震による**国内**の***津波情報は***不明です。",
"Checking": "🧐この地震による**国内**の津波情報を**調査中です。**",
"NonEffective": "😌この地震による**国内**の**海面変動が予想されますが**、被害の心配はありません。",
"Watch": "⚠️この地震により**国内**で津波注意報が発令しています。",
"Warning": "🚨この地震による**国内**の津波予報があります。",
}
const foreignTsunamiMessages: Record<string, string> = {
"None": "😌この地震による**国外**の津波の心配はありません。",
"Unknown": "😕この地震による**国外**の***津波情報は***不明です。",
"Checking": "🧐この地震による**国外**の津波情報を**調査中です。**",
"NonEffectiveNearby": "😌この地震によって**国外**にて震源の近傍で**小さな津波の可能性はありますが**、被害の心配はありません。",
"WarningNearby": "⚠️この地震によって**国外**にて震源の近傍で**津波の可能性**があります。",
"WarningPacific": "⚠️この地震によって**太平洋**にて**津波の可能性**があります。",
"WarningPacificWide": "🚨この地震によって**太平洋の広域**にて**津波の可能性**があります。",
"WarningIndian": "⚠️この地震によって**インド洋**にて**津波の可能性**があります。",
"WarningIndianWide": "🚨この地震によって**インド洋の広域**にて**津波の可能性**があります。",
"Potential": "🚨この地震によって**一般的に**この規模では津波の可能性があると考えられています。",
}
let points: Record<string, any[]> = {};
for (const point of message.points) {
const scaleMsg = scaleMessages[String(point.scale)];
if (!scaleMsg)
break;
points[scaleMsg]?.push(point);
}
const grouped: Record<string, { scale: number; addrs: string[] }> = {};
for (const point of message.points) {
const { addr, scale } = point;
const label = scaleMessages[String(scale)] ?? "不明";
if (!grouped[label]) {
grouped[label] = { scale, addrs: [] };
}
grouped[label].addrs.push(addr);
}
const pointsMsg = Object.entries(grouped)
.sort((a, b) => b[1].scale - a[1].scale)
.map(([label, { addrs }]) =>
`${label}${EOL}${addrs.join("・")}`)
.join(EOL.repeat(2)).trim();
const response = await client.request("ueuse/create", {
text: i18next.t("earthquakeNotice", {
occuredTime: format(new Date(message.earthquake.time), "yyyy年M月d日 H:mm"),
maxScale: scaleMessages[String(message.earthquake.maxScale)],
epicenter: message.earthquake.hypocenter.name === ""
? "不明"
: message.earthquake.hypocenter.name,
magnitude: message.earthquake.hypocenter.magnitude === -1
? "不明"
: `M${message.earthquake.hypocenter.magnitude.toFixed(1)}`,
depth: message.earthquake.hypocenter.depth === 0
? "ごく浅い"
: (message.earthquake.hypocenter.depth === -1
? "不明"
: `${message.earthquake.hypocenter.depth}km`),
domesticTsunami: domesticTsunamiMessages[(message.earthquake.domesticTsunami ?? "Unknown")],
foreignTsunami: foreignTsunamiMessages[(message.earthquake.foreignTsunami ?? "Unknown")],
points: pointsMsg === ""
? ""
: EOL.repeat(2) + pointsMsg,
source: message.issue.source ?? "不明",
comment: message.comments.freeFormComment === ""
? ""
: EOL + message.comments.freeFormComment + EOL,
}),
});
if (!response.success) {
console.warn("ユーズの作成に失敗しました:", response.error_code);
break;
}
console.log("地震発生情報を投稿:", response.uniqid);
}
break;
case 552:
{
console.log("津波予報情報を受信しました");
if (message.cancelled) {
const response = await client.request("ueuse/create", {
text: i18next.t("tsunamiCancelNotice", {
announceTime: format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss"),
source: message.issue.source ?? "不明",
}),
});
if (!response.success) {
console.warn("ユーズの作成に失敗しました:", response.error_code);
break;
}
console.log("津波予報解除情報を投稿:", response.uniqid);
break;
}
const gradeMessages: Record<string, string> = {
"Unknown": "不明",
"Watch": "津波注意報",
"Warning": "津波警報",
"MajorWarning": "大津波警報",
}
let areasMsg = "";
for (const area of message.areas) {
areasMsg += i18next.t("tsunamiAreaMsg", {
name: area.name,
immediate: area.immediate
? EOL + "🚨***直ちに津波が来襲すると予想されています。***"
: "",
grade: gradeMessages[area.grade],
arrivalTime: format(new Date(area.firstHeight.arrivalTime), "yyyy年M月d日 H:mm"),
condition: area.firstHeight.condition
? `${area.firstHeight.condition}されています`
: "不明",
maxHeight: area.maxHeight.value === 0.2
? "0.2m未満"
: (area.maxHeight.value
? `${area.maxHeight.value}m`
: area.maxHeight.description),
}) + EOL.repeat(2);
}
const response = await client.request("ueuse/create", {
text: i18next.t("tsunamiForecastNotice", {
announceTime: format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss"),
areasMsg: areasMsg.trim(),
source: message.issue.source ?? "不明",
}),
});
if (!response.success) {
console.warn("ユーズの作成に失敗しました:", response.error_code);
break;
}
console.log("津波予報情報を投稿:", response.uniqid);
}
break;
case 556:
{
console.log("緊急地震速報(警報)を受信しました");
const kindMessages: Record<string, string> = {
"10": "⏳主要動は、**未到達と予測**されています。",
"11": "🫨主要動が、**既に到達していると予測**されています。",
"19": "🧐PLUM法によると、主要動の***到達予想は***ありません。",
}
let areasMsg = "";
for (const area of message.areas) {
areasMsg += i18next.t("eewAreaMsg", {
name: area.name,
maxScale: scaleMessages[String(Math.floor(area.scaleFrom))] +
(area.scaleTo === 99
? "程度以上"
: area.scaleFrom !== area.scaleTo
? `から${scaleMessages[String(Math.floor(area.scaleTo))]}`
: ""),
kind: kindMessages[area.kindCode] ?? "😕主要動の***到達予想は***ありません。",
arrivalTime: area.arrivalTime !== undefined
? format(new Date(area.arrivalTime), "yyyy年M月d日 H:mm:ss")
: "不明",
}) + EOL.repeat(2);
}
const response = await client.request("ueuse/create", {
text: i18next.t("eewNotice", {
isTest: message.test
? "⚒️これは**テストです。**"
: "🚨これは**テストではありません。**",
announceTime: format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss"),
occuredTime: format(new Date(message.earthquake.originTime), "yyyy年M月d日 H:mm:ss"),
arrivalTime: format(new Date(message.earthquake.arrivalTime), "yyyy年M月d日 H:mm:ss"),
isAssume: message.earthquake.condition === "仮定震源要素"
? `${EOL}❓これは、仮定震源要素です。そのため、震源に関する情報が保証できません。`
: "",
epicenter: message.earthquake.hypocenter.name ?? "不明",
depth: message.earthquake.hypocenter.depth === undefined ||
message.earthquake.hypocenter.depth === -1
? "不明"
: `${Math.floor(message.earthquake.hypocenter.depth)}km`,
magnitude: message.earthquake.hypocenter.magnitude === undefined ||
message.earthquake.hypocenter.magnitude === -1
? "不明"
: `M${message.earthquake.hypocenter.magnitude}`,
areas: areasMsg !== ""
? EOL.repeat(2) + areasMsg.trim()
: "",
}),
});
if (!response.success) {
console.warn("ユーズの作成に失敗しました:", response.error_code);
break;
}
console.log("緊急地震速報(警報)情報を投稿:", response.uniqid);
}
break;
default:
console.log("未対応の情報:", message);
break;
}
} catch (err) {
console.warn("メッセージの処理に失敗しました:", err);
}
}
+26
View File
@@ -0,0 +1,26 @@
import client from "@/lib/client";
import initI18n from "@/lib/i18n";
import { format } from "date-fns";
import i18next from "i18next";
await initI18n();
console.log("時報の投稿を行います");
try {
const response = await client.request("ueuse/create", {
text: i18next.t("timeNotice", { time: format(new Date(), "HH:mm") }),
});
if (!response.success) {
console.warn("時報投稿に失敗しました:", response.error_code);
process.exit(1);
}
console.log("時報投稿:", response.uniqid);
process.exit(0);
} catch (err: any) {
console.error("message" in err
? err.message
: err);
process.exit(1);
}
+162
View File
@@ -0,0 +1,162 @@
import client from "@/lib/client";
import config from "@/lib/config";
import initI18n from "@/lib/i18n";
import i18next from "i18next";
import { readFileSync } from "node:fs";
import { EOL } from "node:os";
import { isMainThread, workerData } from "node:worker_threads";
const cityList = [
"016010",
"020010",
"030010",
"040010",
"050010",
"060010",
"070010",
"080010",
"090010",
"100010",
"110010",
"120010",
"130010",
"140010",
"150010",
"160010",
"170010",
"180010",
"190010",
"200010",
"210010",
"220010",
"230010",
"240010",
"250010",
"260010",
"270000",
"280010",
"290010",
"300010",
"310010",
"320010",
"330010",
"340010",
"350010",
"360010",
"370000",
"380010",
"390010",
"400010",
"410010",
"420010",
"430010",
"440010",
"450010",
"460010",
"471010",
];
if (!isMainThread && workerData === "scheduledWeatherNotice") {
await initI18n();
console.log("天気予報の投稿を行います");
try {
const provisionalUeuse = await client.request("ueuse/create", {
text: i18next.t("weatherProvisional"),
});
if (!provisionalUeuse.success) {
console.error("天気仮投稿に失敗しました:", provisionalUeuse.error_code);
process.exit(1);
}
console.log("天気仮投稿:", provisionalUeuse.uniqid);
weatherReply(provisionalUeuse.uniqid);
process.exit(0);
} catch (err: any) {
console.error("message" in err
? err.message
: err);
process.exit(1);
}
}
export async function weatherReply(uniqid: string) {
// インデックス
const splitCount = config.weather.splits;
const total = cityList.length;
const chunkSizes = Array(splitCount).fill(0).map((_, i) =>
Math.floor((total + i) / splitCount)
);
// 分割インデックス
let start = 0;
const ranges: [number, number][] = chunkSizes.map(size => {
const range: [number, number] = [start, start + size];
start += size;
return range;
});
// 配列作成
const weatherResults = Array(splitCount).fill("");
// package.json取得
const packageJson = JSON.parse(readFileSync(`${import.meta.dirname}/../../package.json`, "utf-8"));
// 天気取得
for (let chunkIndex = 0; chunkIndex < splitCount; chunkIndex++) {
const range = ranges[chunkIndex];
if (!range) continue;
const [chunkStart, chunkEnd] = range;
for (let i = chunkStart; i < chunkEnd; i++) {
const res = await fetch(`https://weather.tsukumijima.net/api/forecast/city/${cityList[i]}`, {
headers: {
"User-Agent": `noticeUwuzu/${packageJson.version}`,
},
});
const data = await res.json();
const today = data.forecasts[0];
// 天気
const weather = today.telop ?? "取得できませんでした";
const maxTemp = today.temperature.max.celsius
? `${today.temperature.max.celsius}`
: "取得できませんでした";
const minTemp = today.temperature.min.celsius
? `${today.temperature.min.celsius}`
: "取得できませんでした";
const chanceOfRain = (
today.chanceOfRain.T06_12 !== null &&
today.chanceOfRain.T06_12 !== "--%"
)
? today.chanceOfRain.T06_12
: "取得できませんでした";
weatherResults[chunkIndex] += `${i18next.t("weatherReply", {
city: data.location.city,
weather,
maxTemp,
minTemp,
chanceOfRain,
})}${EOL.repeat(2)}`;
}
}
// 分割投稿
for (let i = 0; i < splitCount; i++) {
const replyUeuse = await client.request("ueuse/create", {
text: weatherResults[i].trim(),
replyid: uniqid,
});
if (!replyUeuse.success) {
console.error("天気返信に失敗しました:", replyUeuse.error_code);
}
console.log("天気返信:", replyUeuse.uniqid);
}
}
+43
View File
@@ -0,0 +1,43 @@
import { schedule } from "node-cron";
import { readFileSync } from "node:fs";
import config from "@/lib/config";
import { initUserID } from "@/lib/memory";
import { styleText } from "node:util";
import { Worker } from "node:worker_threads";
try {
console.log(readFileSync(`${import.meta.dirname}/../asciiart.txt`, "utf-8"));
console.log(JSON.parse(readFileSync(`${import.meta.dirname}/../package.json`, "utf-8")).version);
if (config.debug) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
console.log(styleText(
["bgRed", "cyan", "bold"],
"デバッグモードが有効です",
));
}
console.log();
await initUserID();
new Worker(`${import.meta.dirname}/feature/earthquakeNotice.js`);
console.log("Botが起動しました");
} catch (err: any) {
console.error("message" in err
? err.message
: err);
process.exit(1);
}
schedule("0 * * * *", async () => {
new Worker(`${import.meta.dirname}/feature/timeNotice.js`);
});
schedule("0 7 * * *", async () => {
new Worker(`${import.meta.dirname}/feature/weatherNotice.js`);
});
schedule(`*/${config.command.interval} * * * *`, async () => {
new Worker(`${import.meta.dirname}/feature/command/index.js`);
});
+13
View File
@@ -0,0 +1,13 @@
import uwuzu from "better-uwuzu-sdk";
import config from "@/lib/config";
import Parser from "better-uwuzu-sdk/1.6.8/parser";
import ApiMap from "better-uwuzu-sdk/types/1.6.8/map";
const client = new uwuzu<ApiMap>({
origin: config.uwuzu.origin,
parser: Parser,
});
client.token = config.uwuzu.token;
export default client;
+40
View File
@@ -0,0 +1,40 @@
import z from "zod";
import { readFileSync } from "node:fs";
import { parse as yamlParse } from "yaml";
import { EOL } from "node:os";
const schema = z.object({
command: z.object({
interval: z.number().int().positive(),
}),
weather: z.object({
splits: z.number().int().positive(),
}),
earthquake: z.object({
useHistoryData: z.boolean(),
}).optional(),
uwuzu: z.object({
token: z.string().length(64),
origin: z.string().refine(data => {
try {
return new URL(data).origin === data;
} catch {
return false;
}
}),
}),
debug: z.boolean().optional(),
});
const configFile = readFileSync(`${import.meta.dirname}/../../config/config.yaml`, "utf-8");
const configObj = yamlParse(configFile);
const result = schema.safeParse(configObj);
if (!result.success) {
console.error("Config: configが無効です。");
console.error(` ${result.error.issues.map(issue => issue.message).join(EOL).replaceAll(EOL, `${EOL} `)}`);
process.exit(1);
}
const config = result.data;
export default config;
+28
View File
@@ -0,0 +1,28 @@
import i18next from "i18next";
import config from "@/lib/config";
import { parse as yamlParse } from "yaml";
import { readFileSync } from "node:fs";
const translation = Object.fromEntries(Object.entries(
yamlParse(readFileSync(`${import.meta.dirname}/../../locales/ja.yaml`, "utf-8"))
).map(([key, value]) => [
key,
typeof value === "string"
? value.trim()
: value,
]));
export default async function initI18n() {
await i18next.init({
lng: "ja",
debug: config.debug,
resources: {
ja: {
translation,
},
},
interpolation: {
escapeValue: false,
},
});
};
+44
View File
@@ -0,0 +1,44 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import client from "@/lib/client";
const path = `${import.meta.dirname}/../../memory.json`;
class MemoryClass {
private cachedMemory: any;
constructor() {
if (!existsSync(path)) {
writeFileSync(path, JSON.stringify({
repliedUeuse: [],
permissions: {},
userid: "",
}));
}
this.cachedMemory = JSON.parse(readFileSync(path, "utf-8"));
}
get memory() {
return this.cachedMemory;
}
set memory(data: any) {
this.cachedMemory = data;
writeFileSync(path, JSON.stringify(this.cachedMemory), "utf-8");
}
}
const Memory = new MemoryClass();
export const initUserID = async () => {
const response = await client.request("me/");
if (!response.success)
throw new Error("meの取得に失敗しました");
const mem = Memory.memory;
mem.userid = response.userid;
Memory.memory = mem;
}
export default Memory;
-53
View File
@@ -1,53 +0,0 @@
import config from "../config";
import * as nodemailer from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport";
export interface EmailMessage {
from: string;
to: string | string[];
subject: string;
text?: string;
html?: string;
}
async function createTransporter() {
const transporter = nodemailer.createTransport({
host: config.emergency.mail.host,
port: config.emergency.mail.port,
secure: config.emergency.mail.secure,
auth: {
user: config.emergency.mail.user,
pass: config.emergency.mail.password,
},
} as SMTPTransport.Options);
// 接続テスト
try {
await transporter.verify();
console.log("SMTPサーバーに接続できました");
} catch (error) {
console.error("SMTP接続テストに失敗:", error);
throw error;
}
return transporter;
}
export default async function sendMail(message: EmailMessage): Promise<void> {
try {
const transporter = await createTransporter();
await transporter.sendMail({
from: message.from,
to: Array.isArray(message.to) ? message.to.join(",") : message.to,
subject: message.subject,
text: message.text,
html: message.html,
});
console.log("メール送信成功");
} catch (error) {
console.error("メール送信に失敗しました:", error);
throw error;
}
}
-49
View File
@@ -1,49 +0,0 @@
export const cityList: Array<String> = [
"016010",
"020010",
"030010",
"040010",
"050010",
"060010",
"070010",
"080010",
"090010",
"100010",
"110010",
"120010",
"130010",
"140010",
"150010",
"160010",
"170010",
"180010",
"190010",
"200010",
"210010",
"220010",
"230010",
"240010",
"250010",
"260010",
"270000",
"280010",
"290010",
"300010",
"310010",
"320010",
"330010",
"340010",
"350010",
"360010",
"370000",
"380010",
"390010",
"400010",
"410010",
"420010",
"430010",
"440010",
"450010",
"460010",
"471010",
];
+11 -11
View File
@@ -1,22 +1,22 @@
{
"compilerOptions": {
"target": "es2022",
"module": "ES2022",
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": "./",
"typeRoots": [
"./node_modules/@types",
"./types"
],
"baseUrl": "./src",
"paths": {
"ws": ["./node_modules/ws/index.js"],
"@types/ws": ["./node_modules/@types/ws/index.d.ts"]
}
}
"@/*": ["./*"],
},
"removeComments": true,
},
"tsc-alias": {
"resolveFullPaths": true,
},
}
-46
View File
@@ -1,46 +0,0 @@
interface earthquakeTypes {
reconnectTimes: number;
websocketUrl: string;
areasCsvUrl: string;
maxScaleMin: number;
rateLimit: number;
}
interface weatherTypes {
splitCount: number;
}
interface stopsTypes {
start: number;
stop: number;
}
interface timeTypes {
stopTimes: stopsTypes;
}
interface emergencyMailTypes {
function: Boolean;
host: string | undefined;
port: number;
user: string;
password: string;
secure: Boolean;
to: string;
}
interface emergencyTypes {
function: Boolean;
mail: emergencyMailTypes;
}
export interface configTypes {
time: timeTypes,
earthquake: earthquakeTypes;
weather: weatherTypes;
emergency: emergencyTypes;
apiToken: string;
uwuzuServer: string;
}
-33
View File
@@ -1,33 +0,0 @@
export interface Role {
name: string;
color: string;
effect: string;
id: string;
}
export interface meApi {
username: string;
userid: string;
profile: string;
user_icon: string;
user_header: string;
registered_date: string;
followee: Array;
followee_cnt: number;
follower: Array;
follower_cnt: number;
ueuse_cnt: number;
isBot: Boolean;
isAdmin: Boolean;
role: Role[];
language: String;
}
export interface ueuseCreateApi {
uniqid: string;
userid: string;
}
export interface followApi {
userid: string;
}
-68
View File
@@ -1,68 +0,0 @@
declare module 'ws' {
import { EventEmitter } from 'events';
import { IncomingMessage } from 'http';
import { Socket } from 'net';
export type Data = string | Buffer | ArrayBuffer | Buffer[];
export interface WebSocketEventMap {
close: CloseEvent;
error: ErrorEvent;
message: MessageEvent;
open: Event;
}
export default class WebSocket extends EventEmitter {
static readonly CONNECTING: 0;
static readonly OPEN: 1;
static readonly CLOSING: 2;
static readonly CLOSED: 3;
readonly CONNECTING: 0;
readonly OPEN: 1;
readonly CLOSING: 2;
readonly CLOSED: 3;
readonly readyState: 0 | 1 | 2 | 3;
readonly url: string;
readonly protocol: string;
constructor(address: string | URL, protocols?: string | string[]);
close(code?: number, reason?: string): void;
send(data: Data): void;
ping(data?: Data): void;
pong(data?: Data): void;
terminate(): void;
on(event: 'close', listener: (code: number, reason: Buffer) => void): this;
on(event: 'error', listener: (error: Error) => void): this;
on(event: 'message', listener: (data: Data) => void): this;
on(event: 'open', listener: () => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this;
addEventListener(type: 'close', listener: (event: CloseEvent) => void): void;
addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void;
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
addEventListener(type: 'open', listener: (event: Event) => void): void;
}
export interface CloseEvent {
code: number;
reason: string;
wasClean: boolean;
}
export interface ErrorEvent {
error: Error;
message: string;
type: string;
}
export namespace WebSocket {
export const CONNECTING: 0;
export const OPEN: 1;
export const CLOSING: 2;
export const CLOSED: 3;
}
}