240 lines
6.7 KiB
TypeScript
240 lines
6.7 KiB
TypeScript
import { ARTALK_SERVER, ARTALK_SITE, ARTALK_ADMIN_EMAIL, ARTALK_ADMIN_PASSWORD } from "@/lib/config";
|
|
import { sendMessage } from "./tg";
|
|
import { ALLOWED_IDS } from "./config";
|
|
|
|
interface ArtalkComment {
|
|
id: number;
|
|
content: string;
|
|
page_key: string;
|
|
page_title?: string;
|
|
nick: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface ArtalkPage {
|
|
id: number;
|
|
key: string;
|
|
title: string;
|
|
}
|
|
|
|
let lastCheckedId = 0;
|
|
let notifierStarted = false;
|
|
let authToken: string | null = null;
|
|
let tokenExpiry = 0;
|
|
|
|
// 登录 Artalk 获取管理员 token
|
|
async function loginToArtalk(): Promise<string | null> {
|
|
if (!ARTALK_ADMIN_EMAIL || !ARTALK_ADMIN_PASSWORD) {
|
|
console.error("[comment-notify] ARTALK_ADMIN_EMAIL or ARTALK_ADMIN_PASSWORD not configured");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const url = `${ARTALK_SERVER}/api/v2/auth/email/login`;
|
|
console.log("[comment-notify] logging in to Artalk...");
|
|
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
email: ARTALK_ADMIN_EMAIL,
|
|
password: ARTALK_ADMIN_PASSWORD,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.error("[comment-notify] login failed:", res.status);
|
|
return null;
|
|
}
|
|
|
|
const data = await res.json() as { token?: string; user?: { name: string } };
|
|
if (!data.token) {
|
|
console.error("[comment-notify] no token in response");
|
|
return null;
|
|
}
|
|
|
|
console.log("[comment-notify] logged in as:", data.user?.name || "admin");
|
|
return data.token;
|
|
} catch (e) {
|
|
console.error("[comment-notify] login error:", e instanceof Error ? e.message : String(e));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 获取或刷新 token
|
|
async function getAuthToken(): Promise<string | null> {
|
|
const now = Date.now();
|
|
|
|
// Token 有效(提前 5 分钟刷新)
|
|
if (authToken && now < tokenExpiry - 5 * 60 * 1000) {
|
|
return authToken;
|
|
}
|
|
|
|
// 登录获取新 token
|
|
authToken = await loginToArtalk();
|
|
if (authToken) {
|
|
// Token 有效期设为 24 小时
|
|
tokenExpiry = now + 24 * 60 * 60 * 1000;
|
|
}
|
|
|
|
return authToken;
|
|
}
|
|
|
|
// 获取所有页面
|
|
async function fetchAllPages(): Promise<ArtalkPage[]> {
|
|
try {
|
|
const token = await getAuthToken();
|
|
if (!token) {
|
|
console.error("[comment-notify] no auth token for fetching pages");
|
|
return [];
|
|
}
|
|
|
|
const url = `${ARTALK_SERVER}/api/v2/pages?site_name=${encodeURIComponent(ARTALK_SITE)}&limit=200`;
|
|
console.log("[comment-notify] fetching pages...");
|
|
|
|
const res = await fetch(url, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.error("[comment-notify] fetch pages failed:", res.status);
|
|
return [];
|
|
}
|
|
|
|
const data = await res.json() as { pages?: ArtalkPage[]; count?: number };
|
|
console.log("[comment-notify] found", data.pages?.length || 0, "pages (total:", data.count || 0, ")");
|
|
return data.pages || [];
|
|
} catch (e) {
|
|
console.error("[comment-notify] fetch pages error:", e instanceof Error ? e.message : String(e));
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 获取特定页面的评论
|
|
async function fetchCommentsForPage(pageKey: string): Promise<ArtalkComment[]> {
|
|
try {
|
|
const token = await getAuthToken();
|
|
if (!token) {
|
|
return [];
|
|
}
|
|
|
|
const url = `${ARTALK_SERVER}/api/v2/comments?site_name=${encodeURIComponent(ARTALK_SITE)}&page_key=${encodeURIComponent(pageKey)}&limit=5&sort_by=date&sort_order=desc`;
|
|
|
|
const res = await fetch(url, {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!res.ok) {
|
|
return [];
|
|
}
|
|
|
|
const data = await res.json() as { comments?: ArtalkComment[] };
|
|
return data.comments || [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 获取所有页面的最新评论
|
|
async function fetchLatestComments(): Promise<ArtalkComment[]> {
|
|
try {
|
|
// 1. 获取所有页面
|
|
const pages = await fetchAllPages();
|
|
if (pages.length === 0) {
|
|
console.log("[comment-notify] no pages found");
|
|
return [];
|
|
}
|
|
|
|
console.log("[comment-notify] checking comments from", pages.length, "pages...");
|
|
|
|
// 2. 并行获取每个页面的评论(只查最新的 20 个页面,避免过多请求)
|
|
const recentPages = pages.slice(0, 20);
|
|
const promises = recentPages.map(async (page) => {
|
|
const comments = await fetchCommentsForPage(page.key);
|
|
return comments;
|
|
});
|
|
|
|
const results = await Promise.all(promises);
|
|
const allComments: ArtalkComment[] = [];
|
|
results.forEach(comments => allComments.push(...comments));
|
|
|
|
// 3. 按时间排序,取最新的 5 条
|
|
allComments.sort((a, b) =>
|
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
);
|
|
|
|
const latestComments = allComments.slice(0, 5);
|
|
console.log("[comment-notify] fetched", latestComments.length, "latest comments from", recentPages.length, "recent pages");
|
|
|
|
return latestComments;
|
|
} catch (e) {
|
|
console.error("[comment-notify] fetch error:", e instanceof Error ? e.message : String(e));
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function checkNewComments(): Promise<void> {
|
|
const comments = await fetchLatestComments();
|
|
if (comments.length === 0) return;
|
|
|
|
// 首次运行,记录最新 ID,不发通知
|
|
if (lastCheckedId === 0) {
|
|
lastCheckedId = comments[0].id;
|
|
return;
|
|
}
|
|
|
|
// 找出新评论
|
|
const newComments = comments.filter(c => c.id > lastCheckedId);
|
|
if (newComments.length === 0) return;
|
|
|
|
// 更新最新 ID
|
|
lastCheckedId = Math.max(lastCheckedId, comments[0].id);
|
|
|
|
// 发送通知(倒序,最早的先发)
|
|
const targets = ALLOWED_IDS.length > 0 ? ALLOWED_IDS : [];
|
|
if (targets.length === 0) return;
|
|
|
|
for (const comment of newComments.reverse()) {
|
|
const text = comment.content.length > 100
|
|
? comment.content.slice(0, 100) + "..."
|
|
: comment.content;
|
|
|
|
const msg = [
|
|
`💬 <b>新评论</b>`,
|
|
`👤 ${comment.nick}`,
|
|
`📄 ${comment.page_title || comment.page_key}`,
|
|
``,
|
|
`${text}`,
|
|
``,
|
|
`🕐 ${new Date(comment.created_at).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`,
|
|
].join("\n");
|
|
|
|
for (const chatId of targets) {
|
|
try {
|
|
await sendMessage(chatId, msg, { parse_mode: "HTML" });
|
|
} catch (e) {
|
|
console.error("[comment-notify] send error:", e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function startCommentNotifier(): void {
|
|
if (notifierStarted) return;
|
|
notifierStarted = true;
|
|
|
|
console.log("[comment-notify] starting comment notifier...");
|
|
|
|
// 首次检查(记录当前最新 ID,不发通知)
|
|
checkNewComments();
|
|
|
|
// 每 60 秒检查一次
|
|
setInterval(checkNewComments, 60_000);
|
|
}
|