feat(telegram): 新增 Telegram 机器人通知与快捷审核功能

- 新增 Telegram 通知功能,支持通过机器人接收新评论通知
- 在 Telegram 消息中提供“批准”和“删除”按钮,支持快捷审核
- 新增后台 Telegram 设置页面,支持配置 Bot Token、Chat ID 和开关
- 新增 `/api/telegram/webhook` 端点处理按钮回调
- 更新邮件通知逻辑,统一使用评论设置中的 `adminEmail` 作为收件人
- 更新相关文档,包括功能说明和 API 文档
This commit is contained in:
anghunk
2026-01-30 17:49:47 +08:00
parent c4954fca89
commit e1568f60ff
16 changed files with 678 additions and 85 deletions

View File

@@ -409,12 +409,22 @@ export async function getAdminNotifyEmail(env: Bindings): Promise<string> {
await env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const row = await env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('admin_notify_email')
.first<{ value: string }>();
if (row?.value && isValidEmail(row.value)) {
const cleanEmail = row.value.trim();
return cleanEmail;
const rows = await env.CWD_DB.prepare(
'SELECT key, value FROM Settings WHERE key IN (?, ?)'
)
.bind('comment_admin_email', 'admin_notify_email')
.all<{ key: string; value: string }>();
let email: string | null = null;
if (rows && Array.isArray(rows.results)) {
const commentEmailRow = rows.results.find((row) => row.key === 'comment_admin_email');
const legacyEmailRow = rows.results.find((row) => row.key === 'admin_notify_email');
email = commentEmailRow?.value || legacyEmailRow?.value || null;
}
if (email && isValidEmail(email)) {
return email.trim();
}
throw new Error('未配置管理员通知邮箱或格式不正确');
}

View File

@@ -0,0 +1,120 @@
import { Bindings } from '../bindings';
export const TG_BOT_TOKEN_KEY = 'telegram_bot_token';
export const TG_CHAT_ID_KEY = 'telegram_chat_id';
export const TG_NOTIFY_ENABLED_KEY = 'telegram_notify_enabled';
export interface TelegramSettings {
botToken: string | null;
chatId: string | null;
notifyEnabled: boolean;
}
export async function loadTelegramSettings(env: Bindings): Promise<TelegramSettings> {
const keys = [TG_BOT_TOKEN_KEY, TG_CHAT_ID_KEY, TG_NOTIFY_ENABLED_KEY];
const { results } = await env.CWD_DB.prepare(
'SELECT key, value FROM Settings WHERE key IN (?, ?, ?)'
)
.bind(...keys)
.all<{ key: string; value: string }>();
const map = new Map<string, string>();
for (const row of results) {
map.set(row.key, row.value);
}
return {
botToken: map.get(TG_BOT_TOKEN_KEY) ?? null,
chatId: map.get(TG_CHAT_ID_KEY) ?? null,
notifyEnabled: map.get(TG_NOTIFY_ENABLED_KEY) === '1',
};
}
export async function saveTelegramSettings(env: Bindings, settings: TelegramSettings) {
const entries = [
{ key: TG_BOT_TOKEN_KEY, value: settings.botToken },
{ key: TG_CHAT_ID_KEY, value: settings.chatId },
{ key: TG_NOTIFY_ENABLED_KEY, value: settings.notifyEnabled ? '1' : '0' },
];
for (const entry of entries) {
if (entry.value !== undefined && entry.value !== null) {
await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)')
.bind(entry.key, entry.value)
.run();
} else if (entry.value === null) { // Explicit null means delete
await env.CWD_DB.prepare('DELETE FROM Settings WHERE key = ?').bind(entry.key).run();
}
}
}
export async function sendTelegramMessage(
token: string,
chatId: string,
text: string,
options: any = {}
) {
const url = `https://api.telegram.org/bot${token}/sendMessage`;
const body = {
chat_id: chatId,
text,
parse_mode: 'Markdown',
...options,
};
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return response.json();
}
export async function setTelegramWebhook(token: string, webhookUrl: string) {
const url = `https://api.telegram.org/bot${token}/setWebhook`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: webhookUrl }),
});
return response.json();
}
export async function deleteMessage(token: string, chatId: string | number, messageId: number) {
const url = `https://api.telegram.org/bot${token}/deleteMessage`;
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, message_id: messageId }),
});
}
export async function editMessageText(token: string, chatId: string | number, messageId: number, text: string, options: any = {}) {
const url = `https://api.telegram.org/bot${token}/editMessageText`;
const body = {
chat_id: chatId,
message_id: messageId,
text,
parse_mode: 'Markdown',
...options,
};
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return response.json();
}
export async function answerCallbackQuery(token: string, callbackQueryId: string, text?: string) {
const url = `https://api.telegram.org/bot${token}/answerCallbackQuery`;
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ callback_query_id: callbackQueryId, text }),
});
}