const express = require("express");
const path = require("path");
const crypto = require("crypto");
const fs = require("fs");
const { Permissions } = require("discord.js");
let multer = null;
try {
multer = require("multer");
} catch {
multer = null;
}
const session = require("express-session");
const BetterSqlite3Store = require("better-sqlite3-session-store")(session);
const { db } = require("../services/db");
const { getSetting, setSetting, getAllSettings } = require("../services/settings");
const { getRoleFlags, hasAccess } = require("../services/rbac");
const {
buildDiscordAuthUrl,
exchangeDiscordCode,
fetchDiscordUser,
fetchDiscordGuildMember,
buildTwitchAuthUrl,
exchangeTwitchCode,
fetchTwitchUser,
buildYouTubeAuthUrl,
exchangeYouTubeCode,
fetchYouTubeChannel
} = require("../services/auth");
const { getPluginProfileStats } = require("../services/plugin-stats");
const {
getLeaderboardSections,
getTopCommandOptions
} = require("../services/top");
const { log, listLogs } = require("../services/logger");
const { createWebhookService } = require("../services/webhooks");
const {
getPlatformStatus,
getEnabledPlatformIds,
getLoginPlatforms,
getLinkPlatforms,
getPlatformLabel,
getPlatformBadge,
isPlatformEnabled,
isPlatformConfigured,
normalizePlatformSelection,
serializePlatformSelection
} = require("../services/platforms");
const { getClient: getTwitchClient } = require("../services/twitch");
const { getClient: getYouTubeClient } = require("../services/youtube");
const {
ensureUserForIdentity,
linkIdentityToUser,
getUserProfileById,
getUserIdentities,
updateInternalUsername,
listUsersWithIdentities
} = require("../services/users");
const {
getPlugins,
syncPluginRegistry,
setPluginEnabled,
removePlugin,
installFromGit,
updatePluginFromGit,
createLocalPlugin
} = require("../services/plugins");
const { checkForUpdates, pullUpdates, requestRestart } = require("../services/updater");
const {
applyBotUpdate,
applyPluginUpdate,
listSnapshots
} = require("../services/update-manager");
function ensureSessionSecret() {
let secret = getSetting("session_secret");
if (!secret) {
secret = crypto.randomBytes(32).toString("hex");
setSetting("session_secret", secret);
}
return secret;
}
function isConfigured() {
const platforms = getPlatformStatus().filter(
(platform) => platform.supported && platform.enabled
);
if (!platforms.length) {
return false;
}
return platforms.some((platform) => platform.configured);
}
function getPrimaryLoginPlatform() {
const platforms = getPlatformStatus().filter(
(platform) =>
platform.supported && platform.enabled && platform.supportsLogin
);
if (!platforms.length) {
return null;
}
return platforms.find((platform) => platform.configured) || platforms[0];
}
function getLoginRedirectPath() {
const platform = getPrimaryLoginPlatform();
return platform?.loginPath || "/setup";
}
function requireConfigured(req, res, next) {
if (!isConfigured() && !req.path.startsWith("/setup")) {
return res.redirect("/setup");
}
next();
}
function requireAuth(req, res, next) {
if (!req.session.user) {
return res.redirect(getLoginRedirectPath());
}
next();
}
function trackModRole(db, user) {
if (!user?.id) {
return;
}
const isMod = Boolean(user.isAdmin || user.isMod);
const active = db
.prepare(
"SELECT id FROM mod_role_periods WHERE user_id = ? AND end_at IS NULL"
)
.get(user.id);
if (isMod && !active) {
db.prepare(
"INSERT INTO mod_role_periods (user_id, start_at, end_at) VALUES (?, ?, NULL)"
).run(user.id, Date.now());
} else if (!isMod && active) {
db.prepare("UPDATE mod_role_periods SET end_at = ? WHERE id = ?").run(
Date.now(),
active.id
);
}
}
function formatDuration(totalMs) {
const totalSeconds = Math.max(0, Math.floor(totalMs / 1000));
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h`;
}
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}
function requireRole(role) {
return (req, res, next) => {
if (!req.session.user) {
return res.redirect(getLoginRedirectPath());
}
if (!hasAccess(req.session.user, role)) {
return res.status(403).render("error", {
title: "Access denied",
message: "You do not have access to that page."
});
}
next();
};
}
function storeDiscordUser(user) {
const avatar = user.avatar
? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=128`
: null;
const displayName = user.global_name || user.username;
return ensureUserForIdentity({
provider: "discord",
providerUserId: user.id,
displayName,
avatar
});
}
async function fetchDiscordRolesForUser(discordUserId) {
const guildId = getSetting("discord_guild_id");
const botToken = getSetting("discord_bot_token");
if (!guildId || !botToken || !discordUserId) {
return [];
}
try {
const response = await fetch(
`https://discord.com/api/guilds/${guildId}/members/${discordUserId}`,
{ headers: { Authorization: `Bot ${botToken}` } }
);
if (!response.ok) {
return [];
}
const member = await response.json();
return member?.roles || [];
} catch {
return [];
}
}
function getPreferredAvatar(userId) {
if (!userId) {
return null;
}
const identities = db
.prepare("SELECT provider, avatar FROM user_identities WHERE user_id = ?")
.all(userId);
const preferredOrder = ["discord", "twitch", "youtube"];
for (const provider of preferredOrder) {
const match = identities.find((identity) => identity.provider === provider);
if (match?.avatar) {
return match.avatar;
}
}
const fallback = identities.find((identity) => identity.avatar);
return fallback?.avatar || null;
}
function hasExpressionTables() {
const row = db
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'expression_user_stats'"
)
.get();
return Boolean(row);
}
function getExpressionUserSummary(userId) {
if (!hasExpressionTables()) {
return null;
}
const rows = db
.prepare(
"SELECT action, given_count, received_count FROM expression_user_stats WHERE user_id = ?"
)
.all(userId);
const totals = rows.reduce(
(acc, row) => {
acc.given += row.given_count;
acc.received += row.received_count;
return acc;
},
{ given: 0, received: 0 }
);
return { totals };
}
function buildUserStatsPayload(userId) {
if (!userId) {
return { stats: null, expression: null, pluginStats: [] };
}
const stats = db
.prepare("SELECT * FROM stats WHERE user_id = ?")
.get(userId);
return {
stats,
expression: getExpressionUserSummary(userId),
pluginStats: getPluginProfileStats(userId)
};
}
function buildCompareRows(leftStats, rightStats) {
const rows = [];
const pushSection = (section, leftList, rightList) => {
const leftMap = new Map((leftList || []).map((item) => [item.label, item.value]));
const rightMap = new Map((rightList || []).map((item) => [item.label, item.value]));
const labels = [];
for (const label of leftMap.keys()) {
labels.push(label);
}
for (const label of rightMap.keys()) {
if (!leftMap.has(label)) {
labels.push(label);
}
}
labels.forEach((label) => {
rows.push({
section,
label,
left: leftMap.has(label) ? leftMap.get(label) : null,
right: rightMap.has(label) ? rightMap.get(label) : null
});
});
};
const leftCommunity = [
{ label: "Messages", value: leftStats.stats?.messages ?? 0 },
{ label: "Commands", value: leftStats.stats?.commands ?? 0 }
];
const rightCommunity = [
{ label: "Messages", value: rightStats.stats?.messages ?? 0 },
{ label: "Commands", value: rightStats.stats?.commands ?? 0 }
];
pushSection("Community Interaction", leftCommunity, rightCommunity);
if (leftStats.expression || rightStats.expression) {
const leftExpression = leftStats.expression
? [
{ label: "Actions given", value: leftStats.expression.totals.given },
{ label: "Actions received", value: leftStats.expression.totals.received }
]
: [];
const rightExpression = rightStats.expression
? [
{ label: "Actions given", value: rightStats.expression.totals.given },
{ label: "Actions received", value: rightStats.expression.totals.received }
]
: [];
pushSection("Expression Interaction", leftExpression, rightExpression);
}
const pluginSections = new Map();
leftStats.pluginStats.forEach((section) => {
pluginSections.set(section.title, {
left: section.stats || [],
right: []
});
});
rightStats.pluginStats.forEach((section) => {
const entry = pluginSections.get(section.title) || { left: [], right: [] };
entry.right = section.stats || [];
pluginSections.set(section.title, entry);
});
for (const [title, lists] of pluginSections.entries()) {
pushSection(title, lists.left, lists.right);
}
return rows;
}
function getExpressionLeaderboards(limit = 10) {
if (!hasExpressionTables()) {
return null;
}
const given = db
.prepare(
"SELECT user_profiles.internal_username AS username, SUM(expression_user_stats.given_count) AS total " +
"FROM expression_user_stats " +
"JOIN user_profiles ON user_profiles.id = expression_user_stats.user_id " +
"GROUP BY expression_user_stats.user_id " +
"ORDER BY total DESC LIMIT ?"
)
.all(limit);
const received = db
.prepare(
"SELECT user_profiles.internal_username AS username, SUM(expression_user_stats.received_count) AS total " +
"FROM expression_user_stats " +
"JOIN user_profiles ON user_profiles.id = expression_user_stats.user_id " +
"GROUP BY expression_user_stats.user_id " +
"ORDER BY total DESC LIMIT ?"
)
.all(limit);
return { given, received };
}
const DISCORD_PERMISSION_DETAILS = {
CREATE_INSTANT_INVITE: {
label: "Create instant invites",
description: "Create invitations to the server."
},
KICK_MEMBERS: {
label: "Kick members",
description: "Kick members from the server."
},
BAN_MEMBERS: {
label: "Ban members",
description: "Ban or unban members from the server."
},
ADMINISTRATOR: {
label: "Administrator",
description: "Bypasses all permission checks and grants every permission."
},
MANAGE_CHANNELS: {
label: "Manage channels",
description: "Create, edit, or delete channels."
},
MANAGE_GUILD: {
label: "Manage server",
description: "Edit server settings and features."
},
ADD_REACTIONS: {
label: "Add reactions",
description: "Add new reactions to messages."
},
VIEW_AUDIT_LOG: {
label: "View audit log",
description: "View the server audit log."
},
PRIORITY_SPEAKER: {
label: "Priority speaker",
description: "Use priority speaker in voice channels."
},
STREAM: {
label: "Stream",
description: "Stream video in voice channels."
},
VIEW_CHANNEL: {
label: "View channels",
description: "View channels and read messages."
},
SEND_MESSAGES: {
label: "Send messages",
description: "Send messages in text channels."
},
SEND_TTS_MESSAGES: {
label: "Send TTS messages",
description: "Send text-to-speech messages."
},
MANAGE_MESSAGES: {
label: "Manage messages",
description: "Delete messages and manage pins."
},
EMBED_LINKS: {
label: "Embed links",
description: "Send embedded link previews."
},
ATTACH_FILES: {
label: "Attach files",
description: "Attach files and media."
},
READ_MESSAGE_HISTORY: {
label: "Read message history",
description: "Read messages posted before the bot joined the channel."
},
MENTION_EVERYONE: {
label: "Mention everyone",
description: "Use @everyone, @here, and role mentions."
},
USE_EXTERNAL_EMOJIS: {
label: "Use external emojis",
description: "Use emojis from other servers."
},
VIEW_GUILD_INSIGHTS: {
label: "View server insights",
description: "Access server insights."
},
CONNECT: {
label: "Connect to voice",
description: "Join voice channels."
},
SPEAK: {
label: "Speak in voice",
description: "Speak in voice channels."
},
MUTE_MEMBERS: {
label: "Mute members",
description: "Server mute members in voice."
},
DEAFEN_MEMBERS: {
label: "Deafen members",
description: "Server deafen members in voice."
},
MOVE_MEMBERS: {
label: "Move members",
description: "Move members between voice channels."
},
USE_VAD: {
label: "Use voice activity detection",
description: "Use voice activity detection."
},
CHANGE_NICKNAME: {
label: "Change nickname",
description: "Change the bot's nickname."
},
MANAGE_NICKNAMES: {
label: "Manage nicknames",
description: "Change other members' nicknames."
},
MANAGE_ROLES: {
label: "Manage roles",
description: "Create, edit, and assign roles."
},
MANAGE_WEBHOOKS: {
label: "Manage webhooks",
description: "Create, edit, and delete webhooks."
},
MANAGE_EMOJIS_AND_STICKERS: {
label: "Manage emojis and stickers",
description: "Create, edit, and delete emojis or stickers."
},
USE_APPLICATION_COMMANDS: {
label: "Use application commands",
description: "Use slash commands and context menu commands."
},
REQUEST_TO_SPEAK: {
label: "Request to speak",
description: "Request to speak in stage channels."
},
MANAGE_EVENTS: {
label: "Manage events",
description: "Create and manage scheduled events."
},
MANAGE_THREADS: {
label: "Manage threads",
description: "Manage threads and thread settings."
},
USE_PUBLIC_THREADS: {
label: "Use public threads",
description: "Use public threads (deprecated flag)."
},
CREATE_PUBLIC_THREADS: {
label: "Create public threads",
description: "Create public threads."
},
USE_PRIVATE_THREADS: {
label: "Use private threads",
description: "Use private threads (deprecated flag)."
},
CREATE_PRIVATE_THREADS: {
label: "Create private threads",
description: "Create private threads."
},
USE_EXTERNAL_STICKERS: {
label: "Use external stickers",
description: "Use stickers from other servers."
},
SEND_MESSAGES_IN_THREADS: {
label: "Send messages in threads",
description: "Send messages in threads."
},
START_EMBEDDED_ACTIVITIES: {
label: "Start embedded activities",
description: "Start embedded activities in voice channels."
},
MODERATE_MEMBERS: {
label: "Moderate members",
description: "Timeout members."
},
VIEW_CREATOR_MONETIZATION_ANALYTICS: {
label: "View monetization analytics",
description: "View creator monetization analytics."
},
USE_SOUNDBOARD: {
label: "Use soundboard",
description: "Use the soundboard in voice."
},
SEND_VOICE_MESSAGES: {
label: "Send voice messages",
description: "Send voice messages."
}
};
const TWITCH_SCOPE_DEFS = [
{
scope: "analytics:read:extensions",
label: "Extensions analytics",
description: "Read analytics data for extensions."
},
{
scope: "analytics:read:games",
label: "Games analytics",
description: "Read analytics data for games."
},
{
scope: "bits:read",
label: "Bits",
description: "Read Bits leaderboard data."
},
{
scope: "channel:bot",
label: "Channel bot",
description: "Act as a bot within a channel."
},
{
scope: "channel:edit:commercial",
label: "Run commercials",
description: "Run commercials on a channel."
},
{
scope: "channel:manage:ads",
label: "Manage ads",
description: "Manage ad settings for a channel."
},
{
scope: "channel:manage:broadcast",
label: "Manage broadcast",
description: "Manage stream metadata and settings."
},
{
scope: "channel:manage:extensions",
label: "Manage extensions",
description: "Manage channel extensions."
},
{
scope: "channel:manage:moderators",
label: "Manage moderators",
description: "Add or remove moderators."
},
{
scope: "channel:manage:polls",
label: "Manage polls",
description: "Create and manage polls."
},
{
scope: "channel:manage:predictions",
label: "Manage predictions",
description: "Create and manage predictions."
},
{
scope: "channel:manage:redemptions",
label: "Manage redemptions",
description: "Manage channel points rewards."
},
{
scope: "channel:manage:schedule",
label: "Manage schedule",
description: "Manage channel schedule."
},
{
scope: "channel:manage:videos",
label: "Manage videos",
description: "Manage videos for a channel."
},
{
scope: "channel:manage:vips",
label: "Manage VIPs",
description: "Add or remove VIPs."
},
{
scope: "channel:read:ads",
label: "Read ads",
description: "Read ad schedule data."
},
{
scope: "channel:read:charity",
label: "Read charity",
description: "Read charity campaign data."
},
{
scope: "channel:manage:charity",
label: "Manage charity",
description: "Manage charity campaign settings."
},
{
scope: "channel:read:editors",
label: "Read editors",
description: "Read channel editor list."
},
{
scope: "channel:read:goals",
label: "Read goals",
description: "Read creator goal data."
},
{
scope: "channel:read:hype_train",
label: "Read hype train",
description: "Read hype train data."
},
{
scope: "channel:read:polls",
label: "Read polls",
description: "Read poll data."
},
{
scope: "channel:read:predictions",
label: "Read predictions",
description: "Read prediction data."
},
{
scope: "channel:read:redemptions",
label: "Read redemptions",
description: "Read channel points rewards."
},
{
scope: "channel:read:stream_key",
label: "Read stream key",
description: "Read the channel stream key."
},
{
scope: "channel:read:subscriptions",
label: "Read subscriptions",
description: "Read subscriber data."
},
{
scope: "channel:read:vips",
label: "Read VIPs",
description: "Read the VIP list."
},
{
scope: "channel:moderate",
label: "Channel moderation",
description: "Perform channel moderation actions."
},
{
scope: "chat:edit",
label: "Chat edit",
description: "Send chat messages."
},
{
scope: "chat:read",
label: "Chat read",
description: "Read chat messages."
},
{
scope: "clips:edit",
label: "Edit clips",
description: "Create or edit clips."
},
{
scope: "moderation:read",
label: "Read moderation",
description: "Read moderation data."
},
{
scope: "moderator:manage:announcements",
label: "Manage announcements",
description: "Send announcements in chat."
},
{
scope: "moderator:manage:automod",
label: "Manage AutoMod",
description: "Manage AutoMod actions."
},
{
scope: "moderator:manage:automod_settings",
label: "Manage AutoMod settings",
description: "Update AutoMod settings."
},
{
scope: "moderator:manage:banned_users",
label: "Manage banned users",
description: "Ban or unban users."
},
{
scope: "moderator:manage:blocked_terms",
label: "Manage blocked terms",
description: "Manage blocked terms."
},
{
scope: "moderator:manage:chat_messages",
label: "Manage chat messages",
description: "Delete or manage chat messages."
},
{
scope: "moderator:manage:chat_settings",
label: "Manage chat settings",
description: "Update chat settings."
},
{
scope: "moderator:manage:shield_mode",
label: "Manage shield mode",
description: "Enable or disable shield mode."
},
{
scope: "moderator:manage:shoutouts",
label: "Manage shoutouts",
description: "Send or manage shoutouts."
},
{
scope: "moderator:read:automod_settings",
label: "Read AutoMod settings",
description: "Read AutoMod settings."
},
{
scope: "moderator:read:blocked_terms",
label: "Read blocked terms",
description: "Read blocked terms."
},
{
scope: "moderator:read:chat_settings",
label: "Read chat settings",
description: "Read chat settings."
},
{
scope: "moderator:read:followers",
label: "Read followers",
description: "Read follower list."
},
{
scope: "moderator:read:shield_mode",
label: "Read shield mode",
description: "Read shield mode status."
},
{
scope: "moderator:read:shoutouts",
label: "Read shoutouts",
description: "Read shoutout settings."
},
{
scope: "moderator:read:vips",
label: "Read VIPs",
description: "Read VIP list."
},
{
scope: "user:bot",
label: "User bot",
description: "Act as a bot on behalf of a user."
},
{
scope: "user:edit",
label: "Edit user",
description: "Edit a user's profile."
},
{
scope: "user:edit:follows",
label: "Edit follows",
description: "Manage follows for a user."
},
{
scope: "user:manage:blocked_users",
label: "Manage blocked users",
description: "Block or unblock users."
},
{
scope: "user:manage:chat_color",
label: "Manage chat color",
description: "Change chat color."
},
{
scope: "user:manage:whispers",
label: "Manage whispers",
description: "Send and manage whispers."
},
{
scope: "user:read:blocked_users",
label: "Read blocked users",
description: "Read blocked users list."
},
{
scope: "user:read:broadcast",
label: "Read broadcast",
description: "Read broadcast settings."
},
{
scope: "user:read:email",
label: "Read email",
description: "Read the user's email address."
},
{
scope: "user:read:follows",
label: "Read follows",
description: "Read the user's follows."
},
{
scope: "user:read:subscriptions",
label: "Read subscriptions",
description: "Read user subscription data."
},
{
scope: "user:read:chat",
label: "Read chat",
description: "Read chat messages via API."
},
{
scope: "user:write:chat",
label: "Write chat",
description: "Send chat messages via API."
},
{
scope: "whispers:read",
label: "Read whispers",
description: "Read whispers."
},
{
scope: "whispers:edit",
label: "Send whispers",
description: "Send whispers."
}
];
function buildPrivilegeRow(id, label, description, granted) {
const safeLabel = (label || "").toString();
const safeDescription = (description || "").toString();
const grantedFlag = Boolean(granted);
return {
id,
label: safeLabel,
description: safeDescription,
granted: grantedFlag,
search: `${safeLabel} ${safeDescription} ${grantedFlag ? "granted" : "missing"}`.trim(),
sort: {
label: safeLabel.toLowerCase(),
description: safeDescription.toLowerCase(),
status: grantedFlag ? 1 : 0
}
};
}
function parseChannelList(value) {
return (value || "")
.split(/[,\s]+/g)
.map((entry) => entry.trim())
.filter(Boolean);
}
async function buildDiscordPrivileges(discordClient) {
const rows = [];
const guildId = getSetting("discord_guild_id");
const hasClient = Boolean(discordClient && discordClient.user);
rows.push(
buildPrivilegeRow(
"discord-client-ready",
"Client connected",
"The Discord client is logged in and ready.",
hasClient
)
);
rows.push(
buildPrivilegeRow(
"discord-guild-configured",
"Guild configured",
"A server ID is configured in settings.",
Boolean(guildId)
)
);
let guild = null;
if (hasClient && guildId) {
guild = discordClient.guilds?.cache?.get(guildId) || null;
if (!guild && typeof discordClient.guilds?.fetch === "function") {
try {
guild = await discordClient.guilds.fetch(guildId);
} catch {
guild = null;
}
}
}
rows.push(
buildPrivilegeRow(
"discord-guild-access",
"Bot in guild",
"The bot can access the configured server.",
Boolean(guild)
)
);
let permissions = null;
if (guild && discordClient?.user?.id) {
try {
const cachedMember = guild.members?.cache?.get(discordClient.user.id) || null;
const member = cachedMember || (await guild.members.fetch(discordClient.user.id));
permissions = member?.permissions || null;
} catch {
permissions = null;
}
}
const permissionFlags = Permissions?.FLAGS || {};
const canCheck = Boolean(permissions && typeof permissions.has === "function");
for (const key of Object.keys(permissionFlags)) {
const details = DISCORD_PERMISSION_DETAILS[key] || {
label: toTitleCase(key),
description: "Discord permission flag."
};
const flag = permissionFlags[key];
const granted = canCheck && flag ? permissions.has(flag) : false;
rows.push(
buildPrivilegeRow(
`discord-perm-${key.toLowerCase()}`,
details.label,
details.description,
granted
)
);
}
return {
rows,
guildName: guild?.name || null
};
}
async function buildTwitchPrivileges() {
const clientId = getSetting("twitch_client_id");
const clientSecret = getSetting("twitch_client_secret");
const redirectUri = getSetting("twitch_redirect_uri");
const botUsername = getSetting("twitch_bot_username");
const botOauth = getSetting("twitch_bot_oauth");
const channels = parseChannelList(getSetting("twitch_channels"));
const twitchClient = getTwitchClient();
const connected = Boolean(twitchClient);
const joinedChannels =
twitchClient && typeof twitchClient.getChannels === "function"
? twitchClient.getChannels()
: [];
const rows = [
buildPrivilegeRow(
"twitch-client-id",
"Client ID",
"A Twitch app client ID is configured.",
Boolean(clientId)
),
buildPrivilegeRow(
"twitch-client-secret",
"Client secret",
"A Twitch app client secret is configured.",
Boolean(clientSecret)
),
buildPrivilegeRow(
"twitch-redirect",
"OAuth redirect URL",
"The OAuth redirect URL is configured.",
Boolean(redirectUri)
),
buildPrivilegeRow(
"twitch-bot-username",
"Bot username",
"The chat bot username is configured.",
Boolean(botUsername)
),
buildPrivilegeRow(
"twitch-bot-oauth",
"Bot OAuth token",
"The chat bot OAuth token is configured.",
Boolean(botOauth)
),
buildPrivilegeRow(
"twitch-channels-configured",
"Channels configured",
"At least one channel is configured for chat.",
channels.length > 0
),
buildPrivilegeRow(
"twitch-chat-connected",
"Chat connected",
"The Twitch chat client is connected.",
connected
),
buildPrivilegeRow(
"twitch-channels-joined",
"Channels joined",
"The chat client has joined its configured channels.",
joinedChannels.length > 0
)
];
let grantedScopes = [];
if (botOauth) {
const rawToken = botOauth.startsWith("oauth:") ? botOauth.slice(6) : botOauth;
try {
const response = await fetch("https://id.twitch.tv/oauth2/validate", {
headers: { Authorization: `OAuth ${rawToken}` }
});
if (response.ok) {
const data = await response.json();
if (Array.isArray(data.scopes)) {
grantedScopes = data.scopes;
}
}
} catch {
grantedScopes = [];
}
}
const grantedSet = new Set(grantedScopes.map((scope) => scope.toLowerCase()));
const knownScopeSet = new Set();
const scopeRows = TWITCH_SCOPE_DEFS.map((def) => {
const scopeKey = def.scope.toLowerCase();
knownScopeSet.add(scopeKey);
return buildPrivilegeRow(
`twitch-scope-${slugify(def.scope)}`,
def.label,
def.description,
grantedSet.has(scopeKey)
);
});
const unknownScopes = grantedScopes.filter(
(scope) => !knownScopeSet.has(scope.toLowerCase())
);
for (const scope of unknownScopes) {
scopeRows.push(
buildPrivilegeRow(
`twitch-scope-${slugify(scope)}`,
scope,
"Granted by the token but not recognized by this version of the bot.",
true
)
);
}
return {
rows: [...rows, ...scopeRows],
channelCount: channels.length
};
}
function readJsonSafe(filePath) {
try {
const raw = fs.readFileSync(filePath, "utf8");
return JSON.parse(raw);
} catch {
return null;
}
}
function parseBooleanSetting(value, fallback) {
if (value === undefined || value === null || value === "") {
return fallback;
}
if (typeof value === "boolean") {
return value;
}
const normalized = value.toString().toLowerCase();
return ["1", "true", "yes", "on"].includes(normalized);
}
function getPluginSettingsMap(pluginId) {
const rows = db
.prepare("SELECT key, value FROM plugin_settings WHERE plugin_id = ?")
.all(pluginId);
return rows.reduce((acc, row) => {
acc[row.key] = row.value;
return acc;
}, {});
}
function normalizeCommandTrigger(value, fallback = "") {
const raw = (value || fallback || "").toString().trim().replace(/^!+/, "");
if (!raw) {
return "";
}
return raw.split(/\s+/)[0].toLowerCase();
}
function normalizeSubcommand(value) {
const raw = (value || "").toString().trim().replace(/^!+/, "");
if (!raw) {
return "";
}
return raw.split(/\s+/)[0].toLowerCase();
}
function buildUsage(baseTrigger, usage) {
const raw = (usage || baseTrigger || "").toString().trim().replace(/^!+/, "");
return raw || baseTrigger;
}
function normalizeCustomPlatforms(value, availablePlatforms) {
return normalizePlatformSelection(value, availablePlatforms);
}
function parsePlatformSelectionFromBody(body, availablePlatforms) {
const raw = Array.isArray(body.platforms)
? body.platforms.join(",")
: body.platforms || body.platform;
return normalizePlatformSelection(raw, availablePlatforms);
}
function buildPlatformLabels(platforms) {
return platforms.map((platform) => ({
key: getPlatformBadge(platform),
label: getPlatformLabel(platform)
}));
}
const LOG_LEVELS = new Set(["debug", "info", "warn", "error"]);
const LOG_LIMITS = new Set([50, 100, 250, 500]);
const DEFAULT_LOG_RANGE_MS = 24 * 60 * 60 * 1000;
function normalizeLogLevel(value) {
const normalized = (value || "").toString().trim().toLowerCase();
return LOG_LEVELS.has(normalized) ? normalized : "";
}
function parseLogLevels(value) {
if (!value || value === "all") {
return [];
}
const raw = Array.isArray(value) ? value : value.toString().split(",");
return raw.map(normalizeLogLevel).filter(Boolean);
}
function parseLogRange(value) {
if (value === undefined || value === null || value === "") {
return { rangeMs: DEFAULT_LOG_RANGE_MS, rangeValue: `${DEFAULT_LOG_RANGE_MS}` };
}
const normalized = value.toString().trim().toLowerCase();
if (normalized === "all") {
return { rangeMs: null, rangeValue: "all" };
}
const parsed = Number(normalized);
if (!Number.isNaN(parsed) && parsed > 0) {
return { rangeMs: parsed, rangeValue: `${parsed}` };
}
return { rangeMs: DEFAULT_LOG_RANGE_MS, rangeValue: `${DEFAULT_LOG_RANGE_MS}` };
}
function parseLogLimit(value, { allowAll = false } = {}) {
if (value === undefined || value === null || value === "") {
return { limit: 50, limitValue: "50" };
}
const normalized = value.toString().trim().toLowerCase();
if (allowAll && normalized === "all") {
return { limit: null, limitValue: "all" };
}
const parsed = Number(normalized);
if (!Number.isNaN(parsed) && LOG_LIMITS.has(parsed)) {
return { limit: parsed, limitValue: `${parsed}` };
}
return { limit: 50, limitValue: "50" };
}
function normalizePageFormat(value) {
const normalized = (value || "").toString().trim().toLowerCase();
return normalized === "markdown" ? "markdown" : "html";
}
function escapeHtml(value) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return (value || "").toString().replace(/[&<>"']/g, (char) => map[char]);
}
function renderMarkdownInline(value) {
let output = escapeHtml(value);
output = output.replace(/`([^`]+)`/g, "$1");
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
return `${text}`;
});
output = output.replace(/\*\*([^*]+)\*\*/g, "$1");
output = output.replace(/\*([^*]+)\*/g, "$1");
return output;
}
function renderMarkdown(value) {
const lines = (value || "").toString().replace(/\r\n?/g, "\n").split("\n");
let html = "";
let paragraph = [];
let listType = null;
let inCode = false;
let codeLang = "";
let codeLines = [];
const flushParagraph = () => {
if (!paragraph.length) {
return;
}
html += `
${renderMarkdownInline(paragraph.join(" "))}
`; paragraph = []; }; const closeList = () => { if (!listType) { return; } html += `${listType}>`; listType = null; }; lines.forEach((line) => { const trimmed = line.trim(); if (inCode) { if (trimmed.startsWith("```")) { const codeBlock = escapeHtml(codeLines.join("\n")); const langClass = codeLang ? ` class="language-${codeLang}"` : ""; html += `${codeBlock}`;
inCode = false;
codeLang = "";
codeLines = [];
return;
}
codeLines.push(line);
return;
}
if (trimmed.startsWith("```")) {
flushParagraph();
closeList();
inCode = true;
codeLang = trimmed.slice(3).trim();
return;
}
if (!trimmed) {
flushParagraph();
closeList();
return;
}
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
if (headingMatch) {
flushParagraph();
closeList();
const level = headingMatch[1].length;
html += `${codeBlock}`;
}
flushParagraph();
closeList();
return html || "";
}
function buildCustomPageSrcdoc(page, theme) {
const content = (page?.content || "").toString();
const css = (page?.content_css || "").toString();
const themeCss = theme
? [
":root {",
` --ink: ${theme.light.text};`,
` --ink-soft: ${theme.light.muted};`,
` --sea: ${theme.light.accent};`,
` --sun: ${theme.light.accentAlt};`,
` --rose: ${theme.light.danger};`,
` --card: ${theme.light.surface};`,
` --surface-2: ${theme.light.surface2};`,
` --surface-3: ${theme.light.surface3};`,
` --border: ${theme.light.border};`,
` --bg-1: ${theme.light.bg1};`,
` --bg-2: ${theme.light.bg2};`,
` --bg-3: ${theme.light.bg3};`,
"}",
"@media (prefers-color-scheme: dark) {",
" :root {",
` --ink: ${theme.dark.text};`,
` --ink-soft: ${theme.dark.muted};`,
` --sea: ${theme.dark.accent};`,
` --sun: ${theme.dark.accentAlt};`,
` --rose: ${theme.dark.danger};`,
` --card: ${theme.dark.surface};`,
` --surface-2: ${theme.dark.surface2};`,
` --surface-3: ${theme.dark.surface3};`,
` --border: ${theme.dark.border};`,
` --bg-1: ${theme.dark.bg1};`,
` --bg-2: ${theme.dark.bg2};`,
` --bg-3: ${theme.dark.bg3};`,
" }",
"}"
].join("\n")
: "";
const baseCss = [
"* { box-sizing: border-box; }",
"html, body { margin: 0; padding: 0; }",
"body {",
" font-family: \"Source Sans 3\", sans-serif;",
" color: var(--ink, #121518);",
" background: transparent;",
"}"
].join("\n");
const fullCss = [themeCss, baseCss, css].filter(Boolean).join("\n\n");
return [
"",
"",
"",
" ",
" ",
" ",
" ",
"",
"",
content,
"",
""
].join("\n");
}
function buildCommandUsageMap() {
const rows = db.prepare("SELECT command_id, count FROM command_usage").all();
const map = new Map();
for (const row of rows) {
map.set(row.command_id, row.count);
}
return map;
}
function slugify(value) {
const raw = (value || "").toString().toLowerCase();
const slug = raw.replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
return slug || "command";
}
function toTitleCase(value) {
const raw = (value || "").toString().replace(/[-_]+/g, " ").trim();
if (!raw) {
return "";
}
return raw.replace(/\b\w/g, (match) => match.toUpperCase());
}
function truncateText(value, maxLength) {
const raw = (value || "").toString();
if (raw.length <= maxLength) {
return raw;
}
return `${raw.slice(0, Math.max(0, maxLength - 3))}...`;
}
function setFlash(req, type, message) {
req.session.flash = { type, message };
}
function getThemeSettings() {
return {
light: {
bg1: getSetting("theme_light_bg_1", "#ffe5c4"),
bg2: getSetting("theme_light_bg_2", "#f4efe8"),
bg3: getSetting("theme_light_bg_3", "#e9f3f1"),
text: getSetting("theme_light_text", "#121518"),
muted: getSetting("theme_light_text_muted", "#2c3137"),
accent: getSetting("theme_light_accent", "#0f6a78"),
accentAlt: getSetting("theme_light_accent_alt", "#f4a340"),
danger: getSetting("theme_light_danger", "#d66d5c"),
surface: getSetting("theme_light_surface", "#ffffff"),
surface2: getSetting("theme_light_surface_2", "#fbf9f6"),
surface3: getSetting("theme_light_surface_3", "#f9f5ef"),
border: getSetting("theme_light_border", "#e3ddd6")
},
dark: {
bg1: getSetting("theme_dark_bg_1", "#1b1d1f"),
bg2: getSetting("theme_dark_bg_2", "#16181b"),
bg3: getSetting("theme_dark_bg_3", "#0f1113"),
text: getSetting("theme_dark_text", "#f2f0ec"),
muted: getSetting("theme_dark_text_muted", "#c5bfb7"),
accent: getSetting("theme_dark_accent", "#4fb6c2"),
accentAlt: getSetting("theme_dark_accent_alt", "#f1b765"),
danger: getSetting("theme_dark_danger", "#e08173"),
surface: getSetting("theme_dark_surface", "#232629"),
surface2: getSetting("theme_dark_surface_2", "#2b2f33"),
surface3: getSetting("theme_dark_surface_3", "#30353a"),
border: getSetting("theme_dark_border", "#34393d")
},
role: {
public: getSetting("theme_role_public", "#ffffff"),
mod: getSetting("theme_role_mod", "#2cb678"),
admin: getSetting("theme_role_admin", "#e35678")
}
};
}
function getDiscordSettings() {
return {
discord_client_id: getSetting("discord_client_id", ""),
discord_client_secret: getSetting("discord_client_secret", ""),
discord_redirect_uri: getSetting("discord_redirect_uri", ""),
discord_bot_token: getSetting("discord_bot_token", ""),
discord_guild_id: getSetting("discord_guild_id", ""),
discord_admin_role_id: getSetting("discord_admin_role_id", ""),
discord_mod_role_id: getSetting("discord_mod_role_id", "")
};
}
function getTwitchSettings() {
return {
twitch_client_id: getSetting("twitch_client_id", ""),
twitch_client_secret: getSetting("twitch_client_secret", ""),
twitch_redirect_uri: getSetting("twitch_redirect_uri", ""),
twitch_bot_username: getSetting("twitch_bot_username", ""),
twitch_bot_oauth: getSetting("twitch_bot_oauth", ""),
twitch_channels: getSetting("twitch_channels", "")
};
}
function getYouTubeSettings() {
return {
youtube_client_id: getSetting("youtube_client_id", ""),
youtube_client_secret: getSetting("youtube_client_secret", ""),
youtube_redirect_uri: getSetting("youtube_redirect_uri", ""),
youtube_bot_channel_id: getSetting("youtube_bot_channel_id", "")
};
}
function saveSettingsMap(settings) {
for (const [key, value] of Object.entries(settings)) {
setSetting(key, value ?? "");
}
}
function storeSnapshot(req, key, settings) {
req.session[key] = settings;
}
function restoreSnapshot(req, key) {
const snapshot = req.session[key];
if (!snapshot) {
return;
}
saveSettingsMap(snapshot);
delete req.session[key];
}
function mergeSecrets(existing, incoming, secretKeys) {
const merged = { ...existing };
for (const [key, value] of Object.entries(incoming)) {
const trimmed = typeof value === "string" ? value.trim() : value;
if (secretKeys.has(key) && !trimmed) {
continue;
}
merged[key] = trimmed;
}
return merged;
}
async function verifyDiscordSettings(settings) {
const errors = [];
const checks = [];
const required = [
"discord_client_id",
"discord_client_secret",
"discord_bot_token",
"discord_guild_id"
];
const missing = required.filter((field) => !settings[field]);
if (missing.length) {
errors.push("Client ID, Client Secret, Bot Token, and Guild ID are required.");
return { ok: false, errors, checks };
}
try {
const botResponse = await fetch("https://discord.com/api/users/@me", {
headers: { Authorization: `Bot ${settings.discord_bot_token}` }
});
if (!botResponse.ok) {
errors.push("Bot token is invalid or missing permissions.");
} else {
const botUser = await botResponse.json();
checks.push(`Bot token verified (${botUser.username}).`);
}
} catch {
errors.push("Unable to reach Discord to verify bot token.");
}
try {
const guildResponse = await fetch(
`https://discord.com/api/guilds/${settings.discord_guild_id}`,
{ headers: { Authorization: `Bot ${settings.discord_bot_token}` } }
);
if (!guildResponse.ok) {
errors.push("Guild ID is invalid or the bot is not in the guild.");
} else {
const guild = await guildResponse.json();
checks.push(`Guild verified (${guild.name}).`);
}
} catch {
errors.push("Unable to verify the Discord guild.");
}
try {
const body = new URLSearchParams({
client_id: settings.discord_client_id,
client_secret: settings.discord_client_secret,
grant_type: "client_credentials",
scope: "identify"
});
const tokenResponse = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body
});
if (!tokenResponse.ok) {
errors.push("Client ID/Secret failed verification.");
} else {
checks.push("Client ID and secret verified.");
}
} catch {
errors.push("Unable to verify Discord client credentials.");
}
return { ok: errors.length === 0, errors, checks };
}
async function verifyTwitchSettings(settings) {
const errors = [];
const checks = [];
if (!settings.twitch_client_id || !settings.twitch_client_secret) {
errors.push("Twitch Client ID and Client Secret are required.");
return { ok: false, errors, checks };
}
let appToken = null;
try {
const tokenUrl =
"https://id.twitch.tv/oauth2/token" +
`?client_id=${encodeURIComponent(settings.twitch_client_id)}` +
`&client_secret=${encodeURIComponent(settings.twitch_client_secret)}` +
"&grant_type=client_credentials";
const tokenResponse = await fetch(tokenUrl, { method: "POST" });
if (!tokenResponse.ok) {
errors.push("Client ID/Secret failed verification.");
} else {
const data = await tokenResponse.json();
appToken = data.access_token;
checks.push("Client ID and secret verified.");
}
} catch {
errors.push("Unable to verify Twitch client credentials.");
}
if (settings.twitch_bot_username || settings.twitch_bot_oauth) {
if (!settings.twitch_bot_username || !settings.twitch_bot_oauth) {
errors.push("Bot username and OAuth token must both be filled in.");
} else {
const rawToken = settings.twitch_bot_oauth.startsWith("oauth:")
? settings.twitch_bot_oauth.slice(6)
: settings.twitch_bot_oauth;
try {
const validateResponse = await fetch(
"https://id.twitch.tv/oauth2/validate",
{ headers: { Authorization: `OAuth ${rawToken}` } }
);
if (!validateResponse.ok) {
errors.push("Bot OAuth token failed validation.");
} else {
const data = await validateResponse.json();
checks.push(`Bot token verified (${data.login}).`);
if (
settings.twitch_bot_username &&
data.login &&
data.login.toLowerCase() !== settings.twitch_bot_username.toLowerCase()
) {
errors.push("Bot username does not match the OAuth token.");
}
}
} catch {
errors.push("Unable to verify Twitch bot OAuth token.");
}
}
}
if (appToken && settings.twitch_bot_username) {
try {
const userResponse = await fetch(
`https://api.twitch.tv/helix/users?login=${encodeURIComponent(
settings.twitch_bot_username
)}`,
{
headers: {
"Client-Id": settings.twitch_client_id,
Authorization: `Bearer ${appToken}`
}
}
);
if (userResponse.ok) {
const data = await userResponse.json();
if (data.data && data.data.length) {
checks.push(`Bot account found (${data.data[0].display_name}).`);
} else {
errors.push("Bot username was not found on Twitch.");
}
}
} catch {
errors.push("Unable to verify bot username with Twitch.");
}
}
return { ok: errors.length === 0, errors, checks };
}
async function verifyYouTubeSettings(settings) {
const errors = [];
const checks = [];
if (!settings.youtube_client_id || !settings.youtube_client_secret) {
errors.push("YouTube Client ID and Client Secret are required.");
return { ok: false, errors, checks };
}
const refreshToken =
settings.youtube_bot_refresh_token || getSetting("youtube_bot_refresh_token", "");
if (!refreshToken) {
checks.push("Bot account not connected yet.");
return { ok: errors.length === 0, errors, checks };
}
try {
const body = new URLSearchParams({
client_id: settings.youtube_client_id,
client_secret: settings.youtube_client_secret,
refresh_token: refreshToken,
grant_type: "refresh_token"
});
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body
});
if (!response.ok) {
errors.push("Bot refresh token failed verification.");
return { ok: false, errors, checks };
}
const token = await response.json();
checks.push("Bot refresh token verified.");
const channel = await fetchYouTubeChannel(token.access_token);
if (channel?.snippet?.title) {
checks.push(`Bot channel verified (${channel.snippet.title}).`);
} else {
checks.push("Bot channel verified.");
}
return { ok: errors.length === 0, errors, checks, channel };
} catch {
errors.push("Unable to verify YouTube bot credentials.");
return { ok: false, errors, checks };
}
}
function createWebServer({ loadPlugins, discordClient }) {
const app = express();
const webhooks = createWebhookService();
if (!global.lumiFrameworks) {
global.lumiFrameworks = {};
}
global.lumiFrameworks.webhooks = webhooks;
const assetVersion = Date.now().toString();
const sessionStore = new BetterSqlite3Store({
client: db
});
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.use("/webhooks", webhooks.router);
const originalAppRender = app.render.bind(app);
app.render = (view, options, callback) => {
if (typeof options === "function") {
callback = options;
options = {};
}
const safeOptions = options || {};
const respond = (err, html) => {
if (!err) {
if (typeof callback === "function") {
return callback(null, html);
}
return html;
}
const message = err?.message || "";
const detail = {
view,
message,
stack: err?.stack || ""
};
const isMissing = message.includes("Failed to lookup view");
if (!isMissing) {
log("error", "View render failed", detail);
if (typeof callback === "function") {
return callback(err);
}
throw err;
}
log("warn", "Missing view fallback", detail);
if (view === "missing-view") {
const fallback =
"Some content could not be loaded.
"; if (typeof callback === "function") { return callback(null, fallback); } return fallback; } return originalAppRender( "missing-view", { ...safeOptions, title: "Content missing", resource: "A page component failed to load. Please try again.", softError: "Some content could not be loaded." }, callback ); }; return originalAppRender(view, safeOptions, respond); }; app.use( session({ secret: ensureSessionSecret(), resave: false, saveUninitialized: false, store: sessionStore }) ); app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, "public"))); const uploadDir = path.join(__dirname, "..", "..", "data", "uploads"); fs.mkdirSync(uploadDir, { recursive: true }); const navIconDir = path.join(__dirname, "..", "..", "data", "nav-icons"); fs.mkdirSync(navIconDir, { recursive: true }); app.use("/assets/nav-icons", express.static(navIconDir)); const upload = multer ? multer({ dest: uploadDir }) : null; const uploadSingle = (fieldName) => { if (!upload) { return (req, _res, next) => { req.uploadError = "File uploads require npm install."; next(); }; } return upload.single(fieldName); }; const navIconUpload = multer && multer({ storage: multer.diskStorage({ destination: (_req, _file, cb) => cb(null, navIconDir), filename: (_req, file, cb) => { const ext = path.extname(file.originalname || ".svg").slice(0, 8); cb(null, `${crypto.randomUUID()}${ext}`); } }), fileFilter: (_req, file, cb) => { if (file.mimetype === "image/png" || file.mimetype === "image/svg+xml") { return cb(null, true); } cb(new Error("Only SVG or PNG files are allowed.")); } }); const navIconSingle = (req, _res, next) => { if (!navIconUpload) { req.uploadError = "File uploads require npm install."; return next(); } return navIconUpload.single("icon_file")(req, _res, next); }; const navItems = []; const profileSections = []; const web = { createRouter: () => express.Router(), mount: (mountPath, router, navItem) => { app.use(mountPath, router); if (navItem) { navItems.push({ ...navItem, path: mountPath }); } }, addNavItem: (navItem) => { navItems.push(navItem); }, addProfileSection: (section) => { if (!section || (!section.view && !section.content)) { return; } profileSections.push(section); } }; app.use(requireConfigured); app.use((req, res, next) => { res.locals.siteTitle = getSetting("site_title", "Lumi Bot"); res.locals.assetVersion = assetVersion; res.locals.user = req.session.user || null; res.locals.flash = req.session.flash || null; res.locals.softError = null; res.locals.theme = getThemeSettings(); res.locals.botAvatar = getSetting("bot_avatar_url", null); const platformStatus = getPlatformStatus(); res.locals.platforms = platformStatus; res.locals.platformLogins = platformStatus.filter( (platform) => platform.supported && platform.enabled && platform.supportsLogin ); res.locals.platformLinks = platformStatus.filter( (platform) => platform.supported && platform.enabled && platform.supportsLink ); const twitchPlatform = platformStatus.find((platform) => platform.id === "twitch"); res.locals.twitchConfigured = Boolean(twitchPlatform?.configured); res.locals.currentPath = req.path; res.locals.userAvatar = req.session.user ? getPreferredAvatar(req.session.user.id) : null; res.locals.userInitial = req.session.user?.username ? req.session.user.username.charAt(0).toUpperCase() : ""; req.session.flash = null; trackModRole(db, req.session.user); res.locals.navSections = buildNavSections( req.session.user, navItems, req.path ); next(); }); app.use((req, res, next) => { const originalRender = res.render.bind(res); res.render = (view, options, callback) => { if (typeof options === "function") { callback = options; options = {}; } const handleError = (err) => { if (!err) { return; } const message = err?.message || ""; const isMissing = message.includes("Failed to lookup view"); const context = { view, method: req.method, path: req.path, userId: req.session.user?.id || null, message, stack: err?.stack || "" }; if (!isMissing) { log("error", "Render failed", context); return originalRender( "error", { title: "Something went wrong", message: "An unexpected error occurred. Please try again." }, callback ); } log("warn", "Missing view fallback", context); res.locals.softError = "Some content could not be loaded."; if (view === "missing-view") { return res .status(200) .send( "Some content could not be loaded.
" ); } return originalRender( "missing-view", { title: "Content missing", resource: "A page component failed to load. Please try again." }, callback ); }; return originalRender(view, options, (err, html) => { if (err) { return handleError(err); } if (typeof callback === "function") { return callback(null, html); } res.send(html); }); }; next(); }); app.get("/", (req, res) => { res.render("home", { title: "Home" }); }); app.get("/setup", (req, res) => { if (isConfigured()) { return res.redirect("/"); } const platforms = getPlatformStatus(); res.render("setup", { title: "Initial setup", platforms }); }); app.get("/setup/discord", (req, res) => { if (!isPlatformEnabled("discord")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getDiscordSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, discord_client_secret: "", discord_bot_token: "", discord_redirect_uri: current.discord_redirect_uri || `${baseUrl}/auth/discord/callback` }; storeSnapshot(req, "discordWizardSnapshot", current); res.render("wizard-discord", { title: "Discord setup", form, checks: [], errors: [], actionBase: "/setup/discord", cancelPath: "/setup" }); }); app.post("/setup/discord/verify", async (req, res) => { if (!isPlatformEnabled("discord")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getDiscordSettings(); const incoming = { discord_client_id: req.body.discord_client_id || "", discord_client_secret: req.body.discord_client_secret || "", discord_redirect_uri: req.body.discord_redirect_uri || "", discord_bot_token: req.body.discord_bot_token || "", discord_guild_id: req.body.discord_guild_id || "", discord_admin_role_id: req.body.discord_admin_role_id || "", discord_mod_role_id: req.body.discord_mod_role_id || "" }; const merged = mergeSecrets( current, incoming, new Set(["discord_client_secret", "discord_bot_token"]) ); if (!merged.discord_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.discord_redirect_uri = `${baseUrl}/auth/discord/callback`; } const result = await verifyDiscordSettings(merged); if (!result.ok) { res.render("wizard-discord", { title: "Discord setup", form: { ...incoming, discord_client_secret: "", discord_bot_token: "", discord_redirect_uri: merged.discord_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/setup/discord", cancelPath: "/setup" }); return; } saveSettingsMap(merged); delete req.session.discordWizardSnapshot; setFlash(req, "success", "Discord setup saved. Please log in."); res.redirect("/auth/discord"); }); app.post("/setup/discord/cancel", (req, res) => { if (!isPlatformEnabled("discord")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } restoreSnapshot(req, "discordWizardSnapshot"); setFlash(req, "info", "Discord setup canceled."); res.redirect("/setup"); }); app.get("/setup/twitch", (req, res) => { if (!isPlatformEnabled("twitch")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getTwitchSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, twitch_client_secret: "", twitch_bot_oauth: "", twitch_redirect_uri: current.twitch_redirect_uri || `${baseUrl}/auth/twitch/callback` }; storeSnapshot(req, "twitchWizardSnapshot", current); res.render("wizard-twitch", { title: "Twitch setup", form, checks: [], errors: [], actionBase: "/setup/twitch", cancelPath: "/setup" }); }); app.post("/setup/twitch/verify", async (req, res) => { if (!isPlatformEnabled("twitch")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getTwitchSettings(); const incoming = { twitch_client_id: req.body.twitch_client_id || "", twitch_client_secret: req.body.twitch_client_secret || "", twitch_redirect_uri: req.body.twitch_redirect_uri || "", twitch_bot_username: req.body.twitch_bot_username || "", twitch_bot_oauth: req.body.twitch_bot_oauth || "", twitch_channels: req.body.twitch_channels || "" }; const merged = mergeSecrets( current, incoming, new Set(["twitch_client_secret", "twitch_bot_oauth"]) ); if (!merged.twitch_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.twitch_redirect_uri = `${baseUrl}/auth/twitch/callback`; } const result = await verifyTwitchSettings(merged); if (!result.ok) { res.render("wizard-twitch", { title: "Twitch setup", form: { ...incoming, twitch_client_secret: "", twitch_bot_oauth: "", twitch_redirect_uri: merged.twitch_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/setup/twitch", cancelPath: "/setup" }); return; } saveSettingsMap(merged); delete req.session.twitchWizardSnapshot; setFlash(req, "success", "Twitch setup saved."); res.redirect("/setup"); }); app.post("/setup/twitch/cancel", (req, res) => { if (!isPlatformEnabled("twitch")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } restoreSnapshot(req, "twitchWizardSnapshot"); setFlash(req, "info", "Twitch setup canceled."); res.redirect("/setup"); }); app.get("/setup/youtube", (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getYouTubeSettings(); const baseUrl = `${req.protocol}://${req.get("host")}`; const form = { ...current, youtube_client_secret: "", youtube_redirect_uri: current.youtube_redirect_uri || `${baseUrl}/auth/youtube/callback` }; const snapshot = { ...current, youtube_bot_refresh_token: getSetting("youtube_bot_refresh_token", ""), youtube_bot_channel_id: getSetting("youtube_bot_channel_id", "") }; storeSnapshot(req, "youtubeWizardSnapshot", snapshot); res.render("wizard-youtube", { title: "YouTube setup", form, checks: [], errors: [], actionBase: "/setup/youtube", cancelPath: "/setup", connectPath: "/setup/youtube/connect", botConnected: Boolean(getSetting("youtube_bot_refresh_token", "")), botChannelId: getSetting("youtube_bot_channel_id", ""), botChannelName: getYouTubeClient()?.channelName || null }); }); app.post("/setup/youtube/connect", (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const baseUrl = `${req.protocol}://${req.get("host")}`; const clientId = (req.body.youtube_client_id || "").trim(); const clientSecret = (req.body.youtube_client_secret || "").trim(); const redirectUri = (req.body.youtube_redirect_uri || "").trim() || `${baseUrl}/auth/youtube/callback`; if (!clientId || !clientSecret) { setFlash(req, "error", "Client ID and Client Secret are required."); return res.redirect("/setup/youtube"); } setSetting("youtube_client_id", clientId); setSetting("youtube_client_secret", clientSecret); setSetting("youtube_redirect_uri", redirectUri); const state = crypto.randomBytes(16).toString("hex"); req.session.youtubeBotState = state; req.session.youtubeBotReturnTo = "/setup/youtube"; const url = buildYouTubeAuthUrl(state, redirectUri, { prompt: "consent" }); res.redirect(url); }); app.post("/setup/youtube/verify", async (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } const current = getYouTubeSettings(); const incoming = { youtube_client_id: req.body.youtube_client_id || "", youtube_client_secret: req.body.youtube_client_secret || "", youtube_redirect_uri: req.body.youtube_redirect_uri || "" }; const merged = mergeSecrets( current, incoming, new Set(["youtube_client_secret"]) ); if (!merged.youtube_redirect_uri) { const baseUrl = `${req.protocol}://${req.get("host")}`; merged.youtube_redirect_uri = `${baseUrl}/auth/youtube/callback`; } const result = await verifyYouTubeSettings({ ...merged, youtube_bot_refresh_token: getSetting("youtube_bot_refresh_token", "") }); if (!result.ok) { res.render("wizard-youtube", { title: "YouTube setup", form: { ...incoming, youtube_client_secret: "", youtube_redirect_uri: merged.youtube_redirect_uri }, checks: result.checks, errors: result.errors, actionBase: "/setup/youtube", cancelPath: "/setup", connectPath: "/setup/youtube/connect", botConnected: Boolean(getSetting("youtube_bot_refresh_token", "")), botChannelId: getSetting("youtube_bot_channel_id", ""), botChannelName: getYouTubeClient()?.channelName || null }); return; } if (result.channel?.id) { merged.youtube_bot_channel_id = result.channel.id; } saveSettingsMap(merged); delete req.session.youtubeWizardSnapshot; setFlash(req, "success", "YouTube setup saved."); res.redirect("/setup"); }); app.post("/setup/youtube/cancel", (req, res) => { if (!isPlatformEnabled("youtube")) { return res.redirect("/setup"); } if (isConfigured()) { return res.redirect("/"); } restoreSnapshot(req, "youtubeWizardSnapshot"); setFlash(req, "info", "YouTube setup canceled."); res.redirect("/setup"); }); app.get("/auth/discord", (req, res) => { if (!isPlatformEnabled("discord")) { setFlash(req, "error", "Discord is disabled in Platform Integration."); return res.redirect("/setup"); } if (!isPlatformConfigured("discord")) { setFlash(req, "error", "Discord is not configured yet."); return res.redirect("/setup"); } const state = crypto.randomBytes(16).toString("hex"); if (req.session.user) { req.session.discordLinkState = state; } else { req.session.discordState = state; } const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("discord_redirect_uri") || `${baseUrl}/auth/discord/callback`; const url = buildDiscordAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/discord/callback", async (req, res) => { if (!isPlatformEnabled("discord")) { return res.status(400).render("error", { title: "Login failed", message: "Discord is disabled in Platform Integration." }); } const { code, state } = req.query; const isLogin = state && state === req.session.discordState; const isLink = state && state === req.session.discordLinkState && req.session.user; if (!code || (!isLogin && !isLink)) { return res.status(400).render("error", { title: "Login failed", message: "Invalid OAuth state." }); } try { if (isLogin) { delete req.session.discordState; } if (isLink) { delete req.session.discordLinkState; } const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("discord_redirect_uri") || `${baseUrl}/auth/discord/callback`; const token = await exchangeDiscordCode(code, redirectUri); const user = await fetchDiscordUser(token.access_token); const guildId = getSetting("discord_guild_id"); const member = guildId ? await fetchDiscordGuildMember(token.access_token, guildId) : null; const roles = member?.roles || []; const flags = getRoleFlags(roles); if (isLink) { const linked = linkIdentityToUser({ userId: req.session.user.id, provider: "discord", providerUserId: user.id, displayName: user.global_name || user.username, avatar: user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=128` : null }); req.session.user = { id: linked.id, username: linked.internal_username, avatar: user.avatar, roles, ...flags }; req.session.discordToken = token; setFlash(req, "success", "Discord account linked."); res.redirect("/profile"); return; } const profile = storeDiscordUser(user); req.session.user = { id: profile.id, username: profile.internal_username, avatar: user.avatar, roles, ...flags }; req.session.discordToken = token; setFlash(req, "success", "Logged in."); res.redirect("/"); } catch (error) { console.error(error); res.status(500).render("error", { title: "Login failed", message: "Discord authentication failed." }); } }); app.post("/auth/logout", (req, res) => { req.session.destroy(() => { res.redirect("/"); }); }); app.get("/auth/twitch", requireAuth, (req, res) => { if (!isPlatformEnabled("twitch")) { setFlash(req, "error", "Twitch is disabled in Platform Integration."); return res.redirect("/profile"); } if (!isPlatformConfigured("twitch")) { setFlash(req, "error", "Twitch is not configured yet."); return res.redirect("/profile"); } const state = crypto.randomBytes(16).toString("hex"); req.session.twitchState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("twitch_redirect_uri") || `${baseUrl}/auth/twitch/callback`; const url = buildTwitchAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/twitch/login", (req, res) => { if (!isPlatformEnabled("twitch")) { setFlash(req, "error", "Twitch is disabled in Platform Integration."); return res.redirect("/setup"); } if (!isPlatformConfigured("twitch")) { setFlash(req, "error", "Twitch is not configured yet."); return res.redirect("/setup"); } const state = crypto.randomBytes(16).toString("hex"); req.session.twitchLoginState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("twitch_redirect_uri") || `${baseUrl}/auth/twitch/callback`; const url = buildTwitchAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/twitch/callback", async (req, res) => { if (!isPlatformEnabled("twitch")) { return res.status(400).render("error", { title: "Auth failed", message: "Twitch is disabled in Platform Integration." }); } const { code, state } = req.query; const isLogin = state && state === req.session.twitchLoginState; const isLink = state && state === req.session.twitchState && req.session.user; if (!code || (!isLogin && !isLink)) { return res.status(400).render("error", { title: "Auth failed", message: "Invalid Twitch OAuth state." }); } try { const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("twitch_redirect_uri") || `${baseUrl}/auth/twitch/callback`; const token = await exchangeTwitchCode(code, redirectUri); const twitchUser = await fetchTwitchUser(token.access_token); if (!twitchUser) { throw new Error("Twitch user not found."); } const now = Date.now(); if (isLogin) { const profile = ensureUserForIdentity({ provider: "twitch", providerUserId: twitchUser.id, displayName: twitchUser.display_name, avatar: twitchUser.profile_image_url || null }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'twitch', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, twitchUser.id, twitchUser.display_name, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); const discordIdentity = db .prepare( "SELECT provider_user_id FROM user_identities WHERE user_id = ? AND provider = 'discord'" ) .get(profile.id); const roles = discordIdentity?.provider_user_id ? await fetchDiscordRolesForUser(discordIdentity.provider_user_id) : []; const flags = getRoleFlags(roles); req.session.user = { id: profile.id, username: profile.internal_username, roles, ...flags }; req.session.twitchToken = token; setFlash(req, "success", "Logged in with Twitch."); res.redirect("/"); } else { const profile = linkIdentityToUser({ userId: req.session.user.id, provider: "twitch", providerUserId: twitchUser.id, displayName: twitchUser.display_name, avatar: twitchUser.profile_image_url || null }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'twitch', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, twitchUser.id, twitchUser.display_name, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); req.session.user.id = profile.id; req.session.user.username = profile.internal_username; setFlash(req, "success", "Twitch account linked."); res.redirect("/profile"); } } catch (error) { console.error(error); res.status(500).render("error", { title: isLogin ? "Login failed" : "Link failed", message: isLogin ? "Unable to sign in with Twitch." : "Unable to link Twitch account." }); } }); app.get("/auth/youtube", requireAuth, (req, res) => { if (!isPlatformEnabled("youtube")) { setFlash(req, "error", "YouTube is disabled in Platform Integration."); return res.redirect("/profile"); } if (!isPlatformConfigured("youtube")) { setFlash(req, "error", "YouTube is not configured yet."); return res.redirect("/profile"); } const state = crypto.randomBytes(16).toString("hex"); req.session.youtubeState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("youtube_redirect_uri") || `${baseUrl}/auth/youtube/callback`; const url = buildYouTubeAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/youtube/login", (req, res) => { if (!isPlatformEnabled("youtube")) { setFlash(req, "error", "YouTube is disabled in Platform Integration."); return res.redirect("/setup"); } if (!isPlatformConfigured("youtube")) { setFlash(req, "error", "YouTube is not configured yet."); return res.redirect("/setup"); } const state = crypto.randomBytes(16).toString("hex"); req.session.youtubeLoginState = state; const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("youtube_redirect_uri") || `${baseUrl}/auth/youtube/callback`; const url = buildYouTubeAuthUrl(state, redirectUri); res.redirect(url); }); app.get("/auth/youtube/callback", async (req, res) => { if (!isPlatformEnabled("youtube")) { return res.status(400).render("error", { title: "Auth failed", message: "YouTube is disabled in Platform Integration." }); } const { code, state } = req.query; const isLogin = state && state === req.session.youtubeLoginState; const isLink = state && state === req.session.youtubeState && req.session.user; const isBot = state && state === req.session.youtubeBotState; if (!code || (!isLogin && !isLink && !isBot)) { return res.status(400).render("error", { title: "Auth failed", message: "Invalid YouTube OAuth state." }); } try { const baseUrl = `${req.protocol}://${req.get("host")}`; const redirectUri = getSetting("youtube_redirect_uri") || `${baseUrl}/auth/youtube/callback`; const token = await exchangeYouTubeCode(code, redirectUri); const channel = await fetchYouTubeChannel(token.access_token); if (!channel) { throw new Error("YouTube channel not found."); } const displayName = channel.snippet?.title || "YouTube Channel"; const avatar = channel.snippet?.thumbnails?.default?.url || null; const now = Date.now(); if (isBot) { const refreshToken = token.refresh_token || getSetting("youtube_bot_refresh_token", ""); if (!refreshToken) { throw new Error("Missing YouTube refresh token."); } setSetting("youtube_bot_refresh_token", refreshToken); setSetting("youtube_bot_channel_id", channel.id || ""); req.session.youtubeBotState = null; const returnTo = req.session.youtubeBotReturnTo || "/admin/youtube-wizard"; req.session.youtubeBotReturnTo = null; setFlash(req, "success", "YouTube bot connected."); return res.redirect(returnTo); } if (isLogin) { const profile = ensureUserForIdentity({ provider: "youtube", providerUserId: channel.id, displayName, avatar }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'youtube', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, channel.id, displayName, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); const discordIdentity = db .prepare( "SELECT provider_user_id FROM user_identities WHERE user_id = ? AND provider = 'discord'" ) .get(profile.id); const roles = discordIdentity?.provider_user_id ? await fetchDiscordRolesForUser(discordIdentity.provider_user_id) : []; const flags = getRoleFlags(roles); req.session.user = { id: profile.id, username: profile.internal_username, roles, ...flags }; req.session.youtubeToken = token; setFlash(req, "success", "Logged in with YouTube."); return res.redirect("/"); } const profile = linkIdentityToUser({ userId: req.session.user.id, provider: "youtube", providerUserId: channel.id, displayName, avatar }); db.prepare( "INSERT INTO linked_accounts (user_id, provider, provider_user_id, display_name, access_token, refresh_token, expires_at, created_at, updated_at) " + "VALUES (?, 'youtube', ?, ?, ?, ?, ?, ?, ?) " + "ON CONFLICT(user_id, provider) DO UPDATE SET provider_user_id = excluded.provider_user_id, display_name = excluded.display_name, access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at, updated_at = excluded.updated_at" ).run( profile.id, channel.id, displayName, token.access_token, token.refresh_token || "", token.expires_in ? now + token.expires_in * 1000 : null, now, now ); req.session.user.id = profile.id; req.session.user.username = profile.internal_username; setFlash(req, "success", "YouTube account linked."); res.redirect("/profile"); } catch (error) { console.error(error); res.status(500).render("error", { title: isBot ? "Bot connect failed" : isLogin ? "Login failed" : "Link failed", message: isBot ? "Unable to connect the YouTube bot." : isLogin ? "Unable to sign in with YouTube." : "Unable to link YouTube account." }); } }); app.get("/profile", requireAuth, (req, res) => { const profile = getUserProfileById(req.session.user.id); if (!profile) { req.session.destroy(() => { res.redirect("/"); }); return; } const accounts = getUserIdentities(req.session.user.id); const cooldownDays = 90; const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000; const lastUpdate = profile.username_updated_at || 0; const nextAllowedAt = lastUpdate ? lastUpdate + cooldownMs : 0; const canChangeUsername = !lastUpdate || Date.now() >= nextAllowedAt; const remainingMs = canChangeUsername ? 0 : nextAllowedAt - Date.now(); const remainingDays = canChangeUsername ? 0 : Math.max(1, Math.ceil(remainingMs / (24 * 60 * 60 * 1000))); const profileWidgets = profileSections .filter((section) => hasAccess(req.session.user, section.role || "public")) .slice() .sort((a, b) => (a.order || 100) - (b.order || 100)) .map((section) => ({ ...section, locals: { ...(section.locals || {}), user: req.session.user, profile, accounts } })); res.render("profile", { title: "Your profile", accounts, profile, profileSections: profileWidgets, canChangeUsername, usernameCooldownDays: cooldownDays, usernameCooldownRemainingDays: remainingDays, usernameNextAllowedAt: nextAllowedAt }); }); app.post("/profile/unlink/:provider", requireAuth, (req, res) => { const provider = req.params.provider; if (provider === "discord") { setFlash(req, "error", "Discord cannot be unlinked from the WebUI."); return res.redirect("/profile"); } db.prepare( "DELETE FROM linked_accounts WHERE user_id = ? AND provider = ?" ).run(req.session.user.id, provider); db.prepare( "DELETE FROM user_identities WHERE user_id = ? AND provider = ?" ).run(req.session.user.id, provider); setFlash(req, "success", "Account unlinked."); res.redirect("/profile"); }); app.post("/profile/username", requireAuth, (req, res) => { const desired = (req.body.internal_username || "").trim(); const profile = getUserProfileById(req.session.user.id); const cooldownMs = 90 * 24 * 60 * 60 * 1000; if (profile?.username_updated_at) { const nextAllowed = profile.username_updated_at + cooldownMs; if (Date.now() < nextAllowed) { setFlash( req, "error", "You can change your username once every 90 days." ); return res.redirect("/profile"); } } const result = updateInternalUsername(req.session.user.id, desired); if (!result.ok) { setFlash(req, "error", result.reason); return res.redirect("/profile"); } req.session.user.username = result.username; setFlash(req, "success", "Username updated."); res.redirect("/profile"); }); app.get("/health", (req, res) => { res.set("Cache-Control", "no-store"); res.json({ ok: true, ts: Date.now() }); }); app.get("/commands", (req, res) => { const prefix = getSetting("command_prefix", "!"); const baseUrl = `${req.protocol}://${req.get("host")}`; const usageMap = buildCommandUsageMap(); const commands = []; const conflictMap = new Map(); const enabledPlatforms = getEnabledPlatformIds(); const addConflictEntries = (command) => { const triggers = command.conflictTriggers || [command.trigger]; for (const trigger of triggers) { for (const platform of command.platforms) { const subcommand = command.subcommand || ""; const key = `${platform}:${trigger}:${subcommand}`; const entries = conflictMap.get(key) || []; if (!entries.some((entry) => entry.id === command.id)) { entries.push({ id: command.id, name: command.name, origin: command.origin }); } conflictMap.set(key, entries); } } }; const addCommand = (command) => { command.count = usageMap.get(command.id) || 0; command.anchor = `cmd-${slugify(command.id)}`; command.link = `${baseUrl}/commands#${command.anchor}`; const subcommand = command.subcommand || ""; command.search = [ command.triggerDisplay, subcommand, command.name, command.description, command.origin, command.platforms.join(" ") ] .join(" ") .toLowerCase(); command.sort = { trigger: subcommand ? `${command.trigger} ${subcommand}` : command.trigger, name: command.name.toLowerCase(), description: command.description.toLowerCase(), level: command.level.toLowerCase(), platform: command.platforms.join(" "), origin: command.origin.toLowerCase(), count: command.count }; if (command.levelHelp) { command.levelHelp = command.levelHelp.toString(); } commands.push(command); addConflictEntries(command); }; const customCommands = db .prepare( "SELECT id, trigger, response, mode, language, platform FROM custom_commands WHERE enabled = 1 ORDER BY trigger" ) .all(); for (const row of customCommands) { const trigger = normalizeCommandTrigger(row.trigger); if (!trigger) { continue; } const platforms = normalizeCustomPlatforms(row.platform, enabledPlatforms); const description = row.mode === "advanced" ? `Advanced command (${row.language})` : truncateText(row.response, 140); addCommand({ id: `custom:${trigger}`, trigger, triggerDisplay: `${prefix}${trigger}`, name: toTitleCase(trigger) || trigger, description, level: "public", origin: "Custom", platforms, platformLabels: buildPlatformLabels(platforms), conflictTriggers: [trigger] }); } const topOptions = getTopCommandOptions(); if (topOptions.length) { const topPlatforms = enabledPlatforms.slice(); const topPlatformLabels = buildPlatformLabels(topPlatforms); const topUsage = buildUsage("top", "top