Files
earthquake-bot-for-mtweet/src/lib/client.ts
T

154 lines
4.2 KiB
TypeScript

import config from "@/lib/config";
import { EOL } from "node:os";
type SuccessPosted<T = string> = {
ok: true;
data: {
status: "SUCCESS";
id: T;
source: "api";
deduped: boolean;
post: {
id: T;
content: string;
zone: "normal" | "free";
created_at: string;
};
};
}
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: {
text: string;
zone?: "normal" | "free";
replyTarget?: "all" | "followers" | "mutual" | "mentioned" | "none";
replyid?: string;
images?: [Blob, string][];
}, title: string) => {
const excessedMessage = "**👉返信に続きがあります。**";
const excessedMessageLength = getRealLength(excessedMessage);
const eolLength = getRealLength(EOL);
let lines = data.text.split(EOL);
let firstUniqid = "";
let count = 0;
const results: SuccessPosted[] = [];
while (lines.length > 0) {
count++;
let currentText = "";
const maxLength = config.post.maxLength;
const limit = maxLength - (excessedMessageLength + eolLength * 2);
while (lines.length > 0) {
const nextLine = lines[0];
if (nextLine === undefined)
break;
const nextLineLength = getRealLength(nextLine);
if (nextLineLength > limit && currentText === "") {
const targetLine = lines.shift() || "";
currentText = realSlice(targetLine, limit);
lines.unshift(realSlice(targetLine, limit * -1));
break;
}
const potentialText = currentText
? currentText + EOL + nextLine
: nextLine;
if (getRealLength(potentialText) <= limit) {
currentText = potentialText;
lines.shift();
} else {
break;
}
}
currentText = currentText.trimEnd();
if (lines.length > 0) {
currentText += EOL.repeat(2) + excessedMessage;
}
let postedUniqid = "";
let success = false;
for (let attempt = 1; attempt <= config.post.maxRetries; attempt++) {
try {
const replyid = data.replyid === undefined && firstUniqid !== ""
? firstUniqid
: data.replyid;
const formData = new FormData();
formData.append("content", currentText);
formData.append("zone", data.zone ?? "normal");
formData.append("reply_restriction", data.replyTarget ?? "all");
if (replyid) {
formData.append("reply_to_id", replyid);
}
if (data.images) {
data.images.forEach(img => formData.append("images", img[0], img[1]));
}
const req = await fetch(`https://collapse.jp/api/v3/bot/posts`, {
method: "POST",
headers: {
"Authorization": `Bearer ${config.mtweet.token}`,
"X-Idempotency-Key": String(process.hrtime.bigint()),
},
body: formData,
});
const res = await req.json();
if (res.ok) {
success = true;
postedUniqid = res.data.id;
results.push(res);
break;
}
console.warn(`${title}の投稿に失敗しました (試行 ${attempt}/${config.post.maxRetries}):`, `${res.error.message}(${res.error.code})`);
if (attempt < config.post.maxRetries) {
const interval = res.error.details?.retry_after
? res.error.details.retry_after * 1000
: config.post.retryInterval;
console.log(`${interval}ms待機します...`);
await new Promise(resolve => setTimeout(resolve, interval));
}
} catch (err) {
console.error(err);
}
}
if (!success) {
console.error(`${title}の全試行が失敗したため、処理を中断します。`);
break;
}
if (firstUniqid === "")
firstUniqid = postedUniqid;
console.log(`${title}を投稿(${count}):`, postedUniqid);
while (lines.length > 0 && lines[0]?.trim() === "") {
lines.shift();
}
}
return results;
}