import WebSocket from "ws"; import * as dotenv from "dotenv"; dotenv.config(); class P2PEarthquakeClient { private ws: WebSocket | null = null; private reconnectInterval: number = 5000; 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("P2P地震情報に接続中"); try { this.ws = new WebSocket("wss://api.p2pquake.net/v2/ws"); this.ws.on("open", () => { console.log("P2P地震情報に接続しました"); 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 { 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(`${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> { const res = await fetch( "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", ); const text = await res.text(); const lines = text.split("\n"); const map: Record = {}; 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 { console.log(JSON.stringify(earthquakeInfo)); // 緊急地震速報の場合 if (earthquakeInfo.code === 554) { ueuse(` ==地震情報== 【緊急地震速報】 時刻:${earthquakeInfo.time} `); } // 地震情報 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; if (earthquakeInfo.earthquake.maxScale === -1) { 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"; } ueuse(` ==地震情報== 【地域情報更新】 時刻:${earthquakeInfo.time} 最大深度:${maxScale} 国内の津波:${domesticTsunami} `); } // 地域情報更新の場合 else if (earthquakeInfo.code === 555) { // 対象地域マッピング const areaMaps: any = await areaMap(); const areaNames: Array = Array.from( new Set( earthquakeInfo.areas.map((a: any) => { areaMaps[a.id].filter(Boolean); }), ), ); const result = areaNames.join("・"); ueuse(` ==地震情報== 【地域情報更新】 時刻:${earthquakeInfo.time} 対象地域:${result} `); } } async function ueuse(text: string) { const res = await fetch(`https://${process.env.SERVER}/api/ueuse/create`, { method: "POST", body: JSON.stringify({ token: process.env.TOKEN, text: text, }), }); const resData = await res.json(); console.log(JSON.stringify(resData)); } export default function earthquake(): void { console.log("地震情報サーバーに接続します"); const client = new P2PEarthquakeClient(); client.start(); }