koishi-plugin-message-counter
Version:
Koishi 的消息数量统计插件。生成各种发言排行榜。
1,386 lines (1,366 loc) • 101 kB
JavaScript
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