154 lines
4.2 KiB
TypeScript
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;
|
|
} |