feat(telegram): 新增 Telegram 机器人通知与快捷审核功能
- 新增 Telegram 通知功能,支持通过机器人接收新评论通知 - 在 Telegram 消息中提供“批准”和“删除”按钮,支持快捷审核 - 新增后台 Telegram 设置页面,支持配置 Bot Token、Chat ID 和开关 - 新增 `/api/telegram/webhook` 端点处理按钮回调 - 更新邮件通知逻辑,统一使用评论设置中的 `adminEmail` 作为收件人 - 更新相关文档,包括功能说明和 API 文档
This commit is contained in:
@@ -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('未配置管理员通知邮箱或格式不正确');
|
||||
}
|
||||
|
||||
120
cwd-api/src/utils/telegram.ts
Normal file
120
cwd-api/src/utils/telegram.ts
Normal 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 }),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user