fix(core): 🛠️ 限制批量删除消息的最大数量为100
通过引入 MAX_BATCH_DELETE 常量,将单次批量删除消息的数量限制在 100 条以内。此更改提高了操作的安全性,防止因误操作或恶意请求导致的大规模数据删除,并同步更新了相关文档说明。 - 在 tg-bot.js 中添加 MAX_BATCH_DELETE 常量 - 重构部分代码逻辑以符合批量删除限制 - 更新 README.md,向用户明确单次删除上限为 100 条
This commit is contained in:
@@ -69,7 +69,7 @@
|
|||||||
- **用户侧**:引用自己发送的消息,发送 `/del` 命令,可以删除该消息并通知管理员。
|
- **用户侧**:引用自己发送的消息,发送 `/del` 命令,可以删除该消息并通知管理员。
|
||||||
- **管理员侧**:
|
- **管理员侧**:
|
||||||
- **单条删除**:在 topic 中引用消息,发送 `/del` 命令,可以同时删除用户侧和管理员侧的消息。
|
- **单条删除**:在 topic 中引用消息,发送 `/del` 命令,可以同时删除用户侧和管理员侧的消息。
|
||||||
- **批量删除**:直接发送 `/del N`(如 `/del 3`),可删除当前话题内最近的 N 条消息(仅限管理员使用)。
|
- **批量删除**:直接发送 `/del N`(如 `/del 3`),可删除当前话题内最近的 N 条消息(仅限管理员使用,单次最多 100 条)。
|
||||||
- **全量清空**:主管理员可使用 `/del all` 清空当前话题的所有历史消息。
|
- **全量清空**:主管理员可使用 `/del all` 清空当前话题的所有历史消息。
|
||||||
- **权限控制**:用户只能删除自己发送的消息,无法删除管理员回复的消息;批量删除功能仅对管理员开放。
|
- **权限控制**:用户只能删除自己发送的消息,无法删除管理员回复的消息;批量删除功能仅对管理员开放。
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
|
|||||||
- **用户侧**:引用自己发送的消息,发送 `/del` 命令,可以删除该消息并通知管理员。
|
- **用户侧**:引用自己发送的消息,发送 `/del` 命令,可以删除该消息并通知管理员。
|
||||||
- **管理员侧**:
|
- **管理员侧**:
|
||||||
- 引用消息发送 `/del`:删除指定的单条双向记录。
|
- 引用消息发送 `/del`:删除指定的单条双向记录。
|
||||||
- 直接发送 `/del N`:删除当前话题内最近的 N 条消息(例如 `/del 5`)。
|
- 直接发送 `/del N`:删除当前话题内最近的 N 条消息(例如 `/del 5`,单次上限 100 条)。
|
||||||
- 主管理员发送 `/del all`:清空当前话题的所有历史记录。
|
- 主管理员发送 `/del all`:清空当前话题的所有历史记录。
|
||||||
- **注意**:用户只能删除自己发送的消息,无法删除管理员回复的消息;批量删除功能仅对管理员开放。
|
- **注意**:用户只能删除自己发送的消息,无法删除管理员回复的消息;批量删除功能仅对管理员开放。
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const MSG_TYPES = [
|
|||||||
|
|
||||||
const REGEX_MAX_PATTERN_LEN = 256;
|
const REGEX_MAX_PATTERN_LEN = 256;
|
||||||
const REGEX_MAX_TEXT_LEN = 512;
|
const REGEX_MAX_TEXT_LEN = 512;
|
||||||
|
const MAX_BATCH_DELETE = 100;
|
||||||
const REGEX_REJECT_PATTERNS = [
|
const REGEX_REJECT_PATTERNS = [
|
||||||
/\([^)]*\)\s*[+*{]/,
|
/\([^)]*\)\s*[+*{]/,
|
||||||
/\(\s*\.\*\s*\)\s*\+/,
|
/\(\s*\.\*\s*\)\s*\+/,
|
||||||
@@ -222,10 +223,8 @@ async function handleUpdate(update, env, ctx) {
|
|||||||
if (msg.chat.type === "private") await handlePrivate(msg, env, ctx);
|
if (msg.chat.type === "private") await handlePrivate(msg, env, ctx);
|
||||||
else if (msg.chat.id.toString() === env.ADMIN_GROUP_ID) {
|
else if (msg.chat.id.toString() === env.ADMIN_GROUP_ID) {
|
||||||
const delCmd = parseDelCommand(msg.text || msg.caption || "");
|
const delCmd = parseDelCommand(msg.text || msg.caption || "");
|
||||||
if (delCmd === "all") {
|
if (delCmd) {
|
||||||
await handleAdminDeleteAll(msg, env);
|
await handleAdminDelete(msg, env, delCmd);
|
||||||
} else if (delCmd === "single" && msg.reply_to_message) {
|
|
||||||
await handleAdminDelete(msg, env);
|
|
||||||
} else {
|
} else {
|
||||||
await handleAdminReply(msg, env);
|
await handleAdminReply(msg, env);
|
||||||
}
|
}
|
||||||
@@ -339,60 +338,19 @@ async function handleUserDelete(msg, u, env) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 管理员侧删除消息处理
|
// 管理员侧删除消息处理
|
||||||
async function handleAdminDelete(msg, env) {
|
async function handleAdminDelete(msg, env, delCmd = parseDelCommand(msg.text || msg.caption || "")) {
|
||||||
if (!msg.message_thread_id || !(await isAuthAdmin(msg.from.id, env))) return;
|
if (!msg.message_thread_id || !(await isAuthAdmin(msg.from.id, env))) return;
|
||||||
|
|
||||||
const text = msg.text || "";
|
|
||||||
// 解析 /del N 格式
|
|
||||||
const match = text.match(/^\/del\s+(\d+)$/i);
|
|
||||||
const count = match ? parseInt(match[1]) : 0;
|
|
||||||
|
|
||||||
// 场景 1: 批量删除最近 N 条
|
if (delCmd?.type === "all") return handleAdminBatchDelete(msg, env, { all: true });
|
||||||
if (count > 0 && count <= 20) { // 限制单次最多删除 20 条以防 API 限流
|
if (delCmd?.type === "count") return handleAdminBatchDelete(msg, env, { count: delCmd.count });
|
||||||
const rows = await sql(env, "SELECT message_id, topic_message_id FROM messages WHERE user_id=(SELECT user_id FROM users WHERE topic_id=?) ORDER BY date DESC LIMIT ?",
|
if (delCmd?.type === "invalid") {
|
||||||
[msg.message_thread_id.toString(), count], 'all');
|
return api(env.BOT_TOKEN, "sendMessage", {
|
||||||
|
|
||||||
if (!rows || !rows.results || rows.results.length === 0) {
|
|
||||||
return api(env.BOT_TOKEN, "sendMessage", {
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
message_thread_id: msg.message_thread_id,
|
|
||||||
text: "❌ 当前话题没有可删除的消息记录"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let deletedCount = 0;
|
|
||||||
for (const r of rows.results) {
|
|
||||||
try {
|
|
||||||
// 删除管理员侧消息
|
|
||||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
message_id: parseInt(r.topic_message_id)
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
// 删除用户侧消息
|
|
||||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
|
||||||
chat_id: (await sql(env, "SELECT user_id FROM users WHERE topic_id=?", msg.message_thread_id.toString(), 'first')).user_id,
|
|
||||||
message_id: parseInt(r.message_id)
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
deletedCount++;
|
|
||||||
} catch (e) { console.error("Batch Delete Error:", e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理数据库记录
|
|
||||||
await sql(env, "DELETE FROM messages WHERE user_id=(SELECT user_id FROM users WHERE topic_id=?) AND rowid IN (SELECT rowid FROM messages WHERE user_id=(SELECT user_id FROM users WHERE topic_id=?) ORDER BY date DESC LIMIT ?)",
|
|
||||||
[msg.message_thread_id.toString(), msg.message_thread_id.toString(), count]);
|
|
||||||
|
|
||||||
// 删除触发命令本身
|
|
||||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
|
||||||
chat_id: msg.chat.id,
|
chat_id: msg.chat.id,
|
||||||
message_id: msg.message_id
|
message_thread_id: msg.message_thread_id,
|
||||||
}).catch(() => {});
|
text: `⚠️ 用法:回复消息使用 /del,或使用 /del N 删除最近 N 条(最多 ${MAX_BATCH_DELETE} 条),/del all 清空当前话题`
|
||||||
|
});
|
||||||
return; // 批量删除完成后直接返回,不发送额外提示以保持界面整洁
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景 2: 删除单条(回复模式)
|
|
||||||
if (!msg.reply_to_message) {
|
if (!msg.reply_to_message) {
|
||||||
return api(env.BOT_TOKEN, "sendMessage", {
|
return api(env.BOT_TOKEN, "sendMessage", {
|
||||||
chat_id: msg.chat.id,
|
chat_id: msg.chat.id,
|
||||||
@@ -445,17 +403,7 @@ async function handleAdminDelete(msg, env) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 管理员侧批量删除:清空当前话题内用户会话消息(保留用户信息卡片)
|
async function handleAdminBatchDelete(msg, env, options = {}) {
|
||||||
async function handleAdminDeleteAll(msg, env) {
|
|
||||||
if (!msg.message_thread_id) return;
|
|
||||||
if (!(await isPrimaryAdmin(msg.from.id, env))) {
|
|
||||||
return api(env.BOT_TOKEN, "sendMessage", {
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
message_thread_id: msg.message_thread_id,
|
|
||||||
text: "❌ 仅主管理员可使用 /del all"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRef = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first');
|
const userRef = await sql(env, "SELECT user_id FROM users WHERE topic_id = ?", msg.message_thread_id.toString(), 'first');
|
||||||
if (!userRef?.user_id) {
|
if (!userRef?.user_id) {
|
||||||
return api(env.BOT_TOKEN, "sendMessage", {
|
return api(env.BOT_TOKEN, "sendMessage", {
|
||||||
@@ -467,7 +415,10 @@ async function handleAdminDeleteAll(msg, env) {
|
|||||||
|
|
||||||
const u = await getUser(userRef.user_id, env);
|
const u = await getUser(userRef.user_id, env);
|
||||||
const keepCardMsgId = u?.user_info?.card_msg_id ? parseInt(u.user_info.card_msg_id) : null;
|
const keepCardMsgId = u?.user_info?.card_msg_id ? parseInt(u.user_info.card_msg_id) : null;
|
||||||
const rows = await sql(env, "SELECT message_id, topic_message_id FROM messages WHERE user_id=?", [u.user_id], 'all');
|
const limit = Math.min(Math.max(parseInt(options.count) || 0, 1), MAX_BATCH_DELETE);
|
||||||
|
const rows = options.all
|
||||||
|
? await sql(env, "SELECT rowid, message_id, topic_message_id FROM messages WHERE user_id=? ORDER BY date DESC, rowid DESC", [u.user_id], 'all')
|
||||||
|
: await sql(env, "SELECT rowid, message_id, topic_message_id FROM messages WHERE user_id=? ORDER BY date DESC, rowid DESC LIMIT ?", [u.user_id, limit], 'all');
|
||||||
const mapped = rows?.results || [];
|
const mapped = rows?.results || [];
|
||||||
|
|
||||||
let adminDeleted = 0;
|
let adminDeleted = 0;
|
||||||
@@ -480,18 +431,26 @@ async function handleAdminDeleteAll(msg, env) {
|
|||||||
message_id: msg.message_id
|
message_id: msg.message_id
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
|
if (!mapped.length) {
|
||||||
|
return api(env.BOT_TOKEN, "sendMessage", {
|
||||||
|
chat_id: msg.chat.id,
|
||||||
|
message_thread_id: msg.message_thread_id,
|
||||||
|
text: "❌ 当前话题没有可删除的消息记录"
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
for (const r of mapped) {
|
for (const r of mapped) {
|
||||||
const topicMid = parseInt(r.topic_message_id);
|
const topicMid = parseInt(r.topic_message_id);
|
||||||
const userMid = parseInt(r.message_id);
|
const userMid = parseInt(r.message_id);
|
||||||
|
|
||||||
if (Number.isInteger(topicMid) && (!keepCardMsgId || topicMid !== keepCardMsgId)) {
|
if (Number.isFinite(topicMid) && (!keepCardMsgId || topicMid !== keepCardMsgId)) {
|
||||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
await api(env.BOT_TOKEN, "deleteMessage", {
|
||||||
chat_id: msg.chat.id,
|
chat_id: msg.chat.id,
|
||||||
message_id: topicMid
|
message_id: topicMid
|
||||||
}).then(() => { adminDeleted += 1; }).catch(() => {});
|
}).then(() => { adminDeleted += 1; }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number.isInteger(userMid)) {
|
if (Number.isFinite(userMid)) {
|
||||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
await api(env.BOT_TOKEN, "deleteMessage", {
|
||||||
chat_id: u.user_id,
|
chat_id: u.user_id,
|
||||||
message_id: userMid
|
message_id: userMid
|
||||||
@@ -499,26 +458,21 @@ async function handleAdminDeleteAll(msg, env) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await sql(env, "DELETE FROM messages WHERE user_id=?", [u.user_id]);
|
if (options.all) {
|
||||||
|
await sql(env, "DELETE FROM messages WHERE user_id=?", [u.user_id]);
|
||||||
const confirmMsg = await api(env.BOT_TOKEN, "sendMessage", {
|
} else if (mapped.length) {
|
||||||
chat_id: msg.chat.id,
|
const rowIds = mapped.map(r => r.rowid).filter(id => id !== undefined && id !== null);
|
||||||
message_thread_id: msg.message_thread_id,
|
const placeholders = rowIds.map(() => "?").join(",");
|
||||||
text: `🧹 已清空当前话题消息\n管理员侧: ${adminDeleted} 条\n用户侧: ${userDeleted} 条\n(用户信息卡片已保留)`
|
await sql(env, `DELETE FROM messages WHERE user_id=? AND rowid IN (${placeholders})`, [u.user_id, ...rowIds]);
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
if (confirmMsg?.message_id) {
|
|
||||||
await api(env.BOT_TOKEN, "deleteMessage", {
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
message_id: confirmMsg.message_id
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Admin batch delete done: all=${!!options.all}, mapped=${mapped.length}, admin=${adminDeleted}, user=${userDeleted}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Admin Delete All Failed:", e);
|
console.error("Admin Batch Delete Failed:", e);
|
||||||
await api(env.BOT_TOKEN, "sendMessage", {
|
await api(env.BOT_TOKEN, "sendMessage", {
|
||||||
chat_id: msg.chat.id,
|
chat_id: msg.chat.id,
|
||||||
message_thread_id: msg.message_thread_id,
|
message_thread_id: msg.message_thread_id,
|
||||||
text: "❌ /del all 执行失败,请稍后重试"
|
text: "❌ 批量删除失败,请稍后重试"
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,8 +512,15 @@ async function handlePrivate(msg, env, ctx) {
|
|||||||
}
|
}
|
||||||
return isAdm ? handleAdminConfig(id, null, 'menu', null, null, env) : sendStart(id, msg, env);
|
return isAdm ? handleAdminConfig(id, null, 'menu', null, null, env) : sendStart(id, msg, env);
|
||||||
}
|
}
|
||||||
if (text === "/help" && isAdm) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "ℹ️ <b>帮助</b>\n• 回复消息即对话\n• /start 打开面板\n• /del 删除单条消息\n• /del all 清空当前话题消息(仅主管理员)", parse_mode: "HTML" });
|
if (text === "/help" && isAdm) return api(env.BOT_TOKEN, "sendMessage", { chat_id: id, text: "ℹ️ <b>帮助</b>\n• 回复消息即对话\n• /start 打开面板\n• /del 删除单条消息\n• /del N 删除最近 N 条消息\n• /del all 清空当前话题消息(保留用户信息卡片)", parse_mode: "HTML" });
|
||||||
if (text === "/del" && !isAdm) return handleUserDelete(msg, u, env);
|
if (text === "/del" && !isAdm) return handleUserDelete(msg, u, env);
|
||||||
|
if (!isAdm && parseDelCommand(text)) {
|
||||||
|
return api(env.BOT_TOKEN, "sendMessage", {
|
||||||
|
chat_id: id,
|
||||||
|
text: "⚠️ 用户侧只能回复要删除的消息后使用 /del 命令",
|
||||||
|
reply_to_message_id: msg.message_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 封禁拦截层
|
// 2. 封禁拦截层
|
||||||
if (u.is_blocked) {
|
if (u.is_blocked) {
|
||||||
@@ -1247,8 +1208,13 @@ function parseDelCommand(raw) {
|
|||||||
const cmd = s.split(/\s+/, 2)[0];
|
const cmd = s.split(/\s+/, 2)[0];
|
||||||
if (!/^\/del(@[a-z0-9_]+)?$/i.test(cmd)) return null;
|
if (!/^\/del(@[a-z0-9_]+)?$/i.test(cmd)) return null;
|
||||||
const rest = s.slice(cmd.length).trim();
|
const rest = s.slice(cmd.length).trim();
|
||||||
if (!rest) return "single";
|
if (!rest) return { type: "single" };
|
||||||
return rest === "all" ? "all" : null;
|
if (rest === "all") return { type: "all" };
|
||||||
|
if (/^\d+$/.test(rest)) {
|
||||||
|
const count = parseInt(rest, 10);
|
||||||
|
return count > 0 && count <= MAX_BATCH_DELETE ? { type: "count", count } : { type: "invalid" };
|
||||||
|
}
|
||||||
|
return { type: "invalid" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeRegexTest(pattern, text) {
|
function safeRegexTest(pattern, text) {
|
||||||
|
|||||||
Reference in New Issue
Block a user