Lumi/src/web/server.js
2026-06-18 22:16:35 +02:00

6577 lines
211 KiB
JavaScript

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 {
deleteCustomTheme,
duplicateTheme,
FONT_STACKS,
getActiveTheme,
getThemeById,
listThemes,
renameCustomTheme,
saveCustomTheme,
setActiveTheme,
valuesFromRequest
} = require("../services/themes");
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,
searchKnownUsers
} = 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");
const { getUpdateStatus } = require("../services/update-index");
const {
applyCoreUpdate,
applyPluginUpdateFromRepo,
revertCoreSnapshot,
revertPluginSnapshot,
disablePluginForRecovery
} = require("../services/repo-update");
const {
safeModeStatus,
clearRecoveryMarker,
updateRecoveryMarker
} = require("../services/recovery-mode");
const {
generateCommandPreview,
previewParts
} = require("../services/command-preview");
const {
isDestructivePath,
issueConfirmation,
consumeConfirmation,
normalizeAction
} = require("../services/destructive-confirm");
const {
publishWebEvent,
subscribeWebEvents
} = require("../services/web-events");
const {
addSubmitterComment,
adminUpdateFeedback,
cleanupFeedback,
createFeedback,
deleteFeedback,
findSimilarFeedback,
feedbackOptions,
getFeedbackForSubmitter,
getFeedbackForAdmin,
getFeedbackAttachment,
getFeedbackForViewer,
listFeedbackForAdmin,
listMyFeedback,
listPublicFeedback,
markFeedbackViewed,
notificationSummary,
supportFeedback
} = require("../services/feedback");
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 normalizeHostName(value) {
const raw = String(value || "").toLowerCase();
if (raw.startsWith("[")) {
return raw.slice(1, raw.indexOf("]"));
}
if (raw === "::1" || raw === "::ffff:127.0.0.1") {
return raw;
}
return raw.replace(/:\d+$/, "");
}
function isLocalhostLoginAvailable(req) {
if (!req) return false;
const host = normalizeHostName(req.hostname || req.get("host"));
return host === "localhost" || host === "::1" || host === "::ffff:127.0.0.1" || host === "127.0.0.1" || host.startsWith("127.");
}
function getLocalhostLoginPlatform(req) {
if (!isLocalhostLoginAvailable(req)) return null;
return {
id: "localhost",
label: "Localhost Login",
configured: true,
enabled: true,
supported: true,
supportsLogin: true,
loginPath: "/auth/localhost"
};
}
function getPrimaryLoginPlatform(req) {
const localhostPlatform = getLocalhostLoginPlatform(req);
if (localhostPlatform) return localhostPlatform;
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(req) {
const platform = getPrimaryLoginPlatform(req);
return platform?.loginPath || "/setup";
}
function requireConfigured(req, res, next) {
if (!isConfigured() && !isLocalhostLoginAvailable(req) && !req.path.startsWith("/setup")) {
return res.redirect("/setup");
}
next();
}
function requireAuth(req, res, next) {
if (!req.session.user) {
return res.redirect(getLoginRedirectPath(req));
}
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(req));
}
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 emptyCommandPreview() {
return {
preview_text: null,
preview_status: null,
preview_error: null,
preview_generated_at: null,
preview_dynamic_segments: "[]"
};
}
function buildCommandPreviewPresentation(command) {
const text = String(command.preview_text || "");
return {
status: command.preview_status || (text ? "ready" : "unavailable"),
error: command.preview_error || "Run preview to generate an example output.",
generatedAt: command.preview_generated_at || null,
isLong: text.length > 160 || text.includes("\n"),
compactParts: previewParts(text, command.preview_dynamic_segments, 160),
fullParts: previewParts(text, command.preview_dynamic_segments)
};
}
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;
const DASHBOARD_SCOPES = {
"5m": 5 * 60 * 1000,
"24h": 24 * 60 * 60 * 1000,
"7d": 7 * 24 * 60 * 60 * 1000
};
const memorySamples = [];
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 parseDashboardScope(value) {
const normalized = String(value || "5m").trim().toLowerCase();
return DASHBOARD_SCOPES[normalized] ? normalized : "5m";
}
function recordMemorySample(memory = process.memoryUsage(), now = Date.now()) {
const last = memorySamples[memorySamples.length - 1];
if (!last || now - last.sampled_at >= 10 * 1000) {
memorySamples.push({
sampled_at: now,
rss: memory.rss,
heap_used: memory.heapUsed,
heap_total: memory.heapTotal
});
}
const oldest = now - DASHBOARD_SCOPES["7d"];
while (memorySamples.length && memorySamples[0].sampled_at < oldest) {
memorySamples.shift();
}
}
function scopedMemorySamples(now = Date.now()) {
return Object.fromEntries(
Object.entries(DASHBOARD_SCOPES).map(([scope, duration]) => [
scope,
memorySamples.filter((sample) => sample.sampled_at >= now - duration)
])
);
}
function countLogsByLevel(logs) {
const counts = { error: 0, warn: 0, info: 0, debug: 0 };
for (const entry of logs) {
if (counts[entry.level] !== undefined) counts[entry.level] += 1;
}
return counts;
}
function countVisibleCommands() {
const enabledPlatforms = getEnabledPlatformIds();
const commands = [];
const addCommand = (command) => {
if (!command.trigger) {
return;
}
commands.push({
trigger: command.trigger,
subcommand: command.subcommand || ""
});
};
try {
const customCommands = db
.prepare("SELECT trigger, platform FROM custom_commands WHERE enabled = 1")
.all();
for (const row of customCommands) {
const trigger = normalizeCommandTrigger(row.trigger);
const platforms = normalizeCustomPlatforms(row.platform, enabledPlatforms);
addCommand({ trigger, platforms });
}
} catch {
return 0;
}
const topOptions = getTopCommandOptions();
if (topOptions.length) {
addCommand({ trigger: "top", platforms: enabledPlatforms });
for (const option of topOptions) {
const subcommand = normalizeSubcommand(option.id);
addCommand({ trigger: "top", subcommand, platforms: enabledPlatforms });
}
}
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;
}
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);
const subcommand = normalizeSubcommand(command.subcommand);
const platforms = (Array.isArray(command.platforms) && command.platforms.length
? command.platforms
: enabledPlatforms
).filter((platform) => platformFlags[platform] && enabledPlatforms.includes(platform));
if (!platforms.length) {
continue;
}
addCommand({ trigger, subcommand, platforms });
}
}
const roots = new Set();
const subcommandTriggers = new Set();
let total = 0;
for (const command of commands) {
if (command.subcommand) {
subcommandTriggers.add(command.trigger);
total += 1;
} else {
roots.add(command.trigger);
total += 1;
}
}
for (const trigger of subcommandTriggers) {
if (!roots.has(trigger)) {
total += 1;
}
}
return total;
}
function normalizePageFormat(value) {
const normalized = (value || "").toString().trim().toLowerCase();
return normalized === "markdown" ? "markdown" : "html";
}
function escapeHtml(value) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
};
return (value || "").toString().replace(/[&<>"']/g, (char) => map[char]);
}
function renderMarkdownInline(value) {
let output = escapeHtml(value);
output = output.replace(/`([^`]+)`/g, "<code>$1</code>");
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${text}</a>`;
});
output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
output = output.replace(/\*([^*]+)\*/g, "<em>$1</em>");
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 += `<p>${renderMarkdownInline(paragraph.join(" "))}</p>`;
paragraph = [];
};
const closeList = () => {
if (!listType) {
return;
}
html += `</${listType}>`;
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 += `<pre><code${langClass}>${codeBlock}</code></pre>`;
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 += `<h${level}>${renderMarkdownInline(headingMatch[2])}</h${level}>`;
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 += `<li>${renderMarkdownInline(listMatch[2])}</li>`;
return;
}
paragraph.push(trimmed);
});
if (inCode) {
const codeBlock = escapeHtml(codeLines.join("\n"));
const langClass = codeLang ? ` class="language-${codeLang}"` : "";
html += `<pre><code${langClass}>${codeBlock}</code></pre>`;
}
flushParagraph();
closeList();
return html || "<p></p>";
}
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};`,
` --success: ${theme.light.success};`,
` --warning: ${theme.light.warning};`,
` --info: ${theme.light.info};`,
` --link: ${theme.light.link};`,
` --radius: ${theme.metrics.radius}px;`,
` --spacing-scale: ${theme.metrics.spacingScale};`,
"}",
"@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};`,
` --success: ${theme.dark.success};`,
` --warning: ${theme.dark.warning};`,
` --info: ${theme.dark.info};`,
` --link: ${theme.dark.link};`,
" }",
"}"
].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 [
"<!doctype html>",
"<html lang=\"en\">",
"<head>",
" <meta charset=\"utf-8\" />",
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />",
" <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Source+Sans+3:wght@400;600&display=swap\" />",
" <style>",
fullCss,
" </style>",
"</head>",
"<body>",
content,
"</body>",
"</html>"
].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 screenshotExtension(mime) {
if (mime === "image/jpeg") return ".jpg";
if (mime === "image/webp") return ".webp";
return ".png";
}
function feedbackAttachmentExtension(file = {}) {
const original = path.extname(file.originalname || "").slice(0, 12).toLowerCase();
if ([".png", ".jpg", ".jpeg", ".webp", ".pdf", ".txt"].includes(original)) return original;
if (file.mimetype === "application/pdf") return ".pdf";
if (file.mimetype === "text/plain") return ".txt";
return screenshotExtension(file.mimetype);
}
function normalizeFeedbackRequestBody(body = {}) {
return {
...body,
target_metadata: parseBodyJson(body.target_metadata),
diagnostics: parseBodyJson(body.diagnostics)
};
}
function parseBodyJson(value) {
if (value && typeof value === "object") return value;
try {
const parsed = JSON.parse(String(value || "{}"));
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
}
function resolveFeedbackScreenshotPath(relativePath, screenshotDir) {
const normalized = String(relativePath || "").replace(/\\/g, "/");
const prefix = "feedback/screenshots/";
if (!normalized.startsWith(prefix)) return null;
const fileName = path.basename(normalized.slice(prefix.length));
if (!fileName || fileName !== normalized.slice(prefix.length)) return null;
const target = path.resolve(screenshotDir, fileName);
const root = path.resolve(screenshotDir);
return target.startsWith(`${root}${path.sep}`) ? target : null;
}
function resolveFeedbackAttachmentPath(relativePath, attachmentDir) {
const normalized = String(relativePath || "").replace(/\\/g, "/");
const prefix = "feedback/attachments/";
if (!normalized.startsWith(prefix)) return null;
const fileName = path.basename(normalized.slice(prefix.length));
if (!fileName || fileName !== normalized.slice(prefix.length)) return null;
const target = path.resolve(attachmentDir, fileName);
const root = path.resolve(attachmentDir);
return target.startsWith(`${root}${path.sep}`) ? target : null;
}
function getThemeSettings() {
return getActiveTheme();
}
function parseJsonSetting(key, fallback) {
const value = getSetting(key, fallback);
if (Array.isArray(value)) return value;
if (typeof value === "string" && value.trim()) {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : fallback;
} catch {
return fallback;
}
}
return fallback;
}
const DISCORD_WIDGET_CACHE_MS = 5 * 60 * 1000;
const discordWidgetCache = new Map();
function safeExternalUrl(value) {
try {
const url = new URL(String(value || ""));
return ["http:", "https:"].includes(url.protocol) ? url.toString() : "";
} catch {
return "";
}
}
function safeHomepageLinkUrl(value) {
const raw = String(value || "").trim();
if (raw.startsWith("/") && !raw.startsWith("//")) {
return raw;
}
return safeExternalUrl(raw);
}
function isLocalHostname(hostname) {
return ["localhost", "127.0.0.1", "::1"].includes(String(hostname || "").toLowerCase());
}
function isDevRequest(req = null) {
return process.env.NODE_ENV !== "production" || isLocalHostname(homepageEmbedParent(req));
}
function safeEmbedUrl(value, options = {}) {
try {
const url = new URL(String(value || "").trim());
if (url.protocol === "https:") return url.toString();
if (url.protocol === "http:" && options.allowInsecure) return url.toString();
return "";
} catch {
return "";
}
}
function parseDomainList(value) {
return String(value || "")
.split(",")
.map((item) => item.trim().toLowerCase().replace(/^\*\./, ""))
.filter(Boolean);
}
function embedDomainAllowed(embedUrl, item) {
const domains = parseDomainList(item.allowed_domains);
if (!domains.length) return true;
try {
const host = new URL(embedUrl).hostname.toLowerCase();
return domains.some((domain) => host === domain || host.endsWith(`.${domain}`));
} catch {
return false;
}
}
function isDiscordUrl(value) {
try {
const host = new URL(value).hostname.toLowerCase().replace(/^www\./, "");
return host === "discord.gg" || host === "discord.com" || host.endsWith(".discord.com");
} catch {
return false;
}
}
function permissionAllows(user, permission = "public") {
const role = ["public", "user", "mod", "admin"].includes(permission) ? permission : "public";
return role === "public" ? true : hasAccess(user, role);
}
function fallbackIconForUrl(url) {
try {
if (String(url || "").startsWith("/")) return "L";
const host = new URL(url).hostname.replace(/^www\./, "");
return host.slice(0, 1).toUpperCase();
} catch {
return "↗";
}
}
function homepageLinksForUser(user) {
return parseJsonSetting("homepage_link_buttons", [])
.filter((item) => item && item.enabled !== false)
.filter((item) => permissionAllows(user, item.permission))
.map((item, index) => {
const url = safeHomepageLinkUrl(item.url);
if (!url) return null;
const iconMode = String(item.icon_mode || "").trim();
const iconUrl =
iconMode === "manual" ? safeExternalUrl(item.icon_url) :
iconMode === "favicon" ? safeExternalUrl(item.fetched_favicon_url) :
iconMode === "letter" ? "" :
safeExternalUrl(item.icon_url || item.fetched_favicon_url);
return {
id: String(item.id || `link-${index}`),
label: String(item.label || item.description || "External link").slice(0, 80),
description: String(item.description || item.label || "Open link").slice(0, 160),
url,
icon_url: iconUrl,
fallback_icon: fallbackIconForUrl(url),
permission: item.permission || "public",
sort_order: Number(item.sort_order) || index
};
})
.filter(Boolean)
.sort((a, b) => a.sort_order - b.sort_order);
}
async function homepageHeroForUser(user, req = null) {
const entries = parseJsonSetting("homepage_hero_entries", [])
.filter((item) => item && item.enabled !== false)
.filter((item) => permissionAllows(user, item.permission))
.sort((a, b) => (Number(a.priority) || 0) - (Number(b.priority) || 0));
for (const item of entries) {
let hero = normalizeHomepageHero(item, {
parentHost: homepageEmbedParent(req),
allowInsecureEmbeds: isDevRequest(req)
});
if (hero?.available && hero.type === "discord_server_overview") {
hero = await enrichDiscordHero(hero, item);
}
if (hero?.available) return hero;
if (hero?.render_error && hasAccess(user, "admin")) return hero;
}
return null;
}
function normalizeHomepageHero(item, options = {}) {
const type = String(item.type || "none");
if (type === "none") {
return item.fallback_behavior === "message"
? { type, available: true, title: item.title || "No featured content", description: item.description || "" }
: null;
}
const sourceUrl = safeExternalUrl(item.source_url);
const embedUrl = safeEmbedUrl(item.embed_url, { allowInsecure: options.allowInsecureEmbeds }) || deriveHomepageEmbedUrl(type, item, options);
const imageUrl = safeExternalUrl(item.image_url);
const fallback = homepageHeroFallback(item);
const title = String(item.title || "Featured content").slice(0, 120);
const description = String(item.description || "").slice(0, 500);
if (type === "static_image" && imageUrl) return { type, available: true, title, description, image_url: imageUrl, source_url: sourceUrl };
if (type === "custom_link" && sourceUrl) return { type, available: true, title, description, source_url: sourceUrl };
if (type === "custom_embed" && embedUrl && !isDiscordUrl(embedUrl) && embedDomainAllowed(embedUrl, item)) {
return {
type,
available: true,
title,
description,
embed_url: embedUrl,
source_url: sourceUrl,
iframe_sandbox: "allow-scripts allow-same-origin allow-presentation allow-popups",
iframe_allow: customEmbedAllow(item),
aspect_ratio: safeAspectRatio(item.aspect_ratio),
embed_height: safeEmbedHeight(item.embed_height),
...fallback
};
}
if (type === "youtube_video") {
const videoId = item.video_id || youtubeVideoId(item.source_url) || youtubeVideoId(sourceUrl);
if (videoId) return { type, available: true, title, description, embed_url: youtubeVideoEmbedUrl(videoId, item, sourceUrl), source_url: sourceUrl, iframe_allow: "autoplay; encrypted-media; picture-in-picture; web-share" };
}
if (type === "discord_server_overview") {
const guildId = item.video_id || discordServerId(item.source_url) || discordServerId(sourceUrl);
if (guildId) return { type, available: true, title, description, source_url: sourceUrl, discord_guild_id: guildId, ...fallback };
}
if (["youtube_channel", "twitch_stream"].includes(type) && (embedUrl || sourceUrl)) {
if (type === "twitch_stream" && item.availability_mode === "live_only" && item.live_now !== true) return null;
return { type, available: Boolean(embedUrl), title, description, embed_url: embedUrl, source_url: sourceUrl, render_error: embedUrl ? "" : heroRenderError(type) };
}
return {
type,
available: false,
title,
description,
source_url: sourceUrl,
...fallback,
render_error: heroRenderError(type)
};
}
function youtubeVideoId(value) {
const raw = String(value || "").trim();
if (/^[A-Za-z0-9_-]{11}$/.test(raw)) return raw;
try {
const url = new URL(raw);
if (url.hostname.includes("youtu.be")) return url.pathname.split("/").filter(Boolean)[0] || "";
const pathMatch = url.pathname.match(/\/(?:embed|shorts|live)\/([^/?#]+)/);
return pathMatch?.[1] || url.searchParams.get("v") || "";
} catch {
return "";
}
}
function youtubeStartSeconds(value, fallbackSource = "") {
const explicit = Number(value);
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
try {
const url = new URL(fallbackSource || "");
const raw = url.searchParams.get("start") || url.searchParams.get("t") || "";
if (/^\d+$/.test(raw)) return Number(raw);
const match = raw.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/i);
if (!match) return 0;
return ((Number(match[1]) || 0) * 3600) + ((Number(match[2]) || 0) * 60) + (Number(match[3]) || 0);
} catch {
return 0;
}
}
function youtubeVideoEmbedUrl(videoId, item = {}, sourceUrl = "") {
const url = new URL(`https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}`);
const autoplay = item.autoplay_mode && item.autoplay_mode !== "off";
if (autoplay) {
url.searchParams.set("autoplay", "1");
if (item.autoplay_mode !== "sound") url.searchParams.set("mute", "1");
}
if (item.show_controls === false) url.searchParams.set("controls", "0");
if (item.loop === true) {
url.searchParams.set("loop", "1");
url.searchParams.set("playlist", videoId);
}
const start = youtubeStartSeconds(item.start_seconds, sourceUrl || item.source_url);
if (start > 0) url.searchParams.set("start", String(start));
return url.toString();
}
function youtubeChannelId(value) {
try {
const url = new URL(value || "");
const match = url.pathname.match(/\/(?:channel|c|user)\/([^/?#]+)/);
return match?.[1] || "";
} catch {
return "";
}
}
function twitchChannelName(value) {
try {
const url = new URL(value || "");
const host = url.hostname.replace(/^www\./, "");
if (!host.endsWith("twitch.tv")) return "";
const first = url.pathname.split("/").filter(Boolean)[0] || "";
return ["videos", "directory", "p"].includes(first) ? "" : first;
} catch {
return "";
}
}
function discordServerId(value) {
const raw = String(value || "").trim();
if (/^\d{10,30}$/.test(raw)) return raw;
try {
const url = new URL(raw);
return url.searchParams.get("id") || "";
} catch {
return "";
}
}
function deriveHomepageEmbedUrl(type, item, options = {}) {
const sourceUrl = safeExternalUrl(item.source_url);
const parentHost = options.parentHost || "localhost";
const allowInsecure = Boolean(options.allowInsecureEmbeds);
if (type === "custom_embed") {
const manual = safeEmbedUrl(item.embed_url, { allowInsecure });
if (manual && !isDiscordUrl(manual)) return manual;
const direct = safeEmbedUrl(item.source_url, { allowInsecure });
if (!direct || isDiscordUrl(direct)) return "";
const youtube = youtubeVideoId(sourceUrl);
if (youtube) return `https://www.youtube-nocookie.com/embed/${youtube}`;
const twitch = twitchChannelName(sourceUrl);
if (twitch) return twitchEmbedUrl(twitch, parentHost);
return direct;
}
if (type === "youtube_video") {
const video = item.video_id || youtubeVideoId(item.source_url) || youtubeVideoId(sourceUrl);
return video ? youtubeVideoEmbedUrl(video, item, sourceUrl) : "";
}
if (type === "youtube_channel") {
const channel = item.video_id || youtubeChannelId(sourceUrl);
return channel ? `https://www.youtube.com/embed/live_stream?channel=${encodeURIComponent(channel)}` : "";
}
if (type === "twitch_stream") {
const channel = item.video_id || twitchChannelName(sourceUrl);
return channel ? twitchEmbedUrl(channel, parentHost) : "";
}
return "";
}
function homepageHeroFallback(item) {
return {
fallback_image_url: safeExternalUrl(item.fallback_image_url),
fallback_url: safeExternalUrl(item.fallback_url) || safeExternalUrl(item.source_url),
fallback_text: String(item.fallback_text || "").slice(0, 500),
fallback_behavior: ["hide", "message", "link"].includes(item.fallback_behavior) ? item.fallback_behavior : "message"
};
}
function safeAspectRatio(value) {
const raw = String(value || "").trim();
return /^\d+(\.\d+)?\s*\/\s*\d+(\.\d+)?$/.test(raw) ? raw.replace(/\s+/g, "") : "";
}
function safeEmbedHeight(value) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) return 0;
return Math.min(1200, Math.max(180, Math.floor(number)));
}
function customEmbedAllow(item) {
const base = new Set(["fullscreen", "autoplay"]);
String(item.iframe_allow || "")
.split(";")
.map((part) => part.trim().toLowerCase())
.filter(Boolean)
.forEach((part) => {
if (["fullscreen", "autoplay", "encrypted-media", "picture-in-picture"].includes(part)) {
base.add(part);
}
if (item.allow_unsafe_permissions === true && ["camera", "microphone", "clipboard-write"].includes(part)) {
base.add(part);
}
});
return Array.from(base).join("; ");
}
async function enrichDiscordHero(hero, item) {
try {
const widget = await fetchDiscordWidget(hero.discord_guild_id);
return {
...hero,
discord_widget: {
name: String(widget.name || hero.title || "Discord server").slice(0, 120),
invite_url: safeExternalUrl(widget.instant_invite) || hero.fallback_url,
presence_count: Number(widget.presence_count) || 0,
channels: Array.isArray(widget.channels)
? widget.channels.slice(0, 8).map((channel) => String(channel.name || "").slice(0, 80)).filter(Boolean)
: [],
members: Array.isArray(widget.members)
? widget.members.slice(0, 8).map((member) => ({
username: String(member.username || "").slice(0, 80),
status: String(member.status || "").slice(0, 30),
avatar_url: safeExternalUrl(member.avatar_url)
})).filter((member) => member.username)
: []
}
};
} catch (error) {
if (hero.fallback_behavior === "hide") return { ...hero, available: false };
return {
...hero,
available: Boolean(hero.fallback_url || hero.fallback_text || hero.fallback_image_url),
render_error: error?.message || "Discord Server Widget is unavailable. Enable the server widget in Discord or add a fallback link."
};
}
}
async function fetchDiscordWidget(guildId) {
const id = String(guildId || "").trim();
if (!/^\d{10,30}$/.test(id)) {
throw new Error("Add a valid Discord server ID.");
}
const now = Date.now();
const cached = discordWidgetCache.get(id);
if (cached && cached.expires > now) {
if (cached.error) throw new Error(cached.error);
return cached.data;
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(`https://discord.com/api/guilds/${encodeURIComponent(id)}/widget.json`, {
headers: { Accept: "application/json" },
signal: controller.signal
});
if (!response.ok) {
const message = response.status === 403 || response.status === 404
? "Discord Server Widget is disabled or unavailable for this server."
: `Discord widget request failed with HTTP ${response.status}.`;
discordWidgetCache.set(id, { expires: now + DISCORD_WIDGET_CACHE_MS, error: message });
throw new Error(message);
}
const data = await response.json();
discordWidgetCache.set(id, { expires: now + DISCORD_WIDGET_CACHE_MS, data });
return data;
} catch (error) {
const message = error?.name === "AbortError"
? "Discord widget request timed out."
: error?.message || "Discord widget could not be loaded.";
discordWidgetCache.set(id, { expires: now + Math.min(DISCORD_WIDGET_CACHE_MS, 60 * 1000), error: message });
throw new Error(message);
} finally {
clearTimeout(timer);
}
}
function homepageEmbedParent(req) {
const host = String(req?.hostname || req?.get?.("host") || "localhost")
.split(":")[0]
.trim();
return host || "localhost";
}
function twitchEmbedUrl(channel, parentHost) {
return `https://player.twitch.tv/?channel=${encodeURIComponent(channel)}&parent=${encodeURIComponent(parentHost)}&muted=true`;
}
function heroRenderError(type) {
const labels = {
static_image: "Add an image URL before this image hero can be displayed.",
custom_link: "Add a public link before this link hero can be displayed.",
custom_embed: "Add a supported YouTube, Twitch, Discord, or direct embed URL before this hero can be displayed.",
youtube_video: "Add a YouTube video URL or video ID before this hero can be displayed.",
youtube_channel: "Add a YouTube channel URL or channel ID before this hero can be displayed.",
twitch_stream: "Add a Twitch channel URL or channel name before this hero can be displayed.",
discord_server_overview: "Add a Discord server widget URL or server ID before this hero can be displayed."
};
return labels[type] || "This hero is missing the required URL or ID.";
}
function validateHomepageHeroEntries(entries) {
const errors = [];
entries.forEach((item, index) => {
if (!item || item.enabled === false) return;
if (item.type === "custom_embed" && !String(item.title || "").trim()) {
errors.push(`hero ${index + 1}: Add a title for this external embed.`);
return;
}
const hero = normalizeHomepageHero({ ...item, availability_mode: "always" }, { parentHost: "localhost" });
if (hero?.available) return;
if (item.type === "none" && item.fallback_behavior !== "hide") return;
const label = item.title || `hero ${index + 1}`;
errors.push(`${label}: ${hero?.render_error || heroRenderError(item.type || "none")}`);
});
return errors;
}
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 =
"<!doctype html><title>Content missing</title><h1>Content unavailable</h1><p>Some content could not be loaded.</p>";
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.json({ limit: "1mb" }));
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 feedbackScreenshotDir = path.join(__dirname, "..", "..", "data", "feedback", "screenshots");
fs.mkdirSync(feedbackScreenshotDir, { recursive: true });
const feedbackAttachmentDir = path.join(__dirname, "..", "..", "data", "feedback", "attachments");
fs.mkdirSync(feedbackAttachmentDir, { recursive: true });
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 feedbackScreenshotUpload =
multer &&
multer({
storage: multer.diskStorage({
destination: (_req, file, cb) => cb(null, file.fieldname === "attachments" ? feedbackAttachmentDir : feedbackScreenshotDir),
filename: (_req, file, cb) => {
const ext = file.fieldname === "attachments" ? feedbackAttachmentExtension(file) : screenshotExtension(file.mimetype);
cb(null, `${crypto.randomUUID()}${ext}`);
}
}),
limits: { fileSize: 8 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
if (file.fieldname === "screenshot" && ["image/png", "image/jpeg", "image/webp"].includes(file.mimetype)) {
return cb(null, true);
}
if (file.fieldname === "attachments" && ["image/png", "image/jpeg", "image/webp", "application/pdf", "text/plain"].includes(file.mimetype)) {
return cb(null, true);
}
cb(new Error("Only PNG, JPEG, WebP, PDF, or text feedback files are allowed."));
}
});
const feedbackUploadFields = (req, res, next) => {
if (!feedbackScreenshotUpload) {
return next();
}
return feedbackScreenshotUpload.fields([
{ name: "screenshot", maxCount: 1 },
{ name: "attachments", maxCount: 3 }
])(req, res, (error) => {
if (error) {
req.feedbackUploadError = error.message;
}
next();
});
};
const navItems = [];
const profileSections = [];
const assistantPanels = [];
const web = {
createRouter: () => express.Router(),
addRoute: (method, routePath, ...handlers) => {
const normalizedMethod = String(method || "get").toLowerCase();
if (typeof app[normalizedMethod] !== "function" || !routePath || !handlers.length) {
throw new Error("Invalid plugin WebUI route registration.");
}
app[normalizedMethod](routePath, ...handlers);
},
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);
},
addAssistantPanel: (panel) => {
if (!panel || !panel.id || !panel.view || !fs.existsSync(panel.view)) {
return () => {};
}
const existingIndex = assistantPanels.findIndex((item) => item.id === panel.id);
if (existingIndex >= 0) {
assistantPanels.splice(existingIndex, 1, panel);
} else {
assistantPanels.push(panel);
}
return () => {
const index = assistantPanels.indexOf(panel);
if (index >= 0) {
assistantPanels.splice(index, 1);
}
};
},
emitEvent: publishWebEvent
};
app.use(requireConfigured);
app.get("/api/events", requireAuth, subscribeWebEvents);
app.post("/api/destructive-confirmations", requireAuth, (req, res) => {
try {
res.json(issueConfirmation(req, req.body.action));
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.get("/api/users/search", requireAuth, (req, res) => {
res.set("Cache-Control", "no-store");
res.json({
ok: true,
users: searchKnownUsers(req.query.q, { limit: req.query.limit || 30 })
});
});
app.get("/api/feedback/similar", requireAuth, (req, res) => {
try {
res.json({
ok: true,
matches: findSimilarFeedback(req.query, {
userId: req.session.user.id,
limit: req.query.limit || 5
})
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post("/api/feedback/:id/support", requireAuth, (req, res) => {
try {
const support_count = supportFeedback(req.params.id, req.session.user);
res.json({ ok: true, support_count });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post("/api/feedback", requireAuth, feedbackUploadFields, (req, res) => {
try {
if (req.feedbackUploadError) {
throw new Error(req.feedbackUploadError);
}
const body = normalizeFeedbackRequestBody(req.body);
const screenshotFile = req.files?.screenshot?.[0] || null;
const attachmentFiles = req.files?.attachments || [];
const entry = createFeedback(body, req.session.user, {
screenshot: screenshotFile
? {
path: `feedback/screenshots/${screenshotFile.filename}`,
mime: screenshotFile.mimetype,
size: screenshotFile.size
}
: null,
attachments: attachmentFiles.map((file) => ({
path: `feedback/attachments/${file.filename}`,
mime: file.mimetype,
size: file.size,
original_name: file.originalname
}))
});
res.status(201).json({
ok: true,
id: entry.id,
message: "Feedback submitted. You can review it from the Feedback page."
});
} catch (error) {
[...(req.files?.screenshot || []), ...(req.files?.attachments || [])].forEach((file) => {
if (file?.path) fs.rmSync(file.path, { force: true });
});
res.status(400).json({ error: error.message });
}
});
app.get("/api/feedback/notifications", requireAuth, (req, res) => {
res.set("Cache-Control", "no-store");
res.json(notificationSummary(req.session.user.id));
});
app.use((req, res, next) => {
if (req.method !== "POST" || !isDestructivePath(req.path)) return next();
const confirmation = consumeConfirmation(req, normalizeAction(req.originalUrl));
if (confirmation.valid) return next();
const message = confirmation.reason === "too_early"
? "Confirmation was submitted before the safety timer completed."
: "This action requires a new timed confirmation.";
if (req.accepts("html")) {
req.session.flash = { type: "error", message };
return res.redirect(req.get("referer") || "/");
}
return res.status(409).json({ error: message, reason: confirmation.reason });
});
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;
const localhostLogin = getLocalhostLoginPlatform(req);
res.locals.localhostLoginAvailable = Boolean(localhostLogin);
res.locals.platformLogins = [
...(localhostLogin ? [localhostLogin] : []),
...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()
: "";
res.locals.feedbackNotifications = notificationSummary(req.session.user?.id);
res.locals.feedbackOptions = feedbackOptions();
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(
"<!doctype html><title>Content missing</title><h1>Content unavailable</h1><p>Some content could not be loaded.</p>"
);
}
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("/", async (req, res, next) => {
try {
res.render("home", {
title: "Home",
homepageLinks: homepageLinksForUser(req.session.user),
homepageHero: await homepageHeroForUser(req.session.user, req)
});
} catch (error) {
next(error);
}
});
app.get("/api/assistant-panels", async (req, res) => {
res.set("Cache-Control", "no-store");
if (!req.session.user) {
return res.json({ panels: [] });
}
const panels = [];
for (const panel of assistantPanels) {
let panelDebug = panel.debug || null;
try {
if (typeof panel.getDebug === "function") {
panelDebug = await panel.getDebug(req.session.user);
}
} catch {}
let panelAccess = null;
try {
panelAccess = typeof panel.canAccess === "function"
? await panel.canAccess(req.session.user, {
origin: "webui",
platform: "webui",
requested_surface: "webui_panel"
})
: { allowed: hasAccess(req.session.user, panel.role || "public"), reason: "role_forbidden" };
} catch (error) {
panelAccess = { allowed: false, reason: "access_check_failed", debug_details: { error: error.message } };
}
if (panelAccess === false || panelAccess?.allowed === false) {
panels.push({
...unavailableAssistantPanel(panel, panelAccess?.reason || "role_forbidden"),
debug: { ...(panelDebug || {}), permission: panelAccess?.debug_details || null }
});
continue;
}
let availability;
try {
availability = typeof panel.getAvailability === "function"
? await panel.getAvailability(req.session.user)
: {
available: typeof panel.isVisible === "function"
? Boolean(await panel.isVisible(req.session.user))
: true
};
} catch (error) {
console.error(`Assistant panel ${panel.id} availability check failed`, error);
panels.push({ ...unavailableAssistantPanel(panel, "availability_check_failed"), debug: panelDebug });
continue;
}
if (!availability?.available) {
panels.push({
...unavailableAssistantPanel(panel, availability?.reason_code || "unavailable"),
status: availability?.status || "offline",
debug: {
...(panelDebug || {}),
permission: availability?.permission?.debug_details || panelAccess?.debug_details || null
}
});
continue;
}
try {
const rendered = await renderAssistantPanel(app, panel, req.session.user);
panel.onRenderDiagnostic?.({
panel_endpoint_status: 200,
panel_html_length: rendered.html.length,
panel_html_error: null,
panel_template_path: panel.view,
missing_locals: []
});
if (typeof panel.getDebug === "function") {
panelDebug = await panel.getDebug(req.session.user);
}
panels.push({
available: true,
panel_id: panel.id,
status: availability.status || "healthy",
reason_code: null,
version: panel.version || assetVersion,
html: rendered.html,
stylesheet: panel.stylesheet || null,
script: panel.script || null,
debug: {
...(panelDebug || {}),
permission: availability?.permission?.debug_details || panelAccess?.debug_details || null
}
});
} catch (error) {
console.error(`Assistant panel ${panel.id} render failed`, error);
panel.onRenderDiagnostic?.({
panel_endpoint_status: 500,
panel_html_length: 0,
panel_html_error: error.message,
panel_template_path: panel.view,
missing_locals: error.missingLocals || []
});
if (typeof panel.getDebug === "function") {
try { panelDebug = await panel.getDebug(req.session.user); } catch {}
}
panels.push({
...unavailableAssistantPanel(panel, "render_failed"),
render_error: {
message: error.message || "Assistant panel rendering failed.",
missing_locals: error.missingLocals || []
},
debug: panelDebug
});
}
}
return res.json({ panels });
});
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.get("/auth/localhost", (req, res) => {
if (!isLocalhostLoginAvailable(req)) {
return res.status(404).render("error", {
title: "Login unavailable",
message: "Localhost Login is only available from a localhost request."
});
}
res.render("localhost-login", {
title: "Localhost Login",
username: getSetting("localhost_login_username", "admin")
});
});
app.post("/auth/localhost", (req, res) => {
if (!isLocalhostLoginAvailable(req)) {
return res.status(404).render("error", {
title: "Login unavailable",
message: "Localhost Login is only available from a localhost request."
});
}
const username = String(req.body.username || "").trim();
const password = String(req.body.password || "");
const expectedUsername = String(getSetting("localhost_login_username", "admin"));
const expectedPassword = String(getSetting("localhost_login_password", "admin"));
if (username !== expectedUsername || password !== expectedPassword) {
setFlash(req, "error", "Invalid localhost username or password.");
return res.redirect("/auth/localhost");
}
const profile = ensureUserForIdentity({
provider: "localhost",
providerUserId: expectedUsername,
displayName: expectedUsername,
fallbackName: "Localhost Admin"
});
req.session.user = {
id: profile.id,
username: profile.internal_username,
avatar: null,
roles: ["localhost-admin"],
isAdmin: true,
isMod: true,
isLocalhost: true
};
setFlash(req, "success", "Logged in with Localhost Login.");
res.redirect("/");
});
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("/feedback", requireAuth, (req, res) => {
markFeedbackViewed(req.session.user.id);
res.locals.feedbackNotifications = notificationSummary(req.session.user.id);
const selected = req.query.id
? getFeedbackForViewer(String(req.query.id), req.session.user.id)
: null;
res.render("feedback", {
title: "Feedback",
feedbackList: listPublicFeedback({ userId: req.session.user.id }),
myFeedback: listMyFeedback(req.session.user.id),
selectedFeedback: selected,
feedbackOptions: feedbackOptions()
});
});
app.post("/feedback/:id/comment", requireAuth, (req, res) => {
try {
addSubmitterComment(req.params.id, req.body.comment, req.session.user);
setFlash(req, "success", "Comment added.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect(`/feedback?id=${encodeURIComponent(req.params.id)}`);
});
app.post("/feedback/:id/support", requireAuth, (req, res) => {
try {
supportFeedback(req.params.id, req.session.user);
setFlash(req, "success", "Feedback supported.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect(req.get("referer") || "/feedback");
});
app.get("/feedback/:id/screenshot", requireAuth, (req, res) => {
const entry = req.session.user?.isAdmin
? getFeedbackForAdmin(req.params.id)
: getFeedbackForSubmitter(req.params.id, req.session.user.id);
if (!entry?.screenshot?.path) {
return res.status(404).render("error", {
title: "Screenshot not found",
message: "That feedback screenshot is not available."
});
}
const screenshotPath = resolveFeedbackScreenshotPath(entry.screenshot.path, feedbackScreenshotDir);
if (!screenshotPath || !fs.existsSync(screenshotPath)) {
return res.status(404).render("error", {
title: "Screenshot not found",
message: "That feedback screenshot is no longer available."
});
}
res.setHeader("Content-Type", entry.screenshot.mime || "image/png");
res.setHeader("Cache-Control", "private, no-store");
res.sendFile(screenshotPath);
});
app.get("/feedback/:id/attachments/:attachmentId", requireAuth, (req, res) => {
const attachment = getFeedbackAttachment(
req.params.id,
req.params.attachmentId,
req.session.user.id,
Boolean(req.session.user?.isAdmin)
);
if (!attachment?.storage_path) {
return res.status(404).render("error", {
title: "Attachment not found",
message: "That feedback attachment is not available."
});
}
const attachmentPath = resolveFeedbackAttachmentPath(attachment.storage_path, feedbackAttachmentDir);
if (!attachmentPath || !fs.existsSync(attachmentPath)) {
return res.status(404).render("error", {
title: "Attachment not found",
message: "That feedback attachment is no longer available."
});
}
res.setHeader("Content-Type", attachment.mime || "application/octet-stream");
res.setHeader("Cache-Control", "private, no-store");
res.setHeader("Content-Disposition", `attachment; filename="${path.basename(attachment.original_name || "attachment")}"`);
res.sendFile(attachmentPath);
});
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"
? `Dynamic 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 <category>");
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("/api/admin/dashboard-metrics", requireRole("admin"), (req, res) => {
const plugins = getPlugins();
const scope = parseDashboardScope(req.query.scope);
const scopeMs = DASHBOARD_SCOPES[scope];
const now = Date.now();
const logs = listLogs({ sinceMs: now - scopeMs });
const memory = process.memoryUsage();
recordMemorySample(memory, now);
const count = (table) => {
try {
return db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count;
} catch {
return 0;
}
};
res.set("Cache-Control", "no-store");
res.json({
uptime_seconds: Math.round(process.uptime()),
memory: {
rss: memory.rss,
heap_used: memory.heapUsed,
heap_total: memory.heapTotal
},
plugins: {
total: plugins.length,
enabled: plugins.filter((plugin) => plugin.enabled).length
},
counts: {
users: count("user_profiles"),
commands: countVisibleCommands(),
pages: count("custom_pages"),
logs: count("logs")
},
scopes: Object.fromEntries(
Object.entries(DASHBOARD_SCOPES).map(([id, duration]) => [id, { duration_ms: duration }])
),
selected_scope: scope,
memory_scopes: scopedMemorySamples(now),
logs: {
scope,
scope_ms: scopeMs,
total: logs.length,
levels: countLogsByLevel(logs)
},
sampled_at: now
});
});
app.get("/admin/settings", requireRole("admin"), (req, res) => {
res.render("admin-settings", {
title: "Settings",
settings: getAllSettings(),
platforms: getPlatformStatus(),
localhostLoginAvailable: isLocalhostLoginAvailable(req),
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());
}
}
}
if (isLocalhostLoginAvailable(req)) {
const localhostUsername = String(req.body.localhost_login_username || "").trim();
if (localhostUsername) {
setSetting("localhost_login_username", localhostUsername);
}
const localhostPassword = String(req.body.localhost_login_password || "");
if (localhostPassword) {
setSetting("localhost_login_password", localhostPassword);
}
}
for (const field of ["homepage_link_buttons", "homepage_hero_entries"]) {
if (req.body[field] === undefined) continue;
const raw = String(req.body[field] || "").trim();
if (!raw) {
setSetting(field, []);
continue;
}
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) throw new Error("Expected an array.");
if (field === "homepage_hero_entries") {
const heroErrors = validateHomepageHeroEntries(parsed);
if (heroErrors.length) {
setFlash(req, "error", `Homepage hero cannot be saved yet. ${heroErrors.slice(0, 3).join(" ")}`);
return res.redirect("/admin/settings");
}
}
setSetting(field, parsed);
} catch (error) {
setFlash(req, "error", `${field.replaceAll("_", " ")} JSON is invalid: ${error.message}`);
return res.redirect("/admin/settings");
}
}
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 && !isLocalhostLoginAvailable(req)) {
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) => {
const activeTheme = getActiveTheme();
const requestedEdit = String(req.query.edit || "");
const editingTheme = requestedEdit
? getThemeById(requestedEdit)
: activeTheme.builtin
? null
: activeTheme;
const editingBaseTheme = editingTheme && !editingTheme.builtin
? getThemeById(editingTheme.baseThemeId)
: null;
res.render("admin-theme", {
title: "Theming",
theme: activeTheme,
activeTheme,
themes: listThemes(),
editingTheme: editingTheme && !editingTheme.builtin ? editingTheme : null,
editingBaseTheme,
fontStacks: FONT_STACKS
});
});
app.post("/admin/theming/select", requireRole("admin"), (req, res) => {
try {
const theme = setActiveTheme(String(req.body.theme_id || ""));
setFlash(req, "success", `${theme.name} is now active.`);
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect("/admin/theming");
});
app.post("/admin/theming/duplicate", requireRole("admin"), (req, res) => {
try {
const theme = duplicateTheme(req.body.theme_id, req.body.name);
setFlash(req, "success", `${theme.name} created. You can edit it below.`);
return res.redirect(`/admin/theming?edit=${encodeURIComponent(theme.id)}#theme-editor`);
} catch (error) {
setFlash(req, "error", error.message);
return res.redirect("/admin/theming");
}
});
app.post("/admin/theming/custom/:id/save", requireRole("admin"), (req, res) => {
const themeId = `custom:${req.params.id}`;
try {
const current = getThemeById(themeId);
if (!current) throw new Error("Theme not found.");
const theme = saveCustomTheme(themeId, valuesFromRequest(req.body, current));
if (req.body.apply === "on") setActiveTheme(theme.id);
setFlash(
req,
"success",
req.body.apply === "on"
? `${theme.name} saved and applied.`
: `${theme.name} saved.`
);
} catch (error) {
const detail = error.validationErrors?.join(" ") || error.message;
setFlash(req, "error", detail);
}
res.redirect(`/admin/theming?edit=${encodeURIComponent(themeId)}#theme-editor`);
});
app.post("/admin/theming/custom/:id/rename", requireRole("admin"), (req, res) => {
const themeId = `custom:${req.params.id}`;
try {
const theme = renameCustomTheme(themeId, req.body.name);
setFlash(req, "success", `Theme renamed to ${theme.name}.`);
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect(`/admin/theming?edit=${encodeURIComponent(themeId)}`);
});
app.post("/admin/theming/custom/:id/delete", requireRole("admin"), (req, res) => {
try {
deleteCustomTheme(`custom:${req.params.id}`);
setFlash(req, "success", "Custom theme deleted.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect("/admin/theming");
});
app.post("/admin/theming", requireRole("admin"), (req, res) => {
let createdTarget = null;
try {
let target = getActiveTheme();
if (target.builtin) {
target = duplicateTheme(target.id, "Legacy Custom");
createdTarget = target.id;
}
const values = JSON.parse(JSON.stringify(target));
const legacyUpdates = [];
const legacyMap = {
theme_light_bg_1: ["light", "bg1"],
theme_light_bg_2: ["light", "bg2"],
theme_light_bg_3: ["light", "bg3"],
theme_light_text: ["light", "text"],
theme_light_text_muted: ["light", "muted"],
theme_light_accent: ["light", "accent"],
theme_light_accent_alt: ["light", "accentAlt"],
theme_light_danger: ["light", "danger"],
theme_light_surface: ["light", "surface"],
theme_light_surface_2: ["light", "surface2"],
theme_light_surface_3: ["light", "surface3"],
theme_light_border: ["light", "border"],
theme_dark_bg_1: ["dark", "bg1"],
theme_dark_bg_2: ["dark", "bg2"],
theme_dark_bg_3: ["dark", "bg3"],
theme_dark_text: ["dark", "text"],
theme_dark_text_muted: ["dark", "muted"],
theme_dark_accent: ["dark", "accent"],
theme_dark_accent_alt: ["dark", "accentAlt"],
theme_dark_danger: ["dark", "danger"],
theme_dark_surface: ["dark", "surface"],
theme_dark_surface_2: ["dark", "surface2"],
theme_dark_surface_3: ["dark", "surface3"],
theme_dark_border: ["dark", "border"],
theme_role_public: ["role", "public"],
theme_role_mod: ["role", "mod"],
theme_role_admin: ["role", "admin"]
};
for (const [field, [group, key]] of Object.entries(legacyMap)) {
if (req.body[field] === undefined) continue;
values[group][key] = String(req.body[field]).trim();
legacyUpdates.push([field, values[group][key]]);
}
saveCustomTheme(target.id, values);
for (const [field, value] of legacyUpdates) setSetting(field, value);
setActiveTheme(target.id);
setFlash(req, "success", "Theme updated.");
} catch (error) {
if (createdTarget) deleteCustomTheme(createdTarget);
setFlash(req, "error", error.message);
}
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/feedback", requireRole("admin"), (req, res) => {
res.render("admin-feedback", {
title: "Feedback review",
feedbackItems: listFeedbackForAdmin(req.query),
filters: {
status: req.query.status || "",
category: req.query.category || "",
severity: req.query.severity || "",
scope: req.query.scope || "",
area: req.query.area || "",
submitter: req.query.submitter || "",
date_from: req.query.date_from || "",
date_to: req.query.date_to || "",
needs_action: req.query.needs_action || "",
sort: req.query.sort || "last_activity"
},
feedbackOptions: feedbackOptions()
});
});
app.post("/admin/feedback/:id", requireRole("admin"), (req, res) => {
try {
const body = { ...req.body };
if (body.review_action === "finalize") {
body.status = "closed";
body.status_note = body.status_note || "Finalized and closed.";
} else if (body.review_action === "reopen") {
body.status = "reviewed";
body.status_note = body.status_note || "Reopened for review.";
}
adminUpdateFeedback(req.params.id, body, req.session.user);
setFlash(req, "success", body.review_action === "finalize"
? "Feedback finalized and closed."
: body.review_action === "reopen"
? "Feedback reopened."
: "Feedback updated.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect(req.get("referer") || "/admin/feedback");
});
app.post("/admin/feedback/:id/finalize", requireRole("admin"), (req, res) => {
try {
adminUpdateFeedback(req.params.id, {
...req.body,
status: "closed",
status_note: req.body.status_note || "Finalized and closed."
}, req.session.user);
setFlash(req, "success", "Feedback finalized and closed.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect(req.get("referer") || "/admin/feedback");
});
app.post("/admin/feedback/:id/reopen", requireRole("admin"), (req, res) => {
try {
adminUpdateFeedback(req.params.id, {
...req.body,
status: "reviewed",
status_note: req.body.status_note || "Reopened for review."
}, req.session.user);
setFlash(req, "success", "Feedback reopened.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect(req.get("referer") || "/admin/feedback");
});
app.post("/admin/feedback/:id/delete", requireRole("admin"), (req, res) => {
try {
deleteFeedback(req.params.id, {
deleteScreenshot(relativePath) {
const screenshotPath = resolveFeedbackScreenshotPath(relativePath, feedbackScreenshotDir);
if (screenshotPath) {
fs.rmSync(screenshotPath, { force: true });
}
},
deleteAttachment(relativePath) {
const attachmentPath = resolveFeedbackAttachmentPath(relativePath, feedbackAttachmentDir);
if (attachmentPath) {
fs.rmSync(attachmentPath, { force: true });
}
}
});
setFlash(req, "success", "Feedback permanently deleted.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect("/admin/feedback");
});
app.post("/admin/feedback/:id/cleanup", requireRole("admin"), (req, res) => {
try {
cleanupFeedback(req.params.id, req.body, req.session.user, {
deleteScreenshot(relativePath) {
const screenshotPath = resolveFeedbackScreenshotPath(relativePath, feedbackScreenshotDir);
if (screenshotPath) {
fs.rmSync(screenshotPath, { force: true });
}
},
deleteAttachment(relativePath) {
const attachmentPath = resolveFeedbackAttachmentPath(relativePath, feedbackAttachmentDir);
if (attachmentPath) {
fs.rmSync(attachmentPath, { force: true });
}
}
});
setFlash(req, "success", "Feedback data cleaned.");
} catch (error) {
setFlash(req, "error", error.message);
}
res.redirect(req.get("referer") || "/admin/feedback");
});
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),
preview: buildCommandPreviewPresentation(command)
}));
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"), async (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", "Dynamic commands are restricted to admins.");
return res.redirect("/admin/commands");
}
if (mode === "advanced") {
if (!code) {
setFlash(req, "error", "Dynamic commands require code.");
return res.redirect("/admin/commands");
}
} else if (!response) {
setFlash(req, "error", "Static commands require a response.");
return res.redirect("/admin/commands");
}
const now = Date.now();
const preview = isAdmin && mode === "advanced"
? await generateCommandPreview({ code, language })
: emptyCommandPreview();
try {
db.prepare(
"INSERT INTO custom_commands (trigger, response, mode, language, code, platform, preview_text, preview_status, preview_error, preview_generated_at, preview_dynamic_segments, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)"
).run(
trigger,
response || "",
isAdmin ? mode : "plain",
isAdmin ? language : "js",
isAdmin && mode === "advanced" ? code : null,
serializePlatformSelection(selectedPlatforms),
preview.preview_text,
preview.preview_status,
preview.preview_error,
preview.preview_generated_at,
preview.preview_dynamic_segments,
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"), async (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", "Dynamic 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", "Dynamic commands are restricted to admins.");
return res.redirect("/admin/commands");
}
if (mode === "advanced") {
if (!code) {
setFlash(req, "error", "Dynamic commands require code.");
return res.redirect("/admin/commands");
}
} else if (!response) {
setFlash(req, "error", "Static commands require a response.");
return res.redirect("/admin/commands");
}
const preview = isAdmin && mode === "advanced"
? await generateCommandPreview({ code, language })
: emptyCommandPreview();
try {
db.prepare(
"UPDATE custom_commands SET trigger = ?, response = ?, mode = ?, language = ?, code = ?, platform = ?, preview_text = ?, preview_status = ?, preview_error = ?, preview_generated_at = ?, preview_dynamic_segments = ?, updated_at = ? WHERE id = ?"
).run(
trigger,
response || "",
isAdmin ? mode : "plain",
isAdmin ? language : "js",
isAdmin && mode === "advanced" ? code : null,
serializePlatformSelection(selectedPlatforms),
preview.preview_text,
preview.preview_status,
preview.preview_error,
preview.preview_generated_at,
preview.preview_dynamic_segments,
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.post("/admin/commands/:id/preview", requireRole("admin"), async (req, res) => {
const command = db
.prepare("SELECT mode, language, code FROM custom_commands WHERE id = ?")
.get(req.params.id);
if (!command || command.mode !== "advanced" || !command.code) {
setFlash(req, "error", "Dynamic command not found.");
return res.redirect("/admin/commands");
}
const preview = await generateCommandPreview(command);
db.prepare(
"UPDATE custom_commands SET preview_text = ?, preview_status = ?, preview_error = ?, preview_generated_at = ?, preview_dynamic_segments = ? WHERE id = ?"
).run(
preview.preview_text,
preview.preview_status,
preview.preview_error,
preview.preview_generated_at,
preview.preview_dynamic_segments,
req.params.id
);
setFlash(req, preview.preview_status === "ready" ? "success" : "error",
preview.preview_status === "ready" ? "Command preview refreshed." : "Preview unavailable.");
return 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();
const plugins = getPlugins().filter((plugin) => !/^lumi_ai_[a-z0-9_-]+$/i.test(plugin.id || ""));
res.render("admin-plugins", {
title: "Plugins",
plugins
});
});
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");
}
});
function wantsJson(req) {
return req.xhr ||
req.get("accept")?.includes("application/json") ||
req.get("content-type")?.includes("application/json");
}
function updateSourceFrom(req) {
const value = req.body?.source || req.query?.source || "stable";
return value === "experimental" ? "experimental" : String(value || "stable");
}
function sendUpdateResult(req, res, result, redirectPath = "/admin/updates") {
if (wantsJson(req)) {
return res.json({ ok: true, ...result });
}
setFlash(req, "success", result.message || "Update action completed.");
return res.redirect(redirectPath);
}
function sendUpdateError(req, res, error, redirectPath = "/admin/updates") {
if (wantsJson(req)) {
return res.status(400).json({ ok: false, error: error.message });
}
setFlash(req, "error", error.message);
return res.redirect(redirectPath);
}
function scheduleRestartAfterNotice() {
setTimeout(() => requestRestart(), 5000);
}
app.get("/admin/updates/events", requireRole("admin"), subscribeWebEvents);
app.get("/admin/updates/status", requireRole("admin"), (req, res) => {
try {
res.json({ ok: true, status: getUpdateStatus({ source: updateSourceFrom(req) }) });
} catch (error) {
res.status(500).json({ ok: false, error: error.message });
}
});
app.get("/admin/updates/recovery", requireRole("admin"), (req, res) => {
res.json({ ok: true, recovery: safeModeStatus(), snapshots: listSnapshots() });
});
app.post("/admin/updates/recovery/clear-marker", requireRole("admin"), (req, res) => {
clearRecoveryMarker();
publishWebEvent("recovery:marker_detected", { status: "cleared" }, { role: "admin" });
sendUpdateResult(req, res, { message: "Recovery marker cleared." });
});
app.post("/admin/updates/recovery/retry-normal-startup", requireRole("admin"), (req, res) => {
updateRecoveryMarker({ status: "retry_startup", retry_at: new Date().toISOString() });
publishWebEvent("recovery:retry_startup", {}, { role: "admin" });
sendUpdateResult(req, res, { message: "Retrying normal startup." });
scheduleRestartAfterNotice();
});
app.get("/admin/updates", requireRole("admin"), (req, res) => {
let updateStatus = null;
let updateStatusError = null;
let snapshots = [];
let recoveryStatus = {};
try {
updateStatus = getUpdateStatus({ source: updateSourceFrom(req) });
} catch (error) {
updateStatusError = error.message;
}
try {
snapshots = listSnapshots();
} catch (error) {
updateStatusError = [updateStatusError, `Snapshots could not be loaded: ${error.message}`]
.filter(Boolean)
.join(" ");
}
try {
recoveryStatus = safeModeStatus();
} catch (error) {
updateStatusError = [updateStatusError, `Recovery status could not be loaded: ${error.message}`]
.filter(Boolean)
.join(" ");
}
res.render("admin-updates", {
title: "Updates",
snapshots,
updateStatus,
updateStatusError,
recoveryStatus
});
});
app.post("/admin/updates/core/check", requireRole("admin"), (req, res) => {
try {
const status = getUpdateStatus({ source: updateSourceFrom(req) });
sendUpdateResult(req, res, {
status,
message: status.core.update_available
? `Safe core target ${status.core.safe_target_version} is available.`
: status.core.blocked
? status.core.blocked_reason
: "No core updates found."
});
} catch (error) {
sendUpdateError(req, res, error);
}
});
app.post("/admin/updates/core/apply", requireRole("admin"), async (req, res) => {
try {
const result = await applyCoreUpdate({
source: updateSourceFrom(req),
publish: publishWebEvent
});
sendUpdateResult(req, res, {
...result,
refresh_after_ms: 5000,
message: "Core update applied. Lumi will restart after the confirmation notice."
});
scheduleRestartAfterNotice();
} catch (error) {
sendUpdateError(req, res, error);
}
});
app.post("/admin/updates/core/revert", requireRole("admin"), async (req, res) => {
try {
const status = getUpdateStatus({ source: updateSourceFrom(req) });
const snapshotId = req.body.snapshot_id || status.core.snapshot.latest_snapshot_id;
if (!snapshotId) throw new Error("No core snapshot is available to revert.");
const result = await revertCoreSnapshot(snapshotId, {
currentVersion: status.core.current_version,
publish: publishWebEvent
});
sendUpdateResult(req, res, {
...result,
refresh_after_ms: 5000,
message: "Core snapshot reverted. Lumi will restart after the confirmation notice."
});
scheduleRestartAfterNotice();
} catch (error) {
sendUpdateError(req, res, error);
}
});
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/core/zip",
requireRole("admin"),
uploadSingle("update_zip"),
async (req, res) => {
if (req.uploadError) return sendUpdateError(req, res, new Error(req.uploadError));
if (!req.file) return sendUpdateError(req, res, new Error("Upload a ZIP archive."));
try {
const patchMode = req.body.patch_mode === "1";
const snapshot = await applyBotUpdate(req.file.path, {
mode: patchMode ? "patch" : "full",
metadata: {
update_method: patchMode ? "zip_patch" : "zip",
source_branch: "manual_zip",
rollback_safe: req.body.rollback_safe === "1"
}
});
sendUpdateResult(req, res, {
snapshot,
restart_required: true,
refresh_after_ms: 5000,
message: patchMode ? "Core patch ZIP applied. Restarting..." : "Core ZIP update applied. Restarting..."
});
scheduleRestartAfterNotice();
} catch (error) {
sendUpdateError(req, res, error);
} finally {
try { fs.rmSync(req.file?.path, { force: true }); } catch {}
}
}
);
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/updates/plugins/:id/check", requireRole("admin"), (req, res) => {
try {
const status = getUpdateStatus({ source: updateSourceFrom(req) });
const plugin = status.plugins.find((item) => item.id === req.params.id);
if (!plugin) throw new Error("Plugin not found.");
sendUpdateResult(req, res, {
plugin,
message: plugin.update_available
? `Safe plugin target ${plugin.safe_target_version} is available.`
: plugin.blocked
? plugin.blocked_reason
: "No plugin updates found."
});
} catch (error) {
sendUpdateError(req, res, error);
}
});
app.post("/admin/updates/plugins/:id/apply", requireRole("admin"), async (req, res) => {
try {
const result = await applyPluginUpdateFromRepo(req.params.id, {
source: updateSourceFrom(req),
publish: publishWebEvent
});
sendUpdateResult(req, res, {
...result,
message: "Plugin update applied."
});
} catch (error) {
sendUpdateError(req, res, error);
}
});
app.post("/admin/updates/plugins/:id/revert", requireRole("admin"), async (req, res) => {
try {
const status = getUpdateStatus({ source: updateSourceFrom(req) });
const plugin = status.plugins.find((item) => item.id === req.params.id);
if (!plugin) throw new Error("Plugin not found.");
const snapshotId = req.body.snapshot_id || plugin.snapshot.latest_snapshot_id;
if (!snapshotId) throw new Error("No plugin snapshot is available to revert.");
const result = await revertPluginSnapshot(req.params.id, snapshotId, {
currentVersion: plugin.current_version,
publish: publishWebEvent
});
sendUpdateResult(req, res, {
...result,
message: "Plugin snapshot reverted. Restarting..."
});
scheduleRestartAfterNotice();
} catch (error) {
sendUpdateError(req, res, error);
}
});
app.post("/admin/updates/plugins/:id/disable", requireRole("admin"), (req, res) => {
try {
const result = disablePluginForRecovery(req.params.id, publishWebEvent);
sendUpdateResult(req, res, { ...result, message: "Plugin disabled for recovery." });
} catch (error) {
sendUpdateError(req, res, error);
}
});
app.post(
"/admin/updates/plugins/:id/zip",
requireRole("admin"),
uploadSingle("plugin_zip"),
async (req, res) => {
if (req.uploadError) return sendUpdateError(req, res, new Error(req.uploadError));
if (!req.file) return sendUpdateError(req, res, new Error("Upload a ZIP archive."));
try {
const snapshot = await applyPluginUpdate(req.file.path, {
expectedPluginId: req.params.id,
metadata: {
target_id: req.params.id,
source_branch: "manual_zip",
rollback_safe: req.body.rollback_safe === "1"
}
});
sendUpdateResult(req, res, {
snapshot,
message: "Plugin ZIP update applied."
});
} catch (error) {
sendUpdateError(req, res, error);
} finally {
try { fs.rmSync(req.file?.path, { force: true }); } catch {}
}
}
);
app.post("/admin/update", requireRole("admin"), async (req, res) => {
try {
const remote = getSetting("git_remote", "origin");
const branch = getSetting("git_branch", "main");
await 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: "Feedback",
path: "/feedback",
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: "Feedback review", path: "/admin/feedback", 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;
}
if (typeof item.canAccess === "function") {
try {
if (!item.canAccess(user)) return false;
} catch {
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}/`);
}
function unavailableAssistantPanel(panel, reasonCode) {
return {
available: false,
panel_id: panel.id,
status: "offline",
reason_code: reasonCode
};
}
function renderAssistantPanel(app, panel, user) {
const requiredLocals = Array.isArray(panel.requiredLocals) ? panel.requiredLocals : [];
const locals = { ...(panel.locals || {}), user };
const missingLocals = requiredLocals.filter((key) => locals[key] === undefined || locals[key] === null);
if (!fs.existsSync(panel.view)) {
return Promise.reject(new Error(`Assistant panel template does not exist: ${panel.view}`));
}
try {
fs.accessSync(panel.view, fs.constants.R_OK);
} catch {
return Promise.reject(new Error(`Assistant panel template is not readable: ${panel.view}`));
}
if (missingLocals.length) {
const error = new Error(`Assistant panel is missing required locals: ${missingLocals.join(", ")}`);
error.missingLocals = missingLocals;
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
app.render(
panel.view,
locals,
(error, html) => {
if (error) return reject(error);
const content = String(html || "");
if (!content.trim()) return reject(new Error(`Assistant panel ${panel.id} rendered empty HTML.`));
if (!content.includes(`data-assistant-panel-id="${panel.id}"`)) {
return reject(new Error(`Assistant panel ${panel.id} HTML is missing its panel marker.`));
}
return resolve({ html: content });
}
);
});
}
module.exports = {
createWebServer
};