UNPKG

koishi-plugin-batch-recall

Version:

基于数据库的高阶撤回,支持撤回某用户的几条消息

198 lines (196 loc) 8.55 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, inject: () => inject, name: () => name }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var name = "batch-recall"; var inject = { required: ["database"] }; var Config = import_koishi.Schema.object({ maxMessagesPerUser: import_koishi.Schema.number().default(99).min(1).description("最多保存消息数量(条/用户)"), maxMessageRetentionHours: import_koishi.Schema.number().default(24).min(1).description("最多保存消息时间(小时)"), cleanupIntervalHours: import_koishi.Schema.number().default(24).min(1).description("自动清理过期消息时间(小时)"), whitelistedGuilds: import_koishi.Schema.array(String).default([]).description("白名单群组ID") }).description("消息记录与存储配置"); function apply(ctx, config) { const logger = ctx.logger("batch-recall"); let cleanupTimer; const isStorageEnabled = config.whitelistedGuilds.length > 0; const activeRecallTasks = /* @__PURE__ */ new Map(); function initializeDatabase() { ctx.model.extend("messages", { messageId: "string", userId: "string", channelId: "string", timestamp: "integer" }, { primary: "messageId", indexes: [ ["channelId", "userId"], ["timestamp"] ] }); } __name(initializeDatabase, "initializeDatabase"); async function saveMessage(session) { if (!session?.messageId || !config.whitelistedGuilds.includes(session.channelId)) return; try { await ctx.database.create("messages", { messageId: session.messageId, userId: session.userId, channelId: session.channelId, timestamp: Date.now() }); } catch (error) { logger.error(`保存消息失败: ${error.message}`); } } __name(saveMessage, "saveMessage"); async function recallMessages(session, messageIds) { const results = await Promise.allSettled(messageIds.map(async (id) => { await session.bot.deleteMessage(session.channelId, id); if (isStorageEnabled) await ctx.database.remove("messages", { messageId: id }); })); return { success: results.filter((r) => r.status === "fulfilled").length, failed: results.filter((r) => r.status === "rejected").length }; } __name(recallMessages, "recallMessages"); async function findMessagesToRecall(session, options) { const userId = options.user?.replace(/^<at:(.+)>$/, "$1"); const count = Math.max(1, Number(options.number) || 1); const query = { channelId: session.channelId }; if (userId) query.userId = userId; return ctx.database.select("messages").where(query).orderBy("timestamp", "desc").limit(count).execute(); } __name(findMessagesToRecall, "findMessagesToRecall"); async function runCleanup() { try { const expirationTime = Date.now() - config.maxMessageRetentionHours * 36e5; const timeRemoved = (await ctx.database.remove("messages", { timestamp: { $lt: expirationTime } }))?.matched || 0; let countRemoved = 0; const pairs = await ctx.database.select("messages").groupBy(["userId", "channelId"]).execute(); for (const { userId, channelId } of pairs) { const messages = await ctx.database.select("messages").where({ channelId, userId }).orderBy("timestamp", "desc").execute(); if (messages.length <= config.maxMessagesPerUser) continue; const messagesToRemove = messages.slice(config.maxMessagesPerUser).map((msg) => msg.messageId); if (messagesToRemove.length) { const result = await ctx.database.remove("messages", { messageId: { $in: messagesToRemove } }); countRemoved += result?.matched || 0; } } const totalRemoved = timeRemoved + countRemoved; if (totalRemoved > 0) { logger.info(`清理完成: 已删除 ${totalRemoved} 条消息记录`); } } catch (error) { logger.error(`清理失败: ${error.message}`); } } __name(runCleanup, "runCleanup"); const recall = ctx.command("recall", "撤回消息", { authority: 2 }).option("user", "-u <user> 撤回指定用户的消息").option("number", "-n <number> 撤回消息数量", { fallback: 1 }).usage("撤回当前会话中指定数量的消息,可以通过引用消息或指定用户和数量进行撤回").example("recall -u @用户 -n 10 - 撤回指定用户的10条最新消息").action(async ({ session, options }) => { try { const quotedMessages = Array.isArray(session.quote) ? session.quote : [session.quote].filter(Boolean); if (quotedMessages?.length) { const { success, failed } = await recallMessages( session, quotedMessages.map((q) => q.id || q.messageId) ); return failed ? `撤回完成:成功 ${success} 条,失败 ${failed} 条` : ""; } if (!isStorageEnabled) return "已禁用消息存储,只能撤回引用消息"; const channelTasks = activeRecallTasks.get(session.channelId) || /* @__PURE__ */ new Set(); const task = { controller: new AbortController(), total: 0, success: 0, failed: 0 }; channelTasks.add(task); activeRecallTasks.set(session.channelId, channelTasks); const messages = await findMessagesToRecall(session, options); task.total = messages.length; if (messages.length === 0) { channelTasks.delete(task); if (channelTasks.size === 0) activeRecallTasks.delete(session.channelId); return "未找到可撤回的消息"; } for (const message of messages) { if (task.controller.signal.aborted) break; const result = await recallMessages(session, [message.messageId]); task.success += result.success; task.failed += result.failed; await new Promise((resolve) => setTimeout(resolve, 1e3)); } channelTasks.delete(task); if (channelTasks.size === 0) activeRecallTasks.delete(session.channelId); return task.failed ? `撤回完成:成功 ${task.success} 条,失败 ${task.failed} 条` : ""; } catch (error) { logger.error(`撤回失败: ${error}`); return "撤回操作失败"; } }); recall.subcommand(".stop", "停止撤回操作").action(({ session }) => { const tasks = activeRecallTasks.get(session.channelId); if (!tasks?.size) return "没有正在进行的撤回操作"; for (const task of tasks) task.controller.abort(); const count = tasks.size; activeRecallTasks.delete(session.channelId); return `已停止${count}个撤回操作`; }); if (isStorageEnabled) { initializeDatabase(); ctx.on("message", saveMessage); ctx.on("send", saveMessage); ctx.on("ready", () => { logger.info(`已启用消息存储(${config.maxMessageRetentionHours} 小时 & ${config.maxMessagesPerUser} 条/用户)`); runCleanup(); cleanupTimer = setInterval(runCleanup, config.cleanupIntervalHours * 3600 * 1e3); logger.info(`已启用自动清理(${config.cleanupIntervalHours} 小时)`); }); ctx.on("dispose", async () => { clearInterval(cleanupTimer); try { await ctx.database.drop("messages"); logger.info("已停止自动清理并删除消息记录表"); } catch (error) { logger.error(`删除消息记录表失败: ${error.message}`); } }); } } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name });