Files
script/telegram/tg-bot.js
Orion a0c06763c9 fix(core): 🩹 修复 tg-bot.js 中的异常文本内容
移除文件中的无意义字符串 yixia,恢复代码或配置文件的正确格式。该变更属于非关键性的简单修复。
2026-05-08 23:49:54 +08:00

1617 lines
79 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 MAX_BATCH_DELETE = 100;
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;
}
// 删除配置项:同步失效缓存,避免旧值被 getCfg 误读
async function deleteCfg(key, env) {
await sql(env, "DELETE FROM config WHERE key=?", key);
delete CACHE.data[key];
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, topic_message_id TEXT, PRIMARY KEY (user_id, message_id))`)
]);
}
// --- 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) {
const delCmd = parseDelCommand(msg.text || msg.caption || "");
if (delCmd) {
await handleAdminDelete(msg, env, delCmd);
} 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, delCmd = parseDelCommand(msg.text || msg.caption || "")) {
if (!msg.message_thread_id || !(await isAuthAdmin(msg.from.id, env))) return;
if (delCmd?.type === "all") return handleAdminBatchDelete(msg, env, { all: true });
if (delCmd?.type === "count") return handleAdminBatchDelete(msg, env, { count: delCmd.count });
if (delCmd?.type === "invalid") {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: `⚠️ 用法:回复消息使用 /del或使用 /del N 删除最近 N 条(最多 ${MAX_BATCH_DELETE} 条),/del all 清空当前话题`
});
}
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 命令,或使用 /del N 删除最近 N 条"
});
}
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 handleAdminBatchDelete(msg, env, options = {}) {
const userRef = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first');
if (!userRef?.user_id) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: "❌ 当前话题未绑定用户"
});
}
const u = await getUser(userRef.user_id, env);
const keepCardMsgId = u?.user_info?.card_msg_id ? parseInt(u.user_info.card_msg_id) : null;
const limit = Math.min(Math.max(parseInt(options.count) || 0, 1), MAX_BATCH_DELETE);
const rows = options.all
? await sql(env, "SELECT rowid, message_id, topic_message_id FROM messages WHERE user_id=? ORDER BY date DESC, rowid DESC", [u.user_id], 'all')
: await sql(env, "SELECT rowid, message_id, topic_message_id FROM messages WHERE user_id=? ORDER BY date DESC, rowid DESC LIMIT ?", [u.user_id, limit], 'all');
const mapped = rows?.results || [];
let adminDeleted = 0;
let userDeleted = 0;
try {
// 删除管理员侧命令消息本身
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: msg.chat.id,
message_id: msg.message_id
}).catch(() => {});
if (!mapped.length) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: "❌ 当前话题没有可删除的消息记录"
}).catch(() => {});
}
for (const r of mapped) {
const topicMid = parseInt(r.topic_message_id);
const userMid = parseInt(r.message_id);
if (Number.isFinite(topicMid) && (!keepCardMsgId || topicMid !== keepCardMsgId)) {
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: msg.chat.id,
message_id: topicMid
}).then(() => { adminDeleted += 1; }).catch(() => {});
}
if (Number.isFinite(userMid)) {
await api(env.BOT_TOKEN, "deleteMessage", {
chat_id: u.user_id,
message_id: userMid
}).then(() => { userDeleted += 1; }).catch(() => {});
}
}
if (options.all) {
await sql(env, "DELETE FROM messages WHERE user_id=?", [u.user_id]);
} else if (mapped.length) {
const rowIds = mapped.map(r => r.rowid).filter(id => id !== undefined && id !== null);
const placeholders = rowIds.map(() => "?").join(",");
await sql(env, `DELETE FROM messages WHERE user_id=? AND rowid IN (${placeholders})`, [u.user_id, ...rowIds]);
}
console.log(`Admin batch delete done: all=${!!options.all}, mapped=${mapped.length}, admin=${adminDeleted}, user=${userDeleted}`);
} catch (e) {
console.error("Admin Batch Delete Failed:", e);
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: "❌ 批量删除失败,请稍后重试"
}).catch(() => {});
}
}
// 私聊消息处理总线 (用户侧逻辑入口)
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) {
if (u.topic_id) await syncTopicProfile(u, msg.from, env);
if (u.user_state === 'verified' && !u.is_blocked) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: id,
text: "✅ 您已经完成验证,可以直接发送消息,我会帮您转达给管理员。",
reply_to_message_id: msg.message_id
});
}
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 删除单条消息\n• /del N 删除最近 N 条消息\n• /del all 清空当前话题消息(保留用户信息卡片)", parse_mode: "HTML" });
if (text === "/del" && !isAdm) return handleUserDelete(msg, u, env);
if (!isAdm && parseDelCommand(text)) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: id,
text: "⚠️ 用户侧只能回复要删除的消息后使用 /del 命令",
reply_to_message_id: msg.message_id
});
}
// 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 {
await syncTopicProfile(u, msg.from, env);
if (!u.user_info.card_msg_id) {
const cardId = await sendInfoCardToTopic(env, u, msg.from, u.topic_id);
if (cardId) {
u.user_info.card_msg_id = cardId;
u.user_info.join_date = msg.date || (Date.now()/1000);
await updUser(id, { user_info: u.user_info }, env);
} else {
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 (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"
});
} else if (isCaptchaOn || isQAOn) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: id,
text: "⚠️ 验证功能暂时不可用,请联系管理员检查配置"
});
}
}
// 正常态用户消息防线:敏感词与类型拦截器
async function handleVerifiedMsg(msg, u, env) {
const id = u.user_id, text = msg.text || "";
// 如果用户之前被标记为屏蔽 Bot但现在能发消息说明已解除屏蔽
if (u.user_info && u.user_info.bot_blocked) {
u.user_info.bot_blocked = false;
delete u.user_info.bot_blocked_ts;
await updUser(id, { user_info: u.user_info }, env);
console.log(`Cleared bot_blocked mark for user ${id} after receiving message`);
// 更新用户卡片显示
try {
if (u.topic_id && u.user_info.card_msg_id) {
const mockTgUser = { id: id, 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));
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(id, u.is_blocked, newMeta.username)
});
}
} catch (e) {
console.log("Failed to update card after bot_blocked cleared:", e.message);
}
}
// 敏感词屏蔽预检系统
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) {
await syncTopicProfile(u, msg.from, env);
}
// 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;
u.user_info.topic_name = uMeta.topicName;
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 {
// 资料卡片必须先落到话题里,后续用户消息才不会把卡片顶到下面。
await ensureInfoCardBeforeRelay(env, u, msg.from, tid, msg.date);
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
});
}
}
// ---------------------------------------------------------------------
// * 修改点:注释/移除了面向用户的 “✅ 已送达” 反馈回执 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.toString(), 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 {
await api(env.BOT_TOKEN, "sendMessage", { chat_id: uid, text: "❌ 发送失败,系统异常" });
}
}
}
async function ensureInfoCardBeforeRelay(env, u, tgUser, tid, date) {
let infoDirty = false;
if (!u.user_info) u.user_info = {};
if (!u.user_info.card_msg_id) {
const cardId = await sendInfoCardToTopic(env, u, tgUser, tid, date);
if (cardId) {
u.user_info.card_msg_id = cardId;
u.user_info.join_date = date || (Date.now()/1000);
infoDirty = true;
}
}
// 回收临时占位符提升界面整洁度;先发卡片再删占位,确保新 topic 里卡片始终早于用户消息。
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(u.user_id, { user_info: u.user_info }, env);
}
// --- 核心:发送用户信息复合卡片 ---
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" + escape(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. 管理员功能模块 (双向交互枢纽) ---
const withReplyTarget = (replyToMsgId) => {
const id = parseInt(replyToMsgId);
return Number.isFinite(id) ? { reply_to_message_id: id } : {};
};
const isReplyTargetError = (e) => {
const msg = (e?.message || "").toLowerCase();
const mentionsReply = msg.includes("reply") || msg.includes("replied");
return mentionsReply && (msg.includes("not found") || msg.includes("invalid"));
};
const isEntityPayloadError = (e) => {
const msg = (e?.message || "").toLowerCase();
return msg.includes("entity") || msg.includes("entities") || msg.includes("parse");
};
const withoutReplyTarget = (body) => {
const next = { ...body };
delete next.reply_to_message_id;
return next;
};
const withoutEntityPayload = (body) => {
const next = { ...body };
delete next.entities;
delete next.caption_entities;
return next;
};
async function apiWithDeliveryFallback(env, method, body) {
try {
return await api(env.BOT_TOKEN, method, body);
} catch (firstErr) {
const candidates = [];
const hasReplyTarget = body.reply_to_message_id !== undefined;
const hasEntityPayload = body.entities || body.caption_entities;
if (hasReplyTarget && isReplyTargetError(firstErr)) {
candidates.push({ body: withoutReplyTarget(body), reason: "reply target missing" });
if (hasEntityPayload) candidates.push({ body: withoutEntityPayload(withoutReplyTarget(body)), reason: "reply target missing + entity fallback" });
}
if (hasEntityPayload && isEntityPayloadError(firstErr)) {
candidates.push({ body: withoutEntityPayload(body), reason: "entity fallback" });
if (hasReplyTarget) candidates.push({ body: withoutReplyTarget(withoutEntityPayload(body)), reason: "entity fallback + reply target removed" });
}
let lastErr = firstErr;
for (const candidate of candidates) {
try {
console.warn(`Delivery fallback [${method}]: ${candidate.reason}`);
return await api(env.BOT_TOKEN, method, candidate.body);
} catch (candidateErr) {
lastErr = candidateErr;
}
}
throw lastErr;
}
}
async function deliverAdminMessageToUser(msg, uid, replyToMsgId, env) {
const reply = withReplyTarget(replyToMsgId);
const base = { chat_id: uid, ...reply };
if (msg.text) {
const body = { ...base, text: msg.text };
if (msg.entities) body.entities = msg.entities;
return apiWithDeliveryFallback(env, "sendMessage", body);
}
if (msg.photo) {
const body = { ...base, photo: msg.photo[msg.photo.length - 1].file_id };
if (msg.caption) body.caption = msg.caption;
if (msg.caption_entities) body.caption_entities = msg.caption_entities;
return apiWithDeliveryFallback(env, "sendPhoto", body);
}
const mediaMap = [
["animation", "sendAnimation", "animation"],
["video", "sendVideo", "video"],
["document", "sendDocument", "document"],
["audio", "sendAudio", "audio"],
["voice", "sendVoice", "voice"],
["video_note", "sendVideoNote", "video_note"],
["sticker", "sendSticker", "sticker"]
];
for (const [field, method, param] of mediaMap) {
if (!msg[field]) continue;
const body = { ...base, [param]: msg[field].file_id };
if (msg.caption) body.caption = msg.caption;
if (msg.caption_entities) body.caption_entities = msg.caption_entities;
return apiWithDeliveryFallback(env, method, body);
}
if (msg.location) {
return apiWithDeliveryFallback(env, "sendLocation", {
...base,
latitude: msg.location.latitude,
longitude: msg.location.longitude
});
}
if (msg.contact) {
return apiWithDeliveryFallback(env, "sendContact", {
...base,
phone_number: msg.contact.phone_number,
first_name: msg.contact.first_name,
last_name: msg.contact.last_name || ""
});
}
return apiWithDeliveryFallback(env, "copyMessage", {
chat_id: uid,
from_chat_id: msg.chat.id,
message_id: msg.message_id,
...reply
});
}
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 deleteCfg(`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 topicIdStr = msg.message_thread_id.toString();
console.log(`Admin reply debug: topic_id=${topicIdStr}, admin_msg_id=${msg.message_id}`);
const userRef = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", topicIdStr, 'first');
console.log(`Admin reply debug: userRef=`, userRef);
const uid = userRef?.user_id;
if (!uid) {
console.error(`Admin reply failed: No user found for topic_id=${topicIdStr}`);
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: `❌ 系统错误未找到关联的用户topic_id=${topicIdStr}`
});
}
// 检查用户状态
const u = await getUser(uid, env);
console.log(`Admin reply debug: user_state=${u.user_state}, is_blocked=${u.is_blocked}`);
if (u.is_blocked) {
return api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: `❌ 该用户已被屏蔽,无法发送消息`
});
}
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;
console.log(`Admin reply debug: Found reply mapping topic_msg=${msg.reply_to_message.message_id} -> user_msg=${replyToMsgId}`);
} else {
console.log(`Admin reply debug: No reply mapping found for topic_msg=${msg.reply_to_message.message_id}`);
}
}
try {
console.log(`Admin reply debug: Delivering message to uid=${uid}, replyToMsgId=${replyToMsgId}`);
const sent = await deliverAdminMessageToUser(msg, uid, replyToMsgId, env);
console.log(`Admin reply debug: Delivery success, sent.message_id=${sent?.message_id}`);
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) {
console.error("Admin Delivery Failed:", e);
console.error("Admin Delivery Failed - Error details:", {
message: e.message,
stack: e.stack,
uid: uid,
topicId: topicIdStr,
replyToMsgId: replyToMsgId
});
// 检测用户是否屏蔽了 Bot
const isBlocked = e.message && e.message.includes("bot was blocked by the user");
if (isBlocked) {
// 自动标记用户为屏蔽状态
try {
const u = await getUser(uid, env);
if (!u.user_info.bot_blocked) {
u.user_info.bot_blocked = true;
u.user_info.bot_blocked_ts = Date.now();
await updUser(uid, { user_info: u.user_info }, env);
console.log(`Auto-marked user ${uid} as bot_blocked`);
// 更新用户卡片显示
if (u.topic_id && u.user_info.card_msg_id) {
const mockTgUser = { id: uid, 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));
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(uid, u.is_blocked, newMeta.username)
});
} catch (editErr) {
console.log("Failed to update card after bot_blocked detection:", editErr.message);
}
}
}
} catch (markErr) {
console.error("Failed to mark user as bot_blocked:", markErr);
}
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: `⚠️ <b>用户已屏蔽 Bot</b>\n\n该用户已在 Telegram 中屏蔽了本机器人,无法接收消息。\n\n请通过其他方式联系用户,让其:\n1⃣ 打开与机器人的聊天\n2⃣ 解除屏蔽\n3⃣ 重新发送 /start`,
parse_mode: "HTML"
});
} else {
await api(env.BOT_TOKEN, "sendMessage", {
chat_id: msg.chat.id,
message_thread_id: msg.message_thread_id,
text: `❌ 内部投递失败:${e.message || "Unknown error"}\n\n调试信息:\n用户ID: ${uid}\n话题ID: ${topicIdStr}`
});
}
}
}
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 (e) {
console.error("Token Submit Failed:", e);
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);
return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "✅ 验证通过!\n现在您可以直接发送消息我会帮您转达给管理员。" });
} else return 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 });
}
try {
await setCfg(`admin_state:${from.id}`, JSON.stringify({ action: 'input_note', target: p2 }), env);
await api(env.BOT_TOKEN, "sendMessage", { chat_id: msg.chat.id, message_thread_id: msg.message_thread_id, text: "⌨️ 请回复备注内容 (回复 /clear 清除):" });
return api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "请直接回复备注内容" });
} catch (e) {
console.error("Set Note State Failed:", e);
await deleteCfg(`admin_state:${from.id}`, env).catch(() => {});
return api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "操作失败,请重试", show_alert: true });
}
}
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') await 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);
// 如果是解封操作,清除 bot_blocked 标记
if (!isB && u.user_info.bot_blocked) {
u.user_info.bot_blocked = false;
delete u.user_info.bot_blocked_ts;
await updUser(uid, { user_info: u.user_info }, env);
console.log(`Cleared bot_blocked mark for user ${uid} after admin unblock`);
}
// 响应变更,刷新目标人员资料卡片上的按钮渲染状态
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) await api(env.BOT_TOKEN, "answerCallbackQuery", { callback_query_id: cb.id, text: "✅ 已解除屏蔽" });
else await 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) {
console.error("Admin Config Failed:", e);
await deleteCfg(`admin_state:${cid}`, env).catch(() => {});
return api(env.BOT_TOKEN, "sendMessage", { chat_id: cid, text: "❌ 面板加载失败,请稍后重试" });
}
}
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 deleteCfg(`admin_state:${id}`, env); 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 deleteCfg(`admin_state:${id}`, env);
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) { await 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 parseDelCommand(raw) {
const s = (raw || "").trim().toLowerCase();
if (!s.startsWith("/del")) return null;
const cmd = s.split(/\s+/, 2)[0];
if (!/^\/del(@[a-z0-9_]+)?$/i.test(cmd)) return null;
const rest = s.slice(cmd.length).trim();
if (!rest) return { type: "single" };
if (rest === "all") return { type: "all" };
if (/^\d+$/.test(rest)) {
const count = parseInt(rest, 10);
return count > 0 && count <= MAX_BATCH_DELETE ? { type: "count", count } : { type: "invalid" };
}
return { type: "invalid" };
}
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 });
// 检测并显示屏蔽状态
let blockStatus = "";
if (dbUser.is_blocked) {
blockStatus = `\n🚫 <b>管理员屏蔽:</b> 是`;
}
if (dbUser.user_info && dbUser.user_info.bot_blocked) {
const blockTime = new Date(dbUser.user_info.bot_blocked_ts || d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
blockStatus += `\n⛔ <b>用户屏蔽Bot:</b> 是 (${blockTime})`;
}
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}${blockStatus}\n🕒: <code>${timeStr}</code>`
};
};
async function syncTopicProfile(u, tgUser, env) {
const meta = getUMeta(tgUser, u, Date.now() / 1000);
const nextInfo = { ...(u.user_info || {}) };
let dirty = false;
if (nextInfo.name !== meta.name) {
nextInfo.name = meta.name;
dirty = true;
}
if (nextInfo.username !== meta.username) {
nextInfo.username = meta.username;
dirty = true;
}
if (u.topic_id && meta.topicName && nextInfo.topic_name !== meta.topicName) {
await api(env.BOT_TOKEN, "editForumTopic", {
chat_id: env.ADMIN_GROUP_ID,
message_thread_id: u.topic_id,
name: meta.topicName
}).catch(() => {});
nextInfo.topic_name = meta.topicName;
dirty = true;
}
if (dirty) {
u.user_info = nextInfo;
await updUser(u.user_id, { user_info: nextInfo }, env);
}
return meta;
}
// 动态键盘矩阵构建:受限 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 };
};