feat(core): ✨ 新增Telegram机器人完整功能脚本
添加完整的Telegram机器人脚本tg-bot.js,包含验证、消息转发、缓存及配置面板等功能。主要修改包括:修复无用户名用户推送问题,移除管理员及用户消息回执提示。
This commit is contained in:
898
tg-bot.js
Normal file
898
tg-bot.js
Normal file
@@ -0,0 +1,898 @@
|
||||
/**
|
||||
* Telegram Bot Worker v3.67 (No-Receipt Mod & No-Username Fix)
|
||||
* 完整功能版:保留所有备份、配置面板、辅助函数
|
||||
* * 修改 1: 修复了无用户名用户无法推送卡片的问题
|
||||
* * 修改 2: 彻底移除了管理员回复的“✅ 已回复”提示
|
||||
* * 修改 3: 彻底移除了用户发送消息后的“✅ 已送达”回执
|
||||
*/
|
||||
|
||||
// --- 1. 静态配置与常量 ---
|
||||
// 缓存系统,用于减少数据库读写压力,降低 Worker KV/D1 计费
|
||||
const CACHE = { data: {}, ts: 0, ttl: 60000, user_locks: {}, warn_cd: {} };
|
||||
|
||||
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: "纯文本" }
|
||||
];
|
||||
|
||||
// --- 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.67 Active", { status: 200 });
|
||||
}
|
||||
// POST 请求处理:Telegram Webhook 核心逻辑接收端
|
||||
if (req.method === "POST") {
|
||||
if (url.pathname === "/submit_token") return handleTokenSubmit(req, env);
|
||||
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;
|
||||
}
|
||||
|
||||
// 获取或初始化用户信息实体
|
||||
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 list = [...(env.ADMIN_IDS||"").split(/[,,]/), ...(safeParse(await getCfg('authorized_admins', env), []))];
|
||||
const admins = [...new Set(list.map(i=>i.trim()).filter(Boolean))];
|
||||
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: `✏️ <b>对方修改了消息</b>\n内容: ${escape(newText)}`,
|
||||
parse_mode: "HTML"
|
||||
});
|
||||
}
|
||||
|
||||
// 私聊消息处理总线 (用户侧逻辑入口)
|
||||
async function handlePrivate(msg, env, ctx) {
|
||||
const id = msg.chat.id.toString();
|
||||
const text = msg.text || "";
|
||||
const isAdm = (env.ADMIN_IDS || "").includes(id);
|
||||
const u = await getUser(id, env);
|
||||
|
||||
// 人机验证拦截器 (非管理人员未完成验证则阻断)
|
||||
if (text !== "/start" && !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 打开面板", 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,'<').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) {
|
||||
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=${id}` } }]] }
|
||||
});
|
||||
} 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 (kws.some(k => new RegExp(k, 'gi').test(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 = rules.find(r => new RegExp(r.keywords, 'gi').test(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 || "";
|
||||
|
||||
// 特殊引用语法降级渲染支持
|
||||
if (rawText && (rawText.startsWith('>') || rawText.startsWith('》') || rawText.startsWith('>'))) {
|
||||
let cleanText = rawText.replace(/^[>》]\s?/, '').replace(/^>\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"
|
||||
});
|
||||
} 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: `<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 {
|
||||
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 });
|
||||
// 此处为管理员端给用户下发消息的主逻辑。根据之前的版本,管理员侧发送成功后的回执代码也已经去除
|
||||
} 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 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="${siteKey}" data-callback="S"></div><div id="m"></div></div><script>const tg=window.Telegram.WebApp;tg.ready();function S(t){document.getElementById('m').innerText='验证中...';fetch('/submit_token',{method:'POST',body:JSON.stringify({token:t,userId:'${uid}'})}).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 } = await req.json();
|
||||
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(userId, { user_state: "pending_verification" }, env);
|
||||
await api(env.BOT_TOKEN, "sendMessage", { chat_id: userId, text: "✅ 验证通过!\n请回答:\n" + await getCfg('verif_q', env) });
|
||||
} else {
|
||||
await updUser(userId, { user_state: "verified" }, env);
|
||||
await api(env.BOT_TOKEN, "sendMessage", { chat_id: userId, 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') {
|
||||
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 (!(env.ADMIN_IDS||"").includes(from.id.toString())) 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.chat.id.toString() === env.ADMIN_GROUP_ID) {
|
||||
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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
const isAuthAdmin = async (id, e) => {
|
||||
const idStr = id.toString();
|
||||
if ((e.ADMIN_IDS||"").includes(idStr)) return true;
|
||||
const list = await getJsonCfg('authorized_admins', e);
|
||||
return list.includes(idStr);
|
||||
};
|
||||
|
||||
// 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 };
|
||||
};
|
||||
Reference in New Issue
Block a user