import { createPost } from "@/lib/client"; import config from "@/lib/config"; import { format } from "date-fns"; import { readFileSync } from "node:fs"; import { EOL } from "node:os"; import { WebSocket } from "ws"; import generateImage from "@/earthquake/generateImage"; 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 connect = () => { 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("close", (err) => { const interval = config.earthquake.reconnectInterval; console.log(`WebSocketが切断されました。${interval / 1000}秒後に再接続します:`, err.reason); setTimeout(() => { connect(); }, interval); }); socket.addEventListener("error", (err) => { console.error("WebSocketでエラーが発生しました:", err); socket.close(); }); 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); }); } connect(); } const processMessage = async (message: any) => { try { const scaleMessages: Record = { "-1": "不明", "0": "震度0", "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 555: console.log("ピアの地域分布情報を受信しました"); break; case 551: { console.log("地震発生情報を受信しました"); if ( message.earthquake.maxScale !== -1 && message.earthquake.maxScale < config.earthquake.requireMaxScale ) { console.log(`最大震度(${scaleMessages[message.earthquake.maxScale]})が投稿に必要な値(${scaleMessages[config.earthquake.requireMaxScale]})に満たないため、スキップします`); break; } const typeMessage: Record = { "ScalePrompt": "震度速報", "Destination": "震源情報", } const domesticTsunamiMessages: Record = { "None": "😌この地震による**国内**の津波の心配はありません。", "Unknown": "😕この地震による**国内**の***津波情報は***不明です。", "Checking": "🧐この地震による**国内**の津波情報を**調査中です。**", "NonEffective": "😌この地震による**国内**の**海面変動が予想されますが**、被害の心配はありません。", "Watch": "⚠️この地震により**国内**で津波注意報が発令しています。", "Warning": "🚨この地震による**国内**の津波予報があります。", } const foreignTsunamiMessages: Record = { "None": "😌この地震による**国外**の津波の心配はありません。", "Unknown": "😕この地震による**国外**の***津波情報は***不明です。", "Checking": "🧐この地震による**国外**の津波情報を**調査中です。**", "NonEffectiveNearby": "😌この地震によって**国外**にて震源の近傍で**小さな津波の可能性はありますが**、被害の心配はありません。", "WarningNearby": "⚠️この地震によって**国外**にて震源の近傍で**津波の可能性**があります。", "WarningPacific": "⚠️この地震によって**太平洋**にて**津波の可能性**があります。", "WarningPacificWide": "🚨この地震によって**太平洋の広域**にて**津波の可能性**があります。", "WarningIndian": "⚠️この地震によって**インド洋**にて**津波の可能性**があります。", "WarningIndianWide": "🚨この地震によって**インド洋の広域**にて**津波の可能性**があります。", "Potential": "🚨この地震によって**一般的に**この規模では津波の可能性があると考えられています。", } const grouped: Record }> = {}; for (const point of message.points) { const { addr, scale, pref } = point; const label = scaleMessages[String(scale)] ?? "不明"; if (!grouped[label]) { grouped[label] = { scale, prefs: {} }; } if (!grouped[label].prefs[pref]) { grouped[label].prefs[pref] = []; } grouped[label].prefs[pref].push(addr); } const pointsMsg = Object.entries(grouped) .sort((a, b) => b[1].scale - a[1].scale) .map(([label, { prefs }]) => { const prefLines = Object.entries(prefs) .map(([pref, addrs]) => `**${pref}:** ${addrs.join("・")}`) .join(EOL); return `【${label}】${EOL}${prefLines}`; }) .join(EOL.repeat(2)) .trim(); const earthquakeNoticeText = []; earthquakeNoticeText.push(`### ${typeMessage[message.issue.type] ?? "地震情報"}`); earthquakeNoticeText.push(`⏰時刻: ${format(new Date(message.earthquake.time), "yyyy年M月d日 H:mm")}頃`); earthquakeNoticeText.push(...(message.earthquake.maxScale === -1 ? [] : [`🫨最大震度: ${scaleMessages[String(message.earthquake.maxScale)]}`])); earthquakeNoticeText.push(...(message.earthquake.hypocenter.name === "" ? [] : [`📍震源地: ${message.earthquake.hypocenter.name}`])); earthquakeNoticeText.push(...(message.earthquake.hypocenter.magnitude === -1 ? [] : [`💪マグニチュード: M${message.earthquake.hypocenter.magnitude.toFixed(1)}`])); earthquakeNoticeText.push(...(message.earthquake.hypocenter.depth === -1 ? [] : [`🪨深さ: ${message.earthquake.hypocenter.depth === 0 ? "ごく浅い" : `${message.earthquake.hypocenter.depth}km`}`])); earthquakeNoticeText.push(...(message.earthquake.domesticTsunami ?? "Unknown" === "Unknown" ? [] : [domesticTsunamiMessages[(message.earthquake.domesticTsunami)]])); earthquakeNoticeText.push(...(message.earthquake.foreignTsunami ?? "Unknown" === "Unknown" ? [] : [foreignTsunamiMessages[(message.earthquake.foreignTsunami)]])); earthquakeNoticeText.push(...(pointsMsg === "" ? [] : [EOL + pointsMsg])); earthquakeNoticeText.push(...(message.comments.freeFormComment === "" ? [] : [message.comments.freeFormComment])); earthquakeNoticeText.push(EOL + `🔬情報源: P2P地震速報 - ${message.issue.source ?? "不明"}`); const earthquakePosts = await createPost({ text: earthquakeNoticeText.join(EOL), }, "地震発生情報"); try { const image = await generateImage(message); if (typeof image === "string") { throw "情報が不足しているため、地震の画像生成ができませんでした"; } else { const infoPostURL = earthquakePosts[0]?.data.id ? `https://collapse.jp/@${config.mtweet.userid}/${earthquakePosts[0].data.id}` : "不明"; const earthquakeImageText = []; earthquakeImageText.push("地震の震度分布画像を生成しました。"); earthquakeImageText.push(`地震情報投稿: ${infoPostURL}`); await createPost({ text: earthquakeImageText.join(EOL), images: [ [ new Blob([new Uint8Array(image)], { type: "image/png" }), "earthquake.png", ], ], }, "震度分布画像"); } } catch (err) { console.warn(err); } } break; case 552: { console.log("津波予報情報を受信しました"); if (message.cancelled) { const tsunamiCancelledNoticeText = []; tsunamiCancelledNoticeText.push("### 津波予報**解除**"); tsunamiCancelledNoticeText.push(`⏰発表時刻: ${format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss")}`); tsunamiCancelledNoticeText.push(`🔬情報源: P2P地震速報 - ${message.issue.source ?? "不明"}`); await createPost({ text: tsunamiCancelledNoticeText.join(EOL), }, "津波予報解除情報"); break; } const gradeMessages: Record = { "Unknown": "不明", "Watch": "津波注意報", "Warning": "津波警報", "MajorWarning": "大津波警報", } let areasMsg = ""; for (const area of message.areas) { const tsunamiAreaText = []; tsunamiAreaText.push(`【${area.name}】`); tsunamiAreaText.push(...(area.immediate ? ["🚨***直ちに津波が来襲すると予想されています。***"] : [])); tsunamiAreaText.push(`🏷️種別: ${gradeMessages[area.grade]}`); tsunamiAreaText.push(`⏳第1波到達予想時刻: ${format(new Date(area.firstHeight.arrivalTime), "yyyy年M月d日 H:mm")}`); tsunamiAreaText.push(`🌊第1波の状態: ${area.firstHeight.condition ? `${area.firstHeight.condition}されています` : "不明"}`); tsunamiAreaText.push(`🗼予想される津波の高さ: ${area.maxHeight.value === 0.2 ? "0.2m未満" : (area.maxHeight.value ? `${area.maxHeight.value}m` : area.maxHeight.description)}`); areasMsg += tsunamiAreaText.join(EOL) + EOL.repeat(2); } const tsunamiNoticeText = []; tsunamiNoticeText.push("### 津波予報**発表**"); tsunamiNoticeText.push(`⏰発表時刻: ${format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss")}`); tsunamiNoticeText.push(EOL + areasMsg.trim() + EOL); tsunamiNoticeText.push(`🔬情報源: P2P地震速報 - message.issue.source ?? "不明"`); await createPost({ text: tsunamiNoticeText.join(EOL), }, "津波予報情報"); } break; case 556: { console.log("緊急地震速報(警報)を受信しました"); if (message.cancelled) { const eewCancelledNotice = []; eewCancelledNotice.push("### 緊急地震速報(警報)**解除**"); eewCancelledNotice.push(message.test ? "⚒️これは**テストです。**" : "🚨これは**テストではありません。**"); eewCancelledNotice.push(`⏰発表時刻: ${format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss")}`); eewCancelledNotice.push(EOL + "🔬情報源: P2P地震速報"); await createPost({ text: eewCancelledNotice.join(EOL), }, "緊急地震速報(警報)解除情報"); } const kindMessages: Record = { "10": "⏳主要動は、**未到達と予測**されています。", "11": "🫨主要動が、**既に到達していると予測**されています。", "19": "🧐PLUM法によると、主要動の***到達予想は***ありません。", } let areasMsg = ""; for (const area of message.areas) { const eewAreaText = []; eewAreaText.push(`【${area.name}】`); eewAreaText.push(`🫨最大予測震度: ${scaleMessages[String(Math.floor(area.scaleFrom))] + (area.scaleTo === 99 ? "程度以上" : area.scaleFrom !== area.scaleTo ? `から${scaleMessages[String(Math.floor(area.scaleTo))]}` : "")}`); eewAreaText.push(`⏰到達予想時刻: ${typeof area.arrivalTime === "string" && area.arrivalTime !== "" ? format(new Date(area.arrivalTime), "yyyy年M月d日 H:mm:ss") : "不明"}`); eewAreaText.push(kindMessages[area.kindCode] ?? "😕主要動の***到達予想は***ありません。"); areasMsg += eewAreaText.join(EOL) + EOL.repeat(2); } const eewNoticeText = []; eewNoticeText.push("### ***緊急地震速報(警報)***"); eewNoticeText.push(message.test ? "⚒️これは**テストです。**" : "🚨これは**テストではありません。**"); eewNoticeText.push(...(message.earthquake.condition === "仮定震源要素" ? ["❓これは、仮定震源要素です。そのため、震源に関する情報が保証できません。"] : [])); eewNoticeText.push(`⏰発表時刻: ${format(new Date(message.issue.time), "yyyy年M月d日 H:mm:ss")}`); eewNoticeText.push(`⏰地震発生時刻: ${format(new Date(message.earthquake.originTime), "yyyy年M月d日 H:mm:ss")}`); eewNoticeText.push(`⏰地震発現時刻: ${format(new Date(message.earthquake.arrivalTime), "yyyy年M月d日 H:mm:ss")}`); eewNoticeText.push(`📍震源地: ${message.earthquake.hypocenter.name ?? "不明"}`); eewNoticeText.push(`💪マグニチュード: ${message.earthquake.hypocenter.magnitude === undefined || message.earthquake.hypocenter.magnitude === -1 ? "不明" : `M${message.earthquake.hypocenter.magnitude.toFixed(1)}`}`); eewNoticeText.push(`🪨深さ: ${message.earthquake.hypocenter.depth === undefined || message.earthquake.hypocenter.depth === -1 ? "不明" : `${Math.floor(message.earthquake.hypocenter.depth)}km`}`); eewNoticeText.push(...(areasMsg !== "" ? [EOL + areasMsg.trim()] : [])); eewNoticeText.push(EOL + "🔬情報源: P2P地震速報"); await createPost({ text: eewNoticeText.join(EOL), }, "緊急地震速報(警報)情報"); } break; default: console.log("未対応の情報:", message); break; } } catch (err) { console.warn("メッセージの処理に失敗しました:", err); } }