koishi-plugin-batch-recall
Version:
基于数据库的高阶撤回,支持撤回某用户的几条消息
198 lines (196 loc) • 8.55 kB
JavaScript
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
});