fix(core): 🐛 优化数据库结构并添加用户状态标记
在 Telegram Bot 核心逻辑中,为消息表增加了 topic_message_id 字段以支持 话题模式。重构了 Telegram API 请求封装逻辑,增强了错误处理能力。 同时在文档中增加了关于“用户屏蔽 Bot”的常见问题说明。系统现在可以自动 检测用户屏蔽状态,并在管理界面展示屏蔽标记,当用户重新互动时会自动清 除该标记。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Telegram Bot Worker
|
||||
yi'xia# Telegram Bot Worker
|
||||
|
||||
`tg-bot.js` 是一个部署在 Cloudflare Workers 上的 Telegram 双向私聊中继 Bot(版本 v3.68)。它会把用户私聊消息转发到管理员论坛群的独立 topic 中,管理员在对应 topic 内回复即可把消息发回用户,同时提供验证、过滤、封禁、备注、自动回复、备份和 Telegram 内联管理面板。
|
||||
|
||||
@@ -240,6 +240,36 @@ curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
|
||||
- **引用块**:以 `>`、`》` 或 `>` 开头的文本会被渲染为 HTML 引用块
|
||||
- **媒体欢迎语**:管理员可以在面板中上传图片/视频/GIF 作为欢迎语
|
||||
|
||||
### ⚠️ 常见问题:用户屏蔽 Bot
|
||||
|
||||
如果管理员回复时收到错误提示:**"⚠️ 用户已屏蔽 Bot"**,说明该用户已在 Telegram 中屏蔽了机器人。
|
||||
|
||||
**症状:**
|
||||
- 管理员可以正常接收用户消息
|
||||
- 管理员回复时显示 "用户已屏蔽 Bot" 错误
|
||||
- 用户收不到管理员的回复
|
||||
|
||||
**原因:**
|
||||
用户在 Telegram 中点击了"屏蔽机器人"(Block Bot),导致 Bot 无法再主动发消息给用户。即使用户之前发送过消息,屏蔽后 Bot 也无法回复。
|
||||
|
||||
**自动标记功能:**
|
||||
系统会自动检测并标记被屏蔽的用户:
|
||||
- 当检测到用户屏蔽 Bot 时,会自动在用户卡片上显示屏蔽状态和时间
|
||||
- 用户卡片会显示 `⛔ 用户屏蔽Bot: 是 (时间)`
|
||||
- 当用户重新发送消息或管理员解封时,自动清除屏蔽标记
|
||||
|
||||
**解决方案:**
|
||||
需要通过其他方式联系该用户,让其按以下步骤解除屏蔽:
|
||||
1. 打开与机器人的聊天窗口
|
||||
2. 点击右上角菜单(三个点或机器人名称)
|
||||
3. 选择"解除屏蔽"或"Unblock bot"
|
||||
4. 重新发送 `/start` 命令激活机器人
|
||||
|
||||
**预防措施:**
|
||||
- 在欢迎语中提醒用户不要屏蔽机器人
|
||||
- 定期检查被屏蔽的用户列表(查看用户卡片上的屏蔽状态)
|
||||
- 对于重要用户,建议通过其他渠道保持联系
|
||||
|
||||
## 安全建议
|
||||
|
||||
### 🔐 认证与授权
|
||||
|
||||
@@ -176,9 +176,8 @@ async function dbInit(env) {
|
||||
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))`)
|
||||
env.TG_BOT_DB.prepare(`CREATE TABLE IF NOT EXISTS messages (user_id TEXT, message_id TEXT, text TEXT, date INTEGER, topic_message_id TEXT, PRIMARY KEY (user_id, message_id))`)
|
||||
]);
|
||||
try { await sql(env, "ALTER TABLE messages ADD COLUMN topic_message_id TEXT"); } catch (e) {}
|
||||
}
|
||||
|
||||
// --- 4. 业务逻辑 (核心流) ---
|
||||
@@ -650,6 +649,31 @@ async function sendStart(id, msg, env) {
|
||||
async function handleVerifiedMsg(msg, u, env) {
|
||||
const id = u.user_id, text = msg.text || "";
|
||||
|
||||
// 如果用户之前被标记为屏蔽 Bot,但现在能发消息,说明已解除屏蔽
|
||||
if (u.user_info && u.user_info.bot_blocked) {
|
||||
u.user_info.bot_blocked = false;
|
||||
delete u.user_info.bot_blocked_ts;
|
||||
await updUser(id, { user_info: u.user_info }, env);
|
||||
console.log(`Cleared bot_blocked mark for user ${id} after receiving message`);
|
||||
|
||||
// 更新用户卡片显示
|
||||
try {
|
||||
if (u.topic_id && u.user_info.card_msg_id) {
|
||||
const mockTgUser = { id: id, username: u.user_info.username || "", first_name: u.user_info.name || "(未获取)", last_name: "" };
|
||||
const newMeta = getUMeta(mockTgUser, u, u.user_info.join_date || (Date.now()/1000));
|
||||
await api(env.BOT_TOKEN, "editMessageCaption", {
|
||||
chat_id: env.ADMIN_GROUP_ID,
|
||||
message_id: u.user_info.card_msg_id,
|
||||
caption: newMeta.card,
|
||||
parse_mode: "HTML",
|
||||
reply_markup: getBtns(id, u.is_blocked, newMeta.username)
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to update card after bot_blocked cleared:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 敏感词屏蔽预检系统
|
||||
if (text) {
|
||||
const kws = await getJsonCfg('block_keywords', env);
|
||||
@@ -1072,17 +1096,49 @@ async function handleAdminReply(msg, env) {
|
||||
}
|
||||
}
|
||||
|
||||
const uid = (await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first'))?.user_id;
|
||||
if (!uid) return;
|
||||
const topicIdStr = msg.message_thread_id.toString();
|
||||
console.log(`Admin reply debug: topic_id=${topicIdStr}, admin_msg_id=${msg.message_id}`);
|
||||
|
||||
const userRef = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", topicIdStr, 'first');
|
||||
console.log(`Admin reply debug: userRef=`, userRef);
|
||||
|
||||
const uid = userRef?.user_id;
|
||||
if (!uid) {
|
||||
console.error(`Admin reply failed: No user found for topic_id=${topicIdStr}`);
|
||||
return api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 系统错误:未找到关联的用户(topic_id=${topicIdStr})`
|
||||
});
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
const u = await getUser(uid, env);
|
||||
console.log(`Admin reply debug: user_state=${u.user_state}, is_blocked=${u.is_blocked}`);
|
||||
|
||||
if (u.is_blocked) {
|
||||
return api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 该用户已被屏蔽,无法发送消息`
|
||||
});
|
||||
}
|
||||
|
||||
let replyToMsgId = undefined;
|
||||
if (msg.reply_to_message) {
|
||||
const ref = await sql(env, "SELECT message_id FROM messages WHERE topic_message_id = ?", msg.reply_to_message.message_id.toString(), 'first');
|
||||
if (ref) replyToMsgId = ref.message_id;
|
||||
if (ref) {
|
||||
replyToMsgId = ref.message_id;
|
||||
console.log(`Admin reply debug: Found reply mapping topic_msg=${msg.reply_to_message.message_id} -> user_msg=${replyToMsgId}`);
|
||||
} else {
|
||||
console.log(`Admin reply debug: No reply mapping found for topic_msg=${msg.reply_to_message.message_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Admin reply debug: Delivering message to uid=${uid}, replyToMsgId=${replyToMsgId}`);
|
||||
const sent = await deliverAdminMessageToUser(msg, uid, replyToMsgId, env);
|
||||
console.log(`Admin reply debug: Delivery success, sent.message_id=${sent?.message_id}`);
|
||||
if (sent && sent.message_id) {
|
||||
const storeText = msg.text || msg.caption || "[Admin Message]";
|
||||
await sql(env, "INSERT OR REPLACE INTO messages (user_id, message_id, text, date, topic_message_id) VALUES (?,?,?,?,?)",
|
||||
@@ -1091,11 +1147,61 @@ async function handleAdminReply(msg, env) {
|
||||
// 此处为管理员端给用户下发消息的主逻辑。根据之前的版本,管理员侧发送成功后的回执代码也已经去除
|
||||
} catch (e) {
|
||||
console.error("Admin Delivery Failed:", e);
|
||||
console.error("Admin Delivery Failed - Error details:", {
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
uid: uid,
|
||||
topicId: topicIdStr,
|
||||
replyToMsgId: replyToMsgId
|
||||
});
|
||||
|
||||
// 检测用户是否屏蔽了 Bot
|
||||
const isBlocked = e.message && e.message.includes("bot was blocked by the user");
|
||||
|
||||
if (isBlocked) {
|
||||
// 自动标记用户为屏蔽状态
|
||||
try {
|
||||
const u = await getUser(uid, env);
|
||||
if (!u.user_info.bot_blocked) {
|
||||
u.user_info.bot_blocked = true;
|
||||
u.user_info.bot_blocked_ts = Date.now();
|
||||
await updUser(uid, { user_info: u.user_info }, env);
|
||||
console.log(`Auto-marked user ${uid} as bot_blocked`);
|
||||
|
||||
// 更新用户卡片显示
|
||||
if (u.topic_id && u.user_info.card_msg_id) {
|
||||
const mockTgUser = { id: uid, username: u.user_info.username || "", first_name: u.user_info.name || "(未获取)", last_name: "" };
|
||||
const newMeta = getUMeta(mockTgUser, u, u.user_info.join_date || (Date.now()/1000));
|
||||
try {
|
||||
await api(env.BOT_TOKEN, "editMessageCaption", {
|
||||
chat_id: env.ADMIN_GROUP_ID,
|
||||
message_id: u.user_info.card_msg_id,
|
||||
caption: newMeta.card,
|
||||
parse_mode: "HTML",
|
||||
reply_markup: getBtns(uid, u.is_blocked, newMeta.username)
|
||||
});
|
||||
} catch (editErr) {
|
||||
console.log("Failed to update card after bot_blocked detection:", editErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (markErr) {
|
||||
console.error("Failed to mark user as bot_blocked:", markErr);
|
||||
}
|
||||
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 内部投递失败:${e.message || "Unknown error"}`
|
||||
text: `⚠️ <b>用户已屏蔽 Bot</b>\n\n该用户已在 Telegram 中屏蔽了本机器人,无法接收消息。\n\n请通过其他方式联系用户,让其:\n1️⃣ 打开与机器人的聊天\n2️⃣ 解除屏蔽\n3️⃣ 重新发送 /start`,
|
||||
parse_mode: "HTML"
|
||||
});
|
||||
} else {
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: msg.chat.id,
|
||||
message_thread_id: msg.message_thread_id,
|
||||
text: `❌ 内部投递失败:${e.message || "Unknown error"}\n\n调试信息:\n用户ID: ${uid}\n话题ID: ${topicIdStr}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1107,7 +1213,7 @@ async function handleEdit(msg, env) {
|
||||
const newTxt = msg.text || msg.caption || "[非文本]";
|
||||
|
||||
const logText = `✏️ 消息修改\n前: ${escape(old?.text||"?")}\n后: ${escape(newTxt)}`;
|
||||
|
||||
yixia
|
||||
await api(env.BOT_TOKEN, "sendMessage", {
|
||||
chat_id: env.ADMIN_GROUP_ID,
|
||||
message_thread_id: u.topic_id,
|
||||
@@ -1223,6 +1329,15 @@ async function handleCallback(cb, env) {
|
||||
else if (['block','unblock'].includes(act)) {
|
||||
const isB = act === 'block'; const uid = p1; const u = await getUser(uid, env); const bid = await getCfg('blocked_topic_id', env);
|
||||
await updUser(uid, { is_blocked: isB, block_count: 0 }, env);
|
||||
|
||||
// 如果是解封操作,清除 bot_blocked 标记
|
||||
if (!isB && u.user_info.bot_blocked) {
|
||||
u.user_info.bot_blocked = false;
|
||||
delete u.user_info.bot_blocked_ts;
|
||||
await updUser(uid, { user_info: u.user_info }, env);
|
||||
console.log(`Cleared bot_blocked mark for user ${uid} after admin unblock`);
|
||||
}
|
||||
|
||||
// 响应变更,刷新目标人员资料卡片上的按钮渲染状态
|
||||
if (u.user_info.card_msg_id) {
|
||||
api(env.BOT_TOKEN, "editMessageReplyMarkup", { chat_id: env.ADMIN_GROUP_ID, message_id: u.user_info.card_msg_id, reply_markup: getBtns(uid, isB, u.user_info.username) }).catch(()=>{});
|
||||
@@ -1435,12 +1550,22 @@ const getUMeta = (tgUser, dbUser, d) => {
|
||||
const labelDisplay = tgUser.username ? `@${tgUser.username}` : "未设公开ID";
|
||||
const timeStr = new Date(d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
||||
|
||||
// 检测并显示屏蔽状态
|
||||
let blockStatus = "";
|
||||
if (dbUser.is_blocked) {
|
||||
blockStatus = `\n🚫 <b>管理员屏蔽:</b> 是`;
|
||||
}
|
||||
if (dbUser.user_info && dbUser.user_info.bot_blocked) {
|
||||
const blockTime = new Date(dbUser.user_info.bot_blocked_ts || d*1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
||||
blockStatus += `\n⛔ <b>用户屏蔽Bot:</b> 是 (${blockTime})`;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: id,
|
||||
name,
|
||||
username: tgUser.username,
|
||||
topicName: name.substring(0, 128),
|
||||
card: `<b>🪪 用户身份卡片</b>\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: <code>${id}</code>${note}\n🕒: <code>${timeStr}</code>`
|
||||
card: `<b>🪪 用户身份卡片</b>\n---\n👤: ${safeName}\n🏷️: ${labelDisplay}\n🆔: <code>${id}</code>${note}${blockStatus}\n🕒: <code>${timeStr}</code>`
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user