const express = require("express"); const path = require("path"); const crypto = require("crypto"); const fs = require("fs"); const { Permissions } = require("discord.js"); let multer = null; try { multer = require("multer"); } catch { multer = null; } const session = require("express-session"); const BetterSqlite3Store = require("better-sqlite3-session-store")(session); const { db } = require("../services/db"); const { getSetting, setSetting, getAllSettings } = require("../services/settings"); const { getRoleFlags, hasAccess } = require("../services/rbac"); const { buildDiscordAuthUrl, exchangeDiscordCode, fetchDiscordUser, fetchDiscordGuildMember, buildTwitchAuthUrl, exchangeTwitchCode, fetchTwitchUser, buildYouTubeAuthUrl, exchangeYouTubeCode, fetchYouTubeChannel } = require("../services/auth"); const { getPluginProfileStats } = require("../services/plugin-stats"); const { getLeaderboardSections, getTopCommandOptions } = require("../services/top"); const { log, listLogs } = require("../services/logger"); const { createWebhookService } = require("../services/webhooks"); const { getPlatformStatus, getEnabledPlatformIds, getLoginPlatforms, getLinkPlatforms, getPlatformLabel, getPlatformBadge, isPlatformEnabled, isPlatformConfigured, normalizePlatformSelection, serializePlatformSelection } = require("../services/platforms"); const { getClient: getTwitchClient } = require("../services/twitch"); const { getClient: getYouTubeClient } = require("../services/youtube"); const { ensureUserForIdentity, linkIdentityToUser, getUserProfileById, getUserIdentities, updateInternalUsername, listUsersWithIdentities } = require("../services/users"); const { getPlugins, syncPluginRegistry, setPluginEnabled, removePlugin, installFromGit, updatePluginFromGit, createLocalPlugin } = require("../services/plugins"); const { checkForUpdates, pullUpdates, requestRestart } = require("../services/updater"); const { applyBotUpdate, applyPluginUpdate, listSnapshots } = require("../services/update-manager"); function ensureSessionSecret() { let secret = getSetting("session_secret"); if (!secret) { secret = crypto.randomBytes(32).toString("hex"); setSetting("session_secret", secret); } return secret; } function isConfigured() { const platforms = getPlatformStatus().filter( (platform) => platform.supported && platform.enabled ); if (!platforms.length) { return false; } return platforms.some((platform) => platform.configured); } function getPrimaryLoginPlatform() { const platforms = getPlatformStatus().filter( (platform) => platform.supported && platform.enabled && platform.supportsLogin ); if (!platforms.length) { return null; } return platforms.find((platform) => platform.configured) || platforms[0]; } function getLoginRedirectPath() { const platform = getPrimaryLoginPlatform(); return platform?.loginPath || "/setup"; } function requireConfigured(req, res, next) { if (!isConfigured() && !req.path.startsWith("/setup")) { return res.redirect("/setup"); } next(); } function requireAuth(req, res, next) { if (!req.session.user) { return res.redirect(getLoginRedirectPath()); } next(); } function trackModRole(db, user) { if (!user?.id) { return; } const isMod = Boolean(user.isAdmin || user.isMod); const active = db .prepare( "SELECT id FROM mod_role_periods WHERE user_id = ? AND end_at IS NULL" ) .get(user.id); if (isMod && !active) { db.prepare( "INSERT INTO mod_role_periods (user_id, start_at, end_at) VALUES (?, ?, NULL)" ).run(user.id, Date.now()); } else if (!isMod && active) { db.prepare("UPDATE mod_role_periods SET end_at = ? WHERE id = ?").run( Date.now(), active.id ); } } 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`; } function requireRole(role) { return (req, res, next) => { if (!req.session.user) { return res.redirect(getLoginRedirectPath()); } if (!hasAccess(req.session.user, role)) { return res.status(403).render("error", { title: "Access denied", message: "You do not have access to that page." }); } next(); }; } function storeDiscordUser(user) { const avatar = user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=128` : null; const displayName = user.global_name || user.username; return ensureUserForIdentity({ provider: "discord", providerUserId: user.id, displayName, avatar }); } async function fetchDiscordRolesForUser(discordUserId) { const guildId = getSetting("discord_guild_id"); const botToken = getSetting("discord_bot_token"); if (!guildId || !botToken || !discordUserId) { return []; } try { const response = await fetch( `https://discord.com/api/guilds/${guildId}/members/${discordUserId}`, { headers: { Authorization: `Bot ${botToken}` } } ); if (!response.ok) { return []; } const member = await response.json(); return member?.roles || []; } catch { return []; } } function getPreferredAvatar(userId) { if (!userId) { return null; } const identities = db .prepare("SELECT provider, avatar FROM user_identities WHERE user_id = ?") .all(userId); const preferredOrder = ["discord", "twitch", "youtube"]; for (const provider of preferredOrder) { const match = identities.find((identity) => identity.provider === provider); if (match?.avatar) { return match.avatar; } } const fallback = identities.find((identity) => identity.avatar); return fallback?.avatar || null; } function hasExpressionTables() { const row = db .prepare( "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'expression_user_stats'" ) .get(); return Boolean(row); } function getExpressionUserSummary(userId) { if (!hasExpressionTables()) { return null; } const rows = db .prepare( "SELECT action, given_count, received_count FROM expression_user_stats WHERE user_id = ?" ) .all(userId); const totals = rows.reduce( (acc, row) => { acc.given += row.given_count; acc.received += row.received_count; return acc; }, { given: 0, received: 0 } ); return { totals }; } function buildUserStatsPayload(userId) { if (!userId) { return { stats: null, expression: null, pluginStats: [] }; } const stats = db .prepare("SELECT * FROM stats WHERE user_id = ?") .get(userId); return { stats, expression: getExpressionUserSummary(userId), pluginStats: getPluginProfileStats(userId) }; } function buildCompareRows(leftStats, rightStats) { const rows = []; const pushSection = (section, leftList, rightList) => { const leftMap = new Map((leftList || []).map((item) => [item.label, item.value])); const rightMap = new Map((rightList || []).map((item) => [item.label, item.value])); const labels = []; for (const label of leftMap.keys()) { labels.push(label); } for (const label of rightMap.keys()) { if (!leftMap.has(label)) { labels.push(label); } } labels.forEach((label) => { rows.push({ section, label, left: leftMap.has(label) ? leftMap.get(label) : null, right: rightMap.has(label) ? rightMap.get(label) : null }); }); }; const leftCommunity = [ { label: "Messages", value: leftStats.stats?.messages ?? 0 }, { label: "Commands", value: leftStats.stats?.commands ?? 0 } ]; const rightCommunity = [ { label: "Messages", value: rightStats.stats?.messages ?? 0 }, { label: "Commands", value: rightStats.stats?.commands ?? 0 } ]; pushSection("Community Interaction", leftCommunity, rightCommunity); if (leftStats.expression || rightStats.expression) { const leftExpression = leftStats.expression ? [ { label: "Actions given", value: leftStats.expression.totals.given }, { label: "Actions received", value: leftStats.expression.totals.received } ] : []; const rightExpression = rightStats.expression ? [ { label: "Actions given", value: rightStats.expression.totals.given }, { label: "Actions received", value: rightStats.expression.totals.received } ] : []; pushSection("Expression Interaction", leftExpression, rightExpression); } const pluginSections = new Map(); leftStats.pluginStats.forEach((section) => { pluginSections.set(section.title, { left: section.stats || [], right: [] }); }); rightStats.pluginStats.forEach((section) => { const entry = pluginSections.get(section.title) || { left: [], right: [] }; entry.right = section.stats || []; pluginSections.set(section.title, entry); }); for (const [title, lists] of pluginSections.entries()) { pushSection(title, lists.left, lists.right); } return rows; } function getExpressionLeaderboards(limit = 10) { if (!hasExpressionTables()) { return null; } const given = db .prepare( "SELECT user_profiles.internal_username AS username, SUM(expression_user_stats.given_count) AS total " + "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 total DESC LIMIT ?" ) .all(limit); const received = db .prepare( "SELECT user_profiles.internal_username AS username, SUM(expression_user_stats.received_count) AS total " + "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 total DESC LIMIT ?" ) .all(limit); return { given, received }; } const DISCORD_PERMISSION_DETAILS = { CREATE_INSTANT_INVITE: { label: "Create instant invites", description: "Create invitations to the server." }, KICK_MEMBERS: { label: "Kick members", description: "Kick members from the server." }, BAN_MEMBERS: { label: "Ban members", description: "Ban or unban members from the server." }, ADMINISTRATOR: { label: "Administrator", description: "Bypasses all permission checks and grants every permission." }, MANAGE_CHANNELS: { label: "Manage channels", description: "Create, edit, or delete channels." }, MANAGE_GUILD: { label: "Manage server", description: "Edit server settings and features." }, ADD_REACTIONS: { label: "Add reactions", description: "Add new reactions to messages." }, VIEW_AUDIT_LOG: { label: "View audit log", description: "View the server audit log." }, PRIORITY_SPEAKER: { label: "Priority speaker", description: "Use priority speaker in voice channels." }, STREAM: { label: "Stream", description: "Stream video in voice channels." }, VIEW_CHANNEL: { label: "View channels", description: "View channels and read messages." }, SEND_MESSAGES: { label: "Send messages", description: "Send messages in text channels." }, SEND_TTS_MESSAGES: { label: "Send TTS messages", description: "Send text-to-speech messages." }, MANAGE_MESSAGES: { label: "Manage messages", description: "Delete messages and manage pins." }, EMBED_LINKS: { label: "Embed links", description: "Send embedded link previews." }, ATTACH_FILES: { label: "Attach files", description: "Attach files and media." }, READ_MESSAGE_HISTORY: { label: "Read message history", description: "Read messages posted before the bot joined the channel." }, MENTION_EVERYONE: { label: "Mention everyone", description: "Use @everyone, @here, and role mentions." }, USE_EXTERNAL_EMOJIS: { label: "Use external emojis", description: "Use emojis from other servers." }, VIEW_GUILD_INSIGHTS: { label: "View server insights", description: "Access server insights." }, CONNECT: { label: "Connect to voice", description: "Join voice channels." }, SPEAK: { label: "Speak in voice", description: "Speak in voice channels." }, MUTE_MEMBERS: { label: "Mute members", description: "Server mute members in voice." }, DEAFEN_MEMBERS: { label: "Deafen members", description: "Server deafen members in voice." }, MOVE_MEMBERS: { label: "Move members", description: "Move members between voice channels." }, USE_VAD: { label: "Use voice activity detection", description: "Use voice activity detection." }, CHANGE_NICKNAME: { label: "Change nickname", description: "Change the bot's nickname." }, MANAGE_NICKNAMES: { label: "Manage nicknames", description: "Change other members' nicknames." }, MANAGE_ROLES: { label: "Manage roles", description: "Create, edit, and assign roles." }, MANAGE_WEBHOOKS: { label: "Manage webhooks", description: "Create, edit, and delete webhooks." }, MANAGE_EMOJIS_AND_STICKERS: { label: "Manage emojis and stickers", description: "Create, edit, and delete emojis or stickers." }, USE_APPLICATION_COMMANDS: { label: "Use application commands", description: "Use slash commands and context menu commands." }, REQUEST_TO_SPEAK: { label: "Request to speak", description: "Request to speak in stage channels." }, MANAGE_EVENTS: { label: "Manage events", description: "Create and manage scheduled events." }, MANAGE_THREADS: { label: "Manage threads", description: "Manage threads and thread settings." }, USE_PUBLIC_THREADS: { label: "Use public threads", description: "Use public threads (deprecated flag)." }, CREATE_PUBLIC_THREADS: { label: "Create public threads", description: "Create public threads." }, USE_PRIVATE_THREADS: { label: "Use private threads", description: "Use private threads (deprecated flag)." }, CREATE_PRIVATE_THREADS: { label: "Create private threads", description: "Create private threads." }, USE_EXTERNAL_STICKERS: { label: "Use external stickers", description: "Use stickers from other servers." }, SEND_MESSAGES_IN_THREADS: { label: "Send messages in threads", description: "Send messages in threads." }, START_EMBEDDED_ACTIVITIES: { label: "Start embedded activities", description: "Start embedded activities in voice channels." }, MODERATE_MEMBERS: { label: "Moderate members", description: "Timeout members." }, VIEW_CREATOR_MONETIZATION_ANALYTICS: { label: "View monetization analytics", description: "View creator monetization analytics." }, USE_SOUNDBOARD: { label: "Use soundboard", description: "Use the soundboard in voice." }, SEND_VOICE_MESSAGES: { label: "Send voice messages", description: "Send voice messages." } }; const TWITCH_SCOPE_DEFS = [ { scope: "analytics:read:extensions", label: "Extensions analytics", description: "Read analytics data for extensions." }, { scope: "analytics:read:games", label: "Games analytics", description: "Read analytics data for games." }, { scope: "bits:read", label: "Bits", description: "Read Bits leaderboard data." }, { scope: "channel:bot", label: "Channel bot", description: "Act as a bot within a channel." }, { scope: "channel:edit:commercial", label: "Run commercials", description: "Run commercials on a channel." }, { scope: "channel:manage:ads", label: "Manage ads", description: "Manage ad settings for a channel." }, { scope: "channel:manage:broadcast", label: "Manage broadcast", description: "Manage stream metadata and settings." }, { scope: "channel:manage:extensions", label: "Manage extensions", description: "Manage channel extensions." }, { scope: "channel:manage:moderators", label: "Manage moderators", description: "Add or remove moderators." }, { scope: "channel:manage:polls", label: "Manage polls", description: "Create and manage polls." }, { scope: "channel:manage:predictions", label: "Manage predictions", description: "Create and manage predictions." }, { scope: "channel:manage:redemptions", label: "Manage redemptions", description: "Manage channel points rewards." }, { scope: "channel:manage:schedule", label: "Manage schedule", description: "Manage channel schedule." }, { scope: "channel:manage:videos", label: "Manage videos", description: "Manage videos for a channel." }, { scope: "channel:manage:vips", label: "Manage VIPs", description: "Add or remove VIPs." }, { scope: "channel:read:ads", label: "Read ads", description: "Read ad schedule data." }, { scope: "channel:read:charity", label: "Read charity", description: "Read charity campaign data." }, { scope: "channel:manage:charity", label: "Manage charity", description: "Manage charity campaign settings." }, { scope: "channel:read:editors", label: "Read editors", description: "Read channel editor list." }, { scope: "channel:read:goals", label: "Read goals", description: "Read creator goal data." }, { scope: "channel:read:hype_train", label: "Read hype train", description: "Read hype train data." }, { scope: "channel:read:polls", label: "Read polls", description: "Read poll data." }, { scope: "channel:read:predictions", label: "Read predictions", description: "Read prediction data." }, { scope: "channel:read:redemptions", label: "Read redemptions", description: "Read channel points rewards." }, { scope: "channel:read:stream_key", label: "Read stream key", description: "Read the channel stream key." }, { scope: "channel:read:subscriptions", label: "Read subscriptions", description: "Read subscriber data." }, { scope: "channel:read:vips", label: "Read VIPs", description: "Read the VIP list." }, { scope: "channel:moderate", label: "Channel moderation", description: "Perform channel moderation actions." }, { scope: "chat:edit", label: "Chat edit", description: "Send chat messages." }, { scope: "chat:read", label: "Chat read", description: "Read chat messages." }, { scope: "clips:edit", label: "Edit clips", description: "Create or edit clips." }, { scope: "moderation:read", label: "Read moderation", description: "Read moderation data." }, { scope: "moderator:manage:announcements", label: "Manage announcements", description: "Send announcements in chat." }, { scope: "moderator:manage:automod", label: "Manage AutoMod", description: "Manage AutoMod actions." }, { scope: "moderator:manage:automod_settings", label: "Manage AutoMod settings", description: "Update AutoMod settings." }, { scope: "moderator:manage:banned_users", label: "Manage banned users", description: "Ban or unban users." }, { scope: "moderator:manage:blocked_terms", label: "Manage blocked terms", description: "Manage blocked terms." }, { scope: "moderator:manage:chat_messages", label: "Manage chat messages", description: "Delete or manage chat messages." }, { scope: "moderator:manage:chat_settings", label: "Manage chat settings", description: "Update chat settings." }, { scope: "moderator:manage:shield_mode", label: "Manage shield mode", description: "Enable or disable shield mode." }, { scope: "moderator:manage:shoutouts", label: "Manage shoutouts", description: "Send or manage shoutouts." }, { scope: "moderator:read:automod_settings", label: "Read AutoMod settings", description: "Read AutoMod settings." }, { scope: "moderator:read:blocked_terms", label: "Read blocked terms", description: "Read blocked terms." }, { scope: "moderator:read:chat_settings", label: "Read chat settings", description: "Read chat settings." }, { scope: "moderator:read:followers", label: "Read followers", description: "Read follower list." }, { scope: "moderator:read:shield_mode", label: "Read shield mode", description: "Read shield mode status." }, { scope: "moderator:read:shoutouts", label: "Read shoutouts", description: "Read shoutout settings." }, { scope: "moderator:read:vips", label: "Read VIPs", description: "Read VIP list." }, { scope: "user:bot", label: "User bot", description: "Act as a bot on behalf of a user." }, { scope: "user:edit", label: "Edit user", description: "Edit a user's profile." }, { scope: "user:edit:follows", label: "Edit follows", description: "Manage follows for a user." }, { scope: "user:manage:blocked_users", label: "Manage blocked users", description: "Block or unblock users." }, { scope: "user:manage:chat_color", label: "Manage chat color", description: "Change chat color." }, { scope: "user:manage:whispers", label: "Manage whispers", description: "Send and manage whispers." }, { scope: "user:read:blocked_users", label: "Read blocked users", description: "Read blocked users list." }, { scope: "user:read:broadcast", label: "Read broadcast", description: "Read broadcast settings." }, { scope: "user:read:email", label: "Read email", description: "Read the user's email address." }, { scope: "user:read:follows", label: "Read follows", description: "Read the user's follows." }, { scope: "user:read:subscriptions", label: "Read subscriptions", description: "Read user subscription data." }, { scope: "user:read:chat", label: "Read chat", description: "Read chat messages via API." }, { scope: "user:write:chat", label: "Write chat", description: "Send chat messages via API." }, { scope: "whispers:read", label: "Read whispers", description: "Read whispers." }, { scope: "whispers:edit", label: "Send whispers", description: "Send whispers." } ]; function buildPrivilegeRow(id, label, description, granted) { const safeLabel = (label || "").toString(); const safeDescription = (description || "").toString(); const grantedFlag = Boolean(granted); return { id, label: safeLabel, description: safeDescription, granted: grantedFlag, search: `${safeLabel} ${safeDescription} ${grantedFlag ? "granted" : "missing"}`.trim(), sort: { label: safeLabel.toLowerCase(), description: safeDescription.toLowerCase(), status: grantedFlag ? 1 : 0 } }; } function parseChannelList(value) { return (value || "") .split(/[,\s]+/g) .map((entry) => entry.trim()) .filter(Boolean); } async function buildDiscordPrivileges(discordClient) { const rows = []; const guildId = getSetting("discord_guild_id"); const hasClient = Boolean(discordClient && discordClient.user); rows.push( buildPrivilegeRow( "discord-client-ready", "Client connected", "The Discord client is logged in and ready.", hasClient ) ); rows.push( buildPrivilegeRow( "discord-guild-configured", "Guild configured", "A server ID is configured in settings.", Boolean(guildId) ) ); let guild = null; if (hasClient && guildId) { guild = discordClient.guilds?.cache?.get(guildId) || null; if (!guild && typeof discordClient.guilds?.fetch === "function") { try { guild = await discordClient.guilds.fetch(guildId); } catch { guild = null; } } } rows.push( buildPrivilegeRow( "discord-guild-access", "Bot in guild", "The bot can access the configured server.", Boolean(guild) ) ); let permissions = null; if (guild && discordClient?.user?.id) { try { const cachedMember = guild.members?.cache?.get(discordClient.user.id) || null; const member = cachedMember || (await guild.members.fetch(discordClient.user.id)); permissions = member?.permissions || null; } catch { permissions = null; } } const permissionFlags = Permissions?.FLAGS || {}; const canCheck = Boolean(permissions && typeof permissions.has === "function"); for (const key of Object.keys(permissionFlags)) { const details = DISCORD_PERMISSION_DETAILS[key] || { label: toTitleCase(key), description: "Discord permission flag." }; const flag = permissionFlags[key]; const granted = canCheck && flag ? permissions.has(flag) : false; rows.push( buildPrivilegeRow( `discord-perm-${key.toLowerCase()}`, details.label, details.description, granted ) ); } return { rows, guildName: guild?.name || null }; } async function buildTwitchPrivileges() { const clientId = getSetting("twitch_client_id"); const clientSecret = getSetting("twitch_client_secret"); const redirectUri = getSetting("twitch_redirect_uri"); const botUsername = getSetting("twitch_bot_username"); const botOauth = getSetting("twitch_bot_oauth"); const channels = parseChannelList(getSetting("twitch_channels")); const twitchClient = getTwitchClient(); const connected = Boolean(twitchClient); const joinedChannels = twitchClient && typeof twitchClient.getChannels === "function" ? twitchClient.getChannels() : []; const rows = [ buildPrivilegeRow( "twitch-client-id", "Client ID", "A Twitch app client ID is configured.", Boolean(clientId) ), buildPrivilegeRow( "twitch-client-secret", "Client secret", "A Twitch app client secret is configured.", Boolean(clientSecret) ), buildPrivilegeRow( "twitch-redirect", "OAuth redirect URL", "The OAuth redirect URL is configured.", Boolean(redirectUri) ), buildPrivilegeRow( "twitch-bot-username", "Bot username", "The chat bot username is configured.", Boolean(botUsername) ), buildPrivilegeRow( "twitch-bot-oauth", "Bot OAuth token", "The chat bot OAuth token is configured.", Boolean(botOauth) ), buildPrivilegeRow( "twitch-channels-configured", "Channels configured", "At least one channel is configured for chat.", channels.length > 0 ), buildPrivilegeRow( "twitch-chat-connected", "Chat connected", "The Twitch chat client is connected.", connected ), buildPrivilegeRow( "twitch-channels-joined", "Channels joined", "The chat client has joined its configured channels.", joinedChannels.length > 0 ) ]; let grantedScopes = []; if (botOauth) { const rawToken = botOauth.startsWith("oauth:") ? botOauth.slice(6) : botOauth; try { const response = await fetch("https://id.twitch.tv/oauth2/validate", { headers: { Authorization: `OAuth ${rawToken}` } }); if (response.ok) { const data = await response.json(); if (Array.isArray(data.scopes)) { grantedScopes = data.scopes; } } } catch { grantedScopes = []; } } const grantedSet = new Set(grantedScopes.map((scope) => scope.toLowerCase())); const knownScopeSet = new Set(); const scopeRows = TWITCH_SCOPE_DEFS.map((def) => { const scopeKey = def.scope.toLowerCase(); knownScopeSet.add(scopeKey); return buildPrivilegeRow( `twitch-scope-${slugify(def.scope)}`, def.label, def.description, grantedSet.has(scopeKey) ); }); const unknownScopes = grantedScopes.filter( (scope) => !knownScopeSet.has(scope.toLowerCase()) ); for (const scope of unknownScopes) { scopeRows.push( buildPrivilegeRow( `twitch-scope-${slugify(scope)}`, scope, "Granted by the token but not recognized by this version of the bot.", true ) ); } return { rows: [...rows, ...scopeRows], channelCount: channels.length }; } function readJsonSafe(filePath) { try { const raw = fs.readFileSync(filePath, "utf8"); return JSON.parse(raw); } catch { return null; } } function parseBooleanSetting(value, fallback) { if (value === undefined || value === null || value === "") { return fallback; } if (typeof value === "boolean") { return value; } const normalized = value.toString().toLowerCase(); return ["1", "true", "yes", "on"].includes(normalized); } function getPluginSettingsMap(pluginId) { const rows = db .prepare("SELECT key, value FROM plugin_settings WHERE plugin_id = ?") .all(pluginId); return rows.reduce((acc, row) => { acc[row.key] = row.value; return acc; }, {}); } function normalizeCommandTrigger(value, fallback = "") { const raw = (value || fallback || "").toString().trim().replace(/^!+/, ""); if (!raw) { return ""; } return raw.split(/\s+/)[0].toLowerCase(); } function normalizeSubcommand(value) { const raw = (value || "").toString().trim().replace(/^!+/, ""); if (!raw) { return ""; } return raw.split(/\s+/)[0].toLowerCase(); } function buildUsage(baseTrigger, usage) { const raw = (usage || baseTrigger || "").toString().trim().replace(/^!+/, ""); return raw || baseTrigger; } function normalizeCustomPlatforms(value, availablePlatforms) { return normalizePlatformSelection(value, availablePlatforms); } function parsePlatformSelectionFromBody(body, availablePlatforms) { const raw = Array.isArray(body.platforms) ? body.platforms.join(",") : body.platforms || body.platform; return normalizePlatformSelection(raw, availablePlatforms); } function buildPlatformLabels(platforms) { return platforms.map((platform) => ({ key: getPlatformBadge(platform), label: getPlatformLabel(platform) })); } const LOG_LEVELS = new Set(["debug", "info", "warn", "error"]); const LOG_LIMITS = new Set([50, 100, 250, 500]); const DEFAULT_LOG_RANGE_MS = 24 * 60 * 60 * 1000; function normalizeLogLevel(value) { const normalized = (value || "").toString().trim().toLowerCase(); return LOG_LEVELS.has(normalized) ? normalized : ""; } function parseLogLevels(value) { if (!value || value === "all") { return []; } const raw = Array.isArray(value) ? value : value.toString().split(","); return raw.map(normalizeLogLevel).filter(Boolean); } function parseLogRange(value) { if (value === undefined || value === null || value === "") { return { rangeMs: DEFAULT_LOG_RANGE_MS, rangeValue: `${DEFAULT_LOG_RANGE_MS}` }; } const normalized = value.toString().trim().toLowerCase(); if (normalized === "all") { return { rangeMs: null, rangeValue: "all" }; } const parsed = Number(normalized); if (!Number.isNaN(parsed) && parsed > 0) { return { rangeMs: parsed, rangeValue: `${parsed}` }; } return { rangeMs: DEFAULT_LOG_RANGE_MS, rangeValue: `${DEFAULT_LOG_RANGE_MS}` }; } function parseLogLimit(value, { allowAll = false } = {}) { if (value === undefined || value === null || value === "") { return { limit: 50, limitValue: "50" }; } const normalized = value.toString().trim().toLowerCase(); if (allowAll && normalized === "all") { return { limit: null, limitValue: "all" }; } const parsed = Number(normalized); if (!Number.isNaN(parsed) && LOG_LIMITS.has(parsed)) { return { limit: parsed, limitValue: `${parsed}` }; } return { limit: 50, limitValue: "50" }; } function normalizePageFormat(value) { const normalized = (value || "").toString().trim().toLowerCase(); return normalized === "markdown" ? "markdown" : "html"; } function escapeHtml(value) { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; return (value || "").toString().replace(/[&<>"']/g, (char) => map[char]); } function renderMarkdownInline(value) { let output = escapeHtml(value); output = output.replace(/`([^`]+)`/g, "$1"); output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { return `${text}`; }); output = output.replace(/\*\*([^*]+)\*\*/g, "$1"); output = output.replace(/\*([^*]+)\*/g, "$1"); return output; } function renderMarkdown(value) { const lines = (value || "").toString().replace(/\r\n?/g, "\n").split("\n"); let html = ""; let paragraph = []; let listType = null; let inCode = false; let codeLang = ""; let codeLines = []; const flushParagraph = () => { if (!paragraph.length) { return; } html += `

${renderMarkdownInline(paragraph.join(" "))}

`; paragraph = []; }; const closeList = () => { if (!listType) { return; } html += ``; listType = null; }; lines.forEach((line) => { const trimmed = line.trim(); if (inCode) { if (trimmed.startsWith("```")) { const codeBlock = escapeHtml(codeLines.join("\n")); const langClass = codeLang ? ` class="language-${codeLang}"` : ""; html += `
${codeBlock}
`; inCode = false; codeLang = ""; codeLines = []; return; } codeLines.push(line); return; } if (trimmed.startsWith("```")) { flushParagraph(); closeList(); inCode = true; codeLang = trimmed.slice(3).trim(); return; } if (!trimmed) { flushParagraph(); closeList(); return; } const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/); if (headingMatch) { flushParagraph(); closeList(); const level = headingMatch[1].length; html += `${renderMarkdownInline(headingMatch[2])}`; return; } const listMatch = trimmed.match(/^([*-]|\d+\.)\s+(.*)$/); if (listMatch) { flushParagraph(); const isOrdered = listMatch[1].endsWith("."); const nextListType = isOrdered ? "ol" : "ul"; if (listType && listType !== nextListType) { closeList(); } if (!listType) { listType = nextListType; html += `<${listType}>`; } html += `
  • ${renderMarkdownInline(listMatch[2])}
  • `; return; } paragraph.push(trimmed); }); if (inCode) { const codeBlock = escapeHtml(codeLines.join("\n")); const langClass = codeLang ? ` class="language-${codeLang}"` : ""; html += `
    ${codeBlock}
    `; } flushParagraph(); closeList(); return html || "

    "; } function buildCustomPageSrcdoc(page, theme) { const content = (page?.content || "").toString(); const css = (page?.content_css || "").toString(); const themeCss = theme ? [ ":root {", ` --ink: ${theme.light.text};`, ` --ink-soft: ${theme.light.muted};`, ` --sea: ${theme.light.accent};`, ` --sun: ${theme.light.accentAlt};`, ` --rose: ${theme.light.danger};`, ` --card: ${theme.light.surface};`, ` --surface-2: ${theme.light.surface2};`, ` --surface-3: ${theme.light.surface3};`, ` --border: ${theme.light.border};`, ` --bg-1: ${theme.light.bg1};`, ` --bg-2: ${theme.light.bg2};`, ` --bg-3: ${theme.light.bg3};`, "}", "@media (prefers-color-scheme: dark) {", " :root {", ` --ink: ${theme.dark.text};`, ` --ink-soft: ${theme.dark.muted};`, ` --sea: ${theme.dark.accent};`, ` --sun: ${theme.dark.accentAlt};`, ` --rose: ${theme.dark.danger};`, ` --card: ${theme.dark.surface};`, ` --surface-2: ${theme.dark.surface2};`, ` --surface-3: ${theme.dark.surface3};`, ` --border: ${theme.dark.border};`, ` --bg-1: ${theme.dark.bg1};`, ` --bg-2: ${theme.dark.bg2};`, ` --bg-3: ${theme.dark.bg3};`, " }", "}" ].join("\n") : ""; const baseCss = [ "* { box-sizing: border-box; }", "html, body { margin: 0; padding: 0; }", "body {", " font-family: \"Source Sans 3\", sans-serif;", " color: var(--ink, #121518);", " background: transparent;", "}" ].join("\n"); const fullCss = [themeCss, baseCss, css].filter(Boolean).join("\n\n"); return [ "", "", "", " ", " ", " ", " ", "", "", content, "", "" ].join("\n"); } function buildCommandUsageMap() { const rows = db.prepare("SELECT command_id, count FROM command_usage").all(); const map = new Map(); for (const row of rows) { map.set(row.command_id, row.count); } return map; } function slugify(value) { const raw = (value || "").toString().toLowerCase(); const slug = raw.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); return slug || "command"; } function toTitleCase(value) { const raw = (value || "").toString().replace(/[-_]+/g, " ").trim(); if (!raw) { return ""; } return raw.replace(/\b\w/g, (match) => match.toUpperCase()); } function truncateText(value, maxLength) { const raw = (value || "").toString(); if (raw.length <= maxLength) { return raw; } return `${raw.slice(0, Math.max(0, maxLength - 3))}...`; } function setFlash(req, type, message) { req.session.flash = { type, message }; } function getThemeSettings() { return { light: { bg1: getSetting("theme_light_bg_1", "#ffe5c4"), bg2: getSetting("theme_light_bg_2", "#f4efe8"), bg3: getSetting("theme_light_bg_3", "#e9f3f1"), text: getSetting("theme_light_text", "#121518"), muted: getSetting("theme_light_text_muted", "#2c3137"), accent: getSetting("theme_light_accent", "#0f6a78"), accentAlt: getSetting("theme_light_accent_alt", "#f4a340"), danger: getSetting("theme_light_danger", "#d66d5c"), surface: getSetting("theme_light_surface", "#ffffff"), surface2: getSetting("theme_light_surface_2", "#fbf9f6"), surface3: getSetting("theme_light_surface_3", "#f9f5ef"), border: getSetting("theme_light_border", "#e3ddd6") }, dark: { bg1: getSetting("theme_dark_bg_1", "#1b1d1f"), bg2: getSetting("theme_dark_bg_2", "#16181b"), bg3: getSetting("theme_dark_bg_3", "#0f1113"), text: getSetting("theme_dark_text", "#f2f0ec"), muted: getSetting("theme_dark_text_muted", "#c5bfb7"), accent: getSetting("theme_dark_accent", "#4fb6c2"), accentAlt: getSetting("theme_dark_accent_alt", "#f1b765"), danger: getSetting("theme_dark_danger", "#e08173"), surface: getSetting("theme_dark_surface", "#232629"), surface2: getSetting("theme_dark_surface_2", "#2b2f33"), surface3: getSetting("theme_dark_surface_3", "#30353a"), border: getSetting("theme_dark_border", "#34393d") }, role: { public: getSetting("theme_role_public", "#ffffff"), mod: getSetting("theme_role_mod", "#2cb678"), admin: getSetting("theme_role_admin", "#e35678") } }; } function getDiscordSettings() { return { discord_client_id: getSetting("discord_client_id", ""), discord_client_secret: getSetting("discord_client_secret", ""), discord_redirect_uri: getSetting("discord_redirect_uri", ""), discord_bot_token: getSetting("discord_bot_token", ""), discord_guild_id: getSetting("discord_guild_id", ""), discord_admin_role_id: getSetting("discord_admin_role_id", ""), discord_mod_role_id: getSetting("discord_mod_role_id", "") }; } function getTwitchSettings() { return { twitch_client_id: getSetting("twitch_client_id", ""), twitch_client_secret: getSetting("twitch_client_secret", ""), twitch_redirect_uri: getSetting("twitch_redirect_uri", ""), twitch_bot_username: getSetting("twitch_bot_username", ""), twitch_bot_oauth: getSetting("twitch_bot_oauth", ""), twitch_channels: getSetting("twitch_channels", "") }; } function getYouTubeSettings() { return { youtube_client_id: getSetting("youtube_client_id", ""), youtube_client_secret: getSetting("youtube_client_secret", ""), youtube_redirect_uri: getSetting("youtube_redirect_uri", ""), youtube_bot_channel_id: getSetting("youtube_bot_channel_id", "") }; } function saveSettingsMap(settings) { for (const [key, value] of Object.entries(settings)) { setSetting(key, value ?? ""); } } function storeSnapshot(req, key, settings) { req.session[key] = settings; } function restoreSnapshot(req, key) { const snapshot = req.session[key]; if (!snapshot) { return; } saveSettingsMap(snapshot); delete req.session[key]; } function mergeSecrets(existing, incoming, secretKeys) { const merged = { ...existing }; for (const [key, value] of Object.entries(incoming)) { const trimmed = typeof value === "string" ? value.trim() : value; if (secretKeys.has(key) && !trimmed) { continue; } merged[key] = trimmed; } return merged; } async function verifyDiscordSettings(settings) { const errors = []; const checks = []; const required = [ "discord_client_id", "discord_client_secret", "discord_bot_token", "discord_guild_id" ]; const missing = required.filter((field) => !settings[field]); if (missing.length) { errors.push("Client ID, Client Secret, Bot Token, and Guild ID are required."); return { ok: false, errors, checks }; } try { const botResponse = await fetch("https://discord.com/api/users/@me", { headers: { Authorization: `Bot ${settings.discord_bot_token}` } }); if (!botResponse.ok) { errors.push("Bot token is invalid or missing permissions."); } else { const botUser = await botResponse.json(); checks.push(`Bot token verified (${botUser.username}).`); } } catch { errors.push("Unable to reach Discord to verify bot token."); } try { const guildResponse = await fetch( `https://discord.com/api/guilds/${settings.discord_guild_id}`, { headers: { Authorization: `Bot ${settings.discord_bot_token}` } } ); if (!guildResponse.ok) { errors.push("Guild ID is invalid or the bot is not in the guild."); } else { const guild = await guildResponse.json(); checks.push(`Guild verified (${guild.name}).`); } } catch { errors.push("Unable to verify the Discord guild."); } try { const body = new URLSearchParams({ client_id: settings.discord_client_id, client_secret: settings.discord_client_secret, grant_type: "client_credentials", scope: "identify" }); const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body }); if (!tokenResponse.ok) { errors.push("Client ID/Secret failed verification."); } else { checks.push("Client ID and secret verified."); } } catch { errors.push("Unable to verify Discord client credentials."); } return { ok: errors.length === 0, errors, checks }; } async function verifyTwitchSettings(settings) { const errors = []; const checks = []; if (!settings.twitch_client_id || !settings.twitch_client_secret) { errors.push("Twitch Client ID and Client Secret are required."); return { ok: false, errors, checks }; } let appToken = null; try { const tokenUrl = "https://id.twitch.tv/oauth2/token" + `?client_id=${encodeURIComponent(settings.twitch_client_id)}` + `&client_secret=${encodeURIComponent(settings.twitch_client_secret)}` + "&grant_type=client_credentials"; const tokenResponse = await fetch(tokenUrl, { method: "POST" }); if (!tokenResponse.ok) { errors.push("Client ID/Secret failed verification."); } else { const data = await tokenResponse.json(); appToken = data.access_token; checks.push("Client ID and secret verified."); } } catch { errors.push("Unable to verify Twitch client credentials."); } if (settings.twitch_bot_username || settings.twitch_bot_oauth) { if (!settings.twitch_bot_username || !settings.twitch_bot_oauth) { errors.push("Bot username and OAuth token must both be filled in."); } else { const rawToken = settings.twitch_bot_oauth.startsWith("oauth:") ? settings.twitch_bot_oauth.slice(6) : settings.twitch_bot_oauth; try { const validateResponse = await fetch( "https://id.twitch.tv/oauth2/validate", { headers: { Authorization: `OAuth ${rawToken}` } } ); if (!validateResponse.ok) { errors.push("Bot OAuth token failed validation."); } else { const data = await validateResponse.json(); checks.push(`Bot token verified (${data.login}).`); if ( settings.twitch_bot_username && data.login && data.login.toLowerCase() !== settings.twitch_bot_username.toLowerCase() ) { errors.push("Bot username does not match the OAuth token."); } } } catch { errors.push("Unable to verify Twitch bot OAuth token."); } } } if (appToken && settings.twitch_bot_username) { try { const userResponse = await fetch( `https://api.twitch.tv/helix/users?login=${encodeURIComponent( settings.twitch_bot_username )}`, { headers: { "Client-Id": settings.twitch_client_id, Authorization: `Bearer ${appToken}` } } ); if (userResponse.ok) { const data = await userResponse.json(); if (data.data && data.data.length) { checks.push(`Bot account found (${data.data[0].display_name}).`); } else { errors.push("Bot username was not found on Twitch."); } } } catch { errors.push("Unable to verify bot username with Twitch."); } } return { ok: errors.length === 0, errors, checks }; } async function verifyYouTubeSettings(settings) { const errors = []; const checks = []; if (!settings.youtube_client_id || !settings.youtube_client_secret) { errors.push("YouTube Client ID and Client Secret are required."); return { ok: false, errors, checks }; } const refreshToken = settings.youtube_bot_refresh_token || getSetting("youtube_bot_refresh_token", ""); if (!refreshToken) { checks.push("Bot account not connected yet."); return { ok: errors.length === 0, errors, checks }; } try { const body = new URLSearchParams({ client_id: settings.youtube_client_id, client_secret: settings.youtube_client_secret, refresh_token: refreshToken, grant_type: "refresh_token" }); const response = await fetch("https://oauth2.googleapis.com/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body }); if (!response.ok) { errors.push("Bot refresh token failed verification."); return { ok: false, errors, checks }; } const token = await response.json(); checks.push("Bot refresh token verified."); const channel = await fetchYouTubeChannel(token.access_token); if (channel?.snippet?.title) { checks.push(`Bot channel verified (${channel.snippet.title}).`); } else { checks.push("Bot channel verified."); } return { ok: errors.length === 0, errors, checks, channel }; } catch { errors.push("Unable to verify YouTube bot credentials."); return { ok: false, errors, checks }; } } function createWebServer({ loadPlugins, discordClient }) { const app = express(); const webhooks = createWebhookService(); if (!global.lumiFrameworks) { global.lumiFrameworks = {}; } global.lumiFrameworks.webhooks = webhooks; const assetVersion = Date.now().toString(); const sessionStore = new BetterSqlite3Store({ client: db }); app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); app.use("/webhooks", webhooks.router); const originalAppRender = app.render.bind(app); app.render = (view, options, callback) => { if (typeof options === "function") { callback = options; options = {}; } const safeOptions = options || {}; const respond = (err, html) => { if (!err) { if (typeof callback === "function") { return callback(null, html); } return html; } const message = err?.message || ""; const detail = { view, message, stack: err?.stack || "" }; const isMissing = message.includes("Failed to lookup view"); if (!isMissing) { log("error", "View render failed", detail); if (typeof callback === "function") { return callback(err); } throw err; } log("warn", "Missing view fallback", detail); if (view === "missing-view") { const fallback = "Content missing

    Content unavailable

    Some content could not be loaded.

    "; if (typeof callback === "function") { return callback(null, fallback); } return fallback; } return originalAppRender( "missing-view", { ...safeOptions, title: "Content missing", resource: "A page component failed to load. Please try again.", softError: "Some content could not be loaded." }, callback ); }; return originalAppRender(view, safeOptions, respond); }; app.use( session({ secret: ensureSessionSecret(), resave: false, saveUninitialized: false, store: sessionStore }) ); app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, "public"))); const uploadDir = path.join(__dirname, "..", "..", "data", "uploads"); fs.mkdirSync(uploadDir, { recursive: true }); const navIconDir = path.join(__dirname, "..", "..", "data", "nav-icons"); fs.mkdirSync(navIconDir, { recursive: true }); app.use("/assets/nav-icons", express.static(navIconDir)); const upload = multer ? multer({ dest: uploadDir }) : null; const uploadSingle = (fieldName) => { if (!upload) { return (req, _res, next) => { req.uploadError = "File uploads require npm install."; next(); }; } return upload.single(fieldName); }; const navIconUpload = multer && multer({ storage: multer.diskStorage({ destination: (_req, _file, cb) => cb(null, navIconDir), filename: (_req, file, cb) => { const ext = path.extname(file.originalname || ".svg").slice(0, 8); cb(null, `${crypto.randomUUID()}${ext}`); } }), fileFilter: (_req, file, cb) => { if (file.mimetype === "image/png" || file.mimetype === "image/svg+xml") { return cb(null, true); } cb(new Error("Only SVG or PNG files are allowed.")); } }); const navIconSingle = (req, _res, next) => { if (!navIconUpload) { req.uploadError = "File uploads require npm install."; return next(); } return navIconUpload.single("icon_file")(req, _res, next); }; const navItems = []; const profileSections = []; const web = { createRouter: () => express.Router(), mount: (mountPath, router, navItem) => { app.use(mountPath, router); if (navItem) { navItems.push({ ...navItem, path: mountPath }); } }, addNavItem: (navItem) => { navItems.push(navItem); }, addProfileSection: (section) => { if (!section || (!section.view && !section.content)) { return; } profileSections.push(section); } }; app.use(requireConfigured); app.use((req, res, next) => { res.locals.siteTitle = getSetting("site_title", "Lumi Bot"); res.locals.assetVersion = assetVersion; res.locals.user = req.session.user || null; res.locals.flash = req.session.flash || null; res.locals.softError = null; res.locals.theme = getThemeSettings(); res.locals.botAvatar = getSetting("bot_avatar_url", null); const platformStatus = getPlatformStatus(); res.locals.platforms = platformStatus; res.locals.platformLogins = platformStatus.filter( (platform) => platform.supported && platform.enabled && platform.supportsLogin ); res.locals.platformLinks = platformStatus.filter( (platform) => platform.supported && platform.enabled && platform.supportsLink ); const twitchPlatform = platformStatus.find((platform) => platform.id === "twitch"); res.locals.twitchConfigured = Boolean(twitchPlatform?.configured); res.locals.currentPath = req.path; res.locals.userAvatar = req.session.user ? getPreferredAvatar(req.session.user.id) : null; res.locals.userInitial = req.session.user?.username ? req.session.user.username.charAt(0).toUpperCase() : ""; req.session.flash = null; trackModRole(db, req.session.user); res.locals.navSections = buildNavSections( req.session.user, navItems, req.path ); next(); }); app.use((req, res, next) => { const originalRender = res.render.bind(res); res.render = (view, options, callback) => { if (typeof options === "function") { callback = options; options = {}; } const handleError = (err) => { if (!err) { return; } const message = err?.message || ""; const isMissing = message.includes("Failed to lookup view"); const context = { view, method: req.method, path: req.path, userId: req.session.user?.id || null, message, stack: err?.stack || "" }; if (!isMissing) { log("error", "Render failed", context); return originalRender( "error", { title: "Something went wrong", message: "An unexpected error occurred. Please try again." }, callback ); } log("warn", "Missing view fallback", context); res.locals.softError = "Some content could not be loaded."; if (view === "missing-view") { return res .status(200) .send( "Content missing

    Content unavailable

    Some content could not be loaded.

    " ); } return originalRender( "missing-view", { title: "Content missing", resource: "A page component failed to load. Please try again." }, callback ); }; return originalRender(view, options, (err, html) => { if (err) { return handleError(err); } if (typeof callback === "function") { return callback(null, html); } res.send(html); }); }; next(); }); app.get("/", (req, res) => { res.render("home", { title: "Home" }); }); app.get("/setup", (req, res) => { if (isConfigured()) { return res.redirect("/"); } const platforms = getPlatformStatus(); res.render("setup", { title: "Initial setup", platforms }); }); app.get("/setup/discord", (req, res) => { if (!isPlatformEnabled("discord")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getDiscordSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, discord_client_secret: "", discord_bot_token: "", discord_redirect_uri: current.discord_redirect_uri || `${baseUrl}/auth/discord/callback` }; storeSnapshot(req, "discordWizardSnapshot", current); res.render("wizard-discord", { title: "Discord setup", form, checks: [], errors: [], actionBase: "/setup/discord", cancelPath: "/setup" }); }); app.post("/setup/discord/verify", async (req, res) => { if (!isPlatformEnabled("discord")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getDiscordSettings(); const incoming = { discord_client_id: req.body.discord_client_id || "", discord_client_secret: req.body.discord_client_secret || "", discord_redirect_uri: req.body.discord_redirect_uri || "", discord_bot_token: req.body.discord_bot_token || "", discord_guild_id: req.body.discord_guild_id || "", discord_admin_role_id: req.body.discord_admin_role_id || "", discord_mod_role_id: req.body.discord_mod_role_id || "" }; const merged = mergeSecrets( current, incoming, new Set(["discord_client_secret", "discord_bot_token"]) ); if (!merged.discord_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.discord_redirect_uri = `${baseUrl}/auth/discord/callback`; } const result = await verifyDiscordSettings(merged); if (!result.ok) { res.render("wizard-discord", { title: "Discord setup", form: { ...incoming, discord_client_secret: "", discord_bot_token: "", discord_redirect_uri: merged.discord_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/setup/discord", cancelPath: "/setup" }); return; } saveSettingsMap(merged); delete req.session.discordWizardSnapshot; setFlash(req, "success", "Discord setup saved. Please log in."); res.redirect("/auth/discord"); }); app.post("/setup/discord/cancel", (req, res) => { if (!isPlatformEnabled("discord")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } restoreSnapshot(req, "discordWizardSnapshot"); setFlash(req, "info", "Discord setup canceled."); res.redirect("/setup"); }); app.get("/setup/twitch", (req, res) => { if (!isPlatformEnabled("twitch")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getTwitchSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, twitch_client_secret: "", twitch_bot_oauth: "", twitch_redirect_uri: current.twitch_redirect_uri || `${baseUrl}/auth/twitch/callback` }; storeSnapshot(req, "twitchWizardSnapshot", current); res.render("wizard-twitch", { title: "Twitch setup", form, checks: [], errors: [], actionBase: "/setup/twitch", cancelPath: "/setup" }); }); app.post("/setup/twitch/verify", async (req, res) => { if (!isPlatformEnabled("twitch")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getTwitchSettings(); const incoming = { twitch_client_id: req.body.twitch_client_id || "", twitch_client_secret: req.body.twitch_client_secret || "", twitch_redirect_uri: req.body.twitch_redirect_uri || "", twitch_bot_username: req.body.twitch_bot_username || "", twitch_bot_oauth: req.body.twitch_bot_oauth || "", twitch_channels: req.body.twitch_channels || "" }; const merged = mergeSecrets( current, incoming, new Set(["twitch_client_secret", "twitch_bot_oauth"]) ); if (!merged.twitch_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.twitch_redirect_uri = `${baseUrl}/auth/twitch/callback`; } const result = await verifyTwitchSettings(merged); if (!result.ok) { res.render("wizard-twitch", { title: "Twitch setup", form: { ...incoming, twitch_client_secret: "", twitch_bot_oauth: "", twitch_redirect_uri: merged.twitch_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/setup/twitch", cancelPath: "/setup" }); return; } saveSettingsMap(merged); delete req.session.twitchWizardSnapshot; setFlash(req, "success", "Twitch setup saved."); res.redirect("/setup"); }); app.post("/setup/twitch/cancel", (req, res) => { if (!isPlatformEnabled("twitch")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } restoreSnapshot(req, "twitchWizardSnapshot"); setFlash(req, "info", "Twitch setup canceled."); res.redirect("/setup"); }); app.get("/setup/youtube", (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getYouTubeSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, youtube_client_secret: "", youtube_redirect_uri: current.youtube_redirect_uri || `${baseUrl}/auth/youtube/callback` }; const snapshot = { ...current, youtube_bot_refresh_token: getSetting("youtube_bot_refresh_token", ""), youtube_bot_channel_id: getSetting("youtube_bot_channel_id", "") }; storeSnapshot(req, "youtubeWizardSnapshot", snapshot); res.render("wizard-youtube", { title: "YouTube setup", form, checks: [], errors: [], actionBase: "/setup/youtube", cancelPath: "/setup", connectPath: "/setup/youtube/connect", botConnected: Boolean(getSetting("youtube_bot_refresh_token", "")), botChannelId: getSetting("youtube_bot_channel_id", ""), botChannelName: getYouTubeClient()?.channelName || null }); }); app.post("/setup/youtube/connect", (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const baseUrl = `${req.protocol}://${req.get("host")}`; const clientId = (req.body.youtube_client_id || "").trim(); const clientSecret = (req.body.youtube_client_secret || "").trim(); const redirectUri = (req.body.youtube_redirect_uri || "").trim() || `${baseUrl}/auth/youtube/callback`; if (!clientId || !clientSecret) { setFlash(req, "error", "Client ID and Client Secret are required."); return res.redirect("/setup/youtube"); } setSetting("youtube_client_id", clientId); setSetting("youtube_client_secret", clientSecret); setSetting("youtube_redirect_uri", redirectUri); const state = crypto.randomBytes(16).toString("hex"); req.session.youtubeBotState = state; req.session.youtubeBotReturnTo = "/setup/youtube"; const url = buildYouTubeAuthUrl(state, redirectUri, { prompt: "consent" }); res.redirect(url); }); app.post("/setup/youtube/verify", async (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getYouTubeSettings(); const incoming = { youtube_client_id: req.body.youtube_client_id || "", youtube_client_secret: req.body.youtube_client_secret || "", youtube_redirect_uri: req.body.youtube_redirect_uri || "" }; const merged = mergeSecrets( current, incoming, new Set(["youtube_client_secret"]) ); if (!merged.youtube_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.youtube_redirect_uri = `${baseUrl}/auth/youtube/callback`; } const result = await verifyYouTubeSettings({ ...merged, youtube_bot_refresh_token: getSetting("youtube_bot_refresh_token", "") }); if (!result.ok) { res.render("wizard-youtube", { title: "YouTube setup", form: { ...incoming, youtube_client_secret: "", youtube_redirect_uri: merged.youtube_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/setup/youtube", cancelPath: "/setup", connectPath: "/setup/youtube/connect", botConnected: Boolean(getSetting("youtube_bot_refresh_token", "")), botChannelId: getSetting("youtube_bot_channel_id", ""), botChannelName: getYouTubeClient()?.channelName || null }); return; } if (result.channel?.id) { merged.youtube_bot_channel_id = result.channel.id; } saveSettingsMap(merged); delete req.session.youtubeWizardSnapshot; setFlash(req, "success", "YouTube setup saved."); res.redirect("/setup"); }); app.post("/setup/youtube/cancel", (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } restoreSnapshot(req, "youtubeWizardSnapshot"); setFlash(req, "info", "YouTube setup canceled."); res.redirect("/setup"); }); app.get("/auth/discord", (req, res) => { if (!isPlatformEnabled("discord")) { setFlash(req, "error", "Discord is disabled in Platform Integration."); return res.redirect("/setup"); } if (!isPlatformConfigured("discord")) { setFlash(req, "error", "Discord is not configured yet."); return res.redirect("/setup"); } const state = crypto.randomBytes(16).toString("hex"); if (req.session.user) { req.session.discordLinkState = state; } else { req.session.discordState = state; } const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("discord_redirect_uri") || `${baseUrl}/auth/discord/callback`; const url = buildDiscordAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/discord/callback", async (req, res) => { if (!isPlatformEnabled("discord")) { return res.status(400).render("error", { title: "Login failed", message: "Discord is disabled in Platform Integration." }); } const { code, state } = req.query; const isLogin = state && state === req.session.discordState; const isLink = state && state === req.session.discordLinkState && req.session.user; if (!code || (!isLogin && !isLink)) { return res.status(400).render("error", { title: "Login failed", message: "Invalid OAuth state." }); } try { if (isLogin) { delete req.session.discordState; } if (isLink) { delete req.session.discordLinkState; } const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("discord_redirect_uri") || `${baseUrl}/auth/discord/callback`; const token = await exchangeDiscordCode(code, redirectUri); const user = await fetchDiscordUser(token.access_token); const guildId = getSetting("discord_guild_id"); const member = guildId ? await fetchDiscordGuildMember(token.access_token, guildId) : null; const roles = member?.roles || []; const flags = getRoleFlags(roles); if (isLink) { const linked = linkIdentityToUser({ userId: req.session.user.id, provider: "discord", providerUserId: user.id, displayName: user.global_name || user.username, avatar: user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=128` : null }); req.session.user = { id: linked.id, username: linked.internal_username, avatar: user.avatar, roles, ...flags }; req.session.discordToken = token; setFlash(req, "success", "Discord account linked."); res.redirect("/profile"); return; } const profile = storeDiscordUser(user); req.session.user = { id: profile.id, username: profile.internal_username, avatar: user.avatar, roles, ...flags }; req.session.discordToken = token; setFlash(req, "success", "Logged in."); res.redirect("/"); } catch (error) { console.error(error); res.status(500).render("error", { title: "Login failed", message: "Discord authentication failed." }); } }); app.post("/auth/logout", (req, res) => { req.session.destroy(() => { res.redirect("/"); }); }); app.get("/auth/twitch", requireAuth, (req, res) => { if (!isPlatformEnabled("twitch")) { setFlash(req, "error", "Twitch is disabled in Platform Integration."); return res.redirect("/profile"); } if (!isPlatformConfigured("twitch")) { setFlash(req, "error", "Twitch is not configured yet."); return res.redirect("/profile"); } const state = crypto.randomBytes(16).toString("hex"); req.session.twitchState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("twitch_redirect_uri") || `${baseUrl}/auth/twitch/callback`; const url = buildTwitchAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/twitch/login", (req, res) => { if (!isPlatformEnabled("twitch")) { setFlash(req, "error", "Twitch is disabled in Platform Integration."); return res.redirect("/setup"); } if (!isPlatformConfigured("twitch")) { setFlash(req, "error", "Twitch is not configured yet."); return res.redirect("/setup"); } const state = crypto.randomBytes(16).toString("hex"); req.session.twitchLoginState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("twitch_redirect_uri") || `${baseUrl}/auth/twitch/callback`; const url = buildTwitchAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/twitch/callback", async (req, res) => { if (!isPlatformEnabled("twitch")) { return res.status(400).render("error", { title: "Auth failed", message: "Twitch is disabled in Platform Integration." }); } const { code, state } = req.query; const isLogin = state && state === req.session.twitchLoginState; const isLink = state && state === req.session.twitchState && req.session.user; if (!code || (!isLogin && !isLink)) { return res.status(400).render("error", { title: "Auth failed", message: "Invalid Twitch OAuth state." }); } try { const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("twitch_redirect_uri") || `${baseUrl}/auth/twitch/callback`; const token = await exchangeTwitchCode(code, redirectUri); const twitchUser = await fetchTwitchUser(token.access_token); if (!twitchUser) { throw new Error("Twitch user not found."); } const now = Date.now(); if (isLogin) { const profile = ensureUserForIdentity({ provider: "twitch", providerUserId: twitchUser.id, displayName: twitchUser.display_name, avatar: twitchUser.profile_image_url || null }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'twitch', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, twitchUser.id, twitchUser.display_name, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); const discordIdentity = db .prepare( "SELECT provider_user_id FROM user_identities WHERE user_id = ? AND provider = 'discord'" ) .get(profile.id); const roles = discordIdentity?.provider_user_id ? await fetchDiscordRolesForUser(discordIdentity.provider_user_id) : []; const flags = getRoleFlags(roles); req.session.user = { id: profile.id, username: profile.internal_username, roles, ...flags }; req.session.twitchToken = token; setFlash(req, "success", "Logged in with Twitch."); res.redirect("/"); } else { const profile = linkIdentityToUser({ userId: req.session.user.id, provider: "twitch", providerUserId: twitchUser.id, displayName: twitchUser.display_name, avatar: twitchUser.profile_image_url || null }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'twitch', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, twitchUser.id, twitchUser.display_name, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); req.session.user.id = profile.id; req.session.user.username = profile.internal_username; setFlash(req, "success", "Twitch account linked."); res.redirect("/profile"); } } catch (error) { console.error(error); res.status(500).render("error", { title: isLogin ? "Login failed" : "Link failed", message: isLogin ? "Unable to sign in with Twitch." : "Unable to link Twitch account." }); } }); app.get("/auth/youtube", requireAuth, (req, res) => { if (!isPlatformEnabled("youtube")) { setFlash(req, "error", "YouTube is disabled in Platform Integration."); return res.redirect("/profile"); } if (!isPlatformConfigured("youtube")) { setFlash(req, "error", "YouTube is not configured yet."); return res.redirect("/profile"); } const state = crypto.randomBytes(16).toString("hex"); req.session.youtubeState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("youtube_redirect_uri") || `${baseUrl}/auth/youtube/callback`; const url = buildYouTubeAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/youtube/login", (req, res) => { if (!isPlatformEnabled("youtube")) { setFlash(req, "error", "YouTube is disabled in Platform Integration."); return res.redirect("/setup"); } if (!isPlatformConfigured("youtube")) { setFlash(req, "error", "YouTube is not configured yet."); return res.redirect("/setup"); } const state = crypto.randomBytes(16).toString("hex"); req.session.youtubeLoginState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("youtube_redirect_uri") || `${baseUrl}/auth/youtube/callback`; const url = buildYouTubeAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/youtube/callback", async (req, res) => { if (!isPlatformEnabled("youtube")) { return res.status(400).render("error", { title: "Auth failed", message: "YouTube is disabled in Platform Integration." }); } const { code, state } = req.query; const isLogin = state && state === req.session.youtubeLoginState; const isLink = state && state === req.session.youtubeState && req.session.user; const isBot = state && state === req.session.youtubeBotState; if (!code || (!isLogin && !isLink && !isBot)) { return res.status(400).render("error", { title: "Auth failed", message: "Invalid YouTube OAuth state." }); } try { const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("youtube_redirect_uri") || `${baseUrl}/auth/youtube/callback`; const token = await exchangeYouTubeCode(code, redirectUri); const channel = await fetchYouTubeChannel(token.access_token); if (!channel) { throw new Error("YouTube channel not found."); } const displayName = channel.snippet?.title || "YouTube Channel"; const avatar = channel.snippet?.thumbnails?.default?.url || null; const now = Date.now(); if (isBot) { const refreshToken = token.refresh_token || getSetting("youtube_bot_refresh_token", ""); if (!refreshToken) { throw new Error("Missing YouTube refresh token."); } setSetting("youtube_bot_refresh_token", refreshToken); setSetting("youtube_bot_channel_id", channel.id || ""); req.session.youtubeBotState = null; const returnTo = req.session.youtubeBotReturnTo || "/admin/youtube-wizard"; req.session.youtubeBotReturnTo = null; setFlash(req, "success", "YouTube bot connected."); return res.redirect(returnTo); } if (isLogin) { const profile = ensureUserForIdentity({ provider: "youtube", providerUserId: channel.id, displayName, avatar }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'youtube', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, channel.id, displayName, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); const discordIdentity = db .prepare( "SELECT provider_user_id FROM user_identities WHERE user_id = ? AND provider = 'discord'" ) .get(profile.id); const roles = discordIdentity?.provider_user_id ? await fetchDiscordRolesForUser(discordIdentity.provider_user_id) : []; const flags = getRoleFlags(roles); req.session.user = { id: profile.id, username: profile.internal_username, roles, ...flags }; req.session.youtubeToken = token; setFlash(req, "success", "Logged in with YouTube."); return res.redirect("/"); } const profile = linkIdentityToUser({ userId: req.session.user.id, provider: "youtube", providerUserId: channel.id, displayName, avatar }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'youtube', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, channel.id, displayName, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); req.session.user.id = profile.id; req.session.user.username = profile.internal_username; setFlash(req, "success", "YouTube account linked."); res.redirect("/profile"); } catch (error) { console.error(error); res.status(500).render("error", { title: isBot ? "Bot connect failed" : isLogin ? "Login failed" : "Link failed", message: isBot ? "Unable to connect the YouTube bot." : isLogin ? "Unable to sign in with YouTube." : "Unable to link YouTube account." }); } }); app.get("/profile", requireAuth, (req, res) => { const profile = getUserProfileById(req.session.user.id); if (!profile) { req.session.destroy(() => { res.redirect("/"); }); return; } const accounts = getUserIdentities(req.session.user.id); const cooldownDays = 90; const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000; const lastUpdate = profile.username_updated_at || 0; const nextAllowedAt = lastUpdate ? lastUpdate + cooldownMs : 0; const canChangeUsername = !lastUpdate || Date.now() >= nextAllowedAt; const remainingMs = canChangeUsername ? 0 : nextAllowedAt - Date.now(); const remainingDays = canChangeUsername ? 0 : Math.max(1, Math.ceil(remainingMs / (24 * 60 * 60 * 1000))); const profileWidgets = profileSections .filter((section) => hasAccess(req.session.user, section.role || "public")) .slice() .sort((a, b) => (a.order || 100) - (b.order || 100)) .map((section) => ({ ...section, locals: { ...(section.locals || {}), user: req.session.user, profile, accounts } })); res.render("profile", { title: "Your profile", accounts, profile, profileSections: profileWidgets, canChangeUsername, usernameCooldownDays: cooldownDays, usernameCooldownRemainingDays: remainingDays, usernameNextAllowedAt: nextAllowedAt }); }); app.post("/profile/unlink/:provider", requireAuth, (req, res) => { const provider = req.params.provider; if (provider === "discord") { setFlash(req, "error", "Discord cannot be unlinked from the WebUI."); return res.redirect("/profile"); } db.prepare( "DELETE FROM linked_accounts WHERE user_id = ? AND provider = ?" ).run(req.session.user.id, provider); db.prepare( "DELETE FROM user_identities WHERE user_id = ? AND provider = ?" ).run(req.session.user.id, provider); setFlash(req, "success", "Account unlinked."); res.redirect("/profile"); }); app.post("/profile/username", requireAuth, (req, res) => { const desired = (req.body.internal_username || "").trim(); const profile = getUserProfileById(req.session.user.id); const cooldownMs = 90 * 24 * 60 * 60 * 1000; if (profile?.username_updated_at) { const nextAllowed = profile.username_updated_at + cooldownMs; if (Date.now() < nextAllowed) { setFlash( req, "error", "You can change your username once every 90 days." ); return res.redirect("/profile"); } } const result = updateInternalUsername(req.session.user.id, desired); if (!result.ok) { setFlash(req, "error", result.reason); return res.redirect("/profile"); } req.session.user.username = result.username; setFlash(req, "success", "Username updated."); res.redirect("/profile"); }); app.get("/health", (req, res) => { res.set("Cache-Control", "no-store"); res.json({ ok: true, ts: Date.now() }); }); app.get("/commands", (req, res) => { const prefix = getSetting("command_prefix", "!"); const baseUrl = `${req.protocol}://${req.get("host")}`; const usageMap = buildCommandUsageMap(); const commands = []; const conflictMap = new Map(); const enabledPlatforms = getEnabledPlatformIds(); const addConflictEntries = (command) => { const triggers = command.conflictTriggers || [command.trigger]; for (const trigger of triggers) { for (const platform of command.platforms) { const subcommand = command.subcommand || ""; const key = `${platform}:${trigger}:${subcommand}`; const entries = conflictMap.get(key) || []; if (!entries.some((entry) => entry.id === command.id)) { entries.push({ id: command.id, name: command.name, origin: command.origin }); } conflictMap.set(key, entries); } } }; const addCommand = (command) => { command.count = usageMap.get(command.id) || 0; command.anchor = `cmd-${slugify(command.id)}`; command.link = `${baseUrl}/commands#${command.anchor}`; const subcommand = command.subcommand || ""; command.search = [ command.triggerDisplay, subcommand, command.name, command.description, command.origin, command.platforms.join(" ") ] .join(" ") .toLowerCase(); command.sort = { trigger: subcommand ? `${command.trigger} ${subcommand}` : command.trigger, name: command.name.toLowerCase(), description: command.description.toLowerCase(), level: command.level.toLowerCase(), platform: command.platforms.join(" "), origin: command.origin.toLowerCase(), count: command.count }; if (command.levelHelp) { command.levelHelp = command.levelHelp.toString(); } commands.push(command); addConflictEntries(command); }; const customCommands = db .prepare( "SELECT id, trigger, response, mode, language, platform FROM custom_commands WHERE enabled = 1 ORDER BY trigger" ) .all(); for (const row of customCommands) { const trigger = normalizeCommandTrigger(row.trigger); if (!trigger) { continue; } const platforms = normalizeCustomPlatforms(row.platform, enabledPlatforms); const description = row.mode === "advanced" ? `Advanced command (${row.language})` : truncateText(row.response, 140); addCommand({ id: `custom:${trigger}`, trigger, triggerDisplay: `${prefix}${trigger}`, name: toTitleCase(trigger) || trigger, description, level: "public", origin: "Custom", platforms, platformLabels: buildPlatformLabels(platforms), conflictTriggers: [trigger] }); } const topOptions = getTopCommandOptions(); if (topOptions.length) { const topPlatforms = enabledPlatforms.slice(); const topPlatformLabels = buildPlatformLabels(topPlatforms); const topUsage = buildUsage("top", "top "); addCommand({ id: "top", trigger: "top", triggerDisplay: `${prefix}${topUsage}`, name: "Top", description: "Show leaderboard rankings.", level: "public", origin: "Core", platforms: topPlatforms, platformLabels: topPlatformLabels, conflictTriggers: ["top"] }); for (const option of topOptions) { const subcommand = normalizeSubcommand(option.id); if (!subcommand) { continue; } const usage = buildUsage("top", `top ${subcommand}`); addCommand({ id: `top:${subcommand}`, trigger: "top", subcommand, triggerDisplay: `${prefix}${usage}`, name: option.label || toTitleCase(subcommand) || subcommand, description: option.description || `Show ${option.label || toTitleCase(subcommand) || subcommand} rankings.`, level: "public", origin: "Core", platforms: topPlatforms, platformLabels: topPlatformLabels, conflictTriggers: ["top"] }); } } const plugins = getPlugins().filter((plugin) => plugin.enabled); for (const plugin of plugins) { const cmdsPath = path.join(plugin.path, "cmds.json"); if (!fs.existsSync(cmdsPath)) { continue; } const manifest = readJsonSafe(cmdsPath); if (!manifest || !Array.isArray(manifest.commands)) { continue; } const pluginSettings = getPluginSettingsMap(plugin.id); const platformKeys = manifest.platformKeys || {}; const platformFlags = {}; for (const platform of enabledPlatforms) { platformFlags[platform] = platformKeys[platform] ? parseBooleanSetting(pluginSettings[platformKeys[platform]], true) : true; } const pluginName = manifest.pluginName || plugin.name || plugin.id; for (const command of manifest.commands) { if (!command || !command.trigger) { continue; } const enabled = command.enabledKey ? parseBooleanSetting(pluginSettings[command.enabledKey], true) : true; if (!enabled) { continue; } const override = command.triggerKey ? pluginSettings[command.triggerKey] : ""; const trigger = normalizeCommandTrigger(override, command.trigger); if (!trigger) { continue; } const subcommand = normalizeSubcommand(command.subcommand); const usage = buildUsage( trigger, command.usage || (subcommand ? `${trigger} ${subcommand}` : trigger) ); const platforms = (Array.isArray(command.platforms) && command.platforms.length ? command.platforms : enabledPlatforms ).filter((platform) => platformFlags[platform] && enabledPlatforms.includes(platform)); if (!platforms.length) { continue; } const defaultTrigger = normalizeCommandTrigger(command.trigger); const useAliases = Array.isArray(command.aliases) && command.aliases.length && (!command.aliasesEnabledWhenDefault || trigger === defaultTrigger); const aliasTriggers = useAliases ? command.aliases .map((alias) => normalizeCommandTrigger(alias)) .filter(Boolean) : []; const description = truncateText(command.description || "", 140); addCommand({ id: `${plugin.id}:${command.id || trigger}`, trigger, subcommand, triggerDisplay: `${prefix}${usage}`, name: command.name || toTitleCase(trigger) || trigger, description, level: command.level || "public", levelHelp: command.levelHelp || "", origin: pluginName, platforms, platformLabels: buildPlatformLabels(platforms), conflictTriggers: [trigger, ...aliasTriggers] }); } } commands.sort((a, b) => a.triggerDisplay.localeCompare(b.triggerDisplay) ); const conflicts = []; for (const [key, entries] of conflictMap.entries()) { if (entries.length < 2) { continue; } const [platform, trigger, subcommand] = key.split(":"); const triggerLabel = subcommand ? `${trigger} ${subcommand}` : trigger; const sources = entries.map((entry) => { if (entry.origin === entry.name) { return entry.origin; } return `${entry.origin} (${entry.name})`; }); conflicts.push({ trigger: triggerLabel, triggerDisplay: `${prefix}${triggerLabel}`, platform, platformLabel: getPlatformLabel(platform), sourcesLabel: sources.join(", ") }); } conflicts.sort((a, b) => { if (a.trigger === b.trigger) { return a.platform.localeCompare(b.platform); } return a.trigger.localeCompare(b.trigger); }); const subcommandGroups = new Map(); for (const command of commands) { if (!command.subcommand) { continue; } const group = subcommandGroups.get(command.trigger) || { root: null, subcommands: [] }; group.subcommands.push(command); subcommandGroups.set(command.trigger, group); } for (const command of commands) { if (command.subcommand) { continue; } const group = subcommandGroups.get(command.trigger); if (group && !group.root) { group.root = command; } } const buildRootFromSubcommands = (trigger, group) => { const platforms = Array.from( new Set(group.subcommands.flatMap((item) => item.platforms)) ); const origins = Array.from( new Set(group.subcommands.map((item) => item.origin)) ); const levels = Array.from( new Set(group.subcommands.map((item) => item.level)) ); const platformLabels = buildPlatformLabels(platforms); const name = toTitleCase(trigger) || trigger; const subcommandLabels = group.subcommands .map((item) => item.subcommand) .filter(Boolean); const description = subcommandLabels.length ? `Subcommands: ${subcommandLabels.join(", ")}` : "Subcommand group."; const origin = origins.length === 1 ? origins[0] : "Multiple"; const level = levels.length === 1 ? levels[0] : "mixed"; const count = group.subcommands.reduce( (total, item) => total + (item.count || 0), 0 ); const id = `group:${trigger}`; const triggerDisplay = `${prefix}${trigger}`; const anchor = `cmd-${slugify(id)}`; const link = `${baseUrl}/commands#${anchor}`; const search = [ triggerDisplay, name, description, origin, platforms.join(" "), subcommandLabels.join(" ") ] .join(" ") .trim(); const sort = { trigger, name: name.toLowerCase(), description: description.toLowerCase(), level: level.toLowerCase(), platform: platforms.join(" "), origin: origin.toLowerCase(), count }; return { id, groupKey: trigger, trigger, triggerDisplay, name, description, level, origin, platforms, platformLabels, count, anchor, link, search, sort }; }; const commandGroups = []; const processed = new Set(); for (const command of commands) { const group = subcommandGroups.get(command.trigger); if (!group) { command.groupKey = command.id; commandGroups.push({ root: command, subcommands: [] }); continue; } if (processed.has(command.trigger)) { continue; } const root = group.root || buildRootFromSubcommands(command.trigger, group); root.groupKey = root.groupKey || command.trigger; const subcommands = group.subcommands .slice() .sort((a, b) => a.triggerDisplay.localeCompare(b.triggerDisplay)) .map((item) => ({ ...item, groupKey: root.groupKey })); if (subcommands.length) { const extraSearch = subcommands .map((item) => [item.subcommand, item.name].filter(Boolean).join(" ")) .join(" "); root.search = `${root.search} ${extraSearch}`.trim(); } commandGroups.push({ root, subcommands }); processed.add(command.trigger); } res.render("commands", { title: "Commands", commandGroups, conflicts, isAdmin: Boolean(req.session.user?.isAdmin) }); }); app.get("/leaderboards", (req, res) => { res.render("leaderboards", { title: "Leaderboards", sections: getLeaderboardSections({ limit: 25 }) }); }); app.get("/stats", requireAuth, (req, res) => { const payload = buildUserStatsPayload(req.session.user.id); res.render("stats", { title: "Your stats", stats: payload.stats, expression: payload.expression, pluginStats: payload.pluginStats, statsOwner: { username: req.session.user.username, isSelf: true }, compare: null }); }); app.get("/stats/:username", (req, res) => { const username = (req.params.username || "").trim(); if (!username) { return res.status(404).render("error", { title: "Not found", message: "User not found." }); } const profile = db .prepare( "SELECT id, internal_username FROM user_profiles WHERE internal_username = ? LIMIT 1" ) .get(username); if (!profile) { return res.status(404).render("error", { title: "Not found", message: "User not found." }); } const payload = buildUserStatsPayload(profile.id); let compare = null; if (req.session.user && req.session.user.id !== profile.id) { const currentPayload = buildUserStatsPayload(req.session.user.id); compare = { leftLabel: req.session.user.username || "You", rightLabel: profile.internal_username, rows: buildCompareRows(currentPayload, payload) }; } res.render("stats", { title: `${profile.internal_username}'s stats`, stats: payload.stats, expression: payload.expression, pluginStats: payload.pluginStats, statsOwner: { username: profile.internal_username, isSelf: req.session.user?.id === profile.id }, compare }); }); app.get("/pages/:slug", (req, res) => { const page = db .prepare( "SELECT * FROM custom_pages WHERE slug = ? AND enabled = 1 LIMIT 1" ) .get(req.params.slug); if (!page) { return res.status(404).render("error", { title: "Not found", message: "That page does not exist." }); } if (!hasAccess(req.session.user, page.role)) { return res.status(403).render("error", { title: "Access denied", message: "You do not have access to that page." }); } const format = normalizePageFormat(page.format); const pageData = { ...page, format, content_css: page.content_css || "" }; const renderedContent = format === "markdown" ? renderMarkdown(pageData.content) : ""; const pageSrcdoc = format === "html" ? buildCustomPageSrcdoc(pageData, res.locals.theme) : ""; const safePageSrcdoc = pageSrcdoc.replace(/<\/script/gi, "<\\/script"); res.render("custom-page", { title: page.title, page: pageData, renderedContent, pageSrcdoc: safePageSrcdoc }); }); app.get("/moderator", requireRole("mod"), (req, res) => { const now = Date.now(); const totals = db .prepare( "SELECT user_id, SUM(CASE WHEN end_at IS NULL THEN ? - start_at ELSE end_at - start_at END) AS total_ms " + "FROM mod_role_periods GROUP BY user_id" ) .all(now); const totalsMap = new Map( totals.map((row) => [row.user_id, Number(row.total_ms || 0)]) ); const users = listUsersWithIdentities() .filter((user) => totalsMap.has(user.id)) .map((user) => { const identities = (user.identities || []).map((identity) => ({ ...identity, label: getPlatformLabel(identity.provider) || identity.provider })); const aliases = identities .map( (identity) => `${identity.label}: ${identity.display_name || identity.provider_user_id}` ) .join(" | "); return { id: user.id, username: user.internal_username, identities, aliasText: aliases, totalMs: totalsMap.get(user.id) || 0 }; }) .sort((a, b) => b.totalMs - a.totalMs); res.render("moderator", { title: "Mods List", mods: users, formatDuration }); }); app.get("/admin", requireRole("admin"), (req, res) => { res.render("admin-dashboard", { title: "Admin dashboard" }); }); app.get("/admin/settings", requireRole("admin"), (req, res) => { res.render("admin-settings", { title: "Settings", settings: getAllSettings(), platforms: getPlatformStatus(), navIconItems: buildNavIconItems(req.session.user, navItems, req.path) }); }); app.post("/admin/settings", requireRole("admin"), (req, res) => { const fields = [ "site_title", "command_prefix", "auto_update_enabled", "auto_update_interval_minutes", "git_remote", "git_branch" ]; for (const field of fields) { if (req.body[field] !== undefined) { const value = req.body[field]; if (field === "auto_update_enabled") { setSetting(field, value === "on"); } else if (field === "auto_update_interval_minutes") { setSetting(field, Number(value)); } else { setSetting(field, value.trim()); } } } const platformStatus = getPlatformStatus(); const nextPlatformValues = new Map(); for (const platform of platformStatus) { nextPlatformValues.set(platform.id, req.body[platform.enabledKey] === "on"); } const hasLoginPlatform = platformStatus.some( (platform) => platform.supported && platform.supportsLogin && nextPlatformValues.get(platform.id) ); if (!hasLoginPlatform) { setFlash( req, "error", "At least one login platform must remain enabled." ); return res.redirect("/admin/settings"); } let restartNeeded = false; for (const platform of platformStatus) { const nextValue = nextPlatformValues.get(platform.id); const currentValue = isPlatformEnabled(platform.id); if (nextValue !== currentValue) { restartNeeded = true; } setSetting(platform.enabledKey, nextValue); } setFlash( req, "success", restartNeeded ? "Settings saved. Restarting..." : "Settings saved." ); res.redirect("/admin/settings"); if (restartNeeded) { requestRestart(); } }); app.post( "/admin/settings/nav-icons", requireRole("admin"), navIconSingle, (req, res) => { if (req.uploadError) { setFlash(req, "error", req.uploadError); return res.redirect("/admin/settings"); } const itemId = (req.body.item_id || "").trim(); if (!itemId) { setFlash(req, "error", "Missing navigation item."); return res.redirect("/admin/settings"); } if (!req.file) { setFlash(req, "error", "Upload an SVG or PNG icon."); return res.redirect("/admin/settings"); } const map = getSetting("nav_item_icons", {}) || {}; const previous = map[itemId]; map[itemId] = req.file.filename; setSetting("nav_item_icons", map); if (previous) { try { fs.rmSync(path.join(navIconDir, previous), { force: true }); } catch { // ignore cleanup errors } } setFlash(req, "success", "Navigation icon updated."); res.redirect("/admin/settings"); } ); app.post("/admin/settings/nav-icons/reset", requireRole("admin"), (req, res) => { const itemId = (req.body.item_id || "").trim(); if (!itemId) { setFlash(req, "error", "Missing navigation item."); return res.redirect("/admin/settings"); } const map = getSetting("nav_item_icons", {}) || {}; const previous = map[itemId]; delete map[itemId]; setSetting("nav_item_icons", map); if (previous) { try { fs.rmSync(path.join(navIconDir, previous), { force: true }); } catch { // ignore cleanup errors } } setFlash(req, "success", "Navigation icon reset."); res.redirect("/admin/settings"); }); app.get("/admin/navigation", requireRole("admin"), (req, res) => { const availableItems = collectNavItems(req.session.user, navItems, req.path); const sortedItems = availableItems .slice() .sort((a, b) => { if (a.section === b.section) { return a.label.localeCompare(b.label); } return a.section.localeCompare(b.section); }); const defaultStructure = buildDefaultNavStructure(sortedItems); const storedStructure = normalizeNavStructure(getSetting("nav_structure", null)); const navStructure = storedStructure || { ...defaultStructure, enabled: false, includeUnassigned: true }; const navSections = storedStructure?.sections?.length ? storedStructure.sections : defaultStructure.sections; res.render("admin-navigation", { title: "Navigation", navStructure, navSectionsJson: JSON.stringify(navSections, null, 2), navSectionsData: JSON.stringify(navSections), defaultSectionsJson: JSON.stringify(defaultStructure.sections, null, 2), navItems: sortedItems, navItemsJson: JSON.stringify(sortedItems), sectionIcons: NAV_SECTION_ICONS }); }); app.post("/admin/navigation", requireRole("admin"), (req, res) => { const enabled = req.body.nav_enabled === "on"; const includeUnassigned = req.body.nav_include_unassigned === "on"; const unassignedLabel = (req.body.nav_unassigned_label || "").trim(); const unassignedIcon = (req.body.nav_unassigned_icon || "").trim(); const unassignedId = (req.body.nav_unassigned_id || "").trim(); const rawSections = (req.body.nav_sections || "").trim(); let sections = []; if (rawSections) { try { sections = JSON.parse(rawSections); } catch { setFlash(req, "error", "Navigation JSON is invalid."); return res.redirect("/admin/navigation"); } } if (!Array.isArray(sections)) { setFlash(req, "error", "Navigation JSON must be an array of sections."); return res.redirect("/admin/navigation"); } const navStructure = normalizeNavStructure({ enabled, includeUnassigned, unassignedLabel, unassignedIcon, unassignedId, sections }); if (!navStructure) { setFlash(req, "error", "Navigation settings could not be saved."); return res.redirect("/admin/navigation"); } setSetting("nav_structure", navStructure); setFlash(req, "success", "Navigation updated."); res.redirect("/admin/navigation"); }); app.post("/admin/navigation/reset", requireRole("admin"), (req, res) => { setSetting("nav_structure", null); setFlash(req, "success", "Navigation reset to default."); res.redirect("/admin/navigation"); }); app.get("/admin/discord-wizard", requireRole("admin"), (req, res) => { if (!isPlatformEnabled("discord")) { setFlash(req, "error", "Discord is disabled in Platform Integration."); return res.redirect("/admin/settings"); } const current = getDiscordSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, discord_client_secret: "", discord_bot_token: "", discord_redirect_uri: current.discord_redirect_uri || `${baseUrl}/auth/discord/callback` }; storeSnapshot(req, "discordWizardSnapshot", current); res.render("wizard-discord", { title: "Discord setup", form, checks: [], errors: [], actionBase: "/admin/discord-wizard", cancelPath: "/admin/settings" }); }); app.post("/admin/discord-wizard/verify", requireRole("admin"), async (req, res) => { if (!isPlatformEnabled("discord")) { setFlash(req, "error", "Discord is disabled in Platform Integration."); return res.redirect("/admin/settings"); } const current = getDiscordSettings(); const incoming = { discord_client_id: req.body.discord_client_id || "", discord_client_secret: req.body.discord_client_secret || "", discord_redirect_uri: req.body.discord_redirect_uri || "", discord_bot_token: req.body.discord_bot_token || "", discord_guild_id: req.body.discord_guild_id || "", discord_admin_role_id: req.body.discord_admin_role_id || "", discord_mod_role_id: req.body.discord_mod_role_id || "" }; const merged = mergeSecrets( current, incoming, new Set(["discord_client_secret", "discord_bot_token"]) ); if (!merged.discord_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.discord_redirect_uri = `${baseUrl}/auth/discord/callback`; } const result = await verifyDiscordSettings(merged); if (!result.ok) { res.render("wizard-discord", { title: "Discord setup", form: { ...incoming, discord_client_secret: "", discord_bot_token: "", discord_redirect_uri: merged.discord_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/admin/discord-wizard", cancelPath: "/admin/settings" }); return; } saveSettingsMap(merged); delete req.session.discordWizardSnapshot; setFlash(req, "success", "Discord setup saved."); res.redirect("/admin/settings"); }); app.post("/admin/discord-wizard/cancel", requireRole("admin"), (req, res) => { if (!isPlatformEnabled("discord")) { return res.redirect("/admin/settings"); } restoreSnapshot(req, "discordWizardSnapshot"); setFlash(req, "info", "Discord setup canceled."); res.redirect("/admin/settings"); }); app.get("/admin/twitch-wizard", requireRole("admin"), (req, res) => { if (!isPlatformEnabled("twitch")) { setFlash(req, "error", "Twitch is disabled in Platform Integration."); return res.redirect("/admin/settings"); } const current = getTwitchSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, twitch_client_secret: "", twitch_bot_oauth: "", twitch_redirect_uri: current.twitch_redirect_uri || `${baseUrl}/auth/twitch/callback` }; storeSnapshot(req, "twitchWizardSnapshot", current); res.render("wizard-twitch", { title: "Twitch setup", form, checks: [], errors: [], actionBase: "/admin/twitch-wizard", cancelPath: "/admin/settings" }); }); app.post("/admin/twitch-wizard/verify", requireRole("admin"), async (req, res) => { if (!isPlatformEnabled("twitch")) { setFlash(req, "error", "Twitch is disabled in Platform Integration."); return res.redirect("/admin/settings"); } const current = getTwitchSettings(); const incoming = { twitch_client_id: req.body.twitch_client_id || "", twitch_client_secret: req.body.twitch_client_secret || "", twitch_redirect_uri: req.body.twitch_redirect_uri || "", twitch_bot_username: req.body.twitch_bot_username || "", twitch_bot_oauth: req.body.twitch_bot_oauth || "", twitch_channels: req.body.twitch_channels || "" }; const merged = mergeSecrets( current, incoming, new Set(["twitch_client_secret", "twitch_bot_oauth"]) ); if (!merged.twitch_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.twitch_redirect_uri = `${baseUrl}/auth/twitch/callback`; } const result = await verifyTwitchSettings(merged); if (!result.ok) { res.render("wizard-twitch", { title: "Twitch setup", form: { ...incoming, twitch_client_secret: "", twitch_bot_oauth: "", twitch_redirect_uri: merged.twitch_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/admin/twitch-wizard", cancelPath: "/admin/settings" }); return; } saveSettingsMap(merged); delete req.session.twitchWizardSnapshot; setFlash(req, "success", "Twitch setup saved."); res.redirect("/admin/settings"); }); app.post("/admin/twitch-wizard/cancel", requireRole("admin"), (req, res) => { if (!isPlatformEnabled("twitch")) { return res.redirect("/admin/settings"); } restoreSnapshot(req, "twitchWizardSnapshot"); setFlash(req, "info", "Twitch setup canceled."); res.redirect("/admin/settings"); }); app.get("/admin/youtube-wizard", requireRole("admin"), (req, res) => { if (!isPlatformEnabled("youtube")) { setFlash(req, "error", "YouTube is disabled in Platform Integration."); return res.redirect("/admin/settings"); } const current = getYouTubeSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, youtube_client_secret: "", youtube_redirect_uri: current.youtube_redirect_uri || `${baseUrl}/auth/youtube/callback` }; const snapshot = { ...current, youtube_bot_refresh_token: getSetting("youtube_bot_refresh_token", ""), youtube_bot_channel_id: getSetting("youtube_bot_channel_id", "") }; storeSnapshot(req, "youtubeWizardSnapshot", snapshot); res.render("wizard-youtube", { title: "YouTube setup", form, checks: [], errors: [], actionBase: "/admin/youtube-wizard", cancelPath: "/admin/settings", connectPath: "/admin/youtube-wizard/connect", botConnected: Boolean(getSetting("youtube_bot_refresh_token", "")), botChannelId: getSetting("youtube_bot_channel_id", ""), botChannelName: getYouTubeClient()?.channelName || null }); }); app.post("/admin/youtube-wizard/connect", requireRole("admin"), (req, res) => { if (!isPlatformEnabled("youtube")) { setFlash(req, "error", "YouTube is disabled in Platform Integration."); return res.redirect("/admin/settings"); } const baseUrl = `${req.protocol}://${req.get("host")}`; const clientId = (req.body.youtube_client_id || "").trim(); const clientSecret = (req.body.youtube_client_secret || "").trim(); const redirectUri = (req.body.youtube_redirect_uri || "").trim() || `${baseUrl}/auth/youtube/callback`; if (!clientId || !clientSecret) { setFlash(req, "error", "Client ID and Client Secret are required."); return res.redirect("/admin/youtube-wizard"); } setSetting("youtube_client_id", clientId); setSetting("youtube_client_secret", clientSecret); setSetting("youtube_redirect_uri", redirectUri); const state = crypto.randomBytes(16).toString("hex"); req.session.youtubeBotState = state; req.session.youtubeBotReturnTo = "/admin/youtube-wizard"; const url = buildYouTubeAuthUrl(state, redirectUri, { prompt: "consent" }); res.redirect(url); }); app.post("/admin/youtube-wizard/verify", requireRole("admin"), async (req, res) => { if (!isPlatformEnabled("youtube")) { setFlash(req, "error", "YouTube is disabled in Platform Integration."); return res.redirect("/admin/settings"); } const current = getYouTubeSettings(); const incoming = { youtube_client_id: req.body.youtube_client_id || "", youtube_client_secret: req.body.youtube_client_secret || "", youtube_redirect_uri: req.body.youtube_redirect_uri || "" }; const merged = mergeSecrets( current, incoming, new Set(["youtube_client_secret"]) ); if (!merged.youtube_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.youtube_redirect_uri = `${baseUrl}/auth/youtube/callback`; } const result = await verifyYouTubeSettings({ ...merged, youtube_bot_refresh_token: getSetting("youtube_bot_refresh_token", "") }); if (!result.ok) { res.render("wizard-youtube", { title: "YouTube setup", form: { ...incoming, youtube_client_secret: "", youtube_redirect_uri: merged.youtube_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/admin/youtube-wizard", cancelPath: "/admin/settings", connectPath: "/admin/youtube-wizard/connect", botConnected: Boolean(getSetting("youtube_bot_refresh_token", "")), botChannelId: getSetting("youtube_bot_channel_id", ""), botChannelName: getYouTubeClient()?.channelName || null }); return; } if (result.channel?.id) { merged.youtube_bot_channel_id = result.channel.id; } saveSettingsMap(merged); delete req.session.youtubeWizardSnapshot; setFlash(req, "success", "YouTube setup saved."); res.redirect("/admin/settings"); }); app.post("/admin/youtube-wizard/cancel", requireRole("admin"), (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/admin/settings"); } restoreSnapshot(req, "youtubeWizardSnapshot"); setFlash(req, "info", "YouTube setup canceled."); res.redirect("/admin/settings"); }); app.get("/admin/theming", requireRole("admin"), (req, res) => { res.render("admin-theme", { title: "Theming", theme: getThemeSettings() }); }); app.post("/admin/theming", requireRole("admin"), (req, res) => { const fields = [ "theme_light_bg_1", "theme_light_bg_2", "theme_light_bg_3", "theme_light_text", "theme_light_text_muted", "theme_light_accent", "theme_light_accent_alt", "theme_light_danger", "theme_light_surface", "theme_light_surface_2", "theme_light_surface_3", "theme_light_border", "theme_dark_bg_1", "theme_dark_bg_2", "theme_dark_bg_3", "theme_dark_text", "theme_dark_text_muted", "theme_dark_accent", "theme_dark_accent_alt", "theme_dark_danger", "theme_dark_surface", "theme_dark_surface_2", "theme_dark_surface_3", "theme_dark_border", "theme_role_public", "theme_role_mod", "theme_role_admin" ]; for (const field of fields) { if (req.body[field] !== undefined) { setSetting(field, req.body[field].trim()); } } setFlash(req, "success", "Theme updated."); res.redirect("/admin/theming"); }); app.get("/admin/logs", requireRole("admin"), (req, res) => { const range = parseLogRange(req.query.range); const limit = parseLogLimit(req.query.limit); const levelValue = normalizeLogLevel(req.query.level) || "all"; const levels = levelValue === "all" ? [] : [levelValue]; const sinceMs = range.rangeMs ? Date.now() - range.rangeMs : null; const logs = listLogs({ limit: limit.limit, sinceMs, levels }); res.render("admin-logs", { title: "Logs", logs, logFilters: { range: range.rangeValue, level: levelValue, limit: limit.limitValue } }); }); app.get("/admin/logs/download", requireRole("admin"), (req, res) => { const range = parseLogRange(req.query.range); const limit = parseLogLimit(req.query.limit, { allowAll: true }); const levels = parseLogLevels(req.query.level); const sinceMs = range.rangeMs ? Date.now() - range.rangeMs : null; const logs = listLogs({ limit: limit.limit, sinceMs, levels }); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); res.setHeader( "Content-Disposition", `attachment; filename="lumi-logs-${stamp}.txt"` ); res.setHeader("Content-Type", "text/plain; charset=utf-8"); const lines = logs.map((log) => { const timestamp = new Date(log.created_at).toISOString(); const header = `${timestamp} [${log.level.toUpperCase()}] ${log.message}`; if (log.details) { return `${header}\n${log.details}\n`; } return `${header}\n`; }); res.send(lines.join("\n")); }); app.get("/admin/privileges", requireRole("admin"), async (req, res) => { const discord = await buildDiscordPrivileges(discordClient); const twitch = await buildTwitchPrivileges(); res.render("admin-privileges", { title: "Privileges", discord, twitch }); }); app.get("/admin/commands", requireRole("mod"), (req, res) => { const platformStatus = getPlatformStatus().filter((platform) => platform.supported); const availablePlatforms = platformStatus.map((platform) => platform.id); const commands = db .prepare("SELECT * FROM custom_commands ORDER BY trigger") .all() .map((command) => ({ ...command, platforms: normalizeCustomPlatforms(command.platform, availablePlatforms) })); res.render("admin-commands", { title: "Custom commands", commands, isAdmin: Boolean(req.session.user?.isAdmin), platforms: platformStatus.map((platform) => ({ id: platform.id, label: platform.label, enabled: platform.enabled })) }); }); app.post("/admin/commands", requireRole("mod"), (req, res) => { const isAdmin = Boolean(req.session.user?.isAdmin); const availablePlatforms = getPlatformStatus() .filter((platform) => platform.supported) .map((platform) => platform.id); const trigger = (req.body.trigger || "").trim().toLowerCase(); const mode = (req.body.mode || "plain").trim(); const language = (req.body.language || "js").trim(); const response = (req.body.response || "").trim(); const code = (req.body.code || "").trim(); const selectedPlatforms = parsePlatformSelectionFromBody( req.body, availablePlatforms ); if (!trigger) { setFlash(req, "error", "Trigger is required."); return res.redirect("/admin/commands"); } if (!selectedPlatforms.length) { setFlash(req, "error", "Select at least one platform."); return res.redirect("/admin/commands"); } if (!isAdmin && mode === "advanced") { setFlash(req, "error", "Advanced commands are restricted to admins."); return res.redirect("/admin/commands"); } if (mode === "advanced") { if (!code) { setFlash(req, "error", "Advanced commands require code."); return res.redirect("/admin/commands"); } } else if (!response) { setFlash(req, "error", "Plain commands require a response."); return res.redirect("/admin/commands"); } const now = Date.now(); try { db.prepare( "INSERT INTO custom_commands (trigger, response, mode, language, code, platform, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)" ).run( trigger, response || "", isAdmin ? mode : "plain", isAdmin ? language : "js", isAdmin && mode === "advanced" ? code : null, serializePlatformSelection(selectedPlatforms), now, now ); setFlash(req, "success", "Command created."); res.redirect("/admin/commands"); } catch (error) { setFlash(req, "error", "That command already exists."); res.redirect("/admin/commands"); } }); app.post("/admin/commands/:id/toggle", requireRole("mod"), (req, res) => { const row = db .prepare("SELECT enabled FROM custom_commands WHERE id = ?") .get(req.params.id); if (row) { db.prepare( "UPDATE custom_commands SET enabled = ?, updated_at = ? WHERE id = ?" ).run(row.enabled ? 0 : 1, Date.now(), req.params.id); } res.redirect("/admin/commands"); }); app.post("/admin/commands/:id/delete", requireRole("mod"), (req, res) => { db.prepare("DELETE FROM custom_commands WHERE id = ?").run(req.params.id); setFlash(req, "success", "Command deleted."); res.redirect("/admin/commands"); }); app.post("/admin/commands/:id/update", requireRole("mod"), (req, res) => { const isAdmin = Boolean(req.session.user?.isAdmin); const existing = db .prepare("SELECT mode FROM custom_commands WHERE id = ?") .get(req.params.id); if (existing?.mode === "advanced" && !isAdmin) { setFlash(req, "error", "Advanced commands can only be edited by admins."); return res.redirect("/admin/commands"); } const availablePlatforms = getPlatformStatus() .filter((platform) => platform.supported) .map((platform) => platform.id); const trigger = (req.body.trigger || "").trim().toLowerCase(); const mode = (req.body.mode || "plain").trim(); const language = (req.body.language || "js").trim(); const response = (req.body.response || "").trim(); const code = (req.body.code || "").trim(); const selectedPlatforms = parsePlatformSelectionFromBody( req.body, availablePlatforms ); if (!trigger) { setFlash(req, "error", "Trigger is required."); return res.redirect("/admin/commands"); } if (!selectedPlatforms.length) { setFlash(req, "error", "Select at least one platform."); return res.redirect("/admin/commands"); } if (!isAdmin && mode === "advanced") { setFlash(req, "error", "Advanced commands are restricted to admins."); return res.redirect("/admin/commands"); } if (mode === "advanced") { if (!code) { setFlash(req, "error", "Advanced commands require code."); return res.redirect("/admin/commands"); } } else if (!response) { setFlash(req, "error", "Plain commands require a response."); return res.redirect("/admin/commands"); } try { db.prepare( "UPDATE custom_commands SET trigger = ?, response = ?, mode = ?, language = ?, code = ?, platform = ?, updated_at = ? WHERE id = ?" ).run( trigger, response || "", isAdmin ? mode : "plain", isAdmin ? language : "js", isAdmin && mode === "advanced" ? code : null, serializePlatformSelection(selectedPlatforms), Date.now(), req.params.id ); setFlash(req, "success", "Command updated."); res.redirect("/admin/commands"); } catch (error) { setFlash(req, "error", "Unable to update command."); res.redirect("/admin/commands"); } }); app.get("/admin/pages", requireRole("admin"), (req, res) => { const pages = db .prepare("SELECT * FROM custom_pages ORDER BY created_at DESC") .all(); res.render("admin-pages", { title: "Custom pages", pages }); }); app.get("/admin/users", requireRole("mod"), (req, res) => { const users = listUsersWithIdentities(); const notes = db .prepare( "SELECT n.*, s.internal_user_id FROM moderation_notes n " + "LEFT JOIN moderation_subjects s ON s.id = n.subject_id" ) .all(); const notesByUser = notes.reduce((acc, note) => { if (!note.internal_user_id) { return acc; } if (!acc[note.internal_user_id]) { acc[note.internal_user_id] = []; } acc[note.internal_user_id].push(note); return acc; }, {}); res.render("admin-users", { title: "Users", users, notesByUser, isAdmin: Boolean(req.session.user?.isAdmin) }); }); app.post("/admin/users/:id/username", requireRole("admin"), (req, res) => { const desired = (req.body.internal_username || "").trim(); const result = updateInternalUsername(req.params.id, desired); if (!result.ok) { setFlash(req, "error", result.reason); return res.redirect("/admin/users"); } setFlash(req, "success", "Username updated."); res.redirect("/admin/users"); }); app.post("/admin/pages", requireRole("admin"), (req, res) => { const slug = (req.body.slug || "").trim(); const title = (req.body.title || "").trim(); const navLabel = (req.body.nav_label || "").trim(); const content = (req.body.content || "").trim(); const format = normalizePageFormat(req.body.format); const contentCss = format === "html" ? (req.body.content_css || "").trim() : ""; const role = (req.body.role || "public").trim(); const showInNav = req.body.show_in_nav === "on"; if (!slug || !title || !content) { setFlash(req, "error", "Slug, title, and content are required."); return res.redirect("/admin/pages"); } const now = Date.now(); try { db.prepare( "INSERT INTO custom_pages (slug, title, nav_label, content, content_css, format, role, show_in_nav, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)" ).run( slug, title, navLabel || null, content, contentCss, format, role, showInNav ? 1 : 0, now, now ); setFlash(req, "success", "Page created."); res.redirect("/admin/pages"); } catch (error) { setFlash(req, "error", "That page already exists."); res.redirect("/admin/pages"); } }); app.post("/admin/pages/:id/toggle", requireRole("admin"), (req, res) => { const row = db .prepare("SELECT enabled FROM custom_pages WHERE id = ?") .get(req.params.id); if (row) { db.prepare( "UPDATE custom_pages SET enabled = ?, updated_at = ? WHERE id = ?" ).run(row.enabled ? 0 : 1, Date.now(), req.params.id); } res.redirect("/admin/pages"); }); app.post("/admin/pages/:id/delete", requireRole("admin"), (req, res) => { db.prepare("DELETE FROM custom_pages WHERE id = ?").run(req.params.id); setFlash(req, "success", "Page deleted."); res.redirect("/admin/pages"); }); app.post("/admin/pages/:id/update", requireRole("admin"), (req, res) => { const slug = (req.body.slug || "").trim(); const title = (req.body.title || "").trim(); const navLabel = (req.body.nav_label || "").trim(); const content = (req.body.content || "").trim(); const format = normalizePageFormat(req.body.format); const contentCss = format === "html" ? (req.body.content_css || "").trim() : ""; const role = (req.body.role || "public").trim(); const showInNav = req.body.show_in_nav === "on"; if (!slug || !title || !content) { setFlash(req, "error", "Slug, title, and content are required."); return res.redirect("/admin/pages"); } try { db.prepare( "UPDATE custom_pages SET slug = ?, title = ?, nav_label = ?, content = ?, content_css = ?, format = ?, role = ?, show_in_nav = ?, updated_at = ? WHERE id = ?" ).run( slug, title, navLabel || null, content, contentCss, format, role, showInNav ? 1 : 0, Date.now(), req.params.id ); setFlash(req, "success", "Page updated."); res.redirect("/admin/pages"); } catch (error) { setFlash(req, "error", "Unable to update page."); res.redirect("/admin/pages"); } }); app.get("/admin/plugins", requireRole("admin"), (req, res) => { syncPluginRegistry(); res.render("admin-plugins", { title: "Plugins", plugins: getPlugins() }); }); app.post("/admin/plugins/:id/toggle", requireRole("admin"), (req, res) => { setPluginEnabled(req.params.id, req.body.enabled === "true"); setFlash(req, "success", "Plugin updated. Restarting..."); res.redirect("/admin/plugins"); requestRestart(); }); app.post("/admin/plugins/:id/uninstall", requireRole("admin"), (req, res) => { const plugin = db .prepare("SELECT path FROM plugins WHERE id = ?") .get(req.params.id); if (plugin?.path) { try { fs.rmSync(plugin.path, { recursive: true, force: true }); } catch (error) { console.error(error); } } removePlugin(req.params.id); setFlash(req, "success", "Plugin uninstalled. Restarting..."); res.redirect("/admin/plugins"); requestRestart(); }); app.post( "/admin/plugins/upload", requireRole("admin"), uploadSingle("plugin_zip"), async (req, res) => { if (req.uploadError) { setFlash(req, "error", req.uploadError); return res.redirect("/admin/plugins"); } if (!req.file) { setFlash(req, "error", "Upload a ZIP archive."); return res.redirect("/admin/plugins"); } try { await applyPluginUpdate(req.file.path); setFlash(req, "success", "Plugin uploaded. Restarting..."); res.redirect("/admin/plugins"); requestRestart(); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin/plugins"); } finally { try { fs.rmSync(req.file.path, { force: true }); } catch { // ignore cleanup errors } } } ); app.post("/admin/plugins/install", requireRole("admin"), (req, res) => { const url = (req.body.url || "").trim(); if (!url) { setFlash(req, "error", "Plugin URL is required."); return res.redirect("/admin/plugins"); } try { installFromGit(url); setFlash(req, "success", "Plugin installed. Restarting..."); res.redirect("/admin/plugins"); requestRestart(); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin/plugins"); } }); app.post("/admin/plugins/create", requireRole("admin"), (req, res) => { const id = (req.body.id || "").trim(); const name = (req.body.name || "").trim(); const description = (req.body.description || "").trim(); try { createLocalPlugin({ id, name, description }); setFlash(req, "success", "Plugin created. Restarting..."); res.redirect("/admin/plugins"); requestRestart(); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin/plugins"); } }); app.post("/admin/plugins/:id/update", requireRole("admin"), (req, res) => { const plugin = db .prepare("SELECT path FROM plugins WHERE id = ?") .get(req.params.id); if (!plugin?.path) { setFlash(req, "error", "Plugin not found."); return res.redirect("/admin/plugins"); } try { updatePluginFromGit(plugin.path); setFlash(req, "success", "Plugin updated. Restarting..."); res.redirect("/admin/plugins"); requestRestart(); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin/plugins"); } }); app.get("/admin/updates", requireRole("admin"), (req, res) => { res.render("admin-updates", { title: "Updates", snapshots: listSnapshots() }); }); app.post( "/admin/updates/bot", requireRole("admin"), uploadSingle("update_zip"), async (req, res) => { if (req.uploadError) { setFlash(req, "error", req.uploadError); return res.redirect("/admin/updates"); } if (!req.file) { setFlash(req, "error", "Upload a ZIP archive."); return res.redirect("/admin/updates"); } try { const patchMode = req.body.patch_mode === "1"; await applyBotUpdate(req.file.path, { mode: patchMode ? "patch" : "full" }); setFlash( req, "success", patchMode ? "Patch applied. Restarting..." : "Update applied. Restarting..." ); res.redirect("/admin/updates"); requestRestart(); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin/updates"); } finally { try { fs.rmSync(req.file.path, { force: true }); } catch { // ignore cleanup errors } } } ); app.post( "/admin/updates/plugin", requireRole("admin"), uploadSingle("plugin_zip"), async (req, res) => { if (req.uploadError) { setFlash(req, "error", req.uploadError); return res.redirect("/admin/updates"); } if (!req.file) { setFlash(req, "error", "Upload a ZIP archive."); return res.redirect("/admin/updates"); } try { await applyPluginUpdate(req.file.path); setFlash(req, "success", "Plugin update applied. Restarting..."); res.redirect("/admin/updates"); requestRestart(); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin/updates"); } finally { try { fs.rmSync(req.file.path, { force: true }); } catch { // ignore cleanup errors } } } ); app.post("/admin/update", requireRole("admin"), (req, res) => { try { const remote = getSetting("git_remote", "origin"); const branch = getSetting("git_branch", "main"); pullUpdates(remote, branch); setFlash(req, "success", "Update applied. Restarting..."); res.redirect("/admin"); requestRestart(); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin"); } }); app.post("/admin/check-update", requireRole("admin"), (req, res) => { try { const remote = getSetting("git_remote", "origin"); const branch = getSetting("git_branch", "main"); const hasUpdate = checkForUpdates(remote, branch); setFlash( req, "info", hasUpdate ? "Updates are available." : "No updates found." ); res.redirect("/admin"); } catch (error) { setFlash(req, "error", error.message); res.redirect("/admin"); } }); app.post("/admin/restart", requireRole("admin"), (req, res) => { setFlash(req, "success", "Restarting..."); res.redirect("/admin"); requestRestart(); }); app.use((err, req, res, next) => { if (res.headersSent) { return next(err); } const message = err?.message || ""; const isViewMissing = message.includes("Failed to lookup view"); log("error", "Unhandled error", { path: req.path, method: req.method, userId: req.session.user?.id || null, message, stack: err?.stack || "" }); if (isViewMissing) { res.locals.softError = "Some content could not be loaded."; return res.status(200).render("missing-view", { title: "Content missing", resource: "A page component failed to load. Please try again." }); } const status = err?.status || 500; res.status(status).render("error", { title: "Something went wrong", message: "An unexpected error occurred. Please try again." }); }); if (typeof loadPlugins === "function") { loadPlugins(app, web, webhooks); } return app; } const NAV_SECTION_ICONS = ["home", "spark", "shield", "gear", "blocks"]; const DEFAULT_NAV_SECTIONS = [ { id: "overview", label: "Overview", icon: "home" }, { id: "community", label: "Community", icon: "spark" }, { id: "moderation", label: "Mod", icon: "shield" }, { id: "admin", label: "Admin", icon: "gear" }, { id: "plugins", label: "Plugins", icon: "blocks" } ]; function collectNavItems(user, pluginNav, currentPath) { const iconMap = getSetting("nav_item_icons", {}) || {}; const base = [ { label: "Home", path: "/", role: "public", section: "overview" }, { label: "Commands", path: "/commands", role: "public", section: "community" }, { label: "Leaderboards", path: "/leaderboards", role: "public", section: "community" }, { label: "Stats", path: "/stats", role: "public", authRequired: true, section: "community" }, { label: "Profile", path: "/profile", role: "public", authRequired: true, section: "community" }, { label: "Mods List", path: "/moderator", role: "mod", section: "moderation" }, { label: "Admin", path: "/admin", role: "admin", section: "admin" }, { label: "Settings", path: "/admin/settings", role: "admin", section: "admin" }, { label: "Navigation", path: "/admin/navigation", role: "admin", section: "admin" }, { label: "Theming", path: "/admin/theming", role: "admin", section: "admin" }, { label: "Privileges", path: "/admin/privileges", role: "admin", section: "admin" }, { label: "Logs", path: "/admin/logs", role: "admin", section: "admin" }, { label: "Updates", path: "/admin/updates", role: "admin", section: "admin" }, { label: "Custom commands", path: "/admin/commands", role: "mod", section: "moderation" }, { label: "Pages", path: "/admin/pages", role: "admin", section: "admin" }, { label: "Users", path: "/admin/users", role: "mod", section: "moderation" }, { label: "Plugins", path: "/admin/plugins", role: "admin", section: "admin" } ]; const pages = db .prepare( "SELECT slug, title, nav_label, role FROM custom_pages WHERE show_in_nav = 1 AND enabled = 1 ORDER BY created_at ASC" ) .all() .map((page) => ({ label: page.nav_label || page.title, path: `/pages/${page.slug}`, role: page.role, section: "community" })); const pluginItems = (pluginNav || []).map((item) => ({ ...item, section: item.section || "plugins" })); const allItems = [...base, ...pages, ...pluginItems].filter((item) => { if (item.authRequired && !user) { return false; } return hasAccess(user, item.role); }); return allItems.map((item) => { const navId = buildNavItemId(item); const customIcon = iconMap[navId] || null; const defaultIcon = getDefaultNavIcon(item); const icon = customIcon ? `/assets/nav-icons/${customIcon}` : defaultIcon ? `/icons/nav/${defaultIcon}.svg` : null; return { ...item, navId, icon, active: isActivePath(item.path, currentPath) }; }); } function buildNavSections(user, pluginNav, currentPath) { const items = collectNavItems(user, pluginNav, currentPath); const navStructure = normalizeNavStructure(getSetting("nav_structure", null)); if (navStructure?.enabled && navStructure.sections.length) { return buildNavSectionsFromStructure(items, navStructure); } return buildDefaultNavSections(items); } function buildDefaultNavSections(items) { const sections = DEFAULT_NAV_SECTIONS.map((section) => ({ ...section, items: [] })); const sectionMap = new Map(sections.map((section) => [section.id, section])); for (const item of items) { const section = sectionMap.get(item.section); if (section) { section.items.push(item); } } return sections .filter((section) => section.items.length) .map((section) => ({ ...section, items: section.items.slice().sort((a, b) => a.label.localeCompare(b.label)), open: section.items.some((item) => item.active) })); } function buildNavSectionsFromStructure(items, structure) { const itemMap = new Map(items.map((item) => [item.navId, item])); const used = new Set(); const sections = []; const pushSection = (section, sectionItems) => { if (!sectionItems.length) { return; } sections.push({ id: section.id, label: section.label || toTitleCase(section.id) || section.id, icon: normalizeNavSectionIcon(section.icon) || "blocks", items: sectionItems, open: sectionItems.some((item) => item.active) }); }; for (const section of structure.sections) { if (!section?.id) { continue; } const sectionItems = []; for (const rawId of section.items || []) { const navId = (rawId || "").toString().trim(); if (!navId || used.has(navId)) { continue; } const item = itemMap.get(navId); if (!item) { continue; } sectionItems.push(item); used.add(navId); } pushSection(section, sectionItems); } if (structure.includeUnassigned !== false) { const unassigned = items.filter((item) => !used.has(item.navId)); if (unassigned.length) { pushSection( { id: structure.unassignedId || "other", label: structure.unassignedLabel || "Other", icon: structure.unassignedIcon || "blocks" }, unassigned ); } } return sections; } function buildNavIconItems(user, pluginNav, currentPath) { return collectNavItems(user, pluginNav, currentPath).map((item) => ({ id: item.navId, label: item.label, path: item.path, icon: item.icon })); } function buildDefaultNavStructure(items) { const sections = DEFAULT_NAV_SECTIONS.map((section) => ({ id: section.id, label: section.label, icon: section.icon, items: [] })); const sectionMap = new Map(sections.map((section) => [section.id, section])); items.forEach((item) => { const section = sectionMap.get(item.section); if (section) { section.items.push(item); } }); sections.forEach((section) => { section.items = section.items .slice() .sort((a, b) => a.label.localeCompare(b.label)) .map((item) => item.navId); }); return { enabled: false, includeUnassigned: true, sections }; } function normalizeNavStructure(raw) { if (!raw) { return null; } let value = raw; if (typeof value === "string") { try { value = JSON.parse(value); } catch { return null; } } if (!value || typeof value !== "object") { return null; } const sections = Array.isArray(value.sections) ? value.sections.map(normalizeNavSection).filter(Boolean) : []; return { enabled: Boolean(value.enabled), includeUnassigned: value.includeUnassigned !== false, unassignedLabel: typeof value.unassignedLabel === "string" && value.unassignedLabel.trim() ? value.unassignedLabel.trim() : null, unassignedIcon: normalizeNavSectionIcon(value.unassignedIcon), unassignedId: typeof value.unassignedId === "string" && value.unassignedId.trim() ? value.unassignedId.trim() : null, sections }; } function normalizeNavSection(section, index) { if (!section || typeof section !== "object") { return null; } const id = (section.id || section.label || `section-${index + 1}`) .toString() .trim(); if (!id) { return null; } const label = (section.label || toTitleCase(id) || id).toString().trim(); const icon = normalizeNavSectionIcon(section.icon) || "blocks"; const items = Array.isArray(section.items) ? section.items.map((item) => item.toString().trim()).filter(Boolean) : []; return { id, label, icon, items }; } function normalizeNavSectionIcon(value) { if (!value) { return null; } const normalized = value.toString().trim().toLowerCase(); return NAV_SECTION_ICONS.includes(normalized) ? normalized : null; } function buildNavItemId(item) { const base = (item.path || item.label || "item").toString().toLowerCase(); return base.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, ""); } function getDefaultNavIcon(item) { const pathName = (item.path || "").toLowerCase(); if (pathName === "/") return "home"; if (pathName === "/commands") return "commands"; if (pathName === "/leaderboards") return "leaderboards"; if (pathName === "/stats") return "stats"; if (pathName === "/profile") return "profile"; if (pathName === "/moderator") return "users"; if (pathName.startsWith("/pages/")) return "pages"; if (pathName === "/admin") return "admin"; if (pathName === "/admin/settings") return "settings"; if (pathName === "/admin/navigation") return "settings"; if (pathName === "/admin/theming") return "theming"; if (pathName === "/admin/privileges") return "privileges"; if (pathName === "/admin/logs") return "logs"; if (pathName === "/admin/updates") return "updates"; if (pathName === "/admin/commands") return "commands"; if (pathName === "/admin/pages") return "pages"; if (pathName === "/admin/users") return "users"; if (pathName === "/admin/plugins") return "plugins"; if (pathName === "/moderator") return "users"; if (pathName.startsWith("/plugins/moderation/tos-bans")) return "moderation"; if (pathName.startsWith("/plugins/moderation")) return "moderation"; if (pathName.startsWith("/plugins")) return "plugins"; if (pathName.startsWith("/moderator")) return "moderation"; return "pages"; } function isActivePath(itemPath, currentPath) { if (!itemPath || !currentPath) { return false; } if (itemPath === "/") { return currentPath === "/"; } return currentPath === itemPath || currentPath.startsWith(`${itemPath}/`); } module.exports = { createWebServer };