上传文件至 /

This commit is contained in:
2025-11-29 22:13:24 +08:00
parent efe94bc157
commit 37ffa57bfe

961
worker.js Normal file
View File

@@ -0,0 +1,961 @@
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 theres 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 <keyword> | /rmkw <keyword> | /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 <userId> 或对转发消息回复 /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 => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[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 `<a href="tg://user?id=${id}">${escapeHtml(name)}</a>`;
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') ? '✅ 我是人类' : '✅ Im 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} 的消息命中被屏蔽关键词:<code>${escapeHtml(hit)}</code>,已拦截。`,
en: `⚠️ Message from ${actor} contained blocked keyword: <code>${escapeHtml(hit)}</code> and was intercepted.`
});
await sendMessage({
chat_id: parseInt(ADMIN_UID),
text: adminAlert,
parse_mode: 'HTML'
});
return;
}
} catch (err) {
const adminDegrade = getLocalizedPrompt(lang, {
zh: `❗关键词过滤出现故障,已降级直转。\n<code>${escapeHtml(String(err?.message || err))}</code>`,
en: `❗Keyword filter failed; falling back to forward.\n<code>${escapeHtml(String(err?.message || err))}</code>`
});
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' }
});
}