507 lines
14 KiB
TypeScript
507 lines
14 KiB
TypeScript
import WebSocket from "ws";
|
||
import sendMail from "../src/mailer.js";
|
||
|
||
import config from "../config.js";
|
||
|
||
class P2PEarthquakeClient {
|
||
private ws: WebSocket | null = null;
|
||
private reconnectInterval: number = config.earthquake.reconnectTimes;
|
||
private reconnectTimer: NodeJS.Timeout | null = null;
|
||
private isConnecting: boolean = false;
|
||
|
||
public start(): void {
|
||
this.connect();
|
||
this.setupCleanup();
|
||
}
|
||
|
||
private connect(): void {
|
||
if (this.isConnecting) return;
|
||
|
||
this.isConnecting = true;
|
||
console.log("地震情報サーバーに接続中");
|
||
|
||
try {
|
||
this.ws = new WebSocket(config.earthquake.websocketUrl);
|
||
|
||
this.ws.on("open", () => {
|
||
console.log("地震情報サーバーに接続しました");
|
||
this.isConnecting = false;
|
||
|
||
if (this.reconnectTimer) {
|
||
clearTimeout(this.reconnectTimer);
|
||
this.reconnectTimer = null;
|
||
}
|
||
});
|
||
|
||
this.ws.on("message", (data: WebSocket) => {
|
||
try {
|
||
const message = JSON.parse(data.toString());
|
||
this.handleMessage(message);
|
||
} catch (err) {
|
||
console.error(`メッセージのパースでエラーが発生: ${err}`);
|
||
}
|
||
});
|
||
|
||
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("----------------");
|
||
|
||
const supportCode: Array<number> = [
|
||
551,
|
||
552,
|
||
556,
|
||
];
|
||
|
||
if (supportCode.indexOf(message.code) !== -1) {
|
||
event(message);
|
||
} else {
|
||
console.log(`未対応の情報を受信しました(コード: ${message.code})`);
|
||
console.log("受信メッセージ:", message);
|
||
}
|
||
}
|
||
|
||
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();
|
||
};
|
||
|
||
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("受信メッセージ:", earthquakeInfo);
|
||
// ----処理----
|
||
|
||
// 緊急地震速報の場合
|
||
if (earthquakeInfo.code === 556) {
|
||
console.log("緊急地震速報を受信しました");
|
||
|
||
// 地震詳細
|
||
let descriptionEarthquake: string = "";
|
||
|
||
if (
|
||
earthquakeInfo.earthquake.description !== "" ||
|
||
earthquakeInfo.earthquake.description !== undefined
|
||
) {
|
||
descriptionEarthquake = `この地震について:${earthquakeInfo.earthquake.description}`;
|
||
}
|
||
|
||
// 発令詳細
|
||
let description: string = "";
|
||
|
||
if (
|
||
earthquakeInfo.comments.freeFormComment !== "" ||
|
||
earthquakeInfo.comments.freeFormComment !== undefined
|
||
) {
|
||
description = `この発令について:${earthquakeInfo.comments.freeFormComment}`;
|
||
}
|
||
|
||
// テスト・訓練
|
||
let test: string = "";
|
||
|
||
if (earthquakeInfo.test !== undefined) {
|
||
test = "この情報にテスト・訓練かの情報はありません";
|
||
} else if (earthquakeInfo.test) {
|
||
test = "これはテスト、あるいは訓練です";
|
||
} else if (earthquakeInfo.test === false) {
|
||
test = "これはテスト・訓練ではありません";
|
||
}
|
||
|
||
// 対象地域
|
||
let areas: string = "";
|
||
|
||
if (earthquakeInfo.areas !== undefined) {
|
||
const areaNames: Array<string> = Array.from(
|
||
new Set(
|
||
earthquakeInfo.areas.map((point: any) => point.name).filter(Boolean),
|
||
),
|
||
);
|
||
areas = `対象地域:${areaNames.join("・")}`;
|
||
}
|
||
|
||
// 速報取り消し
|
||
let cancelled: string = "";
|
||
|
||
if (earthquakeInfo.cancelled) {
|
||
cancelled = "※以下の緊急地震速報が取り消されました※";
|
||
}
|
||
|
||
// マグニチュード
|
||
let magnitude: string = "マグニチュード:";
|
||
|
||
if (
|
||
earthquakeInfo.earthquake.hypocenter.magnitude != -1 ||
|
||
earthquakeInfo.earthquake.hypocenter.magnitude === undefined
|
||
) {
|
||
magnitude += "マグニチュードの情報はありません";
|
||
} else {
|
||
magnitude += `M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
|
||
}
|
||
|
||
ueuse(`
|
||
==地震情報==
|
||
【緊急地震速報】
|
||
${cancelled}
|
||
時刻:${earthquakeInfo.time}
|
||
${descriptionEarthquake}
|
||
${description}
|
||
${test}
|
||
${areas}
|
||
`);
|
||
}
|
||
|
||
// 地震情報
|
||
else if (earthquakeInfo.code === 551) {
|
||
console.log("地震発生情報を受信しました");
|
||
|
||
if (
|
||
earthquakeInfo.earthquake.maxScale !== undefined &&
|
||
earthquakeInfo.earthquake.maxScale < config.earthquake.maxScaleMin
|
||
) {
|
||
console.log("最低震度に満たしていないため投稿されませんでした");
|
||
return;
|
||
}
|
||
|
||
// 国内津波
|
||
let domesticTsunami;
|
||
|
||
const TsunamiMessages = {
|
||
"None": "この地震による国内の津波の心配はありません",
|
||
"Unknown": "この地震による国内の津波情報はありません",
|
||
"Checking": "この地震による国内の津波情報を調査中です",
|
||
"NonEffective": "この地震による国内の津波影響は若干の海面変動が予想されますが被害の心配はありません",
|
||
"Watch": "この地震により国内で津波注意報が発令しています",
|
||
"Warning": "この地震による国内の津波予報があります",
|
||
} as { [key: string]: string };
|
||
|
||
if (earthquakeInfo.earthquake.domesticTsunami === undefined) {
|
||
domesticTsunami = "この地震による国内の津波情報はありません";
|
||
} else {
|
||
domesticTsunami = TsunamiMessages[earthquakeInfo.earthquake.domesticTsunami];
|
||
}
|
||
|
||
// 最大震度
|
||
let maxScale: string = "最大深度:";
|
||
|
||
const maxScales = {
|
||
10: "震度1",
|
||
20: "震度2",
|
||
30: "震度3",
|
||
40: "震度4",
|
||
45: "震度5弱",
|
||
50: "震度5強",
|
||
55: "震度6弱",
|
||
60: "震度6強",
|
||
70: "震度7",
|
||
} as { [key: number]: string };
|
||
|
||
if (
|
||
earthquakeInfo.earthquake.maxScale == -1 ||
|
||
earthquakeInfo.earthquake.maxScale === undefined
|
||
) {
|
||
maxScale = "最大震度:不明";
|
||
} else {
|
||
maxScale = `最大震度:${maxScales[earthquakeInfo.earthquake.maxScale]}`;
|
||
}
|
||
|
||
// 警告
|
||
if (
|
||
earthquakeInfo.earthquake.maxScale !== undefined &&
|
||
earthquakeInfo.earthquake.maxScale >= 60 &&
|
||
config.emergency.function
|
||
) {
|
||
console.log("----------------");
|
||
|
||
console.log("震度6強以上の地震を受信しました");
|
||
console.log("サーバーがダウンする可能性があります");
|
||
|
||
// メール送信
|
||
if (config.emergency.function) {
|
||
sendMail({
|
||
to: config.emergency.mail.to,
|
||
subject: "【警告】震度6強以上の地震を受信しました",
|
||
text: `
|
||
※noticeUwuzu自動送信によるメールです
|
||
【警告】
|
||
BOT管理者さん、noticeUwuzu自動送信メールです。
|
||
震度6強以上の地震を受信したため警告メールが送信されました。
|
||
物理、システム的にサーバーがダウンする可能性があります。
|
||
ご自身の身をお守りください。
|
||
`
|
||
});
|
||
|
||
console.log("管理者へ警告メールを送信しました");
|
||
}
|
||
|
||
console.log("----------------");
|
||
}
|
||
|
||
// 対象地域
|
||
let areas;
|
||
|
||
if (earthquakeInfo.points !== undefined) {
|
||
const areaNames: Array<string> = Array.from(
|
||
new Set(
|
||
earthquakeInfo.points.map((point: any) => point.addr).filter(Boolean),
|
||
),
|
||
);
|
||
areas = `対象地域:${areaNames.join("・")}`;
|
||
} else {
|
||
areas = "対象地域:不明";
|
||
}
|
||
|
||
// 詳細
|
||
let description;
|
||
|
||
if (
|
||
earthquakeInfo.comments.freeFormComment !== "" &&
|
||
earthquakeInfo.comments.freeFormComment !== undefined
|
||
) {
|
||
description = `この地震について:${earthquakeInfo.comments.freeFormComment}`;
|
||
} else {
|
||
description = "";
|
||
}
|
||
|
||
// 深さ
|
||
let depth;
|
||
|
||
if (
|
||
earthquakeInfo.earthquake.hypocenter.depth !== null ||
|
||
earthquakeInfo.earthquake.hypocenter.depth !== undefined
|
||
) {
|
||
if (earthquakeInfo.earthquake.hypocenter.depth === 0) {
|
||
depth = "深さ:ごく浅い";
|
||
} else if (earthquakeInfo.earthquake.hypocenter.depth === -1) {
|
||
depth = "深さ:不明";
|
||
} else {
|
||
depth = `深さ:${String(earthquakeInfo.earthquake.hypocenter.depth)}km`;
|
||
}
|
||
} else {
|
||
depth = "深さ:不明";
|
||
}
|
||
|
||
// マグニチュード
|
||
let magnitude;
|
||
|
||
if(
|
||
earthquakeInfo.earthquake.hypocenter.magnitude !== null ||
|
||
earthquakeInfo.earthquake.hypocenter.magnitude !== undefined
|
||
) {
|
||
if (earthquakeInfo.earthquake.hypocenter.magnitude === -1) {
|
||
magnitude = "マグニチュード:不明";
|
||
} else {
|
||
magnitude = `マグニチュード:M${String(earthquakeInfo.earthquake.hypocenter.magnitude)}`;
|
||
}
|
||
} else {
|
||
magnitude = "マグニチュード:不明";
|
||
}
|
||
|
||
ueuse(`
|
||
==地震情報==
|
||
【地震発生】
|
||
時刻:${earthquakeInfo.time}
|
||
${description}
|
||
${magnitude}
|
||
${depth}
|
||
${maxScale}
|
||
${areas}
|
||
国内の津波:${domesticTsunami}
|
||
`);
|
||
} else if (earthquakeInfo.code === 552) {
|
||
console.log("津波予報情報を受信しました");
|
||
|
||
// 予報取り消し
|
||
if (earthquakeInfo.cancelled) {
|
||
ueuse(`
|
||
==地震情報==
|
||
【津波予報】
|
||
※津波予報が取り消されました※
|
||
時刻:${earthquakeInfo.time}
|
||
`);
|
||
|
||
return;
|
||
}
|
||
|
||
let result: string = `
|
||
==地震情報==
|
||
【津波予報】
|
||
時刻:${earthquakeInfo.time}
|
||
\n
|
||
`;
|
||
|
||
for (let i = 0; i < earthquakeInfo.areas.length; i++) {
|
||
const data = earthquakeInfo.areas[i];
|
||
|
||
// 種類
|
||
const gradeMessages = {
|
||
"MajorWarning": "大津波警報",
|
||
"Warning": "津波警報",
|
||
"Watch": "津波注意報",
|
||
"Unknown": "不明",
|
||
} as { [key: string]: string };
|
||
|
||
let grade;
|
||
|
||
if (data.grade === undefined) {
|
||
grade = "予報種類:不明";
|
||
} else {
|
||
grade = `予報種類:${gradeMessages[data.grade]}`;
|
||
}
|
||
|
||
// 直後襲来
|
||
let immediate;
|
||
|
||
if (data.immediate === undefined) {
|
||
immediate = "津波の襲来が直後かの情報がありません";
|
||
} else if (data.immediate) {
|
||
immediate = "### 津波が直ちに襲来します";
|
||
} else if (!data.immediate) {
|
||
immediate = "津波は直ちには襲来しません";
|
||
} else {
|
||
immediate = "津波の襲来が直後かの情報がありません";
|
||
}
|
||
|
||
// 第1波
|
||
let firstHeight;
|
||
|
||
if (data.firstHeight === undefined) {
|
||
firstHeight = "第1波の情報がありません";
|
||
} else if (
|
||
data.firstHeight.arrivalTime === undefined &&
|
||
data.firstHeight.condition === undefined
|
||
) {
|
||
firstHeight = "第1波の情報がありません";
|
||
} else {
|
||
let arrivalTime;
|
||
|
||
if (data.arrivalTime === undefined) {
|
||
arrivalTime = "不明";
|
||
} else {
|
||
arrivalTime = data.arrivalTime;
|
||
}
|
||
|
||
let condition;
|
||
|
||
if (data.condition === undefined) {
|
||
condition = "不明";
|
||
} else {
|
||
condition = data.condition;
|
||
}
|
||
|
||
firstHeight = `
|
||
第1波到達予想時刻:${arrivalTime}
|
||
第1波の状態:${condition}
|
||
`
|
||
}
|
||
|
||
// 予想高さ
|
||
let maxHeight;
|
||
|
||
if (data.maxHeight.description === undefined) {
|
||
maxHeight = "津波の高さ(予想):不明";
|
||
} else {
|
||
maxHeight = `津波の高さ(予想):${data.maxHeight.description}`;
|
||
}
|
||
|
||
result = `
|
||
【${data.name}】
|
||
${grade}
|
||
${immediate}
|
||
${firstHeight}
|
||
${maxHeight}\n\n
|
||
`;
|
||
}
|
||
|
||
ueuse(result);
|
||
}
|
||
}
|
||
|
||
async function ueuse(text: string) {
|
||
const res = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
token: config.uwuzu.apiToken,
|
||
text: text,
|
||
}),
|
||
cache: "no-store",
|
||
});
|
||
|
||
const resData = await res.json();
|
||
|
||
console.log(`地震情報投稿:${JSON.stringify(resData)}`);
|
||
}
|
||
|
||
export default function earthquakeNotice(): void {
|
||
console.log("地震情報サーバーに接続します");
|
||
const client = new P2PEarthquakeClient();
|
||
client.start();
|
||
}
|