const { db } = require("./db"); const { incrementCommands } = require("./stats"); const { buildCommandContext, runAdvancedCommand, normalizeCommandResult } = require("./commands"); const { normalizeRandomReplies, selectRandomReply } = require("./command-random"); const { findConditionalReply, normalizeConditionalReplies } = require("./command-conditional"); const { beginCommandUse, commandKey, customCommandKey } = require("./command-policies"); const { getEnabledPlatformIds, normalizePlatformSelection } = require("./platforms"); const placeholders = require("./placeholders"); const { createLogger } = require("./logger"); const commandLog = createLogger("core:commands", { category: "command" }); function createCommandRouter({ settings }) { const commandMap = new Map(); const pluginCommands = new Map(); const registry = 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); registry.delete(pluginId); } function registerCommands(pluginId, commands = []) { if (!pluginId) { throw new Error("Plugin id is required to register commands."); } clearCommands(pluginId); const entries = []; const registered = []; for (const command of commands) { const triggers = (command.triggers || []) .map((trigger) => trigger.toLowerCase()) .filter(Boolean); const handler = buildHandler(pluginId, command, triggers); for (const trigger of triggers) { const list = commandMap.get(trigger) || []; list.push(handler); commandMap.set(trigger, list); entries.push({ trigger, handler }); } registered.push({ key: handler.commandKey, pluginId, id: handler.commandId, triggers, platforms: handler.platforms, description: String(command.description || ""), defaultPolicy: handler.defaultPolicy }); } pluginCommands.set(pluginId, entries); registry.set(pluginId, registered); } function buildHandler(pluginId, command, triggers) { const handler = async (ctx) => { return await command.handler(ctx); }; handler.commandId = command.id || triggers[0] || null; handler.commandKey = commandKey(pluginId, handler.commandId); handler.platforms = Array.isArray(command.platforms) ? command.platforms : []; handler.defaultPolicy = command.defaultPolicy || {}; return handler; } function listCommands() { return Array.from(registry.values()).flat().map((entry) => ({ ...entry, triggers: [...entry.triggers], platforms: [...entry.platforms] })); } 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); commandLog.debug("Custom command completed", { trigger, platform, user_id: user.id }, { event: "custom_command_completed" }); return true; } const handlers = commandMap.get(trigger) || []; for (const handler of handlers) { if (handler.platforms.length && !handler.platforms.includes(ctx.platform)) continue; let lease; try { lease = await beginCommandUse({ commandKey: handler.commandKey, ctx, defaultPolicy: handler.defaultPolicy }); } catch (error) { commandLog.error("Command policy check failed", { command_id: handler.commandId, trigger, platform, user_id: user.id, error }, { event: "command_policy_failed" }); await safeReply(reply, "This command's access settings could not be checked."); return true; } if (!lease.allowed) { await safeReply(reply, lease.message); return true; } try { const result = await handler(ctx); if (typeof result === "string" && result) { await safeReply(reply, result); await lease.commit(); recordCommandUsage(handler.commandId); incrementCommands(user.id); commandLog.debug("Command completed", { command_id: handler.commandId, trigger, platform, user_id: user.id }, { event: "command_completed" }); return true; } if (result === true) { await lease.commit(); recordCommandUsage(handler.commandId); incrementCommands(user.id); commandLog.debug("Command completed", { command_id: handler.commandId, trigger, platform, user_id: user.id }, { event: "command_completed" }); return true; } await lease.rollback("Command handler did not accept the invocation."); } catch (error) { await lease.rollback(error?.message || "Command handler failed."); commandLog.error("Command handler failed", { command_id: handler.commandId, trigger, platform, user_id: user.id, error }, { event: "command_failed" }); await safeReply(reply, "Command failed to execute."); return true; } } return false; } return { registerCommands, clearCommands, handleMessage, listCommands }; } async function handleCustomCommand({ trigger, platform, ctx, raw, reply }) { const row = db .prepare( "SELECT id, response, mode, language, code, platform, random_replies_json, rng_enabled, rng_min, rng_max, conditional_replies_json, conditional_fuzzy 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; } let lease; try { lease = await beginCommandUse({ commandKey: customCommandKey(row.id), ctx }); } catch (error) { commandLog.error("Custom command policy check failed", { trigger, platform, user_id: ctx.user.id, error }, { event: "command_policy_failed" }); await safeReply(reply, "This command's access settings could not be checked."); return true; } if (!lease.allowed) { await safeReply(reply, lease.message); return true; } 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 if (row.mode === "random") { const selected = selectRandomReply({ replies: normalizeRandomReplies(row.random_replies_json), rngEnabled: Boolean(row.rng_enabled), rngMin: row.rng_min, rngMax: row.rng_max }); const rendered = await placeholders.renderTemplate({ fieldId: "core.custom_commands.static_response", template: selected.reply.text, outputAudience: "user", user: { id: ctx.user.id, username: ctx.user.username, isAdmin: false, isMod: false }, runtimeContext: { ctx, user: ctx.user, platform, runtime: true, command: { rng: selected.rng } } }); await safeReply(reply, rendered.rendered); } else if (row.mode === "conditional") { const replies = normalizeConditionalReplies(row.conditional_replies_json); const found = findConditionalReply(replies, ctx.argsText, { fuzzy: Boolean(row.conditional_fuzzy) }); let template = found.match?.response || ""; if (!ctx.argsText) { template = row.response || `Available options: ${replies.map((entry) => entry.keyword).join(", ")}`; } else if (!found.match) { template = `Unknown option. Available options: ${replies.map((entry) => entry.keyword).join(", ")}`; } const rendered = await renderStaticResponse(template, ctx, platform, { conditional: { requested: ctx.argsText, keyword: found.match?.keyword || null, fuzzy: found.fuzzy } }); await safeReply(reply, rendered); } else { await safeReply(reply, await renderStaticResponse(row.response, ctx, platform)); } recordCommandUsage(`custom:${trigger}`); await lease.commit(); return true; } catch (error) { await lease.rollback(error?.message || "Custom command failed."); commandLog.error("Custom command failed", { trigger, platform, user_id: ctx.user.id, error }, { event: "custom_command_failed" }); await safeReply(reply, "Command failed to execute."); return true; } } async function renderStaticResponse(template, ctx, platform, extraRuntime = {}) { const rendered = await placeholders.renderTemplate({ fieldId: "core.custom_commands.static_response", template, outputAudience: "user", user: { id: ctx.user.id, username: ctx.user.username, isAdmin: false, isMod: false }, runtimeContext: { ctx, user: ctx.user, platform, runtime: true, ...extraRuntime } }); return rendered.rendered; } 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) { commandLog.error("Command reply failed", error, { event: "command_reply_failed" }); } } 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 };