const fs = require("fs"); const path = require("path"); const { db } = require("./db"); const { getSetting } = require("./settings"); const { getEnabledPlatformIds } = require("./platforms"); const { getPluginLeaderboards } = require("./plugin-stats"); const { getPlugins, pluginsDir } = require("./plugins"); const coreProviders = new Map(); const coreOrder = []; let coreRegistered = false; function registerTopProvider(provider) { if (!provider || !provider.id) { return; } const id = normalizeProviderId(provider.id); if (!id) { return; } const entry = { id, label: provider.label || id, section: provider.section || "Community", description: provider.description || "", valueLabel: provider.valueLabel || "Total", rowType: provider.rowType || "user", aliases: Array.isArray(provider.aliases) ? provider.aliases : [], order: Number.isFinite(provider.order) ? provider.order : coreOrder.length, getRows: typeof provider.getRows === "function" ? provider.getRows : null }; if (!coreProviders.has(id)) { coreOrder.push(id); } coreProviders.set(id, entry); } function ensureCoreProviders() { if (coreRegistered) { return; } coreRegistered = true; registerTopProvider({ id: "messages", label: "Top messages", section: "Community Interaction", valueLabel: "Messages", description: "Users with the most messages.", getRows: ({ limit }) => buildStatRows("messages", limit) }); registerTopProvider({ id: "commands", label: "Top commands", section: "Community Interaction", valueLabel: "Commands", description: "Users who ran the most commands.", getRows: ({ limit }) => buildStatRows("commands", limit) }); registerTopProvider({ id: "modage", label: "Top mod age", section: "Moderation", valueLabel: "Time", description: "Moderators with the longest mod tenure.", getRows: ({ limit }) => buildModAgeRows(limit) }); registerTopProvider({ id: "coins", label: "Top currency", section: "Economy", valueLabel: getCurrencyLabel(), description: "Top balances from the currency framework.", getRows: ({ limit }) => buildCurrencyRows(limit) }); registerTopProvider({ id: "interactors", label: "Top interactors", section: "Expression Interaction", valueLabel: "Interactions", description: "Most interaction actions given.", getRows: ({ limit }) => buildExpressionRows(limit, "given") }); registerTopProvider({ id: "interactions_received", label: "Top interactions received", section: "Expression Interaction", valueLabel: "Interactions", description: "Most interaction actions received.", getRows: ({ limit }) => buildExpressionRows(limit, "received") }); registerTopProvider({ id: "commands_run", label: "Top commands run", section: "Commands", valueLabel: "Runs", rowType: "command", description: "Most popular commands across platforms.", getRows: ({ limit }) => buildCommandUsageRows(limit) }); registerTopProvider({ id: "followage", label: "Top followage", section: "Platforms", valueLabel: "Days", description: "Longest follower durations.", getRows: () => ({ rows: [], emptyMessage: "Followage tracking is not configured yet." }) }); registerTopProvider({ id: "watchtime", label: "Top watchtime", section: "Platforms", valueLabel: "Hours", description: "Most watch time recorded.", getRows: () => ({ rows: [], emptyMessage: "Watchtime tracking is not configured yet." }) }); registerTopProvider({ id: "games", label: "Top games", section: "Games", valueLabel: "Mentions", rowType: "game", description: "Most popular games tracked by plugins.", getRows: () => ({ rows: [], emptyMessage: "Game tracking is not configured yet." }) }); } function getTopProviders({ includePlugins = true, limit = 10 } = {}) { ensureCoreProviders(); const providers = coreOrder.map((id) => coreProviders.get(id)).filter(Boolean); if (!includePlugins) { return providers; } const pluginProviders = buildPluginProviders({ limit }); return mergeProviders(providers, pluginProviders); } function getTopBoards({ limit = 10 } = {}) { const providers = getTopProviders({ includePlugins: true, limit }); return providers.map((provider) => { if (provider.getRows) { const result = provider.getRows({ db, limit, settings: { getSetting } }); return normalizeProviderResult(provider, result); } return normalizeProviderResult(provider, { rows: provider.rows || [], rowType: provider.rowType, valueLabel: provider.valueLabel, emptyMessage: provider.emptyMessage }); }); } function getLeaderboardSections({ limit = 10 } = {}) { const boards = getTopBoards({ limit }); const sections = []; const sectionMap = new Map(); boards.forEach((board) => { const title = board.section || "Leaderboards"; if (!sectionMap.has(title)) { sectionMap.set(title, { title, boards: [] }); sections.push(sectionMap.get(title)); } sectionMap.get(title).boards.push(board); }); return sections; } function getTopCommandOptions() { return getTopProviders({ includePlugins: true }).map((provider) => ({ id: provider.id, label: provider.label, description: provider.description || "", aliases: provider.aliases || [] })); } function registerTopCommand({ commandRouter, settings }) { if (!commandRouter) { return; } ensureCoreProviders(); const platforms = getEnabledPlatformIds(); commandRouter.registerCommands("core", [ { id: "top", triggers: ["top"], platforms, handler: (ctx) => handleTopCommand({ ctx, settings }) } ]); } async function handleTopCommand({ ctx, settings }) { const prefix = settings.getSetting("command_prefix", "!"); const rawId = (ctx.args[0] || "").trim().toLowerCase(); if (!rawId || ["help", "list"].includes(rawId)) { await ctx.reply(buildTopHelp(prefix)); return true; } const board = findTopBoard(rawId, 5); if (!board) { await ctx.reply(buildTopHelp(prefix)); return true; } if (!board.rows.length) { await ctx.reply(board.emptyMessage || "No data recorded yet."); return true; } const list = board.rows .slice(0, 5) .map((entry, index) => { const label = entry.label || entry.username || entry.name || "Unknown"; return `${index + 1}) ${label} (${entry.value})`; }) .join(" | "); await ctx.reply(`${board.label}: ${list}`); return true; } function findTopBoard(id, limit) { const boards = getTopBoards({ limit }); const normalized = id.toLowerCase(); return boards.find((board) => { if (board.id === normalized) { return true; } return Array.isArray(board.aliases) ? board.aliases.map((alias) => alias.toLowerCase()).includes(normalized) : false; }); } function buildTopHelp(prefix) { const options = getTopCommandOptions().map((entry) => entry.id); if (!options.length) { return "No top categories are available yet."; } return `Usage: ${prefix}top . Available: ${options.join(", ")}`; } function mergeProviders(coreList, pluginList) { const merged = coreList.slice(); const indexMap = new Map(merged.map((provider, index) => [provider.id, index])); for (const provider of pluginList) { const existingIndex = indexMap.get(provider.id); if (existingIndex !== undefined) { if (provider.override) { merged[existingIndex] = provider; } continue; } indexMap.set(provider.id, merged.length); merged.push(provider); } return merged; } function normalizeProviderResult(provider, result) { const rows = normalizeRows(result?.rows || []); return { id: provider.id, label: provider.label, section: provider.section, rowType: result?.rowType || provider.rowType, valueLabel: result?.valueLabel || provider.valueLabel || "Total", rows, emptyMessage: result?.emptyMessage || provider.emptyMessage || "No data recorded yet.", aliases: result?.aliases || provider.aliases || [] }; } function normalizeRows(rows) { return (rows || []).map((row) => { const value = row.value ?? row.total ?? row.count ?? 0; return { username: row.username || row.user || row.label || row.name || null, label: row.label || row.username || row.name || null, href: row.href || row.url || null, value: formatNumber(value), rawValue: Number(value) || 0 }; }); } function buildPluginProviders({ limit }) { const pluginSections = getPluginLeaderboards(limit); const providers = []; pluginSections.forEach((section) => { const sectionId = slugify(section.id || section.title || "plugin"); const sectionLabel = section.title || "Plugin"; (section.boards || []).forEach((board) => { const boardKey = slugify(board.id || board.title || "board"); const topId = board.topId ? normalizeProviderId(board.topId) : ""; const id = topId || `${sectionId}-${boardKey}`; const rows = board.rows || []; const aliases = Array.isArray(board.topAliases) ? board.topAliases.map(normalizeProviderId).filter(Boolean) : Array.isArray(board.aliases) ? board.aliases.map(normalizeProviderId).filter(Boolean) : []; providers.push({ id, label: board.title || boardKey, section: sectionLabel, description: board.description || "", rowType: board.rowType || "user", valueLabel: board.valueLabel || "Total", rows, emptyMessage: board.emptyMessage || section.emptyMessage || "No data yet.", aliases, override: Boolean(board.topOverride) }); }); }); return providers; } function slugify(value) { const slug = (value || "") .toString() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .trim(); return slug || "command"; } function normalizeProviderId(value) { return (value || "") .toString() .trim() .toLowerCase() .replace(/[^a-z0-9_-]+/g, "-") .replace(/^-+|-+$/g, ""); } function buildStatRows(column, limit) { const rows = db .prepare( `SELECT user_profiles.internal_username AS username, stats.${column} AS value ` + "FROM stats " + "JOIN user_profiles ON user_profiles.id = stats.user_id " + `ORDER BY stats.${column} DESC LIMIT ?` ) .all(limit); return { rows }; } function buildModAgeRows(limit) { if (!tableExists("mod_role_periods")) { return { rows: [], emptyMessage: "No moderator history recorded yet." }; } const now = Date.now(); const rows = db .prepare( "SELECT mod_role_periods.user_id AS user_id, " + "SUM(CASE WHEN mod_role_periods.end_at IS NULL THEN ? - mod_role_periods.start_at ELSE mod_role_periods.end_at - mod_role_periods.start_at END) AS total_ms " + "FROM mod_role_periods " + "GROUP BY mod_role_periods.user_id " + "ORDER BY total_ms DESC LIMIT ?" ) .all(now, limit); const mapped = rows .map((row) => { const profile = db .prepare("SELECT internal_username FROM user_profiles WHERE id = ?") .get(row.user_id); return { username: profile?.internal_username || row.user_id, value: formatDuration(row.total_ms || 0) }; }) .filter((row) => row.username); return { rows: mapped, valueLabel: "Time" }; } function buildExpressionRows(limit, type) { if (!tableExists("expression_user_stats")) { return { rows: [], emptyMessage: "No interactions recorded yet." }; } const column = type === "received" ? "received_count" : "given_count"; const rows = db .prepare( `SELECT user_profiles.internal_username AS username, SUM(expression_user_stats.${column}) AS value ` + "FROM expression_user_stats " + "JOIN user_profiles ON user_profiles.id = expression_user_stats.user_id " + "GROUP BY expression_user_stats.user_id " + `ORDER BY value DESC LIMIT ?` ) .all(limit); return { rows }; } function buildCommandUsageRows(limit) { if (!tableExists("command_usage")) { return { rows: [], emptyMessage: "No command usage recorded yet." }; } const commandIndex = buildCommandIndex(); const prefix = getSetting("command_prefix", "!"); const rows = db .prepare("SELECT command_id, count FROM command_usage ORDER BY count DESC LIMIT ?") .all(limit) .map((row) => { const command = commandIndex.get(row.command_id); return { label: command?.label || formatCommandId(row.command_id, prefix), href: command?.href || null, value: row.count }; }); return { rows, rowType: "command" }; } function buildCurrencyRows(limit) { if (!tableExists("economy_accounts")) { return { rows: [], emptyMessage: "Currency framework not active." }; } const rows = db .prepare( "SELECT user_profiles.internal_username AS username, economy_accounts.balance AS value " + "FROM economy_accounts " + "JOIN user_profiles ON user_profiles.id = economy_accounts.user_id " + "ORDER BY economy_accounts.balance DESC LIMIT ?" ) .all(limit); return { rows, valueLabel: getCurrencyLabel() }; } function buildCommandIndex() { const prefix = getSetting("command_prefix", "!"); const index = new Map(); const addEntry = (ids, label, commandPageId) => { const cleanLabel = normalizeCommandDisplay(label, prefix); const href = `/commands#cmd-${slugify(commandPageId)}`; ids .map((id) => (id || "").toString().trim()) .filter(Boolean) .forEach((id) => { if (!index.has(id)) { index.set(id, { label: cleanLabel, href }); } }); }; addEntry(["top"], `${prefix}top`, "top"); for (const option of getTopCommandOptions()) { const subcommand = normalizeSubcommand(option.id); if (subcommand) { addEntry([`top:${subcommand}`], `${prefix}top ${subcommand}`, `top:${subcommand}`); } } if (tableExists("custom_commands")) { const customRows = db .prepare("SELECT trigger FROM custom_commands WHERE enabled = 1 ORDER BY trigger") .all(); for (const row of customRows) { const trigger = normalizeCommandTrigger(row.trigger); if (trigger) { addEntry([`custom:${trigger}`], `${prefix}${trigger}`, `custom:${trigger}`); } } } for (const plugin of listCommandPlugins()) { const manifestPath = path.join(plugin.path, "cmds.json"); const manifest = readJsonSafe(manifestPath); if (!manifest || !Array.isArray(manifest.commands)) { continue; } const pluginSettings = getPluginSettingsMap(plugin.id); for (const command of manifest.commands) { if (!command || !command.trigger) { continue; } const override = command.triggerKey ? pluginSettings[command.triggerKey] : ""; const trigger = normalizeCommandTrigger(override, command.trigger); if (!trigger) { continue; } const subcommand = normalizeSubcommand(command.subcommand); const commandId = `${plugin.id}:${command.id || trigger}`; const ids = [commandId, command.id]; if (command.id && !command.id.toString().includes(":")) { ids.push(`${plugin.id}:${command.id}`); } if (plugin.id === "economy-framework" && command.id === "root") { ids.push("economy:root"); } if (plugin.id === "economy-games" && command.id === "mysterybox") { ids.push("economy-games:mystery"); } const label = subcommand ? `${prefix}${trigger} ${subcommand}` : `${prefix}${trigger}`; addEntry(ids, label, commandId); } } return index; } function listCommandPlugins() { try { const enabled = getPlugins() .filter((plugin) => plugin.enabled) .map((plugin) => ({ id: plugin.id, name: plugin.name, path: plugin.path })) .filter((plugin) => plugin.id && plugin.path); if (enabled.length) { return enabled; } } catch { // Fall back to filesystem manifests below. } if (!fs.existsSync(pluginsDir)) { return []; } return fs .readdirSync(pluginsDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => { const dir = path.join(pluginsDir, entry.name); const manifest = readJsonSafe(path.join(dir, "plugin.json")); return manifest?.id ? { id: manifest.id, name: manifest.name, path: dir } : null; }) .filter(Boolean); } function readJsonSafe(filePath) { try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { return null; } } function getPluginSettingsMap(pluginId) { if (!tableExists("plugin_settings")) { return {}; } const rows = db .prepare("SELECT key, value FROM plugin_settings WHERE plugin_id = ?") .all(pluginId); return Object.fromEntries(rows.map((row) => [row.key, row.value])); } function normalizeCommandTrigger(value, fallback = "") { const raw = (value || fallback || "").toString().trim().toLowerCase(); const firstToken = raw.replace(/^!+/, "").split(/\s+/)[0] || ""; return firstToken.replace(/[^a-z0-9_-]/g, ""); } function normalizeSubcommand(value) { const raw = (value || "").toString().trim().toLowerCase(); const firstToken = raw.replace(/^!+/, "").split(/\s+/)[0] || ""; return firstToken.replace(/[^a-z0-9_-]/g, ""); } function normalizeCommandDisplay(value, prefix) { const raw = (value || "").toString().trim(); if (!raw) { return `${prefix}unknown`; } return raw.startsWith(prefix) ? raw : `${prefix}${raw.replace(/^!+/, "")}`; } function formatCommandId(commandId, prefix = "!") { if (!commandId) { return "Unknown"; } if (commandId.startsWith("custom:")) { return `${prefix}${commandId.slice(7)}`; } return commandId; } function getCurrencyLabel() { const plural = getPluginSetting("economy-framework", "currency_name_plural"); const singular = getPluginSetting("economy-framework", "currency_name"); return plural || singular || "Coins"; } function getPluginSetting(pluginId, key) { const row = db .prepare("SELECT value FROM plugin_settings WHERE plugin_id = ? AND key = ?") .get(pluginId, key); return row?.value ? row.value.toString() : null; } function tableExists(name) { const row = db .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") .get(name); return Boolean(row); } function formatNumber(value) { const number = Number(value); if (!Number.isFinite(number)) { return value ?? "0"; } return number.toLocaleString("en-US"); } function formatDuration(totalMs) { const totalSeconds = Math.max(0, Math.floor(totalMs / 1000)); const days = Math.floor(totalSeconds / 86400); const hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); if (days > 0) { return `${days}d ${hours}h`; } if (hours > 0) { return `${hours}h ${minutes}m`; } return `${minutes}m`; } module.exports = { registerTopProvider, getTopProviders, getTopBoards, getLeaderboardSections, getTopCommandOptions, registerTopCommand };