const { db } = require("./db"); const { incrementCommands } = require("./stats"); const { buildCommandContext, runAdvancedCommand, normalizeCommandResult } = require("./commands"); const { getEnabledPlatformIds, normalizePlatformSelection } = require("./platforms"); function createCommandRouter({ settings }) { const commandMap = new Map(); const pluginCommands = new Map(); function clearCommands(pluginId) { const existing = pluginCommands.get(pluginId) || []; for (const entry of existing) { const handlers = commandMap.get(entry.trigger) || []; const nextHandlers = handlers.filter((handler) => handler !== entry.handler); if (nextHandlers.length) { commandMap.set(entry.trigger, nextHandlers); } else { commandMap.delete(entry.trigger); } } pluginCommands.delete(pluginId); } function registerCommands(pluginId, commands = []) { if (!pluginId) { throw new Error("Plugin id is required to register commands."); } clearCommands(pluginId); const entries = []; for (const command of commands) { const triggers = (command.triggers || []) .map((trigger) => trigger.toLowerCase()) .filter(Boolean); const handler = buildHandler(command); for (const trigger of triggers) { const list = commandMap.get(trigger) || []; list.push(handler); commandMap.set(trigger, list); entries.push({ trigger, handler }); } } pluginCommands.set(pluginId, entries); } function buildHandler(command) { const handler = async (ctx) => { if (command.platforms && command.platforms.length) { if (!command.platforms.includes(ctx.platform)) { return false; } } return await command.handler(ctx); }; handler.commandId = command.id || null; return handler; } async function handleMessage({ platform, raw, user, platformUser, reply, meta }) { const prefix = settings.getSetting("command_prefix", "!"); if (!raw.startsWith(prefix)) { return false; } const rawCommand = raw.slice(prefix.length).trim(); if (!rawCommand) { return false; } const parts = rawCommand.split(/\s+/); const trigger = parts[0].toLowerCase(); const args = parts.slice(1); const argsText = args.join(" "); const ctx = { platform, trigger, raw, args, argsText, user: { id: user.id, username: user.internal_username || user.username, platformId: platformUser.id, displayName: platformUser.displayName || platformUser.username, tag: platformUser.tag }, platformUser, meta, reply }; const customHandled = await handleCustomCommand({ trigger, platform, ctx, raw, reply }); if (customHandled) { incrementCommands(user.id); return true; } const handlers = commandMap.get(trigger) || []; for (const handler of handlers) { try { const result = await handler(ctx); if (typeof result === "string" && result) { await safeReply(reply, result); recordCommandUsage(handler.commandId); incrementCommands(user.id); return true; } if (result === true) { recordCommandUsage(handler.commandId); incrementCommands(user.id); return true; } } catch (error) { console.error("Command handler failed", error); await safeReply(reply, "Command failed to execute."); return true; } } return false; } return { registerCommands, clearCommands, handleMessage }; } async function handleCustomCommand({ trigger, platform, ctx, raw, reply }) { const row = db .prepare( "SELECT response, mode, language, code, platform FROM custom_commands WHERE trigger = ? AND enabled = 1" ) .get(trigger); if (!row) { return false; } const enabledPlatforms = getEnabledPlatformIds(); const allowedPlatforms = normalizePlatformSelection(row.platform, enabledPlatforms); if (!allowedPlatforms.includes(platform)) { return false; } try { if (row.mode === "advanced" && row.code) { const messageInfo = buildMessageInfo(ctx, raw); const commandCtx = buildCommandContext({ platform, user: { id: ctx.user.id, platformId: ctx.user.platformId, username: ctx.user.username, displayName: ctx.user.displayName, tag: ctx.user.tag }, message: messageInfo, args: ctx.args, argsText: ctx.argsText }); const result = await runAdvancedCommand( { code: row.code, language: row.language }, commandCtx ); const output = normalizeCommandResult(result); if (output) { await safeReply(reply, output); } else { await safeReply(reply, "Command ran but returned no output."); } } else { await safeReply(reply, row.response); } recordCommandUsage(`custom:${trigger}`); return true; } catch (error) { console.error("Failed to reply to command", error); await safeReply(reply, "Command failed to execute."); return true; } } function buildMessageInfo(ctx, raw) { if (ctx.platform === "discord" && ctx.meta?.message) { const message = ctx.meta.message; return { id: message.id, content: raw, channelId: message.channelId, guildId: message.guildId }; } if (ctx.platform === "twitch") { return { channel: ctx.meta?.channel, content: raw }; } if (ctx.platform === "youtube") { return { liveChatId: ctx.meta?.liveChatId, messageId: ctx.meta?.messageId, channelId: ctx.meta?.author?.channelId, content: raw }; } return { content: raw }; } async function safeReply(reply, content) { try { await reply(content); } catch (error) { console.error("Command reply failed", error); } } function recordCommandUsage(commandId) { if (!commandId) { return; } const now = Date.now(); db.prepare( "INSERT INTO command_usage (command_id, count, updated_at) VALUES (?, 1, ?) " + "ON CONFLICT(command_id) DO UPDATE SET count = count + 1, updated_at = excluded.updated_at" ).run(commandId, now); } module.exports = { createCommandRouter };