TOKEN = ENV_BOT_TOKEN; const WEBHOOK = '/endpoint'; const SECRET = ENV_BOT_SECRET; const ADMIN_UID = ENV_ADMIN_UID; const USER_TAG_COUNTER_KEY = 'user-tag-counter'; const SESSION_HEADER_INTERVAL = 10 * 60 * 1000; const NOTIFY_INTERVAL = 3600 * 1000; const ENABLE_INSTANT_CONFIRM = false; const START_MSG_ZH_URL = 'https://git.loliloli.li/Administrator/TPM/raw/branch/main/data/startMessage.zh.md'; const START_MSG_EN_URL = 'https://git.loliloli.li/Administrator/TPM/raw/branch/main/data/startMessage.en.md'; const ENABLE_NOTIFICATION = true; const ENABLE_KEYWORD_FILTER = true; const KEYWORD_STORE_KEY = 'kw-list'; const DEFAULT_BLOCKLIST_URL = 'https://git.loliloli.li/Administrator/TPM/raw/branch/main/data/blocklist.txt'; const BLOCKLIST_REFRESH_MS = 15 * 60 * 1000; const REMOTE_CACHE_KEY = 'blocked-words-cache'; const REMOTE_ETAG_KEY = 'blocked-words-etag'; const REMOTE_LASTFETCH_KEY = 'blocked-words-lastfetch'; const VERIFY_STORE_KEY = (uid) => `verify-${uid}`; const VERIFY_REQUIRED_ZH = '🛡 为了防止骚扰,请先完成一次验证:点击下方按钮。'; const VERIFY_REQUIRED_EN = '🛡 To prevent spam, please complete a quick verification: tap the button below.'; const VERIFIED_SUCCESS_ZH = '✅ 验证通过!现在您可以正常发送消息了。'; const VERIFIED_SUCCESS_EN = '✅ Verified! You can now send messages normally.'; const ADMIN_REPLY_PROMPT_ZH = '🙅 请点击**转发的用户消息**进行回复,这样我才能知道您是想回复哪位用户。直接发送消息我无法识别目标用户。'; const ADMIN_REPLY_PROMPT_EN = '🙅 Please click **reply to the forwarded user message** so I know which user you want to reply to. I cannot identify the target user if you send a message directly.'; const USER_BLOCKED_PROMPT_ZH = '🚫 您已被管理员屏蔽,无法发送消息。'; const USER_BLOCKED_PROMPT_EN = '🚫 You have been blocked by the administrator and cannot send messages.'; const MESSAGE_FORWARD_FAIL_PROMPT_ZH = '抱歉,您的消息未能成功转发给管理员,请稍后再试或联系管理员。'; const MESSAGE_FORWARD_FAIL_PROMPT_EN = 'Sorry, your message could not be forwarded to the administrator. Please try again later or contact the administrator.'; const MESSAGE_FORWARDED_NOTIF_ZH = "🔔 您好,您的消息已转发给管理员,请耐心等待回复。如长时间未收到答复,可适当再次留言。"; const MESSAGE_FORWARDED_NOTIF_EN = "🔔 Hello, your message has been forwarded to the administrator. Please wait patiently for a reply. If there’s no response for a long time, feel free to send another message."; const MESSAGE_FORWARDED_OK_ZH = "💬 您的消息已成功转发,管理员将尽快回复您。"; const MESSAGE_FORWARDED_OK_EN = "💬 Your message has been successfully forwarded. The admin will reply soon."; const USER_UNBLOCKED_PROMPT_ZH = '🎉 您已被管理员解除屏蔽,现在可以正常发送消息了。'; const USER_UNBLOCKED_PROMPT_EN = '🎉 You have been unblocked by the administrator. You can now send messages normally.'; const ADMIN_BLOCK_SELF_PROMPT_ZH = '⚠️ 不能屏蔽自己!'; const ADMIN_BLOCK_SELF_PROMPT_EN = '⚠️ You cannot block yourself!'; const ADMIN_CANNOT_IDENTIFY_USER_PROMPT_ZH = '❌ 无法识别要操作的用户。请确保您回复的是用户转发给您的消息。'; const ADMIN_CANNOT_IDENTIFY_USER_PROMPT_EN = '❌ Cannot identify the user to operate on. Please make sure you are replying to a message forwarded to you by the user.'; const ADMIN_CANNOT_FIND_USER_ID_PROMPT_ZH = '⚠️ 无法找到对应的用户ID。可能是旧的转发消息或非转发消息。请检查。'; const ADMIN_CANNOT_FIND_USER_ID_PROMPT_EN = '⚠️ Cannot find the corresponding user ID. This may be an old forwarded message or a non-forwarded message. Please check.'; const USER_KEYWORD_BLOCKED_PROMPT_ZH = '⚠️ 您的消息包含被屏蔽的关键词,未被转发给管理员。'; const USER_KEYWORD_BLOCKED_PROMPT_EN = '⚠️ Your message contains blocked keywords and was not forwarded to the admin.'; const ADMIN_KEYWORD_ADDED_ZH = kw => `✅ 已添加屏蔽关键词:\`${kw}\``; const ADMIN_KEYWORD_ADDED_EN = kw => `✅ Added blocked keyword: \`${kw}\``; const ADMIN_KEYWORD_REMOVED_ZH = kw => `✅ 已移除屏蔽关键词:\`${kw}\``; const ADMIN_KEYWORD_REMOVED_EN = kw => `✅ Removed blocked keyword: \`${kw}\``; const ADMIN_KEYWORD_LIST_TITLE_ZH = '📃 当前屏蔽关键词列表:'; const ADMIN_KEYWORD_LIST_TITLE_EN = '📃 Current blocked keywords:'; const ADMIN_KEYWORD_EMPTY_ZH = '(空)尚未添加任何关键词。'; const ADMIN_KEYWORD_EMPTY_EN = '(empty) no keywords yet.'; const ADMIN_KEYWORD_USAGE_ZH = '用法:/addkw 关键词 | /rmkw 关键词 | /listkw'; const ADMIN_KEYWORD_USAGE_EN = 'Usage: /addkw | /rmkw | /listkw'; const ADMIN_BLOCKLIST_RELOADED_ZH = (source, updated, count, url=undefined) => `✅ 词表已刷新(${source}${updated ? ', 已更新' : ''})。` + (url ? `\n🌐 远程地址:${url}` : '') + `\n📦 当前共 ${count} 条。`; const ADMIN_BLOCKLIST_RELOADED_EN = (source, updated, count, url=undefined) => `✅ Blocklist refreshed (${source}${updated ? ', updated' : ''}).` + (url ? `\n🌐 Remote URL: ${url}` : '') + `\n📦 Now ${count} items.`; const ADMIN_BLOCKLIST_REMOTE_TITLE_ZH = (total, shown) => `🌐 远程词表信息:共 ${total} 条,前 ${shown} 条:`; const ADMIN_BLOCKLIST_REMOTE_TITLE_EN = (total, shown) => `🌐 Remote blocklist: total ${total}, first ${shown}:`; const ADMIN_BLOCKLIST_ALL_TITLE_ZH = (total, shown) => `🧩 合并列表(本地 + 远程):共 ${total} 条,前 ${shown} 条:`; const ADMIN_BLOCKLIST_ALL_TITLE_EN = (total, shown) => `🧩 Merged list (local + remote): total ${total}, first ${shown}:`; const ADMIN_KV_ERROR_ZH = (ctx, err) => `❌ KV操作失败(${ctx}):\n\`${String(err?.message || err)}\``; const ADMIN_KV_ERROR_EN = (ctx, err) => `❌ KV operation failed (${ctx}):\n\`${String(err?.message || err)}\``; const USER_TEMP_ERROR_ZH = '⚠️ 系统临时故障,请稍后再试。'; const USER_TEMP_ERROR_EN = '⚠️ Temporary system issue, please try again later.'; function apiUrl(method, params = null) { let query = ''; if (params) { query = '?' + new URLSearchParams(params).toString(); } return `https://api.telegram.org/bot${TOKEN}/${method}${query}`; } function makeReqBody(body) { return { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }; } async function requestTelegram(method, body, params = null) { try { const response = await fetch(apiUrl(method, params), makeReqBody(body)); if (!response.ok) { const errorBody = await response.text().catch(()=> ''); console.error(`Telegram API请求失败 (${method}): ${response.status} ${response.statusText}`, errorBody); return { ok: false, description: `API请求失败: ${response.status} ${response.statusText}`, errorDetails: errorBody }; } return response.json(); } catch (error) { console.error(`执行 ${method} 方法时发生Fetch错误:`, error); return { ok: false, description: `网络或未知错误: ${error.message}` }; } } const sendMessage = (msg) => requestTelegram('sendMessage', msg); const copyMessage = (msg) => requestTelegram('copyMessage', msg); const forwardMessage = (msg) => requestTelegram('forwardMessage', msg); const answerCallbackQuery = (msg) => requestTelegram('answerCallbackQuery', msg); const setMyCommands = (commands, scope = null) => { const body = { commands }; if (scope && Object.keys(scope).length > 0) body.scope = scope; return requestTelegram('setMyCommands', body); }; const setWebhook = (url, secret_token, opts = {}) => requestTelegram('setWebhook', { url, secret_token, allowed_updates: opts.allowed_updates || ["message", "callback_query"], drop_pending_updates: !!opts.drop_pending_updates }); addEventListener('fetch', event => { const url = new URL(event.request.url); if (url.pathname === WEBHOOK) { event.respondWith(handleWebhook(event)); } else if (url.pathname === '/registerWebhook') { event.respondWith(registerWebhook(event, url)); } else if (url.pathname === '/unRegisterWebhook') { event.respondWith(unRegisterWebhook()); } else if (url.pathname === '/setMenu') { event.respondWith(handleSetMenu()); } else if (url.pathname === '/debugWebhook') { event.respondWith(debugWebhook()); } else { event.respondWith(new Response('请求路径未找到处理程序', { status: 404 })); } }); async function handleWebhook(event) { if (event.request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) { return new Response('未经授权', { status: 403 }); } try { const update = await event.request.json(); event.waitUntil(onUpdate(update)); return new Response('Ok'); } catch (error) { console.error('解析Webhook更新数据时出错:', error); return new Response('错误请求,JSON解析失败', { status: 400 }); } } async function onUpdate(update) { if ('message' in update) { await onMessage(update.message); } else if ('callback_query' in update) { await onCallbackQuery(update.callback_query); } } function getLocalizedPrompt(langCode, prompts) { if (langCode && langCode.startsWith('zh')) return prompts.zh; return prompts.en; } async function notifyAdminKvError(lang, context, error) { const text = getLocalizedPrompt(lang, { zh: ADMIN_KV_ERROR_ZH(context, error), en: ADMIN_KV_ERROR_EN(context, error) }); try { await sendMessage({ chat_id: parseInt(ADMIN_UID), text, parse_mode: 'Markdown' }); } catch (e) { console.error('通知管理员KV错误时再次失败:', e); } } async function kvPutJson(key, value) { await KV.put(key, JSON.stringify(value)); } async function loadKeywordsLocal() { const arr = await KV.get(KEYWORD_STORE_KEY, { type: "json" }); return Array.isArray(arr) ? arr : []; } async function saveKeywords(list) { const cleaned = Array.from(new Set( list.map(s => String(s || '').trim()).filter(Boolean) )); await kvPutJson(KEYWORD_STORE_KEY, cleaned); return cleaned; } async function addKeyword(kw) { const list = await loadKeywordsLocal(); list.push(kw); return saveKeywords(list); } async function removeKeyword(kw) { const list = await loadKeywordsLocal(); const lowered = String(kw).toLowerCase(); const filtered = list.filter(x => String(x).toLowerCase() !== lowered); return saveKeywords(filtered); } function extractSearchableText(message) { const segs = []; if (typeof message.text === 'string') segs.push(message.text); if (typeof message.caption === 'string') segs.push(message.caption); return segs.join('\n').trim(); } function hitBlockedKeyword(text, keywords) { if (!text) return null; const low = text.toLowerCase(); for (const kw of keywords) { const k = String(kw || '').trim().toLowerCase(); if (!k) continue; if (low.includes(k)) return kw; } return null; } function parseBlocklist(text) { const trimmed = (text || '').trim(); if (!trimmed) return []; if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) { try { const data = JSON.parse(trimmed); if (Array.isArray(data)) return data.map(s => String(s).trim()).filter(Boolean); if (data && Array.isArray(data.words)) return data.words.map(s => String(s).trim()).filter(Boolean); } catch {} } return trimmed .split(/\r?\n/) .map(l => l.trim()) .filter(l => l && !l.startsWith('#')); } async function getRemoteUrl() { return DEFAULT_BLOCKLIST_URL; } async function getRemoteCachedWords() { try { const txt = await KV.get(REMOTE_CACHE_KEY, { type: 'text' }); if (!txt) return []; const obj = JSON.parse(txt); if (obj && Array.isArray(obj.words)) return obj.words; } catch {} return []; } async function saveRemoteCache(words) { const payload = { words, updatedAt: Date.now() }; await KV.put(REMOTE_CACHE_KEY, JSON.stringify(payload)); await KV.put(REMOTE_LASTFETCH_KEY, String(payload.updatedAt)); } async function fetchRemoteBlocklist({ force = false } = {}) { const url = await getRemoteUrl(); const lastFetchTxt = await KV.get(REMOTE_LASTFETCH_KEY, { type: 'text' }); const lastFetch = lastFetchTxt ? parseInt(lastFetchTxt, 10) : 0; if (!force && lastFetch && (Date.now() - lastFetch) < BLOCKLIST_REFRESH_MS) { const words = await getRemoteCachedWords(); return { words, updated: false, source: 'cache-fresh', url }; } const etag = await KV.get(REMOTE_ETAG_KEY, { type: 'text' }); const headers = {}; if (etag) headers['If-None-Match'] = etag; let res; try { res = await fetch(url, { headers }); } catch (e) { const words = await getRemoteCachedWords(); return { words, updated: false, source: 'cache-fallback', url }; } if (res.status === 304) { await KV.put(REMOTE_LASTFETCH_KEY, String(Date.now())); const words = await getRemoteCachedWords(); return { words, updated: false, source: 'not-modified', url }; } if (!res.ok) { const words = await getRemoteCachedWords(); return { words, updated: false, source: 'cache-on-error', url }; } const text = await res.text(); const words = parseBlocklist(text); await saveRemoteCache(words); const newEtag = res.headers.get('ETag'); if (newEtag) await KV.put(REMOTE_ETAG_KEY, newEtag); return { words, updated: true, source: 'remote', url }; } async function getBlockedWordsRemote({ force = false } = {}) { const { words } = await fetchRemoteBlocklist({ force }); return words; } async function getAllBlockedWords() { const local = await loadKeywordsLocal(); const remote = await getBlockedWordsRemote(); const set = new Set(local.map(x => String(x).toLowerCase())); for (const w of remote) set.add(String(w).toLowerCase()); return Array.from(set); } async function onMessage(message) { const chatId = message.chat.id; const isAdmin = (message.from?.id?.toString() === ADMIN_UID); const lang = message.from?.language_code || 'en'; if (message.text === '/start') { const startMsgUrl = lang.startsWith('zh') ? START_MSG_ZH_URL : START_MSG_EN_URL; try { const startMsg = await fetch(startMsgUrl).then(r => r.text()); await sendMessage({ chat_id: chatId, text: startMsg, parse_mode: 'Markdown' }); } catch (error) { const fallbackWelcome = getLocalizedPrompt(lang, { zh: '欢迎!很抱歉,未能加载完整的欢迎消息。', en: 'Welcome! Sorry, the full welcome message could not be loaded.' }); await sendMessage({ chat_id: chatId, text: fallbackWelcome }); } return; } if (isAdmin) { if (/^\/addkw(?:\s+(.+))?$/i.test(message.text || '')) { const m = (message.text || '').match(/^\/addkw(?:\s+(.+))?$/i); const kw = (m && m[1] || '').trim(); const usage = getLocalizedPrompt(lang, { zh: ADMIN_KEYWORD_USAGE_ZH, en: ADMIN_KEYWORD_USAGE_EN }); if (!kw) return sendMessage({ chat_id: ADMIN_UID, text: usage }); try { await addKeyword(kw); const ok = getLocalizedPrompt(lang, { zh: ADMIN_KEYWORD_ADDED_ZH(kw), en: ADMIN_KEYWORD_ADDED_EN(kw) }); await sendMessage({ chat_id: ADMIN_UID, text: ok, parse_mode: 'Markdown' }); } catch (err) { await notifyAdminKvError(lang, 'addKeyword', err); } return; } if (/^\/rmkw(?:\s+(.+))?$/i.test(message.text || '')) { const m = (message.text || '').match(/^\/rmkw(?:\s+(.+))?$/i); const kw = (m && m[1] || '').trim(); const usage = getLocalizedPrompt(lang, { zh: ADMIN_KEYWORD_USAGE_ZH, en: ADMIN_KEYWORD_USAGE_EN }); if (!kw) return sendMessage({ chat_id: ADMIN_UID, text: usage }); try { await removeKeyword(kw); const ok = getLocalizedPrompt(lang, { zh: ADMIN_KEYWORD_REMOVED_ZH(kw), en: ADMIN_KEYWORD_REMOVED_EN(kw) }); await sendMessage({ chat_id: ADMIN_UID, text: ok, parse_mode: 'Markdown' }); } catch (err) { await notifyAdminKvError(lang, 'removeKeyword', err); } return; } if (/^\/listkw$/i.test(message.text || '')) { try { const list = await loadKeywordsLocal(); const title = getLocalizedPrompt(lang, { zh: ADMIN_KEYWORD_LIST_TITLE_ZH, en: ADMIN_KEYWORD_LIST_TITLE_EN }); const empty = getLocalizedPrompt(lang, { zh: ADMIN_KEYWORD_EMPTY_ZH, en: ADMIN_KEYWORD_EMPTY_EN }); const body = list.length ? list.map((x, i) => `${i + 1}. \`${x}\``).join('\n') : empty; await sendMessage({ chat_id: ADMIN_UID, text: `${title}\n${body}`, parse_mode: 'Markdown' }); } catch (err) { await notifyAdminKvError(lang, 'listKeywordsLocal', err); } return; } if ((message.text || '') === '/reloadblock') { try { const { words, updated, source, url } = await fetchRemoteBlocklist({ force: true }); const t = getLocalizedPrompt(lang, { zh: ADMIN_BLOCKLIST_RELOADED_ZH(source, updated, words.length, url), en: ADMIN_BLOCKLIST_RELOADED_EN(source, updated, words.length, url) }); await sendMessage({ chat_id: ADMIN_UID, text: t }); } catch (err) { await notifyAdminKvError(lang, 'reloadblock', err); } return; } if ((message.text || '') === '/listkw_remote') { try { const words = await getBlockedWordsRemote(); const sample = words.slice(0, 100); const t = getLocalizedPrompt(lang, { zh: ADMIN_BLOCKLIST_REMOTE_TITLE_ZH(words.length, sample.length), en: ADMIN_BLOCKLIST_REMOTE_TITLE_EN(words.length, sample.length) }) + '\n' + sample.join(', '); await sendMessage({ chat_id: ADMIN_UID, text: t }); } catch (err) { await notifyAdminKvError(lang, 'listkw_remote', err); } return; } if ((message.text || '') === '/listkw_all') { try { const local = await loadKeywordsLocal(); const remote = await getBlockedWordsRemote(); const merged = Array.from(new Set([...local.map(String), ...remote.map(String)])); const sample = merged.slice(0, 100); const t = getLocalizedPrompt(lang, { zh: ADMIN_BLOCKLIST_ALL_TITLE_ZH(merged.length, sample.length), en: ADMIN_BLOCKLIST_ALL_TITLE_EN(merged.length, sample.length) }) + '\n' + sample.join(', '); await sendMessage({ chat_id: ADMIN_UID, text: t }); } catch (err) { await notifyAdminKvError(lang, 'listkw_all', err); } return; } if ((message.text || '') === '/version') { await sendMessage({ chat_id: ADMIN_UID, text: `ZH:\n${MESSAGE_FORWARDED_NOTIF_ZH}\n\nEN:\n${MESSAGE_FORWARDED_NOTIF_EN}` }); return; } if ((message.text || '') === '/notifytest') { const lang2 = message.from?.language_code || 'en'; const notificationText = getLocalizedPrompt(lang2, { zh: MESSAGE_FORWARDED_NOTIF_ZH, en: MESSAGE_FORWARDED_NOTIF_EN }); await sendMessage({ chat_id: ADMIN_UID, text: notificationText }); return; } if (/^\/resetnotify(?:\s+(\d+))?$/i.test(message.text || '')) { const m = (message.text || '').match(/^\/resetnotify(?:\s+(\d+))?$/i); const targetId = (m && m[1]) ? m[1] : String(message.reply_to_message?.forward_from?.id || ''); if (!targetId) { await sendMessage({ chat_id: ADMIN_UID, text: '用法: /resetnotify 或对转发消息回复 /resetnotify' }); return; } try { await KV.delete(`notify:until:${targetId}`); await KV.delete(`notify:last:${targetId}`); await KV.delete(`lastmsg-${targetId}`); } catch (_) {} await sendMessage({ chat_id: ADMIN_UID, text: `已清理节流键:${targetId}` }); return; } if (message.reply_to_message) { if (/^\/block$/.test(message.text)) return handleBlock(message, lang); if (/^\/unblock$/.test(message.text)) return handleUnblock(message, lang); if (/^\/checkblock$/.test(message.text)) return checkBlock(message, lang); try { const guestId = await KV.get('msg-map-' + message.reply_to_message.message_id, { type: "text" }); if (guestId) { await copyMessage({ chat_id: guestId, from_chat_id: message.chat.id, message_id: message.message_id }); } else { const prompt = getLocalizedPrompt(lang, { zh: ADMIN_CANNOT_FIND_USER_ID_PROMPT_ZH, en: ADMIN_CANNOT_FIND_USER_ID_PROMPT_EN }); await sendMessage({ chat_id: ADMIN_UID, text: prompt }); } } catch (err) { await notifyAdminKvError(lang, 'admin_reply_lookup_msg_map', err); } } else { const prompt = getLocalizedPrompt(lang, { zh: ADMIN_REPLY_PROMPT_ZH, en: ADMIN_REPLY_PROMPT_EN }); await sendMessage({ chat_id: ADMIN_UID, text: prompt }); } return; } await handleGuestMessage(message, lang); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); } function formatUserForAdmin(u) { const id = u?.id; const uname = u?.username; const name = [u?.first_name, u?.last_name].filter(Boolean).join(' ') || 'user'; if (uname) return `@${uname}`; if (id) return `${escapeHtml(name)}`; return escapeHtml(name); } async function getOrCreateUserTag(userId) { const key = `user-tag-${userId}`; const existing = await KV.get(key, { type: 'text' }).catch(() => null); if (existing) return existing; let counter = 0; try { const raw = await KV.get(USER_TAG_COUNTER_KEY, { type: 'text' }); counter = raw ? parseInt(raw, 10) || 0 : 0; } catch (_) {} counter += 1; const tag = 'U' + String(counter).padStart(3, '0'); await KV.put(USER_TAG_COUNTER_KEY, String(counter)); await KV.put(key, tag); return tag; } async function sendAdminSessionHeader(message, guestId, lang) { const tag = await getOrCreateUserTag(guestId); const key = `session-header-last:${guestId}`; let last = 0; try { const raw = await KV.get(key, { type: 'text' }); last = raw ? parseInt(raw, 10) || 0 : 0; } catch (_) {} const now = Date.now(); if (last && (now - last) < SESSION_HEADER_INTERVAL) { return; } await KV.put(key, String(now)); const actor = formatUserForAdmin(message.from || {}); const langCode = message.from?.language_code || 'n/a'; const text = `📂 会话 ${tag}\n` + `👤 用户: ${actor}\n` + `🆔 ID: \`${guestId}\`\n` + `🌐 语言: \`${langCode}\``; await sendMessage({ chat_id: parseInt(ADMIN_UID), text, parse_mode: 'Markdown' }); } async function ensureVerified(userId, lang) { const state = await KV.get(VERIFY_STORE_KEY(userId), { type: "json" }).catch(() => null); if (state && state.verified === true && state.verifiedAt && (Date.now() - state.verifiedAt) <= 3 * 60 * 60 * 1000) return true; const token = Math.random().toString(36).slice(2, 10); const payload = { token, exp: Date.now() + 10 * 60 * 1000, verified: false, verifiedAt: null }; await kvPutJson(VERIFY_STORE_KEY(userId), payload); const text = getLocalizedPrompt(lang, { zh: VERIFY_REQUIRED_ZH, en: VERIFY_REQUIRED_EN }); await sendMessage({ chat_id: userId, text, reply_markup: { inline_keyboard: [[{ text: lang && lang.startsWith('zh') ? '✅ 我是人类' : '✅ I’m human', callback_data: `verify:${token}` }]] } }); return false; } async function onCallbackQuery(cbq) { const fromId = cbq.from?.id; const lang = cbq.from?.language_code || 'en'; const data = cbq.data || ''; if (!fromId || !data.startsWith('verify:')) { await answerCallbackQuery({ callback_query_id: cbq.id }); return; } const token = data.split(':')[1]; const key = VERIFY_STORE_KEY(fromId); const state = await KV.get(key, { type: "json" }).catch(() => null); if (!state || state.exp < Date.now() || state.verified === true) { await KV.delete(key).catch(()=>{}); await ensureVerified(fromId, lang); await answerCallbackQuery({ callback_query_id: cbq.id }); return; } if (state.token === token) { await kvPutJson(key, { verified: true, verifiedAt: Date.now() }); await answerCallbackQuery({ callback_query_id: cbq.id, text: lang.startsWith('zh') ? '已验证' : 'Verified' }); const ok = getLocalizedPrompt(lang, { zh: VERIFIED_SUCCESS_ZH, en: VERIFIED_SUCCESS_EN }); await sendMessage({ chat_id: fromId, text: ok }); } else { await answerCallbackQuery({ callback_query_id: cbq.id, text: lang.startsWith('zh') ? '验证失败,请重试' : 'Verification failed. Try again.' }); } } async function handleGuestMessage(message, lang) { const chatId = message.chat.id; const blocked = await KV.get(`isblocked-${chatId}`, { type: "json" }).catch(() => false); if (blocked) { const prompt = getLocalizedPrompt(lang, { zh: USER_BLOCKED_PROMPT_ZH, en: USER_BLOCKED_PROMPT_EN }); return sendMessage({ chat_id: chatId, text: prompt }); } const verifyState = await KV.get(VERIFY_STORE_KEY(chatId), { type: "json" }).catch(() => null); const now = Date.now(); const maxValidMs = 3 * 60 * 60 * 1000; let needVerify = false; if (!verifyState || verifyState.verified !== true) { needVerify = true; } else if (!verifyState.verifiedAt || (now - verifyState.verifiedAt) > maxValidMs) { needVerify = true; } if (needVerify) { const ok = await ensureVerified(chatId, lang); if (!ok) return; } if (ENABLE_KEYWORD_FILTER) { try { const text = extractSearchableText(message); const allWords = await getAllBlockedWords(); const hit = hitBlockedKeyword(text, allWords); if (hit) { const userMsg = getLocalizedPrompt(lang, { zh: USER_KEYWORD_BLOCKED_PROMPT_ZH, en: USER_KEYWORD_BLOCKED_PROMPT_EN }); await sendMessage({ chat_id: chatId, text: userMsg }); const actor = formatUserForAdmin(message.from || {}); const adminAlert = getLocalizedPrompt(lang, { zh: `⚠️ ${actor} 的消息命中被屏蔽关键词:${escapeHtml(hit)},已拦截。`, en: `⚠️ Message from ${actor} contained blocked keyword: ${escapeHtml(hit)} and was intercepted.` }); await sendMessage({ chat_id: parseInt(ADMIN_UID), text: adminAlert, parse_mode: 'HTML' }); return; } } catch (err) { const adminDegrade = getLocalizedPrompt(lang, { zh: `❗关键词过滤出现故障,已降级直转。\n${escapeHtml(String(err?.message || err))}`, en: `❗Keyword filter failed; falling back to forward.\n${escapeHtml(String(err?.message || err))}` }); await sendMessage({ chat_id: parseInt(ADMIN_UID), text: adminDegrade, parse_mode: 'HTML' }); } } const forwardResult = await forwardMessage({ chat_id: parseInt(ADMIN_UID), from_chat_id: chatId, message_id: message.message_id }); if (forwardResult.ok) { await KV.put('msg-map-' + forwardResult.result.message_id, chatId.toString()) .catch(err => notifyAdminKvError(lang, 'write_msg_map', err)); await sendAdminSessionHeader(message, chatId, lang); if (ENABLE_INSTANT_CONFIRM) { const okText = getLocalizedPrompt(lang, { zh: MESSAGE_FORWARDED_OK_ZH, en: MESSAGE_FORWARDED_OK_EN }); await sendMessage({ chat_id: chatId, text: okText }); } await handleNotify(message, lang); } else { const prompt = getLocalizedPrompt(lang, { zh: MESSAGE_FORWARD_FAIL_PROMPT_ZH, en: MESSAGE_FORWARD_FAIL_PROMPT_EN }); await sendMessage({ chat_id: chatId, text: prompt }); } } async function handleNotify(message, lang) { const chatId = message.chat.id; if (!ENABLE_NOTIFICATION) return; const now = Date.now(); const interval = NOTIFY_INTERVAL; const keyUntil = `notify:until:${chatId}`; const legacyJsonKey = `notify:last:${chatId}`; const legacyTextKey = 'lastmsg-' + chatId; let until = 0; try { const obj = await KV.get(keyUntil, { type: "json" }); if (obj && typeof obj.until === "number" && isFinite(obj.until)) { until = obj.until; } } catch (_) {} if (!until) { try { const j = await KV.get(legacyJsonKey, { type: "json" }); if (j && typeof j.t === "number" && isFinite(j.t)) { until = j.t + interval; } } catch (_) {} if (!until) { try { const s = await KV.get(legacyTextKey, { type: "text" }); const t = s ? parseInt(s, 10) : 0; if (t && isFinite(t)) until = t + interval; } catch (_) {} } } if (until && now < until) return; try { await KV.put(keyUntil, JSON.stringify({ until: now + interval })); } catch (_) {} const notificationText = getLocalizedPrompt(lang, { zh: MESSAGE_FORWARDED_NOTIF_ZH, en: MESSAGE_FORWARDED_NOTIF_EN }); await sendMessage({ chat_id: chatId, text: notificationText }); } async function handleBlock(message, lang) { try { const guestId = await KV.get('msg-map-' + message.reply_to_message.message_id, { type: "text" }); if (!guestId) { const prompt = getLocalizedPrompt(lang, { zh: ADMIN_CANNOT_IDENTIFY_USER_PROMPT_ZH, en: ADMIN_CANNOT_IDENTIFY_USER_PROMPT_EN }); return sendMessage({ chat_id: ADMIN_UID, text: prompt }); } if (guestId === ADMIN_UID) { const prompt = getLocalizedPrompt(lang, { zh: ADMIN_BLOCK_SELF_PROMPT_ZH, en: ADMIN_BLOCK_SELF_PROMPT_EN }); return sendMessage({ chat_id: ADMIN_UID, text: prompt }); } await kvPutJson('isblocked-' + guestId, true) .catch(err => notifyAdminKvError(lang, 'block_user', err)); await sendMessage({ chat_id: parseInt(ADMIN_UID), text: getLocalizedPrompt(lang, { zh: `✅ 用户 \`${guestId}\` 已被成功屏蔽。`, en: `✅ User \`${guestId}\` has been successfully blocked.` }), parse_mode: 'Markdown' }); await sendMessage({ chat_id: parseInt(guestId), text: `${USER_BLOCKED_PROMPT_ZH}\n${USER_BLOCKED_PROMPT_EN}` }); } catch (err) { await notifyAdminKvError(lang, 'handleBlock', err); } } async function handleUnblock(message, lang) { try { const guestId = await KV.get('msg-map-' + message.reply_to_message.message_id, { type: "text" }); if (!guestId) { const prompt = getLocalizedPrompt(lang, { zh: ADMIN_CANNOT_IDENTIFY_USER_PROMPT_ZH, en: ADMIN_CANNOT_IDENTIFY_USER_PROMPT_EN }); return sendMessage({ chat_id: ADMIN_UID, text: prompt }); } await kvPutJson('isblocked-' + guestId, false) .catch(err => notifyAdminKvError(lang, 'unblock_user', err)); await sendMessage({ chat_id: parseInt(ADMIN_UID), text: getLocalizedPrompt(lang, { zh: `✅ 用户 \`${guestId}\` 已被成功解除屏蔽。`, en: `✅ User \`${guestId}\` has been successfully unblocked.` }), parse_mode: 'Markdown' }); await sendMessage({ chat_id: parseInt(guestId), text: `${USER_UNBLOCKED_PROMPT_ZH}\n${USER_UNBLOCKED_PROMPT_EN}` }); } catch (err) { await notifyAdminKvError(lang, 'handleUnblock', err); } } async function checkBlock(message, lang) { try { const guestId = await KV.get('msg-map-' + message.reply_to_message.message_id, { type: "text" }); if (!guestId) { const prompt = getLocalizedPrompt(lang, { zh: ADMIN_CANNOT_IDENTIFY_USER_PROMPT_ZH, en: ADMIN_CANNOT_IDENTIFY_USER_PROMPT_EN }); return sendMessage({ chat_id: ADMIN_UID, text: prompt }); } let blocked = false; blocked = await KV.get('isblocked-' + guestId, { type: "json" }).catch(err => { notifyAdminKvError(lang, 'read_block_state_in_checkBlock', err); return false; }); await sendMessage({ chat_id: parseInt(ADMIN_UID), text: getLocalizedPrompt(lang, { zh: `用户信息:\`${guestId}\` ${blocked ? '已被屏蔽 🚫' : '未被屏蔽 ✅'}`, en: `User Info: \`${guestId}\` ${blocked ? 'is blocked 🚫' : 'is not blocked ✅'}` }), parse_mode: 'Markdown' }); } catch (err) { await notifyAdminKvError(lang, 'checkBlock', err); } } async function registerWebhook(event, url) { const webhookUrl = `${url.protocol}//${url.hostname}${WEBHOOK}`; const res = await setWebhook(webhookUrl, SECRET, { allowed_updates: ["message", "callback_query"], drop_pending_updates: true }); return new Response(JSON.stringify(res, null, 2), { headers: { 'Content-Type': 'application/json' } }); } async function unRegisterWebhook() { const res = await setWebhook('', undefined, { drop_pending_updates: false }); return new Response(JSON.stringify(res, null, 2), { headers: { 'Content-Type': 'application/json' } }); } async function setBotCommands() { const adminCommands = [ { command: "block", description: "屏蔽用户 (需回复用户消息)" }, { command: "unblock", description: "解除屏蔽 (需回复用户消息)" }, { command: "checkblock", description: "查询屏蔽状态 (需回复用户消息)" }, { command: "addkw", description: "添加屏蔽关键词" }, { command: "rmkw", description: "移除屏蔽关键词" }, { command: "listkw", description: "查看本地关键词" }, { command: "reloadblock", description: "刷新远程拦截词" }, { command: "listkw_all", description: "查看合并关键词预览" } ]; const userCommands = [ { command: "start", description: "获取关于此机器人的信息" } ]; const userRes = await setMyCommands(userCommands); if (!userRes.ok) console.error('设置用户命令失败:', userRes); const adminScope = { type: "chat", chat_id: parseInt(ADMIN_UID) }; const adminRes = await setMyCommands(adminCommands, adminScope); if (!adminRes.ok) console.error('设置管理员命令失败:', adminRes); return { userCommandsSet: userRes.ok, adminCommandsSet: adminRes.ok, adminResponse: adminRes }; } async function handleSetMenu() { const res = await setBotCommands(); return new Response(JSON.stringify(res, null, 2), { headers: { 'Content-Type': 'application/json' } }); } async function debugWebhook() { const r = await fetch(`https://api.telegram.org/bot${TOKEN}/getWebhookInfo`); const j = await r.json(); return new Response(JSON.stringify(j, null, 2), { headers: { 'Content-Type': 'application/json' } }); }