Lumi/src/services/command-router.js
2026-05-30 20:37:42 +02:00

238 lines
6.3 KiB
JavaScript

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
};