/** * 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]*\)/, /\(\? 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) 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: `✏️ 对方修改了消息\n内容: ${escape(newText)}`, parse_mode: "HTML" }); } // 私聊消息处理总线 (用户侧逻辑入口) 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: "ℹ️ 帮助\n• 回复消息即对话\n• /start 打开面板", parse_mode: "HTML" }); // 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,'&').replace(//g,'>'); 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: "🛡️ 安全验证\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: "❓ 安全提问\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 = ref.topic_message_id; } // 特殊引用语法降级渲染支持 if (rawText && (rawText.startsWith('>') || rawText.startsWith('》') || rawText.startsWith('>'))) { let cleanText = rawText.replace(/^[>》]\s?/, '').replace(/^>\s?/, ''); cleanText = escape(cleanText); const customHtml = `
${cleanText}`; 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 { // 标准转发处理尝试,若触碰受限隐私配置则回退到原生复制 if (topicReplyToMsgId) { forwardedMsg = await api(env.BOT_TOKEN, "copyMessage", { chat_id: env.ADMIN_GROUP_ID, from_chat_id: uid, message_id: msg.message_id, message_thread_id: tid, reply_to_message_id: topicReplyToMsgId }); } else try { 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 }); } catch(fwdErr) { forwardedMsg = await api(env.BOT_TOKEN, "copyMessage", { 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: `🚫 用户已屏蔽\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 = `📨 备份 ${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 = `
${id}${note}\n🕒: ${timeStr}`
};
};
// 动态键盘矩阵构建:受限 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 };
};