Files
script/telegram/tg-bot.js
Orion 8e0a56c4ba refactor(core): ♻️ 限制用户仅能删除自己发送的消息
通过增加对回复消息发送者的校验,防止用户尝试删除管理员(Bot)回复的消息。当检测到目标消息来自 Bot 时,将拦截删除请求并向用户发送提醒。同时移除了一些冗余的日志打印和旧的数据库查询逻辑。
2026-05-04 02:52:21 +08:00

1190 lines
62 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Telegram Bot Worker v3.68 (No-Receipt Mod, No-Username Fix & Security Hardening)
* 完整功能版:保留所有备份、配置面板、辅助函数
* * 修改 1: 修复了无用户名用户无法推送卡片的问题
* * 修改 2: 彻底移除了管理员回复的“✅ 已回复”提示
* * 修改 3: 彻底移除了用户发送消息后的“✅ 已送达”回执
* * 修改 4: 增加 Webhook secret、WebApp initData、nonce、管理员精确匹配与正则安全检查
*/
// --- 1. 静态配置与常量 ---
// 缓存系统,用于减少数据库读写压力,降低 Worker KV/D1 计费
const CACHE = { data: {}, ts: 0, ttl: 60000, user_locks: {}, warn_cd: {}, admin: { ts: 0, ttl: 60000, primary: new Set(), auth: new Set() } };
const DEFAULTS = {
// 基础设置
welcome_msg: "欢迎 {name}!使用前请先完成验证。",
// 验证设置
enable_verify: "true",
enable_qa_verify: "true",
captcha_mode: "turnstile",
verif_q: "1+1=?\n提示答案在简介中。",
verif_a: "3",
// 风控设置
block_threshold: "5",
enable_admin_receipt: "false", // 默认关闭管理员回执
// 转发类型开关配置
enable_image_forwarding: "true", enable_link_forwarding: "true", enable_text_forwarding: "true",
enable_channel_forwarding: "true", enable_forward_forwarding: "true", enable_audio_forwarding: "true", enable_sticker_forwarding: "true",
// 话题与列表存储占位
backup_group_id: "", blocked_topic_id: "",
busy_mode: "false", busy_msg: "当前是非营业时间,消息已收到,管理员稍后回复。",
block_keywords: "[]", keyword_responses: "[]", authorized_admins: "[]"
};
// 消息类型检查与映射字典
const MSG_TYPES = [
{ check: m => m.forward_from || m.forward_from_chat, key: 'enable_forward_forwarding', name: "转发消息", extra: m => m.forward_from_chat?.type === 'channel' ? 'enable_channel_forwarding' : null },
{ check: m => m.audio || m.voice, key: 'enable_audio_forwarding', name: "语音/音频" },
{ check: m => m.sticker || m.animation, key: 'enable_sticker_forwarding', name: "贴纸/GIF" },
{ check: m => m.photo || m.video || m.document, key: 'enable_image_forwarding', name: "媒体文件" },
{ check: m => (m.entities||[]).some(e => ['url','text_link'].includes(e.type)), key: 'enable_link_forwarding', name: "链接" },
{ check: m => m.text, key: 'enable_text_forwarding', name: "纯文本" }
];
const REGEX_MAX_PATTERN_LEN = 256;
const REGEX_MAX_TEXT_LEN = 512;
const REGEX_REJECT_PATTERNS = [
/\([^)]*\)\s*[+*{]/,
/\(\s*\.\*\s*\)\s*\+/,
/\(\s*\.\+\s*\)\s*\+/,
/\\[1-9]/,
/\(\?<=[\s\S]*\)/,
/\(\?<![\s\S]*\)/
];
// --- 2. 核心入口 (Entry Point) ---
export default {
async fetch(req, env, ctx) {
// 确保数据库初始化完毕waitUntil不会阻塞主线程的即时响应
ctx.waitUntil(dbInit(env).catch(err => console.error("DB Init Failed:", err)));
const url = new URL(req.url);
try {
// GET 请求处理:验证页面加载或连通性测试
if (req.method === "GET") {
if (url.pathname === "/verify") return handleVerifyPage(url, env);
if (url.pathname === "/") return new Response("Bot v3.68 Active", { status: 200 });
}
// POST 请求处理Telegram Webhook 核心逻辑接收端
if (req.method === "POST") {
if (url.pathname === "/submit_token") return handleTokenSubmit(req, env);
if (!isTelegramWebhook(req, env)) return new Response("Forbidden", { status: 403 });
try {
const update = await req.json();
ctx.waitUntil(handleUpdate(update, env, ctx));
return new Response("OK");
} catch (jsonErr) {
console.error("Invalid JSON Update:", jsonErr);
return new Response("Bad Request", { status: 400 });
}
}
} catch (e) {
console.error("Critical Worker Error:", e);
return new Response("Internal Server Error", { status: 500 });
}
return new Response("404 Not Found", { status: 404 });
}
};
// --- 3. 数据库与工具函数 ---
// 安全解析JSON避免非规范格式导致脚本执行中断
const safeParse = (str, fallback = {}) => {
if (!str) return fallback;
try { return JSON.parse(str); }
catch (e) { console.error("JSON Parse Error:", e); return fallback; }
};
// SQL 执行封装,支持不同的运行类型 (run, all, first)
const sql = async (env, query, args = [], type = 'run') => {
try {
const stmt = env.TG_BOT_DB.prepare(query).bind(...(Array.isArray(args) ? args : [args]));
return type === 'run' ? await stmt.run() : await stmt[type]();
} catch (e) {
console.error(`SQL Error [${query}]:`, e);
return null;
}
};
// 获取配置项:优先命中内存缓存以提升响应速度
async function getCfg(key, env) {
const now = Date.now();
if (CACHE.ts && (now - CACHE.ts) < CACHE.ttl && CACHE.data[key] !== undefined) return CACHE.data[key];
const rows = await sql(env, "SELECT * FROM config", [], 'all');
if (rows && rows.results) {
CACHE.data = {};
rows.results.forEach(r => CACHE.data[r.key] = r.value);
CACHE.ts = now;
}
const envKey = key.toUpperCase().replace(/_MSG|_Q|_A/, m => ({'_MSG':'_MESSAGE','_Q':'_QUESTION','_A':'_ANSWER'}[m]));
return CACHE.data[key] !== undefined ? CACHE.data[key] : (env[envKey] || DEFAULTS[key] || "");
}
// 设置配置项:同步使当前内存缓存失效
async function setCfg(key, val, env) {
await sql(env, "INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", [key, val]);
CACHE.ts = 0;
if (key === 'authorized_admins') CACHE.admin.ts = 0;
}
// 获取或初始化用户信息实体
async function getUser(id, env) {
let u = await sql(env, "SELECT * FROM users WHERE user_id = ?", id, 'first');
if (!u) {
try { await sql(env, "INSERT OR IGNORE INTO users (user_id, user_state) VALUES (?, 'new')", id); } catch {}
u = await sql(env, "SELECT * FROM users WHERE user_id = ?", id, 'first');
}
if (!u) u = { user_id: id, user_state: 'new', is_blocked: 0, block_count: 0, first_message_sent: 0, topic_id: null, user_info_json: "{}" };
// 布尔状态类型转换及附属 JSON 解析
u.is_blocked = !!u.is_blocked;
u.first_message_sent = !!u.first_message_sent;
u.user_info = safeParse(u.user_info_json);
return u;
}
// 增量更新用户信息记录
async function updUser(id, data, env) {
if (data.user_info) {
data.user_info_json = JSON.stringify(data.user_info);
delete data.user_info;
}
const keys = Object.keys(data);
if (!keys.length) return;
const query = `UPDATE users SET ${keys.map(k => `${k}=?`).join(',')} WHERE user_id=?`;
const values = [...keys.map(k => typeof data[k] === 'boolean' ? (data[k]?1:0) : data[k]), id];
await sql(env, query, values);
}
// 数据库表结构初始化与防御性兼容处理
async function dbInit(env) {
if (!env.TG_BOT_DB) return;
await env.TG_BOT_DB.batch([
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)`),
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS users (user_id TEXT PRIMARY KEY, user_state TEXT DEFAULT 'new', is_blocked INTEGER DEFAULT 0, block_count INTEGER DEFAULT 0, first_message_sent INTEGER DEFAULT 0, topic_id TEXT, user_info_json TEXT)`),
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS messages (user_id TEXT, message_id TEXT, text TEXT, date INTEGER, PRIMARY KEY (user_id, message_id))`)
]);
try { await sql(env, "ALTER TABLE messages ADD COLUMN topic_message_id TEXT"); } catch (e) {}
}
// --- 4. 业务逻辑 (核心流) ---
// Telegram Bot API 原生请求封装
async function api(token, method, body) {
try {
const r = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body)
});
const d = await r.json();
if (!d.ok) { console.warn(`TG API Error [${method}]:`, d.description); throw new Error(d.description); }
return d.result;
} catch (e) { throw e; }
}
// 自动向 Telegram 注册快捷菜单命令
async function registerCommands(env) {
try {
await api(env.BOT_TOKEN, "deleteMyCommands", { scope: { type: "default" } });
await api(env.BOT_TOKEN, "setMyCommands", { commands: [{ command: "start", description: "开始 / Start" }], scope: { type: "default" } });
const sets = await getAdminSets(env);
const admins = [...sets.auth];
for (const id of admins) await api(env.BOT_TOKEN, "setMyCommands", { commands: [{ command: "start", description: "⚙️ 管理面板" }, { command: "help", description: "📄 帮助说明" }], scope: { type: "chat", chat_id: id } });
} catch (e) { console.error("Register Commands Failed:", e); }
}
// 全局更新对象分发调度中心
async function handleUpdate(update, env, ctx) {
const msg = update.message || update.edited_message;
if (!msg) return update.callback_query ? handleCallback(update.callback_query, env) : null;
// 监听管理员侧的消息变更事件
if (update.edited_message && msg.chat.id.toString() === env.ADMIN_GROUP_ID) {
return handleAdminEdit(msg, env);
}
// 监听用户侧的消息变更事件
if (update.edited_message) return (msg.chat.type === "private") ? handleEdit(msg, env) : null;
// 会话路由
if (msg.chat.type === "private") await handlePrivate(msg, env, ctx);
else if (msg.chat.id.toString() === env.ADMIN_GROUP_ID) {
// 检查是否是管理员的 /del 命令
if ((msg.text === "/del" || msg.caption === "/del") && msg.reply_to_message) {
await handleAdminDelete(msg, env);
} else {
await handleAdminReply(msg, env);
}
}
}
// 管理员编辑群组内消息时的逻辑,主动通知用户变更内容
async function handleAdminEdit(msg, env) {
if (!msg.message_thread_id) return;
const u = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first');
if (!u) return;
const newText = msg.text || msg.caption || "[媒体消息]";
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: u.user_id,
text: `✏️ <b>对方修改了消息</b>\n内容: ${escape(newText)}`,
parse_mode: "HTML"
});
}
// 用户侧删除消息处理
async function handleUserDelete(msg, u, env) {
if (!msg.reply_to_message) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: u.user_id,
text: "⚠️ 请回复要删除的消息后使用 /del 命令",
reply_to_message_id: msg.message_id
});
}
// 检查是否是 Bot 发送的消息(管理员回复)
if (msg.reply_to_message.from && msg.reply_to_message.from.is_bot) {
console.log(`Delete blocked: User tried to delete bot's message`);
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: u.user_id,
text: "❌ 您只能删除自己发送的消息,无法删除管理员回复的消息",
reply_to_message_id: msg.message_id
});
}
const targetMsgIdRaw = msg.reply_to_message.message_id;
const targetMsgId = targetMsgIdRaw.toString();
console.log(`Delete request: user=${u.user_id}, target_msg_raw=${targetMsgIdRaw} (type: ${typeof targetMsgIdRaw}), target_msg_str=${targetMsgId}`);
// 查询对应的管理员侧消息ID - 尝试多种可能的格式
let ref = await sql(env, "SELECT topic_message_id FROM messages WHERE user_id=? AND message_id=?", [u.user_id, targetMsgId], 'first');
// 如果没找到,尝试用整数查询(以防数据库中存的是数字)
if (!ref || !ref.topic_message_id) {
console.log(`First query failed, trying with integer...`);
ref = await sql(env, "SELECT topic_message_id FROM messages WHERE user_id=? AND message_id=?", [u.user_id, parseInt(targetMsgId)], 'first');
}
if (!ref || !ref.topic_message_id) {
console.log(`Delete failed: No mapping found for user=${u.user_id}, msg=${targetMsgId}`);
console.log(`Tip: Check database records with: SELECT * FROM messages WHERE user_id='${u.user_id}'`);
// 帮助用户排查列出该用户的最近5条消息记录
try {
const recentMsgs = await sql(env, "SELECT message_id, topic_message_id, text FROM messages WHERE user_id=? ORDER BY date DESC LIMIT 5", [u.user_id], 'all');
if (recentMsgs && recentMsgs.results) {
console.log(`Recent messages for user ${u.user_id}:`, JSON.stringify(recentMsgs.results));
}
} catch (e) {
console.log(`Failed to fetch recent messages:`, e.message);
}
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: u.user_id,
text: "❌ 未找到对应的消息记录,可能该消息未被转发或已被删除",
reply_to_message_id: msg.message_id
});
}
console.log(`Delete success: Found mapping topic_msg=${ref.topic_message_id}`);
try {
// 1. 删除被回复的目标消息(先删目标,再删命令)
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: u.user_id,
message_id: parseInt(targetMsgId)
}).catch((e) => console.log("Failed to delete target msg:", e.message));
// 2. 删除用户侧的 /del 命令消息
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: u.user_id,
message_id: msg.message_id
}).catch((e) => console.log("Failed to delete /del cmd:", e.message));
// 3. 通知管理员(引用原消息)
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: env.ADMIN_GROUP_ID,
message_thread_id: u.topic_id,
text: `🗑️ <b>用户已删除消息</b>`,
parse_mode: "HTML",
reply_to_message_id: parseInt(ref.topic_message_id)
}).catch((e) => console.log("Failed to notify admin:", e.message));
// 4. 清理数据库记录
await sql(env, "DELETE FROM messages WHERE user_id=? AND message_id=?", [u.user_id, targetMsgId]);
console.log(`Delete completed: Cleaned up database record`);
} catch (e) {
console.error("User Delete Failed:", e);
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: u.user_id,
text: "❌ 删除失败,请稍后重试",
reply_to_message_id: msg.message_id
});
}
}
// 管理员侧删除消息处理
async function handleAdminDelete(msg, env) {
if (!msg.message_thread_id || !(await isAuthAdmin(msg.from.id, env))) return;
if (!msg.reply_to_message) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: "⚠️ 请回复要删除的消息后使用 /del 命令"
});
}
const targetTopicMsgId = msg.reply_to_message.message_id;
// 查询对应的用户侧消息ID
const ref = await sql(env, "SELECT user_id, message_id FROM messages WHERE topic_message_id=?", targetTopicMsgId.toString(), 'first');
if (!ref) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: "❌ 未找到对应的消息记录"
});
}
try {
// 1. 删除管理员侧消息(包括 /del 命令本身和被回复的消息)
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: msg.chat.id,
message_id: msg.message_id
}).catch(() => {});
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: msg.chat.id,
message_id: targetTopicMsgId
}).catch(() => {});
// 2. 删除用户侧消息
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: ref.user_id,
message_id: parseInt(ref.message_id)
}).catch(() => {});
// 3. 清理数据库记录
await sql(env, "DELETE FROM messages WHERE user_id=? AND message_id=?", [ref.user_id, ref.message_id]);
} catch (e) {
console.error("Admin Delete Failed:", e);
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: "❌ 删除失败,请稍后重试"
});
}
}
// 私聊消息处理总线 (用户侧逻辑入口)
async function handlePrivate(msg, env, ctx) {
const id = msg.chat.id.toString();
const text = msg.text || "";
const isAdm = await isPrimaryAdmin(id, env);
const u = await getUser(id, env);
// 人机验证拦截器 (非管理人员未完成验证则阻断)
if (text !== "/start" && u.user_state !== 'pending_verification' && !isAdm) {
const isCaptchaOn = await getBool('enable_verify', env);
const isQAOn = await getBool('enable_qa_verify', env);
if ((isCaptchaOn || isQAOn) && u.user_state !== 'verified') {
const now = Date.now();
const lastWarn = CACHE.warn_cd[id] || 0;
// 设定3秒冷却限流阈值防止恶意并发攻击刷量
if (now - lastWarn < 3000) return;
CACHE.warn_cd[id] = now;
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: id,
text: "❗️❗️❗️请先进行验证,再发送消息",
reply_to_message_id: msg.message_id
});
}
}
// 1. 命令显式路由处理
if (text === "/start") {
if (isAdm && ctx) ctx.waitUntil(registerCommands(env));
if (!isAdm) {
await updUser(id, { user_state: 'new' }, env);
u.user_state = 'new';
}
return isAdm ? handleAdminConfig(id, null, 'menu', null, null, env) : sendStart(id, msg, env);
}
if (text === "/help" && isAdm) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: " <b>帮助</b>\n• 回复消息即对话\n• /start 打开面板\n• /del 删除消息", parse_mode: "HTML" });
if (text === "/del" && !isAdm) return handleUserDelete(msg, u, env);
// 2. 封禁拦截层
if (u.is_blocked) {
return;
}
// 3. 授权状态补偿验证
if (await isAuthAdmin(id, env)) {
if(u.user_state !== "verified") await updUser(id, { user_state: "verified" }, env);
if(text === "/start" && ctx) ctx.waitUntil(registerCommands(env));
}
// 4. 管理员配置状态机判定(面板输入模式截取)
if (isAdm) {
const stateStr = await getCfg(`admin_state:${id}`, env);
if (stateStr) {
const state = safeParse(stateStr);
if (state.action === 'input') return handleAdminInput(id, msg, state, env);
}
}
// 5. 常规状态验证路由
const isCaptchaOn = await getBool('enable_verify', env);
const isQAOn = await getBool('enable_qa_verify', env);
if (!isCaptchaOn && !isQAOn) {
if (u.user_state !== 'verified') await updUser(id, { user_state: "verified" }, env);
return handleVerifiedMsg(msg, u, env);
}
const state = u.user_state;
if (state === 'pending_verification') return verifyAnswer(id, text, env);
if (state === 'verified') return handleVerifiedMsg(msg, u, env);
return sendStart(id, msg, env);
}
// 首次接入:渲染欢迎页面与验证环节
async function sendStart(id, msg, env) {
const u = await getUser(id, env);
if (u.topic_id) {
try {
const success = await sendInfoCardToTopic(env, u, msg.from, u.topic_id);
if (!success) await updUser(id, { topic_id: null }, env);
} catch (e) { await updUser(id, { topic_id: null }, env); }
}
let welcomeRaw = await getCfg('welcome_msg', env);
const firstName = (msg.from.first_name || "用户").replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const nameDisplay = escape(firstName);
let mediaConfig = null;
let welcomeText = welcomeRaw;
try {
if (welcomeRaw.trim().startsWith('{')) {
mediaConfig = safeParse(welcomeRaw, null);
if(mediaConfig) welcomeText = mediaConfig.caption || "";
}
} catch {}
welcomeText = welcomeText.replace(/{name}|{user}/g, nameDisplay);
try {
if (mediaConfig && mediaConfig.type) {
const method = `send${mediaConfig.type.charAt(0).toUpperCase() + mediaConfig.type.slice(1)}`;
let body = { chat_id: id, caption: welcomeText, parse_mode: "HTML" };
if (mediaConfig.type === 'photo') body.photo = mediaConfig.file_id;
else if (mediaConfig.type === 'video') body.video = mediaConfig.file_id;
else if (mediaConfig.type === 'animation') body.animation = mediaConfig.file_id;
else body = { chat_id: id, text: welcomeText, parse_mode: "HTML" };
await api(env.BOT_TOKEN, method, body);
} else {
await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: welcomeText, parse_mode: "HTML" });
}
} catch (e) {
await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "Welcome!", parse_mode: "HTML" });
}
const url = (env.WORKER_URL || "").replace(/\/$/, '');
const mode = await getCfg('captcha_mode', env);
const hasKey = mode === 'recaptcha' ? env.RECAPTCHA_SITE_KEY : env.TURNSTILE_SITE_KEY;
const isCaptchaOn = await getBool('enable_verify', env);
const isQAOn = await getBool('enable_qa_verify', env);
if (isCaptchaOn && url && hasKey) {
const nonce = genNonce();
await updUser(id, { user_state: "pending_turnstile", user_info: { ...u.user_info, verify_nonce: nonce, verify_nonce_ts: Date.now() } }, env);
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: id,
text: "🛡️ <b>安全验证</b>\n请点击下方按钮完成人机验证以继续。",
parse_mode: "HTML",
reply_markup: { inline_keyboard: [[{ text: "点击进行验证", web_app: { url: `${url}/verify?user_id=${encodeURIComponent(id)}&nonce=${encodeURIComponent(nonce)}` } }]] }
});
} else if (!isCaptchaOn && isQAOn) {
await updUser(id, { user_state: "pending_verification" }, env);
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: id,
text: "❓ <b>安全提问</b>\n请回答\n" + await getCfg('verif_q', env),
parse_mode: "HTML"
});
}
}
// 正常态用户消息防线:敏感词与类型拦截器
async function handleVerifiedMsg(msg, u, env) {
const id = u.user_id, text = msg.text || "";
// 敏感词屏蔽预检系统
if (text) {
const kws = await getJsonCfg('block_keywords', env);
if ((Array.isArray(kws) ? kws : []).some(k => safeRegexTest(k, text))) {
const c = u.block_count + 1, max = parseInt(await getCfg('block_threshold', env)) || 5;
const willBlock = c >= max;
await updUser(id, { block_count: c, is_blocked: willBlock }, env);
if (willBlock) {
await manageBlacklist(env, u, msg.from, true);
return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "❌ 已封禁 (发送 /start 可申请解封)" });
}
return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `⚠️ 屏蔽词 (${c}/${max})` });
}
}
// 负载类型过滤体系
for (const t of MSG_TYPES) {
if (t.check(msg)) {
const isAdmin = await isAuthAdmin(id, env);
if ((t.extra && !(await getBool(t.extra(msg), env)) && !isAdmin) ||
(!t.extra && !(await getBool(t.key, env)) && !isAdmin)) {
return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `⚠️ 不接收 ${t.name}` });
}
break;
}
}
// 勿扰状态静默应答逻辑
if (await getBool('busy_mode', env)) {
const now = Date.now();
if (now - (u.user_info.last_busy_reply || 0) > 300000) {
await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "🌙 " + await getCfg('busy_msg', env) });
await updUser(id, { user_info: { ...u.user_info, last_busy_reply: now } }, env);
}
}
// 关键词自动回复钩子
if (text) {
const rules = await getJsonCfg('keyword_responses', env);
const match = (Array.isArray(rules) ? rules : []).find(r => safeRegexTest(r?.keywords, text));
if (match) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "自动回复:\n" + match.response });
}
// 所有前置校验通过,放行进入转发链路
await relayToTopic(msg, u, env);
}
// --- 核心:消息转发与话题流控引擎 ---
async function relayToTopic(msg, u, env) {
const uMeta = getUMeta(msg.from, u, msg.date), uid = u.user_id;
let tid = u.topic_id;
// 1. 动态检测并同步用户基础标识信息变更
if (u.user_info.name !== uMeta.name || u.user_info.username !== uMeta.username) {
u.user_info.name = uMeta.name; u.user_info.username = uMeta.username;
await updUser(uid, { user_info: u.user_info }, env);
if (u.topic_id) api(env.BOT_TOKEN, "editForumTopic", { chat_id: env.ADMIN_GROUP_ID, message_thread_id: u.topic_id, name: uMeta.topicName }).catch(e=>{});
}
// 2. 线程池资源申请:建立独立话题
if (!tid) {
if (CACHE.user_locks[uid]) return;
CACHE.user_locks[uid] = true;
try {
const freshU = await getUser(uid, env);
if (freshU.topic_id) tid = freshU.topic_id;
else {
const t = await api(env.BOT_TOKEN, "createForumTopic", { chat_id: env.ADMIN_GROUP_ID, name: uMeta.topicName });
tid = t.message_thread_id.toString();
u.user_info.card_msg_id = null;
const dummy = await api(env.BOT_TOKEN, "sendMessage", { chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, text: "✨ 正在加载用户资料...", disable_notification: true });
u.user_info.dummy_msg_id = dummy.message_id;
await updUser(uid, { topic_id: tid, user_info: u.user_info }, env);
}
} catch (e) {
console.error("Topic Creation Failed:", e);
delete CACHE.user_locks[uid];
return api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "系统忙,请稍后再试" });
}
delete CACHE.user_locks[uid];
}
try {
let forwardedMsg;
const rawText = msg.text || "";
let topicReplyToMsgId = undefined;
if (msg.reply_to_message) {
const ref = await sql(env, "SELECT topic_message_id FROM messages WHERE user_id=? AND message_id=?", [uid, msg.reply_to_message.message_id.toString()], 'first');
if (ref?.topic_message_id) {
topicReplyToMsgId = parseInt(ref.topic_message_id);
console.log(`Found reply mapping: user_msg=${msg.reply_to_message.message_id} -> topic_msg=${topicReplyToMsgId}`);
} else {
console.log(`No reply mapping found for user_msg=${msg.reply_to_message.message_id}`);
}
}
// 特殊引用语法降级渲染支持
if (rawText && (rawText.startsWith('>') || rawText.startsWith('》') || rawText.startsWith('&gt;'))) {
let cleanText = rawText.replace(/^[>》]\s?/, '').replace(/^&gt;\s?/, '');
cleanText = escape(cleanText);
const customHtml = `<blockquote>${cleanText}</blockquote>`;
forwardedMsg = await api(env.BOT_TOKEN, "sendMessage", {
chat_id: env.ADMIN_GROUP_ID,
message_thread_id: tid,
text: customHtml,
parse_mode: "HTML",
reply_to_message_id: topicReplyToMsgId
});
} else {
// 标准转发处理:统一使用 copyMessage 以支持引用关系
try {
const copyParams = {
chat_id: env.ADMIN_GROUP_ID,
from_chat_id: uid,
message_id: msg.message_id,
message_thread_id: tid
};
if (topicReplyToMsgId) {
copyParams.reply_to_message_id = topicReplyToMsgId;
}
forwardedMsg = await api(env.BOT_TOKEN, "copyMessage", copyParams);
} catch(fwdErr) {
console.log("copyMessage failed, trying forwardMessage:", fwdErr.message);
// forwardMessage 不支持 reply_to_message_id只能作为备选
forwardedMsg = await api(env.BOT_TOKEN, "forwardMessage", {
chat_id: env.ADMIN_GROUP_ID,
from_chat_id: uid,
message_id: msg.message_id,
message_thread_id: tid
});
}
}
// 推送资料卡片流程补偿机制
try {
let infoDirty = false;
if (!u.user_info.card_msg_id) {
try {
const cardId = await sendInfoCardToTopic(env, u, msg.from, tid, msg.date);
if (cardId) {
u.user_info.card_msg_id = cardId;
u.user_info.join_date = msg.date || (Date.now()/1000);
infoDirty = true;
}
} catch (innerErr) {
if (innerErr.message && (innerErr.message.includes("thread") || innerErr.message.includes("not found"))) {
throw innerErr;
}
}
}
// 回收临时占位符提升界面整洁度
if (u.user_info.dummy_msg_id) {
await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.dummy_msg_id }).catch(() => {});
delete u.user_info.dummy_msg_id; infoDirty = true;
}
if (infoDirty) await updUser(uid, { user_info: u.user_info }, env);
} catch (processErr) {
if (processErr.message && (processErr.message.includes("thread") || processErr.message.includes("not found"))) {
await updUser(uid, { topic_id: null }, env);
u.topic_id = null;
return relayToTopic(msg, u, env); // 进行一次自愈递归重试
}
}
// ---------------------------------------------------------------------
// * 修改点:注释/移除了面向用户的 “✅ 已送达” 反馈回执 API 调用。
// 此动作去除了多余的底层 fetch() 开销。
// 原逻辑: api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "✅ 已送达", ... }).catch(()=>{});
// ---------------------------------------------------------------------
// 构建映射关联池,支撑双向回复寻址
if (forwardedMsg && forwardedMsg.message_id) {
const storeText = msg.text || "[Media]";
await sql(env, "INSERT OR REPLACE INTO messages (user_id, message_id, text, date, topic_message_id) VALUES (?,?,?,?,?)",
[uid, msg.message_id, storeText, msg.date, forwardedMsg.message_id.toString()]);
}
// 下游归档链路:数据备份
await handleBackup(msg, uMeta, env);
} catch (e) {
console.error("Relay Failed:", e);
if (e.message && (e.message.includes("thread") || e.message.includes("not found") || e.message.includes("Bad Request"))) {
await updUser(uid, { topic_id: null }, env); u.topic_id = null; return relayToTopic(msg, u, env);
} else {
api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "❌ 发送失败,系统异常" });
}
}
}
// --- 核心:发送用户信息复合卡片 ---
async function sendInfoCardToTopic(env, u, tgUser, tid, date) {
const meta = getUMeta(tgUser, u, date || (Date.now()/1000));
let bestPhoto = null;
// 1. [容错防护] 非阻塞尝试获取目标用户头像
try {
const photos = await api(env.BOT_TOKEN, "getUserProfilePhotos", { user_id: u.user_id, limit: 1 });
if (photos && photos.photos && photos.photos.length > 0) {
bestPhoto = photos.photos[0][photos.photos[0].length - 1].file_id;
}
} catch (e) {}
try {
let cardMsg;
// 2. [边界防护] 校验配文长度防止超出 Telegram 1024字符图片 Caption 上限
const isCaptionTooLong = meta.card.length > 1000;
// 根据上下文存在性选择装配发送方式
if (bestPhoto && !isCaptionTooLong) {
try {
cardMsg = await api(env.BOT_TOKEN, "sendPhoto", {
chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, photo: bestPhoto, caption: meta.card, parse_mode: "HTML", reply_markup: getBtns(u.user_id, u.is_blocked, meta.username)
});
} catch (err) {
if (err.message && (err.message.includes("parse") || err.message.includes("MEDIA"))) {
cardMsg = await api(env.BOT_TOKEN, "sendMessage", {
chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, text: meta.card, parse_mode: "HTML", reply_markup: getBtns(u.user_id, u.is_blocked, meta.username)
});
} else {
throw err;
}
}
} else {
cardMsg = await api(env.BOT_TOKEN, "sendMessage", {
chat_id: env.ADMIN_GROUP_ID, message_thread_id: tid, text: meta.card, parse_mode: "HTML", reply_markup: getBtns(u.user_id, u.is_blocked, meta.username)
});
}
try { await api(env.BOT_TOKEN, "pinChatMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: cardMsg.message_id, message_thread_id: tid }); } catch (pinErr) {}
return cardMsg.message_id;
} catch (e) {
console.error("Send Info Card Failed:", e);
if (e.message && (e.message.includes("thread") || e.message.includes("not found"))) {
throw e;
}
return null;
}
}
// --- 5. 通用/黑名单 (管理黑名单控制域) ---
async function manageBlacklist(env, u, tgUser, isBlocking) {
let bid = await getCfg('blocked_topic_id', env);
if (!bid && isBlocking) {
try {
const t = await api(env.BOT_TOKEN, "createForumTopic", { chat_id: env.ADMIN_GROUP_ID, name: "🚫 黑名单" });
bid = t.message_thread_id.toString();
await setCfg('blocked_topic_id', bid, env);
} catch { return; }
}
if (!bid) return;
if (isBlocking) {
const meta = getUMeta(tgUser, u, Date.now()/1000);
const msg = await api(env.BOT_TOKEN, "sendMessage", {
chat_id: env.ADMIN_GROUP_ID, message_thread_id: bid, text: `<b>🚫 用户已屏蔽</b>\n${meta.card}`, parse_mode: "HTML",
reply_markup: { inline_keyboard: [[{ text: "✅ 解除屏蔽", callback_data: `unblock:${u.user_id}` }]] }
});
await updUser(u.user_id, { user_info: { ...u.user_info, blacklist_msg_id: msg.message_id } }, env);
} else {
if (u.user_info.blacklist_msg_id) {
try { await api(env.BOT_TOKEN, "deleteMessage", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.blacklist_msg_id }); } catch (e) { if(e.message && e.message.includes("thread")) await setCfg('blocked_topic_id', "", env); }
await updUser(u.user_id, { user_info: { ...u.user_info, blacklist_msg_id: null } }, env);
}
}
}
// 下游归档链路:异步数据备份到冷频道
async function handleBackup(msg, meta, env) {
const bid = await getCfg('backup_group_id', env);
if (!bid) return;
try {
const header = `<b>📨 备份</b> ${meta.name} (${meta.userId})`;
if (msg.text) await api(env.BOT_TOKEN, "sendMessage", { chat_id: bid, text: header + "\n" + msg.text, parse_mode: "HTML" });
else {
await api(env.BOT_TOKEN, "sendMessage", { chat_id: bid, text: header, parse_mode: "HTML" });
await api(env.BOT_TOKEN, "copyMessage", { chat_id: bid, from_chat_id: msg.chat.id, message_id: msg.message_id });
}
} catch (e) {}
}
// --- 6. 管理员功能模块 (双向交互枢纽) ---
async function handleAdminReply(msg, env) {
if (!msg.message_thread_id || msg.from.is_bot || !(await isAuthAdmin(msg.from.id, env))) return;
const stateStr = await getCfg(`admin_state:${msg.from.id}`, env);
if (stateStr) {
const state = safeParse(stateStr);
if (state.action === 'input_note') {
const targetUid = state.target;
const u = await getUser(targetUid, env);
if (msg.text === '/clear' || msg.text === '清除') delete u.user_info.note;
else u.user_info.note = msg.text;
const mockTgUser = { id: targetUid, username: u.user_info.username || "", first_name: u.user_info.name || "(未获取)", last_name: "" };
const newMeta = getUMeta(mockTgUser, u, u.user_info.join_date || (Date.now()/1000));
// 更新带有新附注的资料实体卡片
if (u.topic_id && u.user_info.card_msg_id) {
try {
await api(env.BOT_TOKEN, "editMessageCaption", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, caption: newMeta.card, parse_mode: "HTML", reply_markup: getBtns(targetUid, u.is_blocked, newMeta.username) });
} catch (e) {
try { await api(env.BOT_TOKEN, "editMessageText", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, text: newMeta.card, parse_mode: "HTML", reply_markup: getBtns(targetUid, u.is_blocked, newMeta.username) }); } catch(e2) {}
}
}
await updUser(targetUid, { user_info: u.user_info }, env);
await setCfg(`admin_state:${msg.from.id}`, "", env);
return api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: `✅ 备注已更新` });
}
}
const uid = (await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first'))?.user_id;
if (!uid) return;
let replyToMsgId = undefined;
if (msg.reply_to_message) {
const ref = await sql(env, "SELECT message_id FROM messages WHERE topic_message_id = ?", msg.reply_to_message.message_id.toString(), 'first');
if (ref) replyToMsgId = ref.message_id;
}
try {
const sent = await api(env.BOT_TOKEN, "copyMessage", { chat_id: uid, from_chat_id: msg.chat.id, message_id: msg.message_id, reply_to_message_id: replyToMsgId });
if (sent && sent.message_id) {
const storeText = msg.text || msg.caption || "[Admin Message]";
await sql(env, "INSERT OR REPLACE INTO messages (user_id, message_id, text, date, topic_message_id) VALUES (?,?,?,?,?)",
[uid, sent.message_id.toString(), storeText, msg.date || Math.floor(Date.now() / 1000), msg.message_id.toString()]);
}
// 此处为管理员端给用户下发消息的主逻辑。根据之前的版本,管理员侧发送成功后的回执代码也已经去除
} catch (e) { api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: "❌ 内部投递失败" }); }
}
async function handleEdit(msg, env) {
const u = await getUser(msg.from.id.toString(), env);
if (!u.topic_id) return;
const old = await sql(env, "SELECT text FROM messages WHERE user_id=? AND message_id=?", [u.user_id, msg.message_id], 'first');
const newTxt = msg.text || msg.caption || "[非文本]";
const logText = `✏️ 消息修改\n前: ${escape(old?.text||"?")}\n后: ${escape(newTxt)}`;
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: env.ADMIN_GROUP_ID,
message_thread_id: u.topic_id,
text: logText,
parse_mode: "HTML"
});
if (old) {
await sql(env, "UPDATE messages SET text=? WHERE user_id=? AND message_id=?", [newTxt, u.user_id, msg.message_id]);
}
}
// --- 7. Web验证外设接口组件 ---
async function handleVerifyPage(url, env) {
const uid = url.searchParams.get('user_id');
const nonce = url.searchParams.get('nonce') || "";
const mode = await getCfg('captcha_mode', env);
const siteKey = mode === 'recaptcha' ? env.RECAPTCHA_SITE_KEY : env.TURNSTILE_SITE_KEY;
if (!uid || !siteKey) return new Response("Miss Config (Check Mode/Key)", { status: 400 });
const scriptUrl = mode === 'recaptcha' ? "https://www.google.com/recaptcha/api.js" : "https://challenges.cloudflare.com/turnstile/v0/api.js";
const divClass = mode === 'recaptcha' ? "g-recaptcha" : "cf-turnstile";
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><script src="https://telegram.org/js/telegram-web-app.js"></script><script src="${scriptUrl}" async defer></script><style>body{display:flex;justify-content:center;align-items:center;height:100vh;background:#fff;font-family:sans-serif}#c{text-align:center;padding:20px;background:#f0f0f0;border-radius:10px}</style></head><body><div id="c"><h3>🛡️ 安全验证</h3><div class="${divClass}" data-sitekey="${escape(siteKey)}" data-callback="S"></div><div id="m"></div></div><script>const tg=window.Telegram.WebApp;tg.ready();const UI_USER_ID=${JSON.stringify(uid)};const UI_NONCE=${JSON.stringify(nonce)};function S(t){document.getElementById('m').innerText='验证中...';fetch('/submit_token',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:t,userId:UI_USER_ID,nonce:UI_NONCE,initData:tg.initData||''})}).then(r=>r.json()).then(d=>{if(d.success){document.getElementById('m').innerText='✅';setTimeout(()=>{tg.close();window.close();},1000)}else{document.getElementById('m').innerText='❌'}}).catch(e=>{document.getElementById('m').innerText='Error'})}</script></body></html>`;
return new Response(html, { headers: { "Content-Type": "text/html" } });
}
async function handleTokenSubmit(req, env) {
try {
const { token, userId, nonce, initData } = await req.json();
const parsed = await verifyTelegramInitData(initData || "", env.BOT_TOKEN, 600);
const verifiedUserId = parsed?.userId?.toString();
if (!verifiedUserId || (userId && userId.toString() !== verifiedUserId)) throw new Error("Invalid Telegram initData");
const user = await getUser(verifiedUserId, env);
if (user.is_blocked && !(await isAuthAdmin(verifiedUserId, env))) throw new Error("Blocked user");
if (!nonce || user.user_info.verify_nonce !== nonce || Date.now() - (user.user_info.verify_nonce_ts || 0) > 15 * 60 * 1000) {
throw new Error("Invalid verification nonce");
}
const mode = await getCfg('captcha_mode', env);
let success = false;
const verifyUrl = mode === 'recaptcha' ? 'https://www.google.com/recaptcha/api/siteverify' : 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const params = mode === 'recaptcha' ? new URLSearchParams({ secret: env.RECAPTCHA_SECRET_KEY, response: token }) : JSON.stringify({ secret: env.TURNSTILE_SECRET_KEY, response: token });
const headers = mode === 'recaptcha' ? { 'Content-Type': 'application/x-www-form-urlencoded' } : { 'Content-Type': 'application/json' };
const r = await fetch(verifyUrl, { method: 'POST', headers, body: params });
const d = await r.json();
success = d.success;
if (!success) throw new Error("Invalid Token");
if (await getBool('enable_qa_verify', env)) {
await updUser(verifiedUserId, { user_state: "pending_verification", user_info: { ...user.user_info, verify_nonce: "", verify_nonce_ts: 0 } }, env);
await api(env.BOT_TOKEN, "sendMessage", { chat_id: verifiedUserId, text: "✅ 验证通过!\n请回答\n" + await getCfg('verif_q', env) });
} else {
await updUser(verifiedUserId, { user_state: "verified", user_info: { ...user.user_info, verify_nonce: "", verify_nonce_ts: 0 } }, env);
await api(env.BOT_TOKEN, "sendMessage", { chat_id: verifiedUserId, text: "✅ 验证通过!\n现在您可以直接发送消息我会帮您转达给管理员。" });
}
return new Response(JSON.stringify({ success: true }));
} catch { return new Response(JSON.stringify({ success: false }), { status: 400 }); }
}
async function verifyAnswer(id, ans, env) {
if (ans.trim() === (await getCfg('verif_a', env)).trim()) {
await updUser(id, { user_state: "verified" }, env);
await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "✅ 验证通过!\n现在您可以直接发送消息我会帮您转达给管理员。" });
} else await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "❌ 错误" });
}
// --- 8. 菜单回调调度控制室 ---
async function handleCallback(cb, env) {
const { data, message: msg, from } = cb;
const [act, p1, p2, p3] = data.split(':');
if (act === 'note' && p1 === 'set') {
if (!msg || msg.chat.id.toString() !== env.ADMIN_GROUP_ID || !(await isAuthAdmin(from.id, env))) {
return api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "无操作权限", show_alert: true });
}
await setCfg(`admin_state:${from.id}`, JSON.stringify({ action: 'input_note', target: p2 }), env);
return api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: "⌨️ 请回复备注内容 (回复 /clear 清除):" });
}
if (act === 'config') {
if (!(await isPrimaryAdmin(from.id, env))) return api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "无操作权限", show_alert: true });
if (p1 === 'rotate_mode') {
const currentMode = await getCfg('captcha_mode', env);
const isEnabled = await getBool('enable_verify', env);
let nextMode = 'turnstile'; let nextEnable = 'true'; let toast = "已切换: Cloudflare";
if (isEnabled) {
if (currentMode === 'turnstile') { nextMode = 'recaptcha'; toast = "已切换: Google Recaptcha"; }
else { nextEnable = 'false'; nextMode = currentMode; toast = "验证码功能已关闭"; }
} else { nextMode = 'turnstile'; nextEnable = 'true'; toast = "已切换: Cloudflare"; }
await setCfg('captcha_mode', nextMode, env); await setCfg('enable_verify', nextEnable, env);
await api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: toast });
return handleAdminConfig(msg.chat.id, msg.message_id, 'menu', 'base', null, env);
}
await api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id });
return handleAdminConfig(msg.chat.id, msg.message_id, p1, p2, p3, env);
}
if (msg && msg.chat.id.toString() === env.ADMIN_GROUP_ID) {
if (!(await isAuthAdmin(from.id, env))) {
return api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "无操作权限", show_alert: true });
}
await api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id });
if (act === 'pin_card') api(env.BOT_TOKEN, "pinChatMessage", { chat_id: msg.chat.id, message_id: msg.message_id, message_thread_id: msg.message_thread_id });
else if (['block','unblock'].includes(act)) {
const isB = act === 'block'; const uid = p1; const u = await getUser(uid, env); const bid = await getCfg('blocked_topic_id', env);
await updUser(uid, { is_blocked: isB, block_count: 0 }, env);
// 响应变更,刷新目标人员资料卡片上的按钮渲染状态
if (u.user_info.card_msg_id) {
api(env.BOT_TOKEN, "editMessageReplyMarkup", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, reply_markup: getBtns(uid, isB, u.user_info.username) }).catch(()=>{});
}
await manageBlacklist(env, u, { id: uid, username: u.user_info.username, first_name: u.user_info.name }, isB);
if (!isB && msg.message_thread_id && bid && msg.message_thread_id.toString() === bid) api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "✅ 已解除屏蔽" });
else api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: isB ? "❌ 已屏蔽" : "✅ 已解封" });
}
}
}
// --- 9. 管理控制后台面板渲染 ---
async function handleAdminConfig(cid, mid, type, key, val, env) {
const render = (txt, kb) => api(env.BOT_TOKEN, mid?"editMessageText":"sendMessage", { chat_id: cid, message_id: mid, text: txt, parse_mode: "HTML", reply_markup: kb });
const back = { text: "🔙 返回", callback_data: "config:menu" };
try {
if (!type || type === 'menu') {
if (!key) return render("⚙️ <b>控制面板</b>", { inline_keyboard: [[{text:"📝 基础",callback_data:"config:menu:base"},{text:"🤖 自动回复",callback_data:"config:menu:ar"}], [{text:"🚫 屏蔽词",callback_data:"config:menu:kw"},{text:"🛠 过滤",callback_data:"config:menu:fl"}], [{text:"👮 协管",callback_data:"config:menu:auth"},{text:"💾 备份/通知",callback_data:"config:menu:bak"}], [{text:"🌙 营业状态",callback_data:"config:menu:busy"}]] });
if (key === 'base') {
const mode = await getCfg('captcha_mode', env); const captchaOn = await getBool('enable_verify', env); const qaOn = await getBool('enable_qa_verify', env);
let statusText = "❌ 已关闭"; if (captchaOn) statusText = mode === 'recaptcha' ? "Google" : "Cloudflare";
return render(`基础配置\n验证码模式: ${statusText}\n问题验证: ${qaOn?"✅":"❌"}`, { inline_keyboard: [[{text:"欢迎语",callback_data:"config:edit:welcome_msg"},{text:"问题",callback_data:"config:edit:verif_q"},{text:"答案",callback_data:"config:edit:verif_a"}], [{text: `验证码模式: ${statusText} (点击切换)`, callback_data:`config:rotate_mode`}], [{text: `问题验证: ${qaOn?"✅ 开启":"❌ 关闭"}`, callback_data:`config:toggle:enable_qa_verify:${!qaOn}`}], [back]] });
}
if (key === 'fl') return render("🛠 <b>过滤设置</b>", await getFilterKB(env));
if (['ar','kw','auth'].includes(key)) return render(`列表: ${key}`, await getListKB(key, env));
if (key === 'bak') {
const bid = await getCfg('backup_group_id', env), blk = await getCfg('blocked_topic_id', env);
return render(`💾 <b>备份与通知</b>\n备份群: ${bid||"无"}\n黑名单话题: ${blk?`✅ (${blk})`:"⏳"}`, { inline_keyboard: [[{text:"设备份群",callback_data:"config:edit:backup_group_id"},{text:"清备份",callback_data:"config:cl:backup_group_id"}],[{text:"重置黑名单",callback_data:"config:cl:blocked_topic_id"}],[back]] });
}
if (key === 'busy') {
const on = await getBool('busy_mode', env), msg = await getCfg('busy_msg', env);
return render(`🌙 <b>营业状态</b>\n当前: ${on?"🔴 休息中":"🟢 营业中"}\n回复语: ${escape(msg)}`, { inline_keyboard: [[{text:`切换为 ${on?"🟢 营业":"🔴 休息"}`,callback_data:`config:toggle:busy_mode:${!on}`}], [{text:"✏️ 修改回复语",callback_data:"config:edit:busy_msg"}], [back]] });
}
}
if (type === 'toggle') { await setCfg(key, val, env); return key==='busy_mode' ? handleAdminConfig(cid,mid,'menu','busy',null,env) : (key==='enable_qa_verify' ? handleAdminConfig(cid,mid,'menu','base',null,env) : render("🛠 <b>过滤设置</b>", await getFilterKB(env))); }
if (type === 'cl') { await setCfg(key, key==='authorized_admins'?'[]':'', env); return handleAdminConfig(cid, mid, 'menu', key==='blocked_topic_id'?'bak':(key==='authorized_admins'?'auth':'bak'), null, env); }
if (type === 'del') {
const realK = key==='kw'?'block_keywords':(key==='auth'?'authorized_admins':'keyword_responses'); let l = await getJsonCfg(realK, env); l = l.filter(i => (i.id||i).toString() !== val); await setCfg(realK, JSON.stringify(l), env); return render(`列表: ${key}`, await getListKB(key, env));
}
if (type === 'edit' || type === 'add') {
await setCfg(`admin_state:${cid}`, JSON.stringify({ action: 'input', key: key + (type==='add'?'_add':'') }), env);
let promptText = `请输入 ${key} 的值 (/cancel 取消):`;
if (key === 'ar' && type === 'add') promptText = `请输入自动回复规则,格式:\n<b>关键词===回复内容</b>\n\n例如:价格===请联系人工客服\n(/cancel 取消)`;
if (key === 'welcome_msg') promptText = `请发送新的欢迎语 (/cancel 取消):\n\n• 支持 <b>文字</b> 或 <b>图片/视频/GIF</b>\n• 支持占位符: {name}\n• 直接发送媒体即可`;
return api(env.BOT_TOKEN, "editMessageText", { chat_id: cid, message_id: mid, text: promptText, parse_mode: "HTML" });
}
} catch (e) { api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: mid, text: "Data Fetch Error", show_alert: true }); }
}
async function getFilterKB(env) {
const s = async k => (await getBool(k, env)) ? "✅" : "❌";
const b = (t, k, v) => ({ text: `${t} ${v}`, callback_data: `config:toggle:${k}:${v==="❌"}` });
const keys = ['enable_admin_receipt', 'enable_forward_forwarding', 'enable_image_forwarding', 'enable_audio_forwarding', 'enable_sticker_forwarding', 'enable_link_forwarding', 'enable_channel_forwarding', 'enable_text_forwarding'];
const vals = await Promise.all(keys.map(k => s(k)));
return { inline_keyboard: [[b("回执", keys[0], vals[0]), b("转发", keys[1], vals[1])], [b("媒体", keys[2], vals[2]), b("语音", keys[3], vals[3])], [b("贴纸", keys[4], vals[4]), b("链接", keys[5], vals[5])], [b("频道", keys[6], vals[6]), b("文本", keys[7], vals[7])], [{ text: "🔙 返回", callback_data: "config:menu" }]] };
}
async function getListKB(type, env) {
const k = type==='ar'?'keyword_responses':(type==='kw'?'block_keywords':'authorized_admins');
const l = await getJsonCfg(k, env);
const btns = l.map((i, idx) => [{ text: `🗑 ${type==='ar'?i.keywords:i}`, callback_data: `config:del:${type}:${i.id||i}` }]);
btns.push([{ text: " 添加", callback_data: `config:add:${type}` }], [{ text: "🔙 返回", callback_data: "config:menu" }]);
return { inline_keyboard: btns };
}
async function handleAdminInput(id, msg, state, env) {
const txt = msg.text || "";
if (txt === '/cancel') { await sql(env, "DELETE FROM config WHERE key=?", `admin_state:${id}`); return handleAdminConfig(id, null, 'menu', null, null, env); }
let k = state.key, val = txt;
try {
if (k === 'welcome_msg') {
if (msg.photo || msg.video || msg.animation) {
let fileId, type;
if (msg.photo) { type = 'photo'; fileId = msg.photo[msg.photo.length - 1].file_id; }
else if (msg.video) { type = 'video'; fileId = msg.video.file_id; }
else if (msg.animation) { type = 'animation'; fileId = msg.animation.file_id; }
val = JSON.stringify({ type: type, file_id: fileId, caption: msg.caption || "" });
} else { val = txt; }
}
else if (k.endsWith('_add')) {
k = k.replace('_add', ''); const realK = k==='ar'?'keyword_responses':(k==='kw'?'block_keywords':'authorized_admins'); const list = await getJsonCfg(realK, env);
if (k === 'ar') { const [kk, rr] = txt.split('==='); if(kk && rr) list.push({keywords:kk, response:rr, id:Date.now()}); else return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "❌ 格式识别异常,请使用:关键词===回复内容" }); }
else list.push(txt);
val = JSON.stringify(list); k = realK;
} else if (k === 'authorized_admins') { val = JSON.stringify(txt.split(/[,]/).map(s => s.trim()).filter(Boolean)); }
await setCfg(k, val, env); await sql(env, "DELETE FROM config WHERE key=?", `admin_state:${id}`);
const displayVal = (val.startsWith('{') && k === 'welcome_msg') ? "[媒体配置组合]" : val.substring(0,100);
await api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `${k} 规则已写入:\n${displayVal}` });
await handleAdminConfig(id, null, 'menu', null, null, env);
} catch (e) { api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: `❌ 指令提交受阻: ${e.message}` }); }
}
// --- 10. 工具函数池 (Pure Functions) ---
const getBool = async (k, e) => (await getCfg(k, e)) === 'true';
const getJsonCfg = async (k, e) => safeParse(await getCfg(k, e), []);
const escape = t => (t||"").toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
function parseIdsToSet(str) {
return new Set((str || "").toString().split(/[,]/).map(s => s.trim()).filter(Boolean));
}
async function getAdminSets(env) {
const now = Date.now();
if (CACHE.admin.ts && now - CACHE.admin.ts < CACHE.admin.ttl) return CACHE.admin;
const primary = parseIdsToSet(env.ADMIN_IDS || "");
const authList = await getJsonCfg('authorized_admins', env);
const auth = new Set([...primary, ...((Array.isArray(authList) ? authList : []).map(x => x.toString()))]);
CACHE.admin = { ts: now, ttl: CACHE.admin.ttl, primary, auth };
return CACHE.admin;
}
const isPrimaryAdmin = async (id, e) => (await getAdminSets(e)).primary.has(id.toString());
const isAuthAdmin = async (id, e) => (await getAdminSets(e)).auth.has(id.toString());
function safeRegexTest(pattern, text) {
try {
if (!pattern || typeof pattern !== "string") return false;
const p = pattern.trim();
if (!p || p.length > REGEX_MAX_PATTERN_LEN) return false;
if (REGEX_REJECT_PATTERNS.some(re => re.test(p))) return false;
const t = (text || "").toString();
return new RegExp(p, 'gi').test(t.length > REGEX_MAX_TEXT_LEN ? t.slice(0, REGEX_MAX_TEXT_LEN) : t);
} catch {
return false;
}
}
function isTelegramWebhook(req, env) {
const secret = (env.TELEGRAM_WEBHOOK_SECRET || "").toString();
if (!secret) return true;
const header = req.headers.get("X-Telegram-Bot-Api-Secret-Token") || "";
return timingSafeEqualStr(header, secret);
}
function timingSafeEqualStr(a, b) {
const aa = (a || "").toString();
const bb = (b || "").toString();
let out = aa.length ^ bb.length;
const len = Math.max(aa.length, bb.length);
for (let i = 0; i < len; i++) out |= (aa.charCodeAt(i) || 0) ^ (bb.charCodeAt(i) || 0);
return out === 0;
}
function genNonce(bytes = 24) {
const data = new Uint8Array(bytes);
crypto.getRandomValues(data);
return btoa(String.fromCharCode(...data)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
async function hmacSha256(key, data) {
const enc = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey("raw", key instanceof Uint8Array ? key : enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
return new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(data)));
}
function hex(bytes) {
return [...bytes].map(b => b.toString(16).padStart(2, "0")).join("");
}
async function verifyTelegramInitData(initData, botToken, maxAgeSec = 600) {
if (!initData || !botToken) return null;
const params = new URLSearchParams(initData);
const receivedHash = params.get("hash") || "";
if (!receivedHash) return null;
params.delete("hash");
const authDate = Number(params.get("auth_date") || 0);
if (!authDate || Math.abs(Math.floor(Date.now() / 1000) - authDate) > maxAgeSec) return null;
const checkString = [...params.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join("\n");
const secret = await hmacSha256("WebAppData", botToken);
const calcHash = hex(await hmacSha256(secret, checkString));
if (!timingSafeEqualStr(calcHash, receivedHash)) return null;
const user = safeParse(params.get("user"), null);
return user?.id ? { userId: user.id.toString(), user } : null;
};
// HTML `<a>` 标签组装逻辑:穿透安全审查拦截,对无用户名账号建立伪链接
const getUMeta = (tgUser, dbUser, d) => {
const id = tgUser.id.toString();
const firstName = tgUser.first_name || "";
const lastName = tgUser.last_name || "";
let name = (firstName + " " + lastName).trim();
if (!name) name = "未命名匿名用户";
// 文本级伪链接构建协议
const safeName = `<a href="tg://user?id=${id}"><b>${escape(name)}</b></a>`;
const note = dbUser.user_info && dbUser.user_info.note ? `\n📝 <b>附加备注:</b> ${escape(dbUser.user_info.note)}` : "";
const labelDisplay = tgUser.username ? `@${tgUser.username}` : "未设公开ID";
const timeStr = new Date(d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
return {
userId: id,
name,
username: tgUser.username,
topicName: name.substring(0, 128),
card: `<b>🪪 用户身份卡片</b>\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: <code>${id}</code>${note}\n🕒: <code>${timeStr}</code>`
};
};
// 动态键盘矩阵构建:受限 API 规避检查点
const getBtns = (id, blk, username) => {
const btns = [];
// username 条件控制路由:缺少公开标识符强制屏蔽按钮注册
if (username) {
btns.push([{ text: "👤 访问个人主页", url: `https://t.me/${username}` }]);
}
btns.push([{ text: blk ? "✅ 执行解封" : "🚫 执行屏蔽", callback_data: `${blk ? 'unblock' : 'block'}:${id}` }]);
btns.push([{ text: "✏️ 录入备注", callback_data: `note:set:${id}` }, { text: "📌 提升置顶", callback_data: `pin_card:${id}` }]);
return { inline_keyboard: btns };
};