UNPKG

koishi-plugin-message-counter

Version:

Koishi 的消息数量统计插件。生成各种发言排行榜。

1,386 lines (1,366 loc) 101 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); 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, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var import_node_schedule = __toESM(require("node-schedule")); var import_path = __toESM(require("path")); var fs = __toESM(require("fs")); var name = "message-counter"; var inject = { required: ["database"], optional: ["markdownToImage", "puppeteer", "canvas"] }; var usage = `## 注意事项 - 仅记录群聊消息。 - 初始化:需要权限等级 3 级。 ## 关键指令 - \`messageCounter.查询 [指定用户]\`: 查询指定用户的发言次数信息(次数[排名])。 - \`--yesterday\`/\`-d\`/\`-w\`/\`-m\`/\`-y\`/\`-t\`: 分别查询昨日/今日/本周/本月/今年/总发言次数[排名] 。 - \`--ydag\`/\`--dag\`/\`--wag\`/\`--mag\`/\`--yag\`/\`-a\`: 分别查询跨群昨日/今日/本周/本月/今年/总发言次数[排名]。 - \`messageCounter.排行榜 [显示的人数]\`: 发言排行榜,使用以下选项指定类型: - \`--whites\`: 白名单,只显示白名单用户,以空格、中英文逗号和顿号作为分隔符。 - \`--blacks\`: 黑名单,不显示黑名单用户,以空格、中英文逗号和顿号作为分隔符。 - \`--yesterday\`/\`-d\`/\`-w\`/\`-m\`/\`-y\`/\`-t\`: 分别查询昨日/今日/本周/本月/今年/总发言排行榜。 - \`--ydag\`/\`--dag\`/\`--wag\`/\`--mag\`/\`--yag\`/\`--dragon\`: 分别查询跨群昨日/今日/本周/本月/今年/总发言排行榜(圣龙王榜)。 - 默认为今日发言榜。 - \`messageCounter.群排行榜 [number:number]\`: 各个群聊的发言排行榜,可以指定显示的数量,也可以使用以下选项来指定排行榜的类型: - \`-s\`: 指定用户的群发言排行榜,可用 at 或 用户 ID 指定。 - \`--whites\`: 白名单,只显示白名单群,以空格、中英文逗号和顿号作为分隔符。 - \`--blacks\`: 黑名单,不显示黑名单群,以空格、中英文逗号和顿号作为分隔符。 - \`-d\`/\`-w\`/\`-m\`/\`-y\`/\`-t\`/\`--yesterday\`: 分别查询昨日/今日/本周/本月/今年/总发言排行榜️。 - 默认为今日发言榜。 ## 自定义水平柱状图 3 1. 用户图标: - 支持为同一用户添加多个图标,它们会同时显示。 - 在 \`data/messageCounterIcons\` 文件夹下添加用户图标,文件名为用户 ID (例如 \`1234567890.png\`)。 - 多个图标的文件名需形如 \`1234567890-1.png\`、 \`1234567890-2.png\` 。 2. 柱状条背景: - 支持为同一用户添加多个背景图片,插件会随机选择一个显示。 - 在 \`data/messageCounterBarBgImgs\` 文件夹下添加水平柱状条背景图片。 - 多个图片的文件名需形如 \`1234567890-1.png\`、\`1234567890-2.png\`。 - 建议图片尺寸为 850x50 像素,文件名为用户 ID (例如\`1234567890.png\`)。 > 重启插件以使更改生效。 ## QQ 群 - 956758505`; var logger = new import_koishi.Logger("messageCounter"); var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ isYesterdayCommentRankingDisabled: import_koishi.Schema.boolean().default(false).description( "是否禁用昨日发言排行榜。开启后可用于解决群组消息过多导致的每日 0 点卡顿问题。" ) }).description("功能设置"), import_koishi.Schema.object({ defaultMaxDisplayCount: import_koishi.Schema.number().min(0).default(20).description("排行榜默认显示的人数。"), isTimeInfoSupplementEnabled: import_koishi.Schema.boolean().default(true).description("是否在显示排行榜时补充时间信息。"), isUserMessagePercentageVisible: import_koishi.Schema.boolean().default(true).description("是否在排行榜中显示用户消息占比。"), hiddenUserIdsInLeaderboard: import_koishi.Schema.array(String).role("table").description("在排行榜中隐藏的用户列表。"), hiddenChannelIdsInLeaderboard: import_koishi.Schema.array(String).role("table").description("在排行榜中隐藏的频道列表。") }).description("排行榜显示设置"), import_koishi.Schema.object({ isBotMessageTrackingEnabled: import_koishi.Schema.boolean().default(false).description("是否统计 Bot 自己发送的消息。") }).description("消息追踪设置"), import_koishi.Schema.object({ isTextToImageConversionEnabled: import_koishi.Schema.boolean().default(false).description( `(可以同时开启下面的功能)是否开启将文本转为图片的功能(可选),如需启用,需要启用 \`markdownToImage\` 服务。` ), isLeaderboardToHorizontalBarChartConversionEnabled: import_koishi.Schema.boolean().default(false).description( "是否开启排行榜转为水平柱状图的功能(可选),如需启用,需要启用 `puppeteer` 服务。" ), imageType: import_koishi.Schema.union(["png", "jpeg", "webp"]).default("png").description(`发送的水平柱状图片类型。`), width: import_koishi.Schema.number().default(600).description("水平柱状图的图片宽度(对样式 3 无效)。"), isFirstProgressFullyVisible: import_koishi.Schema.boolean().default(true).description("横向柱状图第一名的进度条是否占满(对样式 3 无效)。"), maxHorizontalBarLabelLengthBeforeTruncation: import_koishi.Schema.number().min(1).default(6).description( "水平柱状图的标签最大长度,超过该长度的标签将被截断(对样式 3 无效)。" ), waitUntil: import_koishi.Schema.union([ "load", "domcontentloaded", "networkidle0", "networkidle2" ]).default("networkidle0").description("(仅样式 3)等待页面加载的事件。"), shouldMoveIconToBarEndLeft: import_koishi.Schema.boolean().default(true).description( "(仅样式 3)是否将自定义图标移动到水平柱状条末端的左侧,关闭后将放在用户名的右侧。" ), horizontalBarBackgroundOpacity: import_koishi.Schema.number().min(0).max(1).default(0.6).description( "(仅样式 3)自定义水平柱状条背景的不透明度,值越小则越透明。" ), horizontalBarBackgroundFullOpacity: import_koishi.Schema.number().min(0).max(1).default(0).description( "(仅样式 3)自定义水平柱状条背景整条的不透明度,值越小则越透明。" ), backgroundType: import_koishi.Schema.union(["none", "api", "css"]).default("none").description("(仅样式 3)背景自定义类型。"), apiBackgroundConfig: import_koishi.Schema.object({ apiUrl: import_koishi.Schema.string(), apiKey: import_koishi.Schema.string(), responseType: import_koishi.Schema.union(["binary", "url", "base64"]).default("binary") }).collapse().description("(仅样式 3)API 背景配置。"), backgroundValue: import_koishi.Schema.string().role("textarea", { rows: [2, 4] }).default( `body { background: linear-gradient(135deg, #f6f8f9 0%, #e5ebee 100%); }` ).description("(仅样式 3)背景 css 值。"), horizontalBarChartStyle: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("样式 1 (名称与柱状条不同一行)"), import_koishi.Schema.const("2").description("样式 2 (名称与柱状条同一行)"), import_koishi.Schema.const("3").description("样式 3 (默认) 理论上最好看") ]).role("radio").default("3").description("水平柱状图的样式。") }).description("图片转换功能设置"), import_koishi.Schema.intersect([ import_koishi.Schema.object({ autoPush: import_koishi.Schema.boolean().default(false).description("是否自动推送排行榜。") }).description("自动推送设置"), import_koishi.Schema.union([ import_koishi.Schema.object({ autoPush: import_koishi.Schema.const(true).required(), shouldSendDailyLeaderboardAtMidnight: import_koishi.Schema.boolean().default(true).description("是否在每天 0 点发送排行榜。"), dailyScheduledTimers: import_koishi.Schema.array(String).role("table").description( "每日定时发送用户今日发言排行榜的时间列表(中国北京时间),例如 `08:00`、`18:45`。如果开启上面的选项,则自动包含 0 点。" ), isGeneratingRankingListPromptVisible: import_koishi.Schema.boolean().default(true).description("是否在生成排行榜时发送提示消息。"), leaderboardGenerationWaitTime: import_koishi.Schema.number().min(0).default(3).description(`提示消息发送后,自动生成排行榜的等待时间,单位是秒。`), pushChannelIds: import_koishi.Schema.array(String).role("table").description("启用自动推送排行榜功能的频道列表。"), shouldSendLeaderboardNotificationsToAllChannels: import_koishi.Schema.boolean().default(false).description("是否向所有频道推送排行榜。"), excludedLeaderboardChannels: import_koishi.Schema.array(String).role("table").description("不推送排行榜的频道列表。"), delayBetweenGroupPushesInSeconds: import_koishi.Schema.number().min(0).default(5).description("群组推送之间的延迟时间,单位是秒。"), groupPushDelayRandomizationSeconds: import_koishi.Schema.number().min(0).default(10).description( "群组推送延迟时间的随机化范围(上下波动范围),单位是秒。" ) }), import_koishi.Schema.object({}) ]) ]), import_koishi.Schema.intersect([ import_koishi.Schema.object({ enableMostActiveUserMuting: import_koishi.Schema.boolean().default(false).description("是否禁言每天发言最多的人,即龙王。") }).description("用户禁言设置"), import_koishi.Schema.union([ import_koishi.Schema.object({ enableMostActiveUserMuting: import_koishi.Schema.const(true).required(), dragonKingDetainmentTime: import_koishi.Schema.number().min(0).default(5).description(`关押龙王的等待时间,单位是秒。`), detentionDuration: import_koishi.Schema.number().default(1).description(`关押时长,单位是天。`), muteChannelIds: import_koishi.Schema.array(String).role("table").description("生效的频道。") }), import_koishi.Schema.object({}) ]) ]) ]); async function apply(ctx, config) { const messageCounterIconsPath = import_path.default.join( ctx.baseDir, "data", "messageCounterIcons" ); const messageCounterBarBgImgsPath = import_path.default.join( ctx.baseDir, "data", "messageCounterBarBgImgs" ); const filePath = import_path.default.join(__dirname, "emptyHtml.html").replace(/\\/g, "/"); await ensureDirExists(messageCounterIconsPath); await ensureDirExists(messageCounterBarBgImgsPath); const scheduledJobs = []; const iconData = readIconsFromFolder(messageCounterIconsPath); const barBgImgs = readBgImgsFromFolder( messageCounterBarBgImgsPath ); const { autoPush, defaultMaxDisplayCount, isBotMessageTrackingEnabled, isTimeInfoSupplementEnabled, isTextToImageConversionEnabled, enableMostActiveUserMuting, pushChannelIds, muteGuildIds, detentionDuration, dragonKingDetainmentTime, leaderboardGenerationWaitTime, isUserMessagePercentageVisible } = config; createScheduledTasks(config.dailyScheduledTimers); ctx.model.extend( "message_counter_records", { id: "unsigned", channelId: "string", channelName: "string", userId: "string", username: "string", userAvatar: "string", todayPostCount: "unsigned", thisWeekPostCount: "unsigned", thisMonthPostCount: "unsigned", thisYearPostCount: "unsigned", totalPostCount: "unsigned", yesterdayPostCount: "unsigned" }, { primary: "id", autoInc: true } ); ctx = ctx.guild(); ctx.on("message", async (session) => { const { channelId, event, userId } = session; session.observeUser(["id", "name", "permissions"]); const username = session.user?.name || session.username; let groupList; if (typeof session.bot?.getGuildList === "function") { groupList = await session.bot.getGuildList(); } else { groupList = { data: [] }; } const groups = groupList.data; const channelName = getNameFromChannelId(groups, channelId); await ctx.database.set( "message_counter_records", { channelId }, { channelName: channelName ?? event.channel.name ?? channelId } ); const getUser = await ctx.database.get("message_counter_records", { channelId, userId }); if (getUser.length === 0) { if (userId) { await ctx.database.create("message_counter_records", { channelId, channelName: channelName ?? event.channel.name ?? channelId, userId, username, userAvatar: event.user.avatar, todayPostCount: 1, thisWeekPostCount: 1, thisMonthPostCount: 1, thisYearPostCount: 1, totalPostCount: 1 }); } } else { const user = getUser[0]; await ctx.database.set( "message_counter_records", { channelId, userId }, { channelName: channelName ?? event.channel.name ?? channelId, username, userAvatar: event.user.avatar, todayPostCount: user.todayPostCount + 1, thisWeekPostCount: user.thisWeekPostCount + 1, thisMonthPostCount: user.thisMonthPostCount + 1, thisYearPostCount: user.thisYearPostCount + 1, totalPostCount: user.totalPostCount + 1 } ); } }); if (isBotMessageTrackingEnabled) { ctx.before("send", async (session) => { if (isBotMessageTrackingEnabled) { const { channelId, bot, event } = session; let groupList; if (typeof session.bot?.getGuildList === "function") { groupList = await session.bot.getGuildList(); } else { groupList = { data: [] }; } const groups = groupList.data; const channelName = getNameFromChannelId(groups, channelId); await ctx.database.set( "message_counter_records", { channelId }, { channelName: channelName ?? event.channel.name ?? channelId } ); const getUser = await ctx.database.get("message_counter_records", { channelId, userId: bot.user.id }); if (getUser.length === 0) { await ctx.database.create("message_counter_records", { channelId, channelName: channelName ?? event.channel.name ?? channelId, userId: bot.user.id, username: bot.user.name, userAvatar: bot.user.avatar, todayPostCount: 1, thisWeekPostCount: 1, thisMonthPostCount: 1, thisYearPostCount: 1, totalPostCount: 1 }); } else { const user = getUser[0]; await ctx.database.set( "message_counter_records", { channelId, userId: bot.user.id }, { channelName: channelName ?? event.channel.name ?? channelId, username: bot.user.name, userAvatar: bot.user.avatar, todayPostCount: user.todayPostCount + 1, thisWeekPostCount: user.thisWeekPostCount + 1, thisMonthPostCount: user.thisMonthPostCount + 1, thisYearPostCount: user.thisYearPostCount + 1, totalPostCount: user.totalPostCount + 1 } ); } } }); } ctx.command("messageCounter", "查看messageCounter帮助").action(async ({ session }) => { await session.execute(`messageCounter -h`); }); ctx.command("messageCounter.初始化", "初始化", { authority: 3 }).action(async ({ session }) => { await session.send("嗯~"); await ctx.database.remove("message_counter_records", {}); await session.send("好啦~"); }); ctx.command("messageCounter.查询 [targetUser:text]", "查询").userFields(["id", "name", "permissions"]).option("yesterday", "--yesterday 昨日发言总次数[排名]").option("day", "-d 今日发言次数[排名]").option("week", "-w 本周发言次数[排名]").option("month", "-m 本月发言次数[排名]").option("year", "-y 今年发言次数[排名]").option("total", "-t 总发言次数[排名]").option("ydag", "--ydag 跨群昨日发言总次数[排名]").option("dag", "--dag 跨群今日发言总次数[排名]").option("wag", "--wag 跨群本周发言总次数[排名]").option("mag", "--mag 跨群本月发言总次数[排名]").option("yag", "--yag 跨群本年发言总次数[排名]").option("across", "-a 跨群发言总次数[排名]").action(async ({ session, options }, targetUser) => { const selectedOptions = { day: false, week: false, month: false, year: false, total: false, yesterday: false, across: false, dag: false, wag: false, mag: false, yag: false, ydag: false }; if (options.day) { selectedOptions.day = true; } if (options.week) { selectedOptions.week = true; } if (options.month) { selectedOptions.month = true; } if (options.year) { selectedOptions.year = true; } if (options.total) { selectedOptions.total = true; } if (options.yesterday) { selectedOptions.yesterday = true; } if (options.across) { selectedOptions.across = true; } if (options.dag) { selectedOptions.dag = true; } if (options.wag) { selectedOptions.wag = true; } if (options.mag) { selectedOptions.mag = true; } if (options.yag) { selectedOptions.yag = true; } if (options.ydag) { selectedOptions.ydag = true; } const allOptionsSelected = Object.values(selectedOptions).every( (value) => value === false ); if (allOptionsSelected) { Object.keys(selectedOptions).forEach((key) => { selectedOptions[key] = true; }); } const { day: day2, week: week2, month: month2, year: year2, total, across, dag, yesterday, wag, yag, mag, ydag } = selectedOptions; let { channelId, userId, username } = session; let targetUserRecord = []; const originalUerId = userId; if (targetUser) { targetUser = await replaceAtTags(session, targetUser); const userIdRegex = /<at id="([^"]+)"(?: name="([^"]+)")?\/>/; const match = targetUser.match(userIdRegex); userId = match?.[1] ?? userId; username = match?.[2] ?? username; if (originalUerId === userId) { targetUserRecord = await ctx.database.get("message_counter_records", { channelId, userId: targetUser }); if (targetUserRecord.length !== 0) { userId = targetUser; } } else { targetUserRecord = await ctx.database.get("message_counter_records", { channelId, userId }); } } else { targetUserRecord = await ctx.database.get("message_counter_records", { channelId, userId }); } if (targetUserRecord.length === 0) { return `被查询对象无任何发言记录。`; } const guildUsers = await ctx.database.get( "message_counter_records", { channelId } ); const getDragons = await ctx.database.get("message_counter_records", {}); const totalSums = { todayPostCount: 0, thisWeekPostCount: 0, thisMonthPostCount: 0, thisYearPostCount: 0, totalPostCount: 0, yesterdayPostCount: 0 }; const acrossTotalSums = { todayPostCount: 0, thisWeekPostCount: 0, thisMonthPostCount: 0, thisYearPostCount: 0, totalPostCount: 0, yesterdayPostCount: 0 }; const accumulateSums = /* @__PURE__ */ __name((sums, user) => { sums.todayPostCount += user.todayPostCount; sums.thisWeekPostCount += user.thisWeekPostCount; sums.thisMonthPostCount += user.thisMonthPostCount; sums.thisYearPostCount += user.thisYearPostCount; sums.totalPostCount += user.totalPostCount; sums.yesterdayPostCount += user.yesterdayPostCount; }, "accumulateSums"); guildUsers.forEach((user) => accumulateSums(totalSums, user)); getDragons.forEach((user) => accumulateSums(acrossTotalSums, user)); const getUserRanking = /* @__PURE__ */ __name((userId2) => { const userRecords2 = guildUsers.find((user) => user.userId === userId2); if (userRecords2) { return { todayRank: getRank("todayPostCount", userId2), thisWeekRank: getRank("thisWeekPostCount", userId2), thisMonthRank: getRank("thisMonthPostCount", userId2), thisYearRank: getRank("thisYearPostCount", userId2), totalRank: getRank("totalPostCount", userId2), yesterdayRank: getRank("yesterdayPostCount", userId2) }; } else { return null; } }, "getUserRanking"); const getRank = /* @__PURE__ */ __name((property, userId2) => { const sortedUsers = guildUsers.slice().sort((a, b) => b[property] - a[property]); const userIndex = sortedUsers.findIndex( (user) => user.userId === userId2 ); return userIndex !== -1 ? userIndex + 1 : null; }, "getRank"); const userRankingData = getUserRanking(userId); const { todayRank, thisWeekRank, thisMonthRank, thisYearRank, totalRank, yesterdayRank } = userRankingData; function getAcrossUserRank(userId2, dragons2) { const userIndex = dragons2.findIndex(([id, _]) => id === userId2); if (userIndex !== -1) { return userIndex + 1; } else { return -1; } } __name(getAcrossUserRank, "getAcrossUserRank"); const dragons = getSortedDragons(getDragons); const acrossRank = getAcrossUserRank(userId, dragons); const userRecords = await ctx.database.get( "message_counter_records", { userId } ); const totalPostCountAcrossGuilds = userRecords.reduce((total2, record) => { return total2 + record.totalPostCount; }, 0); const { todayPostCount, thisWeekPostCount, thisMonthPostCount, thisYearPostCount, totalPostCount, yesterdayPostCount } = targetUserRecord[0]; let message = isTextToImageConversionEnabled ? `# 查询对象:${targetUserRecord[0].username} ` : `查询对象:${targetUserRecord[0].username} `; if (isTimeInfoSupplementEnabled) { const currentBeijingTime = getCurrentBeijingTime(); message = isTextToImageConversionEnabled ? `# ${currentBeijingTime} ${message}` : `${currentBeijingTime} ${message}`; } if (yesterday) { message += `${isTextToImageConversionEnabled ? "## " : ""}本群昨日发言次数[排名]:${yesterdayPostCount} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( yesterdayPostCount, totalSums.yesterdayPostCount )}` : ""}[${yesterdayRank}] `; } if (day2) { message += `${isTextToImageConversionEnabled ? "## " : ""}本群今日发言次数[排名]:${todayPostCount} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( todayPostCount, totalSums.todayPostCount )}` : ""}[${todayRank}] `; } if (week2) { message += `${isTextToImageConversionEnabled ? "## " : ""}本群本周发言次数[排名]:${thisWeekPostCount} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( thisWeekPostCount, totalSums.thisWeekPostCount )}` : ""}[${thisWeekRank}] `; } if (month2) { message += `${isTextToImageConversionEnabled ? "## " : ""}本群本月发言次数[排名]:${thisMonthPostCount} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( thisMonthPostCount, totalSums.thisMonthPostCount )}` : ""}[${thisMonthRank}] `; } if (year2) { message += `${isTextToImageConversionEnabled ? "## " : ""}本群今年发言次数[排名]:${thisYearPostCount} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( thisYearPostCount, totalSums.thisYearPostCount )}` : ""}[${thisYearRank}] `; } if (total) { message += `${isTextToImageConversionEnabled ? "## " : ""}本群总发言次数[排名]:${totalPostCount} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( totalPostCount, totalSums.totalPostCount )}` : ""}[${totalRank}] `; } if (ydag) { const ydagResult = getUserRankAndRecord( getDragons, userId, "yesterdayPostCount" ); const ydagUserRecord = ydagResult.userRecord; const ydagRank = ydagResult.acrossRank; message += `${isTextToImageConversionEnabled ? "## " : ""}跨群昨日发言次数[排名]:${ydagUserRecord.postCountAll} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( ydagUserRecord.postCountAll, acrossTotalSums.yesterdayPostCount )}` : ""}[${ydagRank}] `; } if (dag) { const dagResult = getUserRankAndRecord( getDragons, userId, "todayPostCount" ); const dagUserRecord = dagResult.userRecord; const dagRank = dagResult.acrossRank; message += `${isTextToImageConversionEnabled ? "## " : ""}跨群今日发言次数[排名]:${dagUserRecord.postCountAll} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( dagUserRecord.postCountAll, acrossTotalSums.todayPostCount )}` : ""}[${dagRank}] `; } if (wag) { const wagResult = getUserRankAndRecord( getDragons, userId, "thisWeekPostCount" ); const wagUserRecord = wagResult.userRecord; const wagRank = wagResult.acrossRank; message += `${isTextToImageConversionEnabled ? "## " : ""}跨群本周发言次数[排名]:${wagUserRecord.postCountAll} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( wagUserRecord.postCountAll, acrossTotalSums.thisWeekPostCount )}` : ""}[${wagRank}] `; } if (mag) { const magResult = getUserRankAndRecord( getDragons, userId, "thisMonthPostCount" ); const magUserRecord = magResult.userRecord; const magRank = magResult.acrossRank; message += `${isTextToImageConversionEnabled ? "## " : ""}跨群本月发言次数[排名]:${magUserRecord.postCountAll} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( magUserRecord.postCountAll, acrossTotalSums.thisMonthPostCount )}` : ""}[${magRank}] `; } if (yag) { const yagResult = getUserRankAndRecord( getDragons, userId, "thisYearPostCount" ); const yagUserRecord = yagResult.userRecord; const yagRank = yagResult.acrossRank; message += `${isTextToImageConversionEnabled ? "## " : ""}跨群本年发言次数[排名]:${yagUserRecord.postCountAll} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( yagUserRecord.postCountAll, acrossTotalSums.thisYearPostCount )}` : ""}[${yagRank}] `; } if (across) { message += `${isTextToImageConversionEnabled ? "## " : ""}跨群总发言次数[排名]:${totalPostCountAcrossGuilds} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( totalPostCountAcrossGuilds, acrossTotalSums.totalPostCount )}` : ""}[${acrossRank}] `; } if (isTextToImageConversionEnabled) { const imageBuffer = await ctx.markdownToImage.convertToImage(message); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } return message; }); ctx.command("messageCounter.群排行榜 [number:number]", "群发言排行榜").userFields(["id", "name", "permissions"]).option("specificUser", "-s <user:text> 特定用户的群发言榜", { fallback: "" }).option("whites", "--whites <whites:text> 白名单(仅显示)", { fallback: "" }).option("blacks", "--blacks <blacks:text> 黑名单(排除)", { fallback: "" }).option("yesterday", "--yesterday 昨日发言榜").option("day", "-d 今日发言榜").option("week", "-w 本周发言榜").option("month", "-m 本月发言榜").option("year", "-y 今年发言榜").option("total", "-t 总发言榜").action(async ({ session, options }, number) => { if (!number) { number = defaultMaxDisplayCount; } if (typeof number !== "number" || isNaN(number) || number < 0) { return "请输入大于等于 0 的数字作为排行榜的参数。"; } if (config.hiddenChannelIdsInLeaderboard.length !== 0) { options.blacks += "" + config.hiddenChannelIdsInLeaderboard.join(" "); } let userId = ""; if (options.specificUser) { const atElements = import_koishi.h.select(options.specificUser, "at"); if (atElements.length > 0) { userId = atElements[0].attrs.id; } if (!userId) { userId = options.specificUser; } } let username = ""; if (userId) { const userRecords = await ctx.database.get( "message_counter_records", { userId } ); if (userRecords.length === 0) { return `指定用户不存在。`; } username = getUsernameByChannelId(userRecords, session.channelId); if (!username) { username = userRecords[0].username; } } const whites = splitWhitesOrBlacksString(options.whites); const blacks = splitWhitesOrBlacksString(options.blacks); let messageCounterRecords = await ctx.database.get("message_counter_records", {}); if (messageCounterRecords.length === 0) { return; } messageCounterRecords = filterRecordsByWhitesAndBlacks( whites, blacks, messageCounterRecords, "channelId" ); let sortByProperty; let countProperty; if (options.day) { sortByProperty = "todayPostCount"; countProperty = "今日发言次数"; } else if (options.week) { sortByProperty = "thisWeekPostCount"; countProperty = "本周发言次数"; } else if (options.month) { sortByProperty = "thisMonthPostCount"; countProperty = "本月发言次数"; } else if (options.year) { sortByProperty = "thisYearPostCount"; countProperty = "今年发言次数"; } else if (options.total) { sortByProperty = "totalPostCount"; countProperty = "总发言次数"; } else if (options.yesterday) { sortByProperty = "yesterdayPostCount"; countProperty = "昨日发言次数"; } else { sortByProperty = "todayPostCount"; countProperty = "今日发言次数"; } const result = sumValuesByKey( messageCounterRecords, sortByProperty, userId ); const totalSum = calculateTotalSum(result); const currentBeijingTime = getCurrentBeijingTime(); const rankTimeTitle = `${currentBeijingTime}`; const prefix = `群排行榜:` + (username ? `${username} 的` : ``); const rankTitle = `${prefix}${countProperty}`; const rankingData = []; let rank = `${isTextToImageConversionEnabled ? `# ` : ``}${prefix}${countProperty} `; result.sort((a, b) => b.sum - a.sum); const rankingString = await generateRankingString( result, totalSum, rankingData, number ); if (isTimeInfoSupplementEnabled) { rank = isTextToImageConversionEnabled ? `# ${currentBeijingTime} ${rank} ${rankingString}` : `${currentBeijingTime} ${rank} ${rankingString}`; } if (config.isLeaderboardToHorizontalBarChartConversionEnabled) { const thisRankInfo = await getChannelResultWithRank( session.channelId, result, totalSum ); let updatedRankingData = markUserInRanking( rankingData, session.channelId ); const showUserInExtraRow = thisRankInfo && !rankingData.some((item) => item.userId === thisRankInfo.userId); if (showUserInExtraRow) { updatedRankingData = [...updatedRankingData, thisRankInfo]; } updatedRankingData = markUserInRanking( updatedRankingData, session.channelId ); const imageBuffer = await LeaderboardToHorizontalBarChartConversion( rankTimeTitle, rankTitle, updatedRankingData, thisRankInfo ); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } if (isTextToImageConversionEnabled) { const imageBuffer = await ctx.markdownToImage.convertToImage(rank); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } return rank; }); ctx.command("messageCounter.排行榜 [number:number]", "用户发言排行榜").userFields(["id", "name", "permissions"]).option("whites", "--whites <whites:text> 白名单(仅显示)", { fallback: "" }).option("blacks", "--blacks <blacks:text> 黑名单(排除)", { fallback: "" }).option("yesterday", "--yesterday 昨日发言榜").option("day", "-d 今日发言榜").option("week", "-w 本周发言榜").option("month", "-m 本月发言榜").option("year", "-y 今年发言榜").option("total", "-t 总发言榜").option("ydag", "--ydag 跨群昨日发言榜").option("dag", "--dag 跨群日发言榜").option("wag", "--wag 跨群周发言榜").option("mag", "--mag 跨群月发言榜").option("yag", "--yag 跨群年发言榜").option("dragon", "--dragon 圣龙王榜").action(async ({ session, options }, number) => { const { channelId } = session; if (!number) { number = defaultMaxDisplayCount; } if (typeof number !== "number" || isNaN(number) || number < 0) { return "请输入大于等于 0 的数字作为排行榜的参数。"; } if (config.hiddenUserIdsInLeaderboard.length !== 0) { options.blacks += "" + config.hiddenUserIdsInLeaderboard.join(" "); } const whites = splitWhitesOrBlacksString(options.whites); const blacks = splitWhitesOrBlacksString(options.blacks); let getUsers = await ctx.database.get("message_counter_records", { channelId }); let acrossGetUsers = await ctx.database.get( "message_counter_records", {} ); getUsers = filterRecordsByWhitesAndBlacks( whites, blacks, getUsers, "userId" ); acrossGetUsers = filterRecordsByWhitesAndBlacks( whites, blacks, acrossGetUsers, "userId" ); const totalSums = { todayPostCount: 0, thisWeekPostCount: 0, thisMonthPostCount: 0, thisYearPostCount: 0, totalPostCount: 0, yesterdayPostCount: 0 }; const acrossTotalSums = { todayPostCount: 0, thisWeekPostCount: 0, thisMonthPostCount: 0, thisYearPostCount: 0, totalPostCount: 0, yesterdayPostCount: 0 }; const accumulateSums = /* @__PURE__ */ __name((sums, user) => { sums.todayPostCount += user.todayPostCount; sums.thisWeekPostCount += user.thisWeekPostCount; sums.thisMonthPostCount += user.thisMonthPostCount; sums.thisYearPostCount += user.thisYearPostCount; sums.totalPostCount += user.totalPostCount; sums.yesterdayPostCount += user.yesterdayPostCount; }, "accumulateSums"); getUsers.forEach((user) => accumulateSums(totalSums, user)); acrossGetUsers.forEach((user) => accumulateSums(acrossTotalSums, user)); if (getUsers.length === 0 || acrossGetUsers.length === 0) { return; } let sortByProperty; let countProperty; if (options.day) { sortByProperty = "todayPostCount"; countProperty = "今日发言次数"; } else if (options.week) { sortByProperty = "thisWeekPostCount"; countProperty = "本周发言次数"; } else if (options.month) { sortByProperty = "thisMonthPostCount"; countProperty = "本月发言次数"; } else if (options.year) { sortByProperty = "thisYearPostCount"; countProperty = "今年发言次数"; } else if (options.total) { sortByProperty = "totalPostCount"; countProperty = "总发言次数"; } else if (options.yesterday) { sortByProperty = "yesterdayPostCount"; countProperty = "昨日发言次数"; } else { sortByProperty = "todayPostCount"; countProperty = "今日发言次数"; } const currentBeijingTime = getCurrentBeijingTime(); if (options.dag) { return generateAcrossRanking( `排行榜:跨群今日总发言次数`, acrossGetUsers, number, currentBeijingTime, accumulateSums, "todayPostCount", session.userId ); } if (options.wag) { return generateAcrossRanking( `排行榜:跨群本周总发言次数`, acrossGetUsers, number, currentBeijingTime, accumulateSums, "thisWeekPostCount", session.userId ); } if (options.mag) { return generateAcrossRanking( `排行榜:跨群本月总发言次数`, acrossGetUsers, number, currentBeijingTime, accumulateSums, "thisMonthPostCount", session.userId ); } if (options.yag) { return generateAcrossRanking( `排行榜:跨群今年总发言次数`, acrossGetUsers, number, currentBeijingTime, accumulateSums, "thisYearPostCount", session.userId ); } if (options.ydag) { return generateAcrossRanking( `排行榜:跨群昨日总发言次数`, acrossGetUsers, number, currentBeijingTime, accumulateSums, "yesterdayPostCount", session.userId ); } if (options.dragon) { const dragons = getSortedDragons(acrossGetUsers); const topDragons = dragons.slice(0, number); const userExists2 = topDragons.some( (dragon) => dragon[0] === session.userId ); if (!userExists2) { const userDragon = dragons.find( (dragon) => dragon[0] === session.userId ); if (userDragon) { topDragons.push(userDragon); } } const rankingData2 = []; const resultPromises = topDragons.map( async ([key, dragonPostCount], index) => { const getUser = await ctx.database.get("message_counter_records", { userId: key }); const user = getUser[0]; await addToRankingData( rankingData2, user.username, key, dragonPostCount, acrossTotalSums.totalPostCount ); if (user) { return `${isTextToImageConversionEnabled ? "## " : ""}${index + 1}. ${user.username}:${dragonPostCount} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( dragonPostCount, acrossTotalSums.totalPostCount )}` : ""}`; } return null; } ); const result2 = (await Promise.all(resultPromises)).filter( (item) => item !== null ); let rank2 = isTextToImageConversionEnabled ? `# 圣龙王榜: ${result2.join("\n")}` : `圣龙王榜: ${result2.join("\n")}`; if (isTimeInfoSupplementEnabled) { rank2 = isTextToImageConversionEnabled ? `# ${currentBeijingTime} ${rank2}` : `${currentBeijingTime} ${rank2}`; } if (config.isLeaderboardToHorizontalBarChartConversionEnabled) { let updatedRankingData = markUserInRanking( rankingData2, session.userId ); const imageBuffer = await LeaderboardToHorizontalBarChartConversion( `${currentBeijingTime}`, `圣龙王榜`, updatedRankingData ); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } if (isTextToImageConversionEnabled) { const imageBuffer = await ctx.markdownToImage.convertToImage(rank2); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } await session.send(rank2); return; } const rankingData = []; getUsers.sort((a, b) => b[sortByProperty] - a[sortByProperty]); const topUsers = getUsers.slice(0, number); const userExists = topUsers.some( (user) => user.userId === session.userId ); if (!userExists) { const targetUser = getUsers.find( (user) => user.userId === session.userId ); if (targetUser) { topUsers.push(targetUser); } } const userPromises = topUsers.map(async (user, index) => { await addToRankingData( rankingData, user.username, user.userId, user[sortByProperty], totalSums[sortByProperty] ); return `${isTextToImageConversionEnabled ? "## " : ""}${index + 1}. ${user.username}:${user[sortByProperty]} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( user[sortByProperty], totalSums[sortByProperty] )}` : ""}`; }); const userStrings = await Promise.all(userPromises); const result = userStrings.join("\n"); let rank = isTextToImageConversionEnabled ? `# 排行榜:${countProperty} ${result}` : `排行榜:${countProperty} ${result}`; if (isTimeInfoSupplementEnabled) { rank = isTextToImageConversionEnabled ? `# ${currentBeijingTime} ${rank}` : `${currentBeijingTime} ${rank}`; } if (config.isLeaderboardToHorizontalBarChartConversionEnabled) { let updatedRankingData = markUserInRanking( rankingData, session.userId ); const imageBuffer = await LeaderboardToHorizontalBarChartConversion( `${currentBeijingTime}`, `排行榜:${countProperty}`, updatedRankingData ); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } if (isTextToImageConversionEnabled) { const imageBuffer = await ctx.markdownToImage.convertToImage(rank); return import_koishi.h.image(imageBuffer, `image/${config.imageType}`); } await session.send(rank); }); function markUserInRanking(rankingData, userId) { if (userId.includes(`#`)) { userId = "426230045"; } return rankingData.map((item) => { if (item.userId === userId) { return { ...item, name: `🌟${item.name}` }; } return item; }); } __name(markUserInRanking, "markUserInRanking"); async function getChannelResultWithRank(channelId, result, totalPostCount) { const channelResult = result.find((item) => item.channelId === channelId); if (!channelResult) { return void 0; } const sortedResults = [...result].sort((a, b) => b.sum - a.sum); const rank = sortedResults.findIndex((item) => item.channelId === channelId) + 1; const channelId2 = channelResult.channelId.includes(`#`) ? "426230045" : channelResult.channelId; return { userId: channelId2, name: channelResult.channelName, count: channelResult.sum, rank, percentage: calculatePercentage2(channelResult.sum, totalPostCount), avatar: `https://p.qlogo.cn/gh/${channelId2}/${channelId2}/640/`, avatarBase64: await resizeImageToBase64( `https://p.qlogo.cn/gh/${channelId2}/${channelId2}/640/` ) }; } __name(getChannelResultWithRank, "getChannelResultWithRank"); function getUsernameByChannelId(records, channelId) { const record = records.find((record2) => record2.channelId === channelId); return record ? record.username : void 0; } __name(getUsernameByChannelId, "getUsernameByChannelId"); async function resetCounter(_key, countKey, message) { const getUsers = await ctx.database.get("message_counter_records", {}); if (getUsers.length === 0) { return; } if (autoPush && config.shouldSendDailyLeaderboardAtMidnight) { generateLeaderboard(getUsers, countKey); } if (enableMostActiveUserMuting && countKey === "todayPostCount") { for (const currentBot of ctx.bots) { for (const channelId of muteGuildIds) { const usersByGuild = getUsers.filter( (user) => user.channelId === channelId ); if (usersByGuild.length !== 0) { await currentBot.sendMessage( channelId, `正在尝试自动捕捉龙王......` ); const dragonUser = usersByGuild[0]; try { await (0, import_koishi.sleep)(dragonKingDetainmentTime * 1e3); await currentBot.muteGuildMember( channelId, dragonUser.userId, detentionDuration * 24 * 60 * 60 * 1e3 ); await currentBot.sendMessage( channelId, `诸位请放心,龙王已被成功捕捉,关押时间为 ${detentionDuration} 天!` ); } catch (error) { logger.error( `在【${channelId}】中禁言用户【${dragonUser.username}】(${dragonUser.userId})失败!${error}` ); } } } } } if (countKey === "todayPostCount" && !config.isYesterdayCommentRankingDisabled) { updateYesterdayCount(getUsers); } await ctx.database.set("message_counter_records", {}, { [countKey]: 0 }); logger.success(message); } __name(resetCounter, "resetCounter"); async function updateYesterdayCount(users) { const batchSize = 100; const totalUsers = users.length; for (let i = 0; i < totalUsers; i += batchSize) { const batchUsers = users.slice(i, i + batchSize); const batchPromises = batchUsers.map((user) => { return ctx.database.set( "message_counter_records", { userId: user.userId, channelId: user.channelId }, { yesterdayPostCount: user.todayPostCount } ); }); await Promise.all(batchPromises); } } __name(updateYesterdayCount, "updateYesterdayCount"); async function replaceAtTags(session, content) { const atRegex = /<at id="(\d+)"(?: name="([^"]*)")?\/>/g; let match; while ((match = atRegex.exec(content)) !== null) { const userId = match[1]; const name2 = match[2]; if (!name2) { let guildMember; try { if (typeof session.bot?.getGuildMember === "function") { guildMember = await session.bot.getGuildMember( session.guildId, userId ); } else { guildMember = { user: { name: "未知用户" } }; } } catch (error) { logger.error(error); } const newAtTag = `<at id="${userId}" name="${guildMember.user.name}"/>`; content = content.replace(match[0], newAtTag); } } return content; } __name(replaceAtTags, "replaceAtTags"); function getUserRankAndRecord(getDragons, userId, postCountType) { if (getDragons.length === 0) { return; } const aggregatedUserRecords = getDragons.reduce((acc, user) => { if (!acc[user.userId]) { acc[user.userId] = { userId: user.userId, postCountAll: 0, username: user.username }; } let postCount = 0; switch (postCountType) { case "todayPostCount": postCount = user.todayPostCount; break; case "thisWeekPostCount": postCount = user.thisWeekPostCount; break; case "thisMonthPostCount": postCount = user.thisMonthPostCount; break; case "thisYearPostCount": postCount = user.thisYearPostCount; break; case "totalPostCount": postCount = user.totalPostCount; break; case "yesterdayPostCount": postCount = user.yesterdayPostCount; break; default: postCount = user.todayPostCount; break; } acc[user.userId].postCountAll += postCount; return acc; }, {}); const sortedUserRecords = Object.values(aggregatedUserRecords).sort( (a, b) => b.postCountAll - a.postCountAll ); const userIndex = sortedUserRecords.findIndex( (user) => user.userId === userId ); const userRecord = sortedUserRecords[userIndex]; const acrossRank = userIndex + 1; return { acrossRank, userRecord }; } __name(getUserRankAndRecord, "getUserRankAndRecord"); async function generateAcrossRanking(rankTitle, acrossGetUsers, number, currentBeijingTime, acrossTotalSums, postCountType, targetUserId) { const userMap = /* @__PURE__ */ new Map(); const usernameMap = /* @__PURE__ */ new Map(); for (const user of acrossGetUsers) { const { userId, todayPostCount, username, thisWeekPostCount, thisMonthPostCount, thisYearPostCount, totalPostCount, yesterdayPostCount } = user; let postCount = 0; switch (postCountType) { case "todayPostCount": postCount = todayPostCount; break; case "thisWeekPostCount": postCount = thisWeekPostCount; break; case "thisMonthPostCount": postCount = thisMonthPostCount; break; case "thisYearPostCount": postCount = thisYearPostCount; break; case "totalPostCount": postCount = totalPostCount; break; case "yesterdayPostCount": postCount = yesterdayPostCount; break; default: postCount = todayPostCount; break; } if (userMap.has(userId)) { userMap.set(userId, userMap.get(userId) + postCount); } else { userMap.set(userId, postCount); usernameMap.set(userId, username); } } let sortedUsers = Array.from(userMap).sort((a, b) => b[1] - a[1]).slice(0, number); if (targetUserId && !sortedUsers.some((user) => user[0] === targetUserId)) { const userInData = acrossGetUsers.find( (user) => user.userId === targetUserId ); if (userInData) { let userPostCount = 0; switch (postCountType) { case "todayPostCount": userPostCount = userInData.todayPostCount; break; case "thisWeekPostCount": userPostCount = userInData.thisWeekPostCount; break; case "thisMonthPostCount": userPostCount = userInData.thisMonthPostCount; break; case "thisYearPostCount": userPostCount = userInData.thisYearPostCount; break; case "totalPostCount": userPostCount = userInData.totalPostCount; break; case "yesterdayPostCount": userPostCount = userInData.yesterdayPostCount; break; default: userPostCount = userInData.todayPostCount; break; } sortedUsers.push([targetUserId, userPostCount]); if (!usernameMap.has(targetUserId)) { usernameMap.set(targetUserId, userInData.username); } } } const rankingData = []; let rank = isTextToImageConversionEnabled ? `# ${rankTitle}: ` : `${rankTitle}: `; const rankTimeTitle = `${currentBeijingTime}`; for (const [index, user] of sortedUsers.entries()) { const userId = user[0]; const postCountAll = user[1]; const username = usernameMap.get(userId); await addToRankingData( rankingData, username, userId, postCountAll, acrossTotalSums[postCountType] ); rank += `${isTextToImageConversionEnabled ? "## " : ""}${index + 1}. ${username}:${postCountAll} 次${isUserMessagePercentageVisible ? ` ${calculatePercentage( postCountAll, acrossTotalSums.totalPostCount )}` : ""} `; } if (isTimeInfo