Compare commits

..

6 Commits

7 changed files with 86 additions and 162 deletions
-5
View File
@@ -1,5 +0,0 @@
# # ###### ##### ### ###### ####### # # # # # # ###### # #
## # # # # # # # # # # # # # # # # #
# ## # # # # # # ####### # # # # # # # # ## # #
# ## # # # # # # # # # # # # # # #
# # ###### # ### ###### ####### ###### # # ###### ###### ######
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "earthquake-bot-for-mtweet", "name": "earthquake-bot-for-mtweet",
"version": "1.0.0", "version": "1.0.3",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,4 +1,4 @@
import { readFileSync, writeFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import sharp from "sharp"; import sharp from "sharp";
/** /**
+45 -56
View File
@@ -6,72 +6,61 @@ import { EOL } from "node:os";
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import generateImage from "@/earthquake/generateImage"; import generateImage from "@/earthquake/generateImage";
if (config.earthquake?.useHistoryData) { export default function earthquake() {
console.log("過去の地震情報を配信します"); if (config.earthquake?.useHistoryData) {
const history = JSON.parse(readFileSync(`${import.meta.dirname}/../../260420.json`, "utf-8")); console.log("過去の地震情報を配信します");
history.reverse(); const history = JSON.parse(readFileSync(`${import.meta.dirname}/../../260420.json`, "utf-8"));
history.reverse();
let i = 0; let i = 0;
setInterval(() => { setInterval(() => {
processMessage(history[i]); processMessage(history[i]);
i++; i++;
}, 10 * 1000); }, 10 * 1000);
} else { } else {
const connect = () => { const connect = () => {
const WEBSOCKET_URL = config.debug const WEBSOCKET_URL = config.debug
? "wss://api-realtime-sandbox.p2pquake.net/v2/ws" ? "wss://api-realtime-sandbox.p2pquake.net/v2/ws"
: "wss://api.p2pquake.net/v2/ws"; : "wss://api.p2pquake.net/v2/ws";
console.log("P2P地震情報のWebSocketに接続します"); console.log("P2P地震情報のWebSocketに接続します");
const socket = new WebSocket(WEBSOCKET_URL); const socket = new WebSocket(WEBSOCKET_URL);
socket.addEventListener("open", () => { socket.addEventListener("open", () => {
console.log("P2P地震情報のWebSocketに接続しました"); console.log("P2P地震情報のWebSocketに接続しました");
}); });
socket.addEventListener("close", (err) => { socket.addEventListener("close", (err) => {
const interval = config.earthquake.reconnectInterval; const interval = config.earthquake.reconnectInterval;
console.log(`WebSocketが切断されました。${interval / 1000}秒後に再接続します:`, err.reason); console.log(`WebSocketが切断されました。${interval / 1000}秒後に再接続します:`, err.reason);
setTimeout(() => { setTimeout(() => {
connect(); connect();
}, interval); }, interval);
}); });
socket.addEventListener("error", (err) => { socket.addEventListener("error", (err) => {
console.error("WebSocketでエラーが発生しました:", err); console.error("WebSocketでエラーが発生しました:", err);
socket.close(); socket.close();
}); });
socket.addEventListener("message", async (event) => { socket.addEventListener("message", async (event) => {
let message; let message;
try { try {
message = typeof event.data === "string" message = typeof event.data === "string"
? JSON.parse(event.data) ? JSON.parse(event.data)
: event.data; : event.data;
} catch (err) { } catch (err) {
console.error("地震情報メッセージのパースでエラーが発生:", err); console.error("地震情報メッセージのパースでエラーが発生:", err);
return; return;
} }
const id = message.id ?? message._id; processMessage(message);
const mem = Memory.memory; });
if (mem.processedInfo.includes(id) && !config.debug) { }
console.log("重複した地震情報:", message.id);
return;
}
processMessage(message); connect();
if (!config.debug) {
const mem = Memory.memory;
mem.processedInfo = mem.processedInfo.concat([id]);
Memory.memory = mem;
}
});
} }
connect();
} }
const processMessage = async (message: any) => { const processMessage = async (message: any) => {
+2 -43
View File
@@ -1,12 +1,9 @@
import { schedule } from "node-cron";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import config from "@/lib/config"; import config from "@/lib/config";
import { initData } from "@/lib/memory";
import { styleText } from "node:util"; import { styleText } from "node:util";
import { Worker } from "node:worker_threads"; import earthquake from "@/earthquake";
try { try {
console.log(readFileSync(`${import.meta.dirname}/../asciiart.txt`, "utf-8"));
console.log(JSON.parse(readFileSync(`${import.meta.dirname}/../package.json`, "utf-8")).version); console.log(JSON.parse(readFileSync(`${import.meta.dirname}/../package.json`, "utf-8")).version);
if (config.debug) { if (config.debug) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
@@ -18,9 +15,7 @@ try {
} }
console.log(); console.log();
await initData(); earthquake();
new Worker(`${import.meta.dirname}/feature/earthquake/index.js`);
console.log("Botが起動しました"); console.log("Botが起動しました");
} catch (err: any) { } catch (err: any) {
@@ -28,40 +23,4 @@ try {
? err.message ? err.message
: err); : err);
process.exit(1); process.exit(1);
}
try {
schedule("0 * * * *", async () => {
new Worker(`${import.meta.dirname}/feature/time/index.js`);
});
schedule("0 7 * * *", async () => {
new Worker(`${import.meta.dirname}/feature/weatherNotice.js`, {
workerData: "scheduledWeatherNotice",
});
});
schedule("0 18 * * *", async () => {
new Worker(`${import.meta.dirname}/feature/weatherNotice.js`, {
workerData: "scheduledWeatherNoticeTomorrow",
});
});
schedule(`*/${config.command.interval} * * * *`, async () => {
new Worker(`${import.meta.dirname}/feature/command/index.js`);
});
let hnyWorker: Worker | undefined = undefined;
schedule("57 59 23 31 12 *", () => {
hnyWorker = new Worker(`${import.meta.dirname}/feature/hnyNotice.js`);
});
schedule("0 0 0 1 1 *", () => {
hnyWorker?.postMessage("");
});
} catch (err: any) {
console.error("message" in err
? err.message
: err);
} }
+37 -28
View File
@@ -11,22 +11,34 @@ type SuccessPosted<T = string> = {
post: { post: {
id: T; id: T;
content: string; content: string;
zone: "normal" | string; zone: "normal" | "free";
created_at: string; created_at: string;
}; };
}; };
} }
const BASE_URL = "https://collapse.jp/api/v3/bot"; const getRealLength = (str: string): number => {
const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
return [...segmenter.segment(str)].length;
};
const realSlice = (str: string, limit: number): string => {
const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
const segments = [...segmenter.segment(str)];
return segments.slice(0, limit).map(s => s.segment).join("");
};
export const createPost = async (data: { export const createPost = async (data: {
text: string; text: string;
zone?: "normal" | string; zone?: "normal" | "free";
replyTarget?: "all" | string; replyTarget?: "all" | "followers" | "mutual" | "mentioned" | "none";
replyid?: string; replyid?: string;
images?: [Blob, string][]; images?: [Blob, string][];
}, title: string) => { }, title: string) => {
const excessedMessage = "**👉返信に続きがあります。**"; const excessedMessage = "**👉返信に続きがあります。**";
const excessedMessageLength = getRealLength(excessedMessage);
const eolLength = getRealLength(EOL);
let lines = data.text.split(EOL); let lines = data.text.split(EOL);
let firstUniqid = ""; let firstUniqid = "";
@@ -40,23 +52,25 @@ export const createPost = async (data: {
const maxLength = config.post.maxLength; const maxLength = config.post.maxLength;
const limit = maxLength - (excessedMessage.length + EOL.length * 2); const limit = maxLength - (excessedMessageLength + eolLength * 2);
while (lines.length > 0) { while (lines.length > 0) {
const nextLine = lines[0]; const nextLine = lines[0];
if (nextLine === undefined) if (nextLine === undefined)
break; break;
if (nextLine.length > limit && currentText === "") { const nextLineLength = getRealLength(nextLine);
if (nextLineLength > limit && currentText === "") {
const targetLine = lines.shift() || ""; const targetLine = lines.shift() || "";
currentText = targetLine.slice(0, limit); currentText = realSlice(targetLine, limit);
lines.unshift(realSlice(targetLine, limit * -1));
lines.unshift(targetLine.slice(limit));
break; break;
} }
const potentialText = currentText ? currentText + EOL + nextLine : nextLine; const potentialText = currentText
if (potentialText.length <= limit) { ? currentText + EOL + nextLine
: nextLine;
if (getRealLength(potentialText) <= limit) {
currentText = potentialText; currentText = potentialText;
lines.shift(); lines.shift();
} else { } else {
@@ -78,31 +92,25 @@ export const createPost = async (data: {
const replyid = data.replyid === undefined && firstUniqid !== "" const replyid = data.replyid === undefined && firstUniqid !== ""
? firstUniqid ? firstUniqid
: data.replyid; : data.replyid;
const reply = replyid !== undefined
? `/${replyid}/reply`
: "";
const url = new URL(`/posts${reply}`, BASE_URL);
const body = new FormData(); const formData = new FormData();
body.append("content", data.text); formData.append("content", currentText);
if (!replyid) { formData.append("zone", data.zone ?? "normal");
body.append("zone", data.zone ?? "normal"); formData.append("reply_restriction", data.replyTarget ?? "all");
body.append("reply_restriction", data.replyTarget ?? "all"); if (replyid) {
formData.append("reply_to_id", replyid);
} }
if (data.images) { if (data.images) {
data.images.forEach(img => body.append("images", img[0], img[1])); data.images.forEach(img => formData.append("images", img[0], img[1]));
} }
const req = await fetch(url, { const req = await fetch(`https://collapse.jp/api/v3/bot/posts`, {
method: "POST", method: "POST",
body: JSON.stringify({
content: data.text,
zone: data.zone ?? "normal",
reply_restriction: data.replyTarget ?? "all",
}),
headers: { headers: {
"Authorization": `Bearer ${config.mtweet.token}`, "Authorization": `Bearer ${config.mtweet.token}`,
"X-Idempotency-Key": String(process.hrtime.bigint()),
}, },
body: formData,
}); });
const res = await req.json(); const res = await req.json();
@@ -114,11 +122,12 @@ export const createPost = async (data: {
break; break;
} }
console.warn(`${title}の投稿に失敗しました (試行 ${attempt}/${config.post.maxRetries}):`, res.error.message); console.warn(`${title}の投稿に失敗しました (試行 ${attempt}/${config.post.maxRetries}):`, `${res.error.message}(${res.error.code})`);
if (attempt < config.post.maxRetries) { if (attempt < config.post.maxRetries) {
const interval = res.error.details?.retry_after const interval = res.error.details?.retry_after
? res.error.details.retry_after * 1000 ? res.error.details.retry_after * 1000
: config.post.retryInterval; : config.post.retryInterval;
console.log(`${interval}ms待機します...`);
await new Promise(resolve => setTimeout(resolve, interval)); await new Promise(resolve => setTimeout(resolve, interval));
} }
} catch (err) { } catch (err) {
-28
View File
@@ -1,28 +0,0 @@
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,
},
});
};