Files
earthquake-bot-for-mtweet/src/earthquake/index.ts
T

347 lines
16 KiB
TypeScript

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<string, string> = {
"-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<string, string> = {
"ScalePrompt": "震度速報",
"Destination": "震源情報",
}
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": "🚨この地震によって**一般的に**この規模では津波の可能性があると考えられています。",
}
const grouped: Record<string, { scale: number; prefs: Record<string, string[]> }> = {};
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<string, string> = {
"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<string, string> = {
"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);
}
}