diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..d78cc34 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules/ +logs/ +images/ +dist/ +coverage/ +*.min.js \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..d83fde2 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,67 @@ +{ + "env": { + "node": true, + "es2022": true + }, + "extends": [ + "eslint:recommended", + "plugin:import/recommended" + ], + "plugins": [ + "import" + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "settings": { + "import/resolver": { + "node": { + "extensions": [ + ".js", + ".mjs" + ] + } + } + }, + "rules": { + // Error prevention + "no-const-assign": "error", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-unreachable": "error", + "valid-typeof": "error", + + // Best practices + "eqeqeq": "error", + "no-eval": "error", + "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "no-var": "error", + "prefer-const": "error", + "no-empty": ["error", { "allowEmptyCatch": true }], + + // Style + "indent": ["error", 4, { "SwitchCase": 1 }], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single"], + "semi": ["error", "always"], + "no-multiple-empty-lines": ["error", { "max": 1 }], + "no-trailing-spaces": "error", + "eol-last": "error", + "no-mixed-spaces-and-tabs": "error", + + // Object and array formatting + "object-curly-spacing": ["error", "always"], + "array-bracket-spacing": ["error", "never"], + "comma-dangle": ["error", "never"], + + // Import/Export + "import/no-duplicates": "error", + "import/order": ["error", { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], + "newlines-between": "always", + "alphabetize": { "order": "asc" } + }] + } +} \ No newline at end of file diff --git a/_opt/ansi.js b/_opt/ansi.js index b6a31ad..8934f7b 100644 --- a/_opt/ansi.js +++ b/_opt/ansi.js @@ -1,5 +1,6 @@ -import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; import { MessageFlags } from 'discord-api-types/v10'; +import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; + import { CODES } from '../_src/ansiColors.js'; /** @@ -9,111 +10,111 @@ import { CODES } from '../_src/ansiColors.js'; * Both commands are Admin-only. */ export const commands = [ - // Preview arbitrary ANSI tags - { - data: new SlashCommandBuilder() - .setName('ansi') - .setDescription('Preview an ANSI-colored code block') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .setDMPermission(false) - .addStringOption(opt => - opt - .setName('text') - .setDescription('Use [red]…[/], [bold,blue]…[/], escape \\[/]') - .setRequired(true) - ) - .addBooleanOption(opt => - opt - .setName('ephemeral') - .setDescription('Reply ephemerally?') - .setRequired(false) - ), - async execute(interaction, client) { - const raw = interaction.options.getString('text', true); - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; - const colored = client.ansi`${raw}`; - const block = client.wrapAnsi(colored); - const opts = { content: block }; - if (ephemeral) opts.flags = MessageFlags.Ephemeral; - await interaction.reply(opts); - } - }, - - // Show complete ANSI theme chart - { - data: new SlashCommandBuilder() - .setName('ansitheme') - .setDescription('Show ANSI color theme chart') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .setDMPermission(false) - .addBooleanOption(opt => - opt - .setName('ephemeral') - .setDescription('Reply ephemerally?') - .setRequired(false) - ), - async execute(interaction, client) { - const fgs = ['gray', 'red', 'green', 'yellow', 'blue', 'pink', 'cyan', 'white']; - const bgs = ['bgGray', 'bgOrange', 'bgBlue', 'bgTurquoise', 'bgFirefly', 'bgIndigo', 'bgLightGray', 'bgWhite']; - const pad = 8; - // Column header with padded labels (no colors) - shifted right by 1 - const header = ' ' + fgs.map(f => f.padEnd(pad, ' ')).join(''); - // Sample row with no background (padded cells) - let defaultRow = ''; - for (const fg of fgs) { - const fgCode = CODES[fg]; - const openNormal = `\u001b[${fgCode}m`; - const openBold = `\u001b[${fgCode};${CODES.bold}m`; - const openUnder = `\u001b[${fgCode};${CODES.underline}m`; - const cell = `${openNormal}sa\u001b[0m${openBold}mp\u001b[0m${openUnder}le\u001b[0m`; - defaultRow += ' ' + cell + ' '; - } - // Append default row label after one pad - defaultRow += 'default'; - // Colored rows per background - const rows = []; - for (const bg of bgs) { - let row = ''; - const bgCode = CODES[bg]; - for (const fg of fgs) { - const fgCode = CODES[fg]; - const openNormal = `\u001b[${bgCode};${fgCode}m`; - const openBold = `\u001b[${bgCode};${fgCode};${CODES.bold}m`; - const openUnder = `\u001b[${bgCode};${fgCode};${CODES.underline}m`; - const cell = `${openNormal}sa\u001b[0m${openBold}mp\u001b[0m${openUnder}le\u001b[0m`; - row += ' ' + cell + ' '; + // Preview arbitrary ANSI tags + { + data: new SlashCommandBuilder() + .setName('ansi') + .setDescription('Preview an ANSI-colored code block') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false) + .addStringOption(opt => + opt + .setName('text') + .setDescription('Use [red]…[/], [bold,blue]…[/], escape \\[/]') + .setRequired(true) + ) + .addBooleanOption(opt => + opt + .setName('ephemeral') + .setDescription('Reply ephemerally?') + .setRequired(false) + ), + async execute(interaction, client) { + const raw = interaction.options.getString('text', true); + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + const colored = client.ansi`${raw}`; + const block = client.wrapAnsi(colored); + const opts = { content: block }; + if (ephemeral) opts.flags = MessageFlags.Ephemeral; + await interaction.reply(opts); + } + }, + + // Show complete ANSI theme chart + { + data: new SlashCommandBuilder() + .setName('ansitheme') + .setDescription('Show ANSI color theme chart') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false) + .addBooleanOption(opt => + opt + .setName('ephemeral') + .setDescription('Reply ephemerally?') + .setRequired(false) + ), + async execute(interaction, client) { + const fgs = ['gray', 'red', 'green', 'yellow', 'blue', 'pink', 'cyan', 'white']; + const bgs = ['bgGray', 'bgOrange', 'bgBlue', 'bgTurquoise', 'bgFirefly', 'bgIndigo', 'bgLightGray', 'bgWhite']; + const pad = 8; + // Column header with padded labels (no colors) - shifted right by 1 + const header = ' ' + fgs.map(f => f.padEnd(pad, ' ')).join(''); + // Sample row with no background (padded cells) + let defaultRow = ''; + for (const fg of fgs) { + const fgCode = CODES[fg]; + const openNormal = `\u001b[${fgCode}m`; + const openBold = `\u001b[${fgCode};${CODES.bold}m`; + const openUnder = `\u001b[${fgCode};${CODES.underline}m`; + const cell = `${openNormal}sa\u001b[0m${openBold}mp\u001b[0m${openUnder}le\u001b[0m`; + defaultRow += ' ' + cell + ' '; + } + // Append default row label after one pad + defaultRow += 'default'; + // Colored rows per background + const rows = []; + for (const bg of bgs) { + let row = ''; + const bgCode = CODES[bg]; + for (const fg of fgs) { + const fgCode = CODES[fg]; + const openNormal = `\u001b[${bgCode};${fgCode}m`; + const openBold = `\u001b[${bgCode};${fgCode};${CODES.bold}m`; + const openUnder = `\u001b[${bgCode};${fgCode};${CODES.underline}m`; + const cell = `${openNormal}sa\u001b[0m${openBold}mp\u001b[0m${openUnder}le\u001b[0m`; + row += ' ' + cell + ' '; + } + // Append uncolored row label immediately after cell padding + row += bg; + rows.push(row); + } + // Determine ephemeral setting + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + // Initial sample table (header + default row) + const sampleContent = [header, defaultRow].join('\n'); + const optsSample = { content: client.wrapAnsi(sampleContent) }; + if (ephemeral) optsSample.flags = MessageFlags.Ephemeral; + await interaction.reply(optsSample); + // Split colored rows into two tables + const half = Math.ceil(rows.length / 2); + const firstRows = rows.slice(0, half); + const secondRows = rows.slice(half); + // First colored table + const table1 = [header, ...firstRows].join('\n'); + const opts1 = { content: client.wrapAnsi(table1) }; + if (ephemeral) opts1.flags = MessageFlags.Ephemeral; + await interaction.followUp(opts1); + // Second colored table + if (secondRows.length > 0) { + const table2 = [header, ...secondRows].join('\n'); + const opts2 = { content: client.wrapAnsi(table2) }; + if (ephemeral) opts2.flags = MessageFlags.Ephemeral; + await interaction.followUp(opts2); + } } - // Append uncolored row label immediately after cell padding - row += bg; - rows.push(row); - } - // Determine ephemeral setting - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; - // Initial sample table (header + default row) - const sampleContent = [header, defaultRow].join('\n'); - const optsSample = { content: client.wrapAnsi(sampleContent) }; - if (ephemeral) optsSample.flags = MessageFlags.Ephemeral; - await interaction.reply(optsSample); - // Split colored rows into two tables - const half = Math.ceil(rows.length / 2); - const firstRows = rows.slice(0, half); - const secondRows = rows.slice(half); - // First colored table - const table1 = [header, ...firstRows].join('\n'); - const opts1 = { content: client.wrapAnsi(table1) }; - if (ephemeral) opts1.flags = MessageFlags.Ephemeral; - await interaction.followUp(opts1); - // Second colored table - if (secondRows.length > 0) { - const table2 = [header, ...secondRows].join('\n'); - const opts2 = { content: client.wrapAnsi(table2) }; - if (ephemeral) opts2.flags = MessageFlags.Ephemeral; - await interaction.followUp(opts2); - } } - } ]; export async function init(client) { - client.logger.info('[module:ansi] Loaded ANSI utilities'); -} \ No newline at end of file + client.logger.info('[module:ansi] Loaded ANSI utilities'); +} diff --git a/_opt/botUtils.js b/_opt/botUtils.js index 071f59f..e552314 100644 --- a/_opt/botUtils.js +++ b/_opt/botUtils.js @@ -1,5 +1,5 @@ -import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js'; import { MessageFlags } from 'discord-api-types/v10'; +import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js'; /** * botUtils module - provides administrative bot control commands @@ -7,157 +7,161 @@ import { MessageFlags } from 'discord-api-types/v10'; */ // Define slash commands export const commands = [ - { - data: new SlashCommandBuilder() - .setName('exit') - .setDescription('Gracefully shutdown the bot (Owner only)') - // Restrict to server administrators by default - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .setDMPermission(false) - .addIntegerOption(option => - option - .setName('code') - .setDescription('Exit code to use (default 0)') - .setRequired(false) - ), - /** + { + data: new SlashCommandBuilder() + .setName('exit') + .setDescription('Gracefully shutdown the bot (Owner only)') + // Restrict to server administrators by default + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false) + .addIntegerOption(option => + option + .setName('code') + .setDescription('Exit code to use (default 0)') + .setRequired(false) + ), + /** * Execute the exit command: only the configured owner can invoke. * @param {import('discord.js').CommandInteraction} interaction * @param {import('discord.js').Client} client */ - async execute(interaction, client) { - const ownerId = client.config.owner; - // Check invoking user is the bot owner - if (interaction.user.id !== String(ownerId)) { - return interaction.reply({ content: 'Only the bot owner can shutdown the bot.', flags: MessageFlags.Ephemeral }); - } - // Determine desired exit code (default 0) - const exitCode = interaction.options.getInteger('code') ?? 0; - // Validate exit code bounds - if (exitCode < 0 || exitCode > 254) { - return interaction.reply({ content: 'Exit code must be between 0 and 254 inclusive.', flags: MessageFlags.Ephemeral }); - } - // Acknowledge before shutting down - await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, flags: MessageFlags.Ephemeral }); - client.logger.info( - `[cmd:exit] Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}` - ); - // Destroy Discord client and exit process - try { - await client.destroy(); - } catch (err) { - client.logger.error(`[cmd:exit] Error during client.destroy(): ${err}`); - } - process.exit(exitCode); - } - }, - /** + async execute(interaction, client) { + const ownerId = client.config.owner; + // Check invoking user is the bot owner + if (interaction.user.id !== String(ownerId)) { + return interaction.reply({ content: 'Only the bot owner can shutdown the bot.', flags: MessageFlags.Ephemeral }); + } + // Determine desired exit code (default 0) + const exitCode = interaction.options.getInteger('code') ?? 0; + // Validate exit code bounds + if (exitCode < 0 || exitCode > 254) { + return interaction.reply({ content: 'Exit code must be between 0 and 254 inclusive.', flags: MessageFlags.Ephemeral }); + } + // Acknowledge before shutting down + await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, flags: MessageFlags.Ephemeral }); + client.logger.info( + `[cmd:exit] Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}` + ); + // Destroy Discord client and exit process + try { + await client.destroy(); + } catch (err) { + client.logger.error(`[cmd:exit] Error during client.destroy(): ${err}`); + } + process.exit(exitCode); + } + }, + /** * Slash command `/status` (Administrator only): - * Shows this bot client’s status including CPU, memory, environment, + * Shows this bot client's status including CPU, memory, environment, * uptime, module list, and entity counts. Optionally displays Git info * (Git Reference and Git Status) when the gitUtils module is loaded. * @param {import('discord.js').CommandInteraction} interaction * @param {import('discord.js').Client} client */ - // /status: admin-only, shows current client info - { - data: new SlashCommandBuilder() - .setName('status') - .setDescription('Show this bot client status and process info') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .setDMPermission(false) - .addBooleanOption(option => - option - .setName('ephemeral') - .setDescription('Whether the response should be ephemeral') - .setRequired(false) - ), - async execute(interaction, client) { - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; - await interaction.deferReply({ flags: ephemeral ? MessageFlags.Ephemeral : undefined }); - // Process metrics - const uptimeSec = process.uptime(); - const hours = Math.floor(uptimeSec / 3600); - const minutes = Math.floor((uptimeSec % 3600) / 60); - const seconds = Math.floor(uptimeSec % 60); - const uptime = `${hours}h ${minutes}m ${seconds}s`; - const mem = process.memoryUsage(); - const toMB = bytes => (bytes / 1024 / 1024).toFixed(2); - const memoryInfo = `RSS: ${toMB(mem.rss)} MB, Heap: ${toMB(mem.heapUsed)}/${toMB(mem.heapTotal)} MB`; - const cpu = process.cpuUsage(); - const cpuInfo = `User: ${(cpu.user / 1000).toFixed(2)} ms, System: ${(cpu.system / 1000).toFixed(2)} ms`; - const nodeVersion = process.version; - const platform = `${process.platform} ${process.arch}`; - // Client-specific stats - const guildCount = client.guilds.cache.size; - const userCount = client.guilds.cache.reduce((sum, g) => sum + (g.memberCount || 0), 0); - const commandCount = client.commands.size; - // List of loaded optional modules - const loadedModules = client.modules ? Array.from(client.modules.keys()) : []; - // Build embed for status - // Determine if gitUtils module is loaded - const gitLoaded = client.modules?.has('gitUtils'); - let branch, build, statusRaw, statusBlock; - if (gitLoaded) { - const git = client.modules.get('gitUtils'); - try { - branch = await git.getBranch(); - build = await git.getShortHash(); - statusRaw = await git.getStatusShort(); - // Format status as fenced code block using template literals - // Normalize each line with a leading space in a code fence - // Prefix raw status output with a single space - // Prefix raw status output with a space, and only if non-empty - if (statusRaw) { - statusBlock = '```\n ' + statusRaw + '\n```'; - } - } catch { - branch = 'error'; - build = 'error'; - // Represent error status in code fence - statusBlock = '```\n (error)\n```'; + // /status: admin-only, shows current client info + { + data: new SlashCommandBuilder() + .setName('status') + .setDescription('Show this bot client status and process info') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false) + .addBooleanOption(option => + option + .setName('ephemeral') + .setDescription('Whether the response should be ephemeral') + .setRequired(false) + ), + async execute(interaction, client) { + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + await interaction.deferReply({ flags: ephemeral ? MessageFlags.Ephemeral : undefined }); + // Process metrics + const uptimeSec = process.uptime(); + const hours = Math.floor(uptimeSec / 3600); + const minutes = Math.floor((uptimeSec % 3600) / 60); + const seconds = Math.floor(uptimeSec % 60); + const uptime = `${hours}h ${minutes}m ${seconds}s`; + const mem = process.memoryUsage(); + const toMB = bytes => (bytes / 1024 / 1024).toFixed(2); + const memoryInfo = `RSS: ${toMB(mem.rss)} MB, Heap: ${toMB(mem.heapUsed)}/${toMB(mem.heapTotal)} MB`; + const cpu = process.cpuUsage(); + const cpuInfo = `User: ${(cpu.user / 1000).toFixed(2)} ms, System: ${(cpu.system / 1000).toFixed(2)} ms`; + const nodeVersion = process.version; + const platform = `${process.platform} ${process.arch}`; + // Client-specific stats + const guildCount = client.guilds.cache.size; + const userCount = client.guilds.cache.reduce((sum, g) => sum + (g.memberCount || 0), 0); + const commandCount = client.commands.size; + // List of loaded optional modules + const loadedModules = client.modules ? Array.from(client.modules.keys()) : []; + // Build embed for status + // Determine if gitUtils module is loaded + const gitLoaded = client.modules?.has('gitUtils'); + let branch, build, statusRaw, statusBlock; + if (gitLoaded) { + const git = client.modules.get('gitUtils'); + try { + branch = await git.getBranch(); + build = await git.getShortHash(); + statusRaw = await git.getStatusShort(); + // Format status as fenced code block using template literals + // Normalize each line with a leading space in a code fence + // Prefix raw status output with a single space + // Prefix raw status output with a space, and only if non-empty + if (statusRaw) { + statusBlock = '```\n ' + statusRaw + '\n```'; + } + } catch { + branch = 'error'; + build = 'error'; + // Represent error status in code fence + statusBlock = '```\n (error)\n```'; + } + } + // Prepare module list as bullet points + const moduleList = loadedModules.length > 0 + ? loadedModules.map(m => `• ${m}`).join('\n') + : 'None'; + // Assemble fields + const fields = []; + // Client identification + fields.push({ name: 'Client', value: client.config.id, inline: false }); + // Performance metrics + fields.push({ name: 'CPU Usage', value: cpuInfo, inline: false }); + fields.push({ name: 'Memory', value: memoryInfo, inline: false }); + // Environment + fields.push({ name: 'Node.js', value: nodeVersion, inline: true }); + fields.push({ name: 'Platform', value: platform, inline: true }); + // Uptime + fields.push({ name: 'Uptime', value: uptime, inline: true }); + // Loaded modules + fields.push({ name: 'Modules', value: moduleList, inline: false }); + // Entity counts + fields.push({ name: 'Commands', value: commandCount.toString(), inline: true }); + fields.push({ name: 'Guilds', value: guildCount.toString(), inline: true }); + fields.push({ name: 'Users', value: userCount.toString(), inline: true }); + // Git reference and status if available + if (gitLoaded) { + fields.push({ name: 'Git Reference', value: `${branch}/${build}`, inline: false }); + fields.push({ name: 'Git Status', value: statusBlock, inline: false }); + } + // Create embed + const embed = new EmbedBuilder() + .setAuthor({ name: 'ClientX', iconURL: client.user.displayAvatarURL() }) + .setThumbnail(client.user.displayAvatarURL()) + .addFields(fields) + .setTimestamp(); + await interaction.editReply({ embeds: [embed] }); + client.logger.info(`[cmd:status] Returned status embed for client ${client.config.id}`); } - } - // Prepare module list as bullet points - const moduleList = loadedModules.length > 0 - ? loadedModules.map(m => `• ${m}`).join('\n') - : 'None'; - // Assemble fields - const fields = []; - // Client identification - fields.push({ name: 'Client', value: client.config.id, inline: false }); - // Performance metrics - fields.push({ name: 'CPU Usage', value: cpuInfo, inline: false }); - fields.push({ name: 'Memory', value: memoryInfo, inline: false }); - // Environment - fields.push({ name: 'Node.js', value: nodeVersion, inline: true }); - fields.push({ name: 'Platform', value: platform, inline: true }); - // Uptime - fields.push({ name: 'Uptime', value: uptime, inline: true }); - // Loaded modules - fields.push({ name: 'Modules', value: moduleList, inline: false }); - // Entity counts - fields.push({ name: 'Commands', value: commandCount.toString(), inline: true }); - fields.push({ name: 'Guilds', value: guildCount.toString(), inline: true }); - fields.push({ name: 'Users', value: userCount.toString(), inline: true }); - // Git reference and status if available - if (gitLoaded) { - fields.push({ name: 'Git Reference', value: `${branch}/${build}`, inline: false }); - fields.push({ name: 'Git Status', value: statusBlock, inline: false }); - } - // Create embed - const embed = new EmbedBuilder() - .setAuthor({ name: 'ClientX', iconURL: client.user.displayAvatarURL() }) - .setThumbnail(client.user.displayAvatarURL()) - .addFields(fields) - .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); - client.logger.info(`[cmd:status] Returned status embed for client ${client.config.id}`); } - } ]; // Module loaded logging -export async function init(client, clientConfig) { - client.logger.info('[module:botUtils] Module loaded'); -} \ No newline at end of file +export async function init(_client, _clientConfig) { + _client.logger.info('[module:botUtils] Module loaded'); +} + +export async function handleInteractionCreate(_client, _clientConfig, _interaction) { + // ... existing code ... +} diff --git a/_opt/condimentX.js b/_opt/condimentX.js index 84bb67f..a18df5e 100644 --- a/_opt/condimentX.js +++ b/_opt/condimentX.js @@ -78,7 +78,6 @@ export const init = async (client, config) => { // Used as a prefix before any line that runs within a loop. const bullet = '>'; - // === OpenAI Interaction === // Chat completion via OpenAI with provided instructions. async function ai(prompt = '') { @@ -86,17 +85,17 @@ export const init = async (client, config) => { debug(`**AI Prompt**: ${prompt}`); // Read instructions. - let openAIInstructions = fs.readFileSync(openAIInstructionsFile, 'utf8'); + const openAIInstructions = fs.readFileSync(openAIInstructionsFile, 'utf8'); const unmention = /<@(\w+)>/g; const completion = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [ - {role: 'user', content: `${prompt.replace(unmention, '$1')}`}, - {role: 'system', content: `${openAIInstructions}`}, - ], + { role: 'user', content: `${prompt.replace(unmention, '$1')}` }, + { role: 'system', content: `${openAIInstructions}` } + ] }); - let chunk = completion.choices[0]?.message?.content; - if (chunk != '') { + const chunk = completion.choices[0]?.message?.content; + if (chunk !== '') { for (const line of chunk.split(/\n\s*\n/).filter(Boolean)) { debug(`${bullet} ${line}`); openAIWebhookClient.send(line); @@ -142,7 +141,7 @@ export const init = async (client, config) => { // === Message Fetching Helpers === // Retrieve recent messages from every text channel since a given timestamp. - async function fetchRecentMessages(since) { + async function _fetchRecentMessages(since) { const allMessages = new Collection(); // Get all text channels in the guild @@ -153,9 +152,9 @@ export const init = async (client, config) => { // For each channel, fetch recent messages for (const channel of textChannels.values()) { try { - const messages = await channel.messages.fetch({ + const messages = await channel.messages.fetch({ limit: messageHistoryLimit, - after: since + after: since }); // Add these messages to our collection @@ -181,7 +180,7 @@ export const init = async (client, config) => { debug(`**Incident Cycle #${incidentCounter++}**`); // Rebuild the list of current index cases, if any. - let indexesList = guild.members.cache.filter(member => member.roles.cache.has(indexRole.id)); + const indexesList = guild.members.cache.filter(member => member.roles.cache.has(indexRole.id)); debug(`${bullet} Index Cases: **${indexesList.size}**`); // Build the victimsList using whitelisted roles. @@ -206,7 +205,7 @@ export const init = async (client, config) => { } // Conditions for potentially starting an incident. - if (indexesList.size == 0 && victimsList.size > 0) { + if (indexesList.size === 0 && victimsList.size > 0) { if ((Math.floor(Math.random() * incidenceDenominator) + 1) === 1) { debug(`${bullet} Incidence Check: **Success**`); const newIndex = victimsList.random(); @@ -240,7 +239,7 @@ export const init = async (client, config) => { } // Prepare the next cycle. - let interval = cycleInterval + Math.floor(Math.random() * (2 * cycleIntervalRange + 1)) - cycleIntervalRange; + const interval = cycleInterval + Math.floor(Math.random() * (2 * cycleIntervalRange + 1)) - cycleIntervalRange; setTimeout(cycleIncidents, interval); debug(`${bullet} Cycle #${incidentCounter} **** at ****`); } catch (error) { @@ -291,17 +290,16 @@ export const init = async (client, config) => { if (message.webhookId) return; guild = client.guilds.cache.get(guildID); - - // Someone mentioned us - respond if openAI is enabled, the message was in the webhook channel, and a trigger was used. - if (openAI === true && openAIWebhook.channel.id === message.channel.id && openAITriggers.some(word => message.content.replace(/[^\w\s]/gi, '').toLowerCase().includes(word.toLowerCase()))) { - // Also check if an active incident is required to respond. - if ((openAITriggerOnlyDuringIncident === true && guild.members.cache.filter(member => member.roles.cache.has(indexRole.id)).size > 0) || openAITriggerOnlyDuringIncident === false) { - // Finally, random roll to respond. - if ((Math.floor(Math.random() * openAIResponseDenominator) + 1) === 1) { - ai(`${message.member.displayName} said: ${message.cleanContent}`); - } - } - } + // Someone mentioned us - respond if openAI is enabled, the message was in the webhook channel, and a trigger was used. + if (openAI === true && openAIWebhook.channel.id === message.channel.id && openAITriggers.some(word => message.content.replace(/[^\w\s]/gi, '').toLowerCase().includes(word.toLowerCase()))) { + // Also check if an active incident is required to respond. + if ((openAITriggerOnlyDuringIncident === true && guild.members.cache.filter(member => member.roles.cache.has(indexRole.id)).size > 0) || openAITriggerOnlyDuringIncident === false) { + // Finally, random roll to respond. + if ((Math.floor(Math.random() * openAIResponseDenominator) + 1) === 1) { + ai(`${message.member.displayName} said: ${message.cleanContent}`); + } + } + } if (blacklistUsers.includes(message.author.id)) return; if (message.member.roles.cache.some(r => blacklistRoles.includes(r.id))) return; @@ -316,7 +314,7 @@ export const init = async (client, config) => { const msgMember = msg.member; if (msgMember) { // Check if author has index or viral role - const isInfected = msgMember.roles.cache.has(indexRole.id) || + const isInfected = msgMember.roles.cache.has(indexRole.id) || msgMember.roles.cache.has(viralRole.id); if (isInfected) infections++; } @@ -334,7 +332,7 @@ export const init = async (client, config) => { let percentage = Math.min(infections / prox.size * 100, probabilityLimit); // Reduce base probability by ${antiViralEffectiveness}% for those with ${antiViralRole} - if (message.member.roles.cache.has(antiViralRole.id)) { + if (message.member.roles.cache.has(antiViralRole.id) && Math.random() * 100 === antiViralEffectiveness) { percentage = Math.round(percentage - (antiViralEffectiveness * (percentage / 100))); } @@ -359,14 +357,14 @@ export const init = async (client, config) => { anomaly('messageCreate', error); } }; - + // Deferred setup on ready const readyHandler = async () => { client.logger.info('[module:condimentX] Initializing module'); if (openAI === true) { openai = new OpenAI({ apiKey: openAIToken }); // credentials loaded openAIWebhook = await client.fetchWebhook(openAIWebhookID, openAIWebhookToken).catch(error => { - client.logger.error(`[module:condimentX] Could not fetch webhook: ${error.message}`); + client.logger.error(`[module:condimentX] Could not fetch webhook: ${error.message}`); return null; }); if (openAIWebhook) openAIWebhookClient = new WebhookClient({ id: openAIWebhookID, token: openAIWebhookToken }); @@ -374,7 +372,7 @@ export const init = async (client, config) => { try { guild = client.guilds.cache.get(guildID); if (!guild) { - client.logger.error(`[module:condimentX] Guild ${guildID} not found`); + client.logger.error(`[module:condimentX] Guild ${guildID} not found`); return; } indexRole = await guild.roles.fetch(indexRoleID); diff --git a/_opt/gitUtils.js b/_opt/gitUtils.js index 6408c01..609892e 100644 --- a/_opt/gitUtils.js +++ b/_opt/gitUtils.js @@ -1,16 +1,17 @@ -import { SlashCommandBuilder } from 'discord.js'; -import { MessageFlags } from 'discord-api-types/v10'; import { execFile } from 'child_process'; import { promisify } from 'util'; + +import { MessageFlags } from 'discord-api-types/v10'; +import { SlashCommandBuilder } from 'discord.js'; // Use execFile to avoid shell interpretation of arguments const execFileAsync = promisify(execFile); // Wrap Git errors class GitError extends Error { - constructor(message) { - super(message); - this.name = 'GitError'; - } + constructor(message) { + super(message); + this.name = 'GitError'; + } } /** @@ -20,25 +21,25 @@ class GitError extends Error { * @throws {GitError} - When the git command exits with an error. */ async function runGit(args) { - // Sanitize arguments: disallow dangerous shell metacharacters - if (!Array.isArray(args)) { - throw new GitError('Invalid git arguments'); - } - const dangerous = /[;&|<>`$\\]/; - for (const arg of args) { - if (dangerous.test(arg)) { - throw new GitError(`Illegal character in git argument: ${arg}`); + // Sanitize arguments: disallow dangerous shell metacharacters + if (!Array.isArray(args)) { + throw new GitError('Invalid git arguments'); } - } - try { + const dangerous = /[;&|<>`$\\]/; + for (const arg of args) { + if (dangerous.test(arg)) { + throw new GitError(`Illegal character in git argument: ${arg}`); + } + } + try { // Exec git directly without shell - const { stdout, stderr } = await execFileAsync('git', args); - const out = (stdout || stderr || '').toString().trim(); - return out || '(no output)'; - } catch (err) { - const msg = err.stderr?.toString().trim() || err.message; - throw new GitError(msg); - } + const { stdout, stderr } = await execFileAsync('git', args); + const out = (stdout || stderr || '').toString().trim(); + return out || '(no output)'; + } catch (err) { + const msg = err.stderr?.toString().trim() || err.message; + throw new GitError(msg); + } } /** @@ -48,10 +49,10 @@ async function runGit(args) { * @returns {string} - The content wrapped in triple backticks. */ function formatCodeBlock(content, lang = '') { - const fence = '```'; - return lang - ? `${fence}${lang}\n${content}\n${fence}` - : `${fence}\n${content}\n${fence}`; + const fence = '```'; + return lang + ? `${fence}${lang}\n${content}\n${fence}` + : `${fence}\n${content}\n${fence}`; } /** @@ -61,76 +62,76 @@ function formatCodeBlock(content, lang = '') { * @returns {string[]} - An array of substring chunks. */ function chunkString(str, chunkSize) { - const chunks = []; - for (let i = 0; i < str.length; i += chunkSize) { - chunks.push(str.slice(i, i + chunkSize)); - } - return chunks; + const chunks = []; + for (let i = 0; i < str.length; i += chunkSize) { + chunks.push(str.slice(i, i + chunkSize)); + } + return chunks; } // Single /git command: run arbitrary git export const commands = [ - { - data: new SlashCommandBuilder() - .setName('git') - .setDescription('Run an arbitrary git command (Owner only)') - .addStringOption(opt => - opt.setName('args') - .setDescription('Arguments to pass to git') - .setRequired(true)) - .addBooleanOption(opt => - opt.setName('ephemeral') - .setDescription('Make the reply ephemeral') - .setRequired(false)), - async execute(interaction, client) { - const ownerId = client.config.owner; - if (interaction.user.id !== ownerId) { - return interaction.reply({ content: 'Only the bot owner can run git commands.', flags: MessageFlags.Ephemeral }); - } - const raw = interaction.options.getString('args'); - // Disallow semicolons to prevent command chaining - if (raw.includes(';')) { - return interaction.reply({ content: 'Semicolons are not allowed in git arguments.', flags: MessageFlags.Ephemeral }); - } - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; - const args = raw.match(/(?:[^\s"]+|"[^"]*")+/g) - .map(s => s.replace(/^"(.+)"$/, '$1')); + { + data: new SlashCommandBuilder() + .setName('git') + .setDescription('Run an arbitrary git command (Owner only)') + .addStringOption(opt => + opt.setName('args') + .setDescription('Arguments to pass to git') + .setRequired(true)) + .addBooleanOption(opt => + opt.setName('ephemeral') + .setDescription('Make the reply ephemeral') + .setRequired(false)), + async execute(interaction, client) { + const ownerId = client.config.owner; + if (interaction.user.id !== ownerId) { + return interaction.reply({ content: 'Only the bot owner can run git commands.', flags: MessageFlags.Ephemeral }); + } + const raw = interaction.options.getString('args'); + // Disallow semicolons to prevent command chaining + if (raw.includes(';')) { + return interaction.reply({ content: 'Semicolons are not allowed in git arguments.', flags: MessageFlags.Ephemeral }); + } + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + const args = raw.match(/(?:[^\s"]+|"[^"]*")+/g) + .map(s => s.replace(/^"(.+)"$/, '$1')); - try { - // Log the exact git command being executed - const cmdStr = args.join(' '); - client.logger.warn(`[cmd:git] Executing git command: git ${cmdStr}`); - const output = await runGit(args); - // Prepend the git command as a header; keep it intact when chunking - const header = `git ${cmdStr}\n`; - // Discord message limit ~2000; reserve for code fences - const maxContent = 1990; - // Calculate how much output can fit after the header in the first chunk - const firstChunkSize = Math.max(0, maxContent - header.length); - // Split the raw output into chunks - const outputChunks = chunkString(output, firstChunkSize); - // Send first block with header + first output chunk - const firstBlock = header + (outputChunks[0] || ''); - const replyOpts = { content: formatCodeBlock(firstBlock) }; - if (ephemeral) replyOpts.flags = MessageFlags.Ephemeral; - await interaction.reply(replyOpts); - // Send any remaining blocks without the header - for (let i = 1; i < outputChunks.length; i++) { - const fuOpts = { content: formatCodeBlock(outputChunks[i]) }; - if (ephemeral) fuOpts.flags = MessageFlags.Ephemeral; - await interaction.followUp(fuOpts); + try { + // Log the exact git command being executed + const cmdStr = args.join(' '); + client.logger.warn(`[cmd:git] Executing git command: git ${cmdStr}`); + const output = await runGit(args); + // Prepend the git command as a header; keep it intact when chunking + const header = `git ${cmdStr}\n`; + // Discord message limit ~2000; reserve for code fences + const maxContent = 1990; + // Calculate how much output can fit after the header in the first chunk + const firstChunkSize = Math.max(0, maxContent - header.length); + // Split the raw output into chunks + const outputChunks = chunkString(output, firstChunkSize); + // Send first block with header + first output chunk + const firstBlock = header + (outputChunks[0] || ''); + const replyOpts = { content: formatCodeBlock(firstBlock) }; + if (ephemeral) replyOpts.flags = MessageFlags.Ephemeral; + await interaction.reply(replyOpts); + // Send any remaining blocks without the header + for (let i = 1; i < outputChunks.length; i++) { + const fuOpts = { content: formatCodeBlock(outputChunks[i]) }; + if (ephemeral) fuOpts.flags = MessageFlags.Ephemeral; + await interaction.followUp(fuOpts); + } + } catch (err) { + const msg = err instanceof GitError ? err.message : String(err); + await interaction.reply({ content: `Error: ${msg}`, flags: MessageFlags.Ephemeral }); + } } - } catch (err) { - const msg = err instanceof GitError ? err.message : String(err); - await interaction.reply({ content: `Error: ${msg}`, flags: MessageFlags.Ephemeral }); - } } - } ]; // No special init logic export async function init(client) { - client.logger.warn('[module:gitUtils] Git utilities module loaded - dangerous module, use with caution'); + client.logger.warn('[module:gitUtils] Git utilities module loaded - dangerous module, use with caution'); } // Helper functions for external use /** @@ -138,28 +139,28 @@ export async function init(client) { * @returns {Promise} */ export async function getBranch() { - return runGit(['rev-parse', '--abbrev-ref', 'HEAD']); + return runGit(['rev-parse', '--abbrev-ref', 'HEAD']); } /** * Get short commit hash of HEAD * @returns {Promise} */ export async function getShortHash() { - return runGit(['rev-parse', '--short', 'HEAD']); + return runGit(['rev-parse', '--short', 'HEAD']); } /** * Get concise working tree status (git status --porcelain) * @returns {Promise} */ export async function getStatusShort() { - return runGit(['status', '--porcelain']); + return runGit(['status', '--porcelain']); } /** * Get Git remote origin URL * @returns {Promise} */ export async function getRemoteUrl() { - return runGit(['config', '--get', 'remote.origin.url']); + return runGit(['config', '--get', 'remote.origin.url']); } /** * Get recent commit log (n lines, one-line format) @@ -167,12 +168,12 @@ export async function getRemoteUrl() { * @returns {Promise} */ export async function getLog(n = 5) { - return runGit(['log', `-n${n}`, '--oneline']); + return runGit(['log', `-n${n}`, '--oneline']); } /** * Get diff summary (git diff --stat) * @returns {Promise} */ export async function getDiffStat() { - return runGit(['diff', '--stat']); -} \ No newline at end of file + return runGit(['diff', '--stat']); +} diff --git a/_opt/messageQueue-example.js b/_opt/messageQueue-example.js index 1002217..7ffd724 100644 --- a/_opt/messageQueue-example.js +++ b/_opt/messageQueue-example.js @@ -4,25 +4,25 @@ import { onMessageQueueEvent } from './pbUtils.js'; /** * Example module that listens for 'test' messages in the message_queue collection. */ -export const init = async (client, config) => { - client.logger.info('[module:messageQueueExample] Initializing Message Queue Example module'); - onMessageQueueEvent(client, async (action, record) => { +export async function init(client, _config) { + client.logger.info('[module:messageQueueExample] Message Queue Example module initialized'); + onMessageQueueEvent(client, async (action, record) => { // Only process newly created records - if (action !== 'create') return; - // Only process messages meant for this client - if (record.destination !== client.config.id) return; - // Only handle test dataType - if (record.dataType !== 'test') return; + if (action !== 'create') return; + // Only process messages meant for this client + if (record.destination !== client.config.id) return; + // Only handle test dataType + if (record.dataType !== 'test') return; - // At this point we have a test message for us - client.logger.info('[module:messageQueueExample] Test message received'); + // At this point we have a test message for us + client.logger.info('[module:messageQueueExample] Test message received'); - // Delete the processed message from the queue - try { - await client.pb.deleteMessageQueue(record.id); - client.logger.debug(`[module:messageQueueExample] Deleted message_queue record ${record.id}`); - } catch (err) { - client.logger.error(`[module:messageQueueExample] Failed to delete message_queue record ${record.id}: ${err.message}`); - } - }); -}; \ No newline at end of file + // Delete the processed message from the queue + try { + await client.pb.deleteMessageQueue(record.id); + client.logger.debug(`[module:messageQueueExample] Deleted message_queue record ${record.id}`); + } catch (err) { + client.logger.error(`[module:messageQueueExample] Failed to delete message_queue record ${record.id}: ${err.message}`); + } + }); +} diff --git a/_opt/pbUtils.js b/_opt/pbUtils.js index ba8739b..973ebb0 100644 --- a/_opt/pbUtils.js +++ b/_opt/pbUtils.js @@ -1,10 +1,11 @@ // _opt/pbutils.js // Polyfill global EventSource for PocketBase realtime in Node.js (using CommonJS require) import { createRequire } from 'module'; + const require = createRequire(import.meta.url); const { EventSource } = require('eventsource'); if (typeof global.EventSource === 'undefined') { - global.EventSource = EventSource; + global.EventSource = EventSource; } /** @@ -16,7 +17,7 @@ if (typeof global.EventSource === 'undefined') { * @param {Object} client - Discord client with attached PocketBase instance * @param {Object} config - Client configuration */ -export const init = async (client, config) => { +export async function init(client, _config) { const { pb, logger } = client; logger.info('[module:pbUtils] Initializing PocketBase utilities module'); @@ -24,24 +25,24 @@ export const init = async (client, config) => { // Attach utility methods to the pb object extendPocketBase(client, pb, logger); - // Add connection state handling - setupConnectionHandling(pb, logger); - - // Subscribe to real-time message queue events and re-emit via client - try { - pb.collection('message_queue').subscribe('*', (e) => { - client.emit('message_queue_event', e.action, e.record); - logger.debug(`PubSub event: ${e.action} on message_queue: ${JSON.stringify(e.record)}`); - }); - logger.info('[module:pbUtils] Subscribed to PocketBase message_queue realtime events'); - } catch (error) { - logger.error(`[module:pbUtils] Failed to subscribe to message_queue realtime: ${error.message}`); - } + // Add connection state handling + setupConnectionHandling(pb, logger); -// end of init() + // Subscribe to real-time message queue events and re-emit via client + try { + pb.collection('message_queue').subscribe('*', (e) => { + client.emit('message_queue_event', e.action, e.record); + logger.debug(`PubSub event: ${e.action} on message_queue: ${JSON.stringify(e.record)}`); + }); + logger.info('[module:pbUtils] Subscribed to PocketBase message_queue realtime events'); + } catch (error) { + logger.error(`[module:pbUtils] Failed to subscribe to message_queue realtime: ${error.message}`); + } - logger.info('PocketBase utilities module initialized'); -}; + // end of init() + + logger.info('PocketBase utilities module initialized'); +} /** * Register a handler for incoming message_queue pub/sub events. @@ -50,13 +51,13 @@ export const init = async (client, config) => { * @param {(action: string, record: object) => void} handler - Callback for each event */ export function onMessageQueueEvent(client, handler) { - client.on('message_queue_event', (action, record) => { - try { - handler(action, record); - } catch (err) { - client.logger.error(`[module:pbUtils] Error in message_queue handler: ${err.message}`); - } - }); + client.on('message_queue_event', (action, record) => { + try { + handler(action, record); + } catch (err) { + client.logger.error(`[module:pbUtils] Error in message_queue handler: ${err.message}`); + } + }); } /** @@ -71,78 +72,78 @@ export function onMessageQueueEvent(client, handler) { * @param {object} logger - Logger instance */ const extendPocketBase = (client, pb, logger) => { - // ===== COLLECTION OPERATIONS ===== + // ===== COLLECTION OPERATIONS ===== - /** + /** * Get a single record with better error handling * @param {string} collection - Collection name * @param {string} id - Record ID * @param {Object} options - Additional options * @returns {Promise} The record or null */ - pb.getOne = async (collection, id, options = {}) => { - try { - return await pb.collection(collection).getOne(id, options); - } catch (error) { - if (error.status === 404) { - return null; - } - logger.error(`Failed to get record ${id} from ${collection}: ${error.message}`); - throw error; - } - }; + pb.getOne = async (collection, id, options = {}) => { + try { + return await pb.collection(collection).getOne(id, options); + } catch (error) { + if (error.status === 404) { + return null; + } + logger.error(`Failed to get record ${id} from ${collection}: ${error.message}`); + throw error; + } + }; - /** + /** * Creates a record with validation and error handling * @param {string} collection - Collection name * @param {Object} data - Record data * @returns {Promise} Created record */ - pb.createOne = async (collection, data) => { - try { - return await pb.collection(collection).create(data); - } catch (error) { - logger.error(`Failed to create record in ${collection}: ${error.message}`); - throw error; - } - }; + pb.createOne = async (collection, data) => { + try { + return await pb.collection(collection).create(data); + } catch (error) { + logger.error(`Failed to create record in ${collection}: ${error.message}`); + throw error; + } + }; - /** + /** * Updates a record with better error handling * @param {string} collection - Collection name * @param {string} id - Record ID * @param {Object} data - Record data * @returns {Promise} Updated record */ - pb.updateOne = async (collection, id, data) => { - try { - return await pb.collection(collection).update(id, data); - } catch (error) { - logger.error(`Failed to update record ${id} in ${collection}: ${error.message}`); - throw error; - } - }; + pb.updateOne = async (collection, id, data) => { + try { + return await pb.collection(collection).update(id, data); + } catch (error) { + logger.error(`Failed to update record ${id} in ${collection}: ${error.message}`); + throw error; + } + }; - /** + /** * Deletes a record with better error handling * @param {string} collection - Collection name * @param {string} id - Record ID * @returns {Promise} Success status */ pb.deleteOne = async (collection, id) => { - try { - await pb.collection(collection).delete(id); - return true; - } catch (error) { - if (error.status === 404) { - logger.warn(`Record ${id} not found in ${collection} for deletion`); - return false; - } - logger.error(`Failed to delete record ${id} from ${collection}: ${error.message}`); - throw error; - } - }; - + try { + await pb.collection(collection).delete(id); + return true; + } catch (error) { + if (error.status === 404) { + logger.warn(`Record ${id} not found in ${collection} for deletion`); + return false; + } + logger.error(`Failed to delete record ${id} from ${collection}: ${error.message}`); + throw error; + } + }; + /** * Convenience: publish a message into the "message_queue" collection, * with source/destination validation. @@ -162,196 +163,196 @@ const extendPocketBase = (client, pb, logger) => { return await pb.collection('message_queue').create({ source, destination, dataType, data: JSON.stringify(data) }); }; - /** + /** * Upsert - creates or updates a record based on whether it exists * @param {string} collection - Collection name * @param {string} id - Record ID or null for new record * @param {Object} data - Record data * @returns {Promise} Created/updated record */ - pb.upsert = async (collection, id, data) => { - if (id) { - const exists = await pb.getOne(collection, id); - if (exists) { - return await pb.updateOne(collection, id, data); - } - } - return await pb.createOne(collection, data); - }; + pb.upsert = async (collection, id, data) => { + if (id) { + const exists = await pb.getOne(collection, id); + if (exists) { + return await pb.updateOne(collection, id, data); + } + } + return await pb.createOne(collection, data); + }; - // ===== QUERY SHORTCUTS ===== + // ===== QUERY SHORTCUTS ===== - /** + /** * Get first record matching a filter * @param {string} collection - Collection name * @param {string} filter - Filter query * @param {Object} options - Additional options * @returns {Promise} First matching record or null */ - pb.getFirst = async (collection, filter, options = {}) => { - try { - const result = await pb.collection(collection).getList(1, 1, { - filter, - ...options - }); + pb.getFirst = async (collection, filter, options = {}) => { + try { + const result = await pb.collection(collection).getList(1, 1, { + filter, + ...options + }); - return result.items.length > 0 ? result.items[0] : null; - } catch (error) { - if (error.status === 404) { - return null; - } - logger.error(`Failed to get first record from ${collection}: ${error.message}`); - throw error; - } - }; + return result.items.length > 0 ? result.items[0] : null; + } catch (error) { + if (error.status === 404) { + return null; + } + logger.error(`Failed to get first record from ${collection}: ${error.message}`); + throw error; + } + }; - /** + /** * Get all records from a collection (handles pagination) * @param {string} collection - Collection name * @param {Object} options - Query options * @returns {Promise} Array of records */ - pb.getAll = async (collection, options = {}) => { - const records = []; - const pageSize = options.pageSize || 200; - let page = 1; + pb.getAll = async (collection, options = {}) => { + const records = []; + const pageSize = options.pageSize || 200; + let page = 1; + const isRunning = true; + while (isRunning) { + try { + const result = await pb.collection(collection).getList(page, pageSize, options); + records.push(...result.items); - try { - while (true) { - const result = await pb.collection(collection).getList(page, pageSize, options); - records.push(...result.items); + if (records.length >= result.totalItems) { + break; + } - if (records.length >= result.totalItems) { - break; - } + page++; + } catch (error) { + logger.error(`Failed to get all records from ${collection}: ${error.message}`); + throw error; + } + } - page++; - } + return records; + }; - return records; - } catch (error) { - logger.error(`Failed to get all records from ${collection}: ${error.message}`); - throw error; - } - }; - - /** + /** * Count records matching a filter * @param {string} collection - Collection name * @param {string} filter - Filter query * @returns {Promise} Count of matching records */ - pb.count = async (collection, filter = '') => { - try { - const result = await pb.collection(collection).getList(1, 1, { - filter, - fields: 'id' - }); + pb.count = async (collection, filter = '') => { + try { + const result = await pb.collection(collection).getList(1, 1, { + filter, + fields: 'id' + }); - return result.totalItems; - } catch (error) { - logger.error(`Failed to count records in ${collection}: ${error.message}`); - throw error; - } - }; + return result.totalItems; + } catch (error) { + logger.error(`Failed to count records in ${collection}: ${error.message}`); + throw error; + } + }; // ===== BATCH OPERATIONS ===== - /** + /** * Perform batch create * @param {string} collection - Collection name * @param {Array} items - Array of items to create * @returns {Promise} Created records */ - pb.batchCreate = async (collection, items) => { - if (!items || items.length === 0) { - return []; - } + pb.batchCreate = async (collection, items) => { + if (!items || items.length === 0) { + return []; + } - const results = []; + const results = []; - try { - // Process in chunks to avoid rate limits - const chunkSize = 50; + try { + // Process in chunks to avoid rate limits + const chunkSize = 50; - for (let i = 0; i < items.length; i += chunkSize) { - const chunk = items.slice(i, i + chunkSize); - const promises = chunk.map(item => pb.createOne(collection, item)); - const chunkResults = await Promise.all(promises); - results.push(...chunkResults); - } + for (let i = 0; i < items.length; i += chunkSize) { + const chunk = items.slice(i, i + chunkSize); + const promises = chunk.map(item => pb.createOne(collection, item)); + const chunkResults = await Promise.all(promises); + results.push(...chunkResults); + } - return results; - } catch (error) { - logger.error(`Failed batch create in ${collection}: ${error.message}`); - throw error; - } - }; + return results; + } catch (error) { + logger.error(`Failed batch create in ${collection}: ${error.message}`); + throw error; + } + }; - /** + /** * Perform batch update * @param {string} collection - Collection name * @param {Array} items - Array of items with id field * @returns {Promise} Updated records */ - pb.batchUpdate = async (collection, items) => { - if (!items || items.length === 0) { - return []; - } + pb.batchUpdate = async (collection, items) => { + if (!items || items.length === 0) { + return []; + } - const results = []; + const results = []; - try { - // Process in chunks to avoid rate limits - const chunkSize = 50; + try { + // Process in chunks to avoid rate limits + const chunkSize = 50; - for (let i = 0; i < items.length; i += chunkSize) { - const chunk = items.slice(i, i + chunkSize); - const promises = chunk.map(item => { - const { id, ...data } = item; - return pb.updateOne(collection, id, data); - }); - const chunkResults = await Promise.all(promises); - results.push(...chunkResults); - } + for (let i = 0; i < items.length; i += chunkSize) { + const chunk = items.slice(i, i + chunkSize); + const promises = chunk.map(item => { + const { id, ...data } = item; + return pb.updateOne(collection, id, data); + }); + const chunkResults = await Promise.all(promises); + results.push(...chunkResults); + } - return results; - } catch (error) { - logger.error(`Failed batch update in ${collection}: ${error.message}`); - throw error; - } - }; + return results; + } catch (error) { + logger.error(`Failed batch update in ${collection}: ${error.message}`); + throw error; + } + }; - /** + /** * Perform batch delete * @param {string} collection - Collection name * @param {Array} ids - Array of record IDs to delete * @returns {Promise} Results of deletion operations */ - pb.batchDelete = async (collection, ids) => { - if (!ids || ids.length === 0) { - return []; - } + pb.batchDelete = async (collection, ids) => { + if (!ids || ids.length === 0) { + return []; + } - const results = []; + const results = []; - try { - // Process in chunks to avoid rate limits - const chunkSize = 50; + try { + // Process in chunks to avoid rate limits + const chunkSize = 50; - for (let i = 0; i < ids.length; i += chunkSize) { - const chunk = ids.slice(i, i + chunkSize); - const promises = chunk.map(id => pb.deleteOne(collection, id)); - const chunkResults = await Promise.all(promises); - results.push(...chunkResults); - } + for (let i = 0; i < ids.length; i += chunkSize) { + const chunk = ids.slice(i, i + chunkSize); + const promises = chunk.map(id => pb.deleteOne(collection, id)); + const chunkResults = await Promise.all(promises); + results.push(...chunkResults); + } - return results; - } catch (error) { - logger.error(`Failed batch delete in ${collection}: ${error.message}`); - throw error; - } - }; + return results; + } catch (error) { + logger.error(`Failed batch delete in ${collection}: ${error.message}`); + throw error; + } + }; /** * Delete a message in the "message_queue" collection by its record ID. * @param {string} id - Record ID to delete. @@ -361,100 +362,99 @@ const extendPocketBase = (client, pb, logger) => { return await pb.deleteOne('message_queue', id); }; + // ===== CACHE MANAGEMENT ===== - // ===== CACHE MANAGEMENT ===== + // Simple in-memory cache + pb.cache = { + _store: new Map(), + _ttls: new Map(), - // Simple in-memory cache - pb.cache = { - _store: new Map(), - _ttls: new Map(), - - /** + /** * Get a value from cache * @param {string} key - Cache key * @returns {*} Cached value or undefined */ - get(key) { - if (this._ttls.has(key) && this._ttls.get(key) < Date.now()) { - this.delete(key); - return undefined; - } - return this._store.get(key); - }, + get(key) { + if (this._ttls.has(key) && this._ttls.get(key) < Date.now()) { + this.delete(key); + return undefined; + } + return this._store.get(key); + }, - /** + /** * Set a value in cache * @param {string} key - Cache key * @param {*} value - Value to store * @param {number} ttlSeconds - Time to live in seconds */ - set(key, value, ttlSeconds = 300) { - this._store.set(key, value); - if (ttlSeconds > 0) { - this._ttls.set(key, Date.now() + (ttlSeconds * 1000)); - } - }, + set(key, value, ttlSeconds = 300) { + this._store.set(key, value); + if (ttlSeconds > 0) { + this._ttls.set(key, Date.now() + (ttlSeconds * 1000)); + } + }, - /** + /** * Delete a value from cache * @param {string} key - Cache key */ - delete(key) { - this._store.delete(key); - this._ttls.delete(key); - }, + delete(key) { + this._store.delete(key); + this._ttls.delete(key); + }, - /** + /** * Clear all cache */ - clear() { - this._store.clear(); - this._ttls.clear(); - } - }; + clear() { + this._store.clear(); + this._ttls.clear(); + } + }; - /** + /** * Get a record with caching * @param {string} collection - Collection name * @param {string} id - Record ID * @param {number} ttlSeconds - Cache TTL in seconds * @returns {Promise} Record or null */ - pb.getCached = async (collection, id, ttlSeconds = 60) => { - const cacheKey = `${collection}:${id}`; - const cached = pb.cache.get(cacheKey); + pb.getCached = async (collection, id, ttlSeconds = 60) => { + const cacheKey = `${collection}:${id}`; + const cached = pb.cache.get(cacheKey); - if (cached !== undefined) { - return cached; - } + if (cached !== undefined) { + return cached; + } - const record = await pb.getOne(collection, id); - pb.cache.set(cacheKey, record, ttlSeconds); + const record = await pb.getOne(collection, id); + pb.cache.set(cacheKey, record, ttlSeconds); - return record; - }; + return record; + }; - /** + /** * Get list with caching * @param {string} collection - Collection name * @param {Object} options - Query options * @param {number} ttlSeconds - Cache TTL in seconds * @returns {Promise} List result */ - pb.getListCached = async (collection, options = {}, ttlSeconds = 30) => { - const cacheKey = `${collection}:list:${JSON.stringify(options)}`; - const cached = pb.cache.get(cacheKey); + pb.getListCached = async (collection, options = {}, ttlSeconds = 30) => { + const cacheKey = `${collection}:list:${JSON.stringify(options)}`; + const cached = pb.cache.get(cacheKey); - if (cached !== undefined) { - return cached; - } + if (cached !== undefined) { + return cached; + } - const { page = 1, perPage = 50, ...restOptions } = options; - const result = await pb.collection(collection).getList(page, perPage, restOptions); - pb.cache.set(cacheKey, result, ttlSeconds); + const { page = 1, perPage = 50, ...restOptions } = options; + const result = await pb.collection(collection).getList(page, perPage, restOptions); + pb.cache.set(cacheKey, result, ttlSeconds); - return result; - }; + return result; + }; }; /** @@ -463,82 +463,82 @@ const extendPocketBase = (client, pb, logger) => { * @param {Object} logger - Winston logger */ const setupConnectionHandling = (pb, logger) => { - // Add connection state tracking - pb.isConnected = true; - pb.lastSuccessfulAuth = null; + // Add connection state tracking + pb.isConnected = true; + pb.lastSuccessfulAuth = null; - // Add auto-reconnect and token refresh - pb.authStore.onChange(() => { - pb.isConnected = pb.authStore.isValid; + // Add auto-reconnect and token refresh + pb.authStore.onChange(() => { + pb.isConnected = pb.authStore.isValid; - if (pb.isConnected) { - pb.lastSuccessfulAuth = new Date(); - logger.info('PocketBase authentication successful'); - } else { - logger.warn('PocketBase auth token expired or invalid'); - } - }); + if (pb.isConnected) { + pb.lastSuccessfulAuth = new Date(); + logger.info('PocketBase authentication successful'); + } else { + logger.warn('PocketBase auth token expired or invalid'); + } + }); - // Helper to check health and reconnect if needed - pb.ensureConnection = async () => { - if (!pb.isConnected || !pb.authStore.isValid) { - try { - logger.info('Reconnecting to PocketBase...'); - // Attempt to refresh the auth if we have a refresh token - if (pb.authStore.token && pb.authStore.model?.id) { + // Helper to check health and reconnect if needed + pb.ensureConnection = async () => { + if (!pb.isConnected || !pb.authStore.isValid) { + try { + logger.info('Reconnecting to PocketBase...'); + // Attempt to refresh the auth if we have a refresh token + if (pb.authStore.token && pb.authStore.model?.id) { // Refresh session using the configured users collection - await pb.collection('_users').authRefresh(); - } else if (pb._config.username && pb._config.password) { - // Fall back to full re-authentication if credentials available + await pb.collection('_users').authRefresh(); + } else if (pb._config.username && pb._config.password) { + // Fall back to full re-authentication if credentials available // Re-authenticate using the configured users collection credentials - await pb.collection('_users').authWithPassword( - pb._config.username, - pb._config.password - ); - } else { - logger.error('No credentials available to reconnect PocketBase'); - pb.isConnected = false; - return false; - } + await pb.collection('_users').authWithPassword( + pb._config.username, + pb._config.password + ); + } else { + logger.error('No credentials available to reconnect PocketBase'); + pb.isConnected = false; + return false; + } - pb.isConnected = true; - pb.lastSuccessfulAuth = new Date(); - logger.info('Successfully reconnected to PocketBase'); + pb.isConnected = true; + pb.lastSuccessfulAuth = new Date(); + logger.info('Successfully reconnected to PocketBase'); - return true; - } catch (error) { - logger.error(`Failed to reconnect to PocketBase: ${error.message}`); - pb.isConnected = false; - return false; - } - } + return true; + } catch (error) { + logger.error(`Failed to reconnect to PocketBase: ${error.message}`); + pb.isConnected = false; + return false; + } + } - return true; - }; + return true; + }; - // Store credentials for reconnection - pb._config = pb._config || {}; - // Ensure only if env provided - if (process.env.SHARED_POCKETBASE_USERNAME && process.env.SHARED_POCKETBASE_PASSWORD) { - pb._config.username = process.env.SHARED_POCKETBASE_USERNAME; - pb._config.password = process.env.SHARED_POCKETBASE_PASSWORD; - } + // Store credentials for reconnection + pb._config = pb._config || {}; + // Ensure only if env provided + if (process.env.SHARED_POCKETBASE_USERNAME && process.env.SHARED_POCKETBASE_PASSWORD) { + pb._config.username = process.env.SHARED_POCKETBASE_USERNAME; + pb._config.password = process.env.SHARED_POCKETBASE_PASSWORD; + } - // Heartbeat function to check connection periodically - const heartbeatInterval = setInterval(async () => { - try { - // Simple health check - await pb.health.check(); - pb.isConnected = true; - } catch (error) { - logger.warn(`PocketBase connection issue: ${error.message}`); - pb.isConnected = false; - await pb.ensureConnection(); - } - }, 5 * 60 * 1000); // Check every 5 minutes + // Heartbeat function to check connection periodically + const heartbeatInterval = setInterval(async () => { + try { + // Simple health check + await pb.health.check(); + pb.isConnected = true; + } catch (error) { + logger.warn(`PocketBase connection issue: ${error.message}`); + pb.isConnected = false; + await pb.ensureConnection(); + } + }, 5 * 60 * 1000); // Check every 5 minutes - // Clean up on client disconnect - pb.cleanup = () => { - clearInterval(heartbeatInterval); - }; + // Clean up on client disconnect + pb.cleanup = () => { + clearInterval(heartbeatInterval); + }; }; diff --git a/_opt/responses.js b/_opt/responses.js index bea6c59..1d03601 100644 --- a/_opt/responses.js +++ b/_opt/responses.js @@ -4,13 +4,15 @@ * and handles text or image (function_call) outputs. */ // Removed local file fallback; prompt now comes exclusively from PocketBase via responsesPrompt module -import { OpenAI } from 'openai'; -import axios from 'axios'; -import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js'; -import { expandTemplate } from '../_src/template.js'; import fs from 'fs/promises'; import path from 'path'; +import axios from 'axios'; +import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js'; +import { OpenAI } from 'openai'; + +import { expandTemplate } from '../_src/template.js'; + // Discord message max length const MAX_DISCORD_MSG_LENGTH = 2000; @@ -21,76 +23,75 @@ const MAX_DISCORD_MSG_LENGTH = 2000; * @returns {string[]} Array of message chunks. */ function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) { - const lines = text.split(/\n/); - const chunks = []; - let chunk = ''; - let codeBlockOpen = false; - let codeBlockFence = '```'; - for (let line of lines) { - const trimmed = line.trim(); - const isFenceLine = trimmed.startsWith('```'); - if (isFenceLine) { - if (!codeBlockOpen) { - codeBlockOpen = true; - codeBlockFence = trimmed; - } else if (trimmed === '```') { - // closing fence - codeBlockOpen = false; - } + const lines = text.split(/\n/); + const chunks = []; + let chunk = ''; + let codeBlockOpen = false; + let codeBlockFence = '```'; + for (const line of lines) { + const trimmed = line.trim(); + const isFenceLine = trimmed.startsWith('```'); + if (isFenceLine) { + if (!codeBlockOpen) { + codeBlockOpen = true; + codeBlockFence = trimmed; + } else if (trimmed === '```') { + // closing fence + codeBlockOpen = false; + } + } + // include the newline that was removed by split + const segment = line + '\n'; + // if adding segment exceeds limit + if (chunk.length + segment.length > maxLength) { + if (chunk.length > 0) { + // close open code block if needed + if (codeBlockOpen) chunk += '\n```'; + chunks.push(chunk); + // start new chunk, reopen code block if needed + chunk = codeBlockOpen ? (codeBlockFence + '\n' + segment) : segment; + continue; + } + // single segment too long, split it directly + let rest = segment; + while (rest.length > maxLength) { + let part = rest.slice(0, maxLength); + if (codeBlockOpen) part += '\n```'; + chunks.push(part); + rest = codeBlockOpen ? (codeBlockFence + '\n' + rest.slice(maxLength)) : rest.slice(maxLength); + } + chunk = rest; + continue; + } + chunk += segment; } - // include the newline that was removed by split - const segment = line + '\n'; - // if adding segment exceeds limit - if (chunk.length + segment.length > maxLength) { - if (chunk.length > 0) { - // close open code block if needed + if (chunk) { + // close any unclosed code block if (codeBlockOpen) chunk += '\n```'; chunks.push(chunk); - // start new chunk, reopen code block if needed - chunk = codeBlockOpen ? (codeBlockFence + '\n' + segment) : segment; - continue; - } - // single segment too long, split it directly - let rest = segment; - while (rest.length > maxLength) { - let part = rest.slice(0, maxLength); - if (codeBlockOpen) part += '\n```'; - chunks.push(part); - rest = codeBlockOpen ? (codeBlockFence + '\n' + rest.slice(maxLength)) : rest.slice(maxLength); - } - chunk = rest; - continue; } - chunk += segment; - } - if (chunk) { - // close any unclosed code block - if (codeBlockOpen) chunk += '\n```'; - chunks.push(chunk); - } - // remove trailing newline from each chunk - return chunks.map(c => c.endsWith('\n') ? c.slice(0, -1) : c); + // remove trailing newline from each chunk + return chunks.map(c => c.endsWith('\n') ? c.slice(0, -1) : c); } - /** * Determine whether the bot should respond to a message. * Controlled by enableMentions and enableReplies in config. */ async function shouldRespond(message, botId, cfg, logger) { - if (message.author.bot || !botId) return false; - const enableMentions = cfg.enableMentions ?? true; - const enableReplies = cfg.enableReplies ?? true; - const isMention = enableMentions && message.mentions.users.has(botId); - let isReply = false; - if (enableReplies && message.reference?.messageId) { - try { - const ref = await message.channel.messages.fetch(message.reference.messageId); - isReply = ref.author.id === botId; - } catch {} - } - logger.debug(`Trigger? mention=${isMention} reply=${isReply}`); - return isMention || isReply; + if (message.author.bot || !botId) return false; + const enableMentions = cfg.enableMentions ?? true; + const enableReplies = cfg.enableReplies ?? true; + const isMention = enableMentions && message.mentions.users.has(botId); + let isReply = false; + if (enableReplies && message.reference?.messageId) { + try { + const ref = await message.channel.messages.fetch(message.reference.messageId); + isReply = ref.author.id === botId; + } catch {} + } + logger.debug(`Trigger? mention=${isMention} reply=${isReply}`); + return isMention || isReply; } /** @@ -101,7 +102,7 @@ async function shouldRespond(message, botId, cfg, logger) { * @param {number} ttlSeconds - Time-to-live for the cache entry in seconds. */ function cacheResponse(client, key, id, ttlSeconds) { - client.pb?.cache?.set(key, id, ttlSeconds); + client.pb?.cache?.set(key, id, ttlSeconds); } /** @@ -112,10 +113,10 @@ function cacheResponse(client, key, id, ttlSeconds) { * @param {number} amount - Number of tokens to award. */ function awardOutput(client, guildId, userId, amount) { - if (client.scorekeeper && amount > 0) { - client.scorekeeper.addOutput(guildId, userId, amount, 'AI_response') - .catch(err => client.logger.error(`Scorekeeper error: ${err.message}`)); - } + if (client.scorekeeper && amount > 0) { + client.scorekeeper.addOutput(guildId, userId, amount, 'AI_response') + .catch(err => client.logger.error(`Scorekeeper error: ${err.message}`)); + } } /** @@ -128,112 +129,112 @@ function awardOutput(client, guildId, userId, amount) { * @returns {Promise} True if the function call was handled. */ async function handleImage(client, message, resp, cfg) { - const calls = Array.isArray(resp.output) ? resp.output : []; - const fn = calls.find(o => o.type === 'function_call' && o.name === 'generate_image'); - if (!fn?.arguments) return false; - client.logger.debug(`Image function args: ${fn.arguments}`); - let args; - try { args = JSON.parse(fn.arguments); } catch (e) { return false; } - if (!args.prompt?.trim()) { - await message.reply('Cannot generate image: empty prompt.'); - return true; - } - // Use image model defined in config - const model = cfg.imageGeneration.defaultModel; - const promptText = args.prompt; - // Determine number of images (1-10); DALL·E-3 only supports 1 - let count = 1; - if (args.n != null) { - const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10); - if (!Number.isNaN(nVal)) count = nVal; - } - // clamp between 1 and 10 - count = Math.max(1, Math.min(10, count)); - if (model === 'dall-e-3') count = 1; - const size = args.size || 'auto'; - // Determine quality based on config and model constraints - let quality = args.quality || cfg.imageGeneration.defaultQuality; - if (model === 'gpt-image-1') { - if (!['low', 'medium', 'high', 'auto'].includes(quality)) quality = 'auto'; - } else if (model === 'dall-e-2') { - quality = 'standard'; - } else if (model === 'dall-e-3') { - if (!['standard', 'hd', 'auto'].includes(quality)) quality = 'standard'; - } - const background = args.background; - const moderation = args.moderation; - const outputFormat = args.output_format; - const compression = args.output_compression; - const style = args.style; - const user = args.user || message.author.id; - try { - // Build generate parameters - const genParams = { model, prompt: promptText, n: count, size, quality, user }; - // response_format supported for DALL·E models (not gpt-image-1) - if (model !== 'gpt-image-1' && args.response_format) { - genParams['response_format'] = args.response_format; + const calls = Array.isArray(resp.output) ? resp.output : []; + const fn = calls.find(o => o.type === 'function_call' && o.name === 'generate_image'); + if (!fn?.arguments) return false; + client.logger.debug(`Image function args: ${fn.arguments}`); + let args; + try { args = JSON.parse(fn.arguments); } catch (e) { return false; } + if (!args.prompt?.trim()) { + await message.reply('Cannot generate image: empty prompt.'); + return true; } - // gpt-image-1 supports background, moderation, output_format, and output_compression + // Use image model defined in config + const model = cfg.imageGeneration.defaultModel; + const promptText = args.prompt; + // Determine number of images (1-10); DALL·E-3 only supports 1 + let count = 1; + if (args.n !== null) { + const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10); + if (!Number.isNaN(nVal)) count = nVal; + } + // clamp between 1 and 10 + count = Math.max(1, Math.min(10, count)); + if (model === 'dall-e-3') count = 1; + const size = args.size || 'auto'; + // Determine quality based on config and model constraints + let quality = args.quality || cfg.imageGeneration.defaultQuality; if (model === 'gpt-image-1') { - if (background) genParams['background'] = background; - if (moderation) genParams['moderation'] = moderation; - if (outputFormat) { - genParams['output_format'] = outputFormat; - // only support compression for JPEG or WEBP formats - if (['jpeg','webp'].includes(outputFormat) && typeof compression === 'number') { - genParams['output_compression'] = compression; + if (!['low', 'medium', 'high', 'auto'].includes(quality)) quality = 'auto'; + } else if (model === 'dall-e-2') { + quality = 'standard'; + } else if (model === 'dall-e-3') { + if (!['standard', 'hd', 'auto'].includes(quality)) quality = 'standard'; + } + const background = args.background; + const moderation = args.moderation; + const outputFormat = args.output_format; + const compression = args.output_compression; + const style = args.style; + const user = args.user || message.author.id; + try { + // Build generate parameters + const genParams = { model, prompt: promptText, n: count, size, quality, user }; + // response_format supported for DALL·E models (not gpt-image-1) + if (model !== 'gpt-image-1' && args.response_format) { + genParams['response_format'] = args.response_format; } - } + // gpt-image-1 supports background, moderation, output_format, and output_compression + if (model === 'gpt-image-1') { + if (background) genParams['background'] = background; + if (moderation) genParams['moderation'] = moderation; + if (outputFormat) { + genParams['output_format'] = outputFormat; + // only support compression for JPEG or WEBP formats + if (['jpeg','webp'].includes(outputFormat) && typeof compression === 'number') { + genParams['output_compression'] = compression; + } + } + } + // dall-e-3 supports style + if (model === 'dall-e-3' && style) { + genParams['style'] = style; + } + // Generate images via OpenAI Images API + const imgRes = await client.openai.images.generate(genParams); + const images = imgRes.data || []; + if (!images.length) throw new Error('No images generated'); + // Ensure save directory exists + const dir = cfg.imageGeneration?.imageSavePath || './images'; + await fs.mkdir(dir, { recursive: true }); + const attachments = []; + const outputs = []; + // Process each generated image + for (let i = 0; i < images.length; i++) { + const img = images[i]; + let buffer, ext = outputFormat || 'png'; + if (img.b64_json) { + buffer = Buffer.from(img.b64_json, 'base64'); + outputs.push({ b64_json: img.b64_json }); + } else if (img.url) { + const dl = await axios.get(img.url, { responseType: 'arraybuffer' }); + buffer = Buffer.from(dl.data); + // derive extension from URL if possible + const parsed = path.extname(img.url.split('?')[0]).replace(/^[.]/, ''); + if (parsed) ext = parsed; + outputs.push({ url: img.url }); + } else { + throw new Error('No image data'); + } + const filename = `${message.author.id}-${Date.now()}-${i}.${ext}`; + const filePath = path.join(dir, filename); + await fs.writeFile(filePath, buffer); + client.logger.info(`Saved image: ${filePath}`); + attachments.push(new AttachmentBuilder(buffer, { name: filename })); + } + // Award output points based on token usage for image generation + const tokens = imgRes.usage?.total_tokens ?? count; + if (client.scorekeeper && tokens > 0) { + client.scorekeeper.addOutput(message.guild.id, message.author.id, tokens, 'image_generation') + .catch(err => client.logger.error(`Scorekeeper error: ${err.message}`)); + } + // Reply with attachments + await message.reply({ content: promptText, files: attachments }); + } catch (err) { + client.logger.error(`Image error: ${err.message}`); + await message.reply(`Image generation error: ${err.message}`); } - // dall-e-3 supports style - if (model === 'dall-e-3' && style) { - genParams['style'] = style; - } - // Generate images via OpenAI Images API - const imgRes = await client.openai.images.generate(genParams); - const images = imgRes.data || []; - if (!images.length) throw new Error('No images generated'); - // Ensure save directory exists - const dir = cfg.imageGeneration?.imageSavePath || './images'; - await fs.mkdir(dir, { recursive: true }); - const attachments = []; - const outputs = []; - // Process each generated image - for (let i = 0; i < images.length; i++) { - const img = images[i]; - let buffer, ext = outputFormat || 'png'; - if (img.b64_json) { - buffer = Buffer.from(img.b64_json, 'base64'); - outputs.push({ b64_json: img.b64_json }); - } else if (img.url) { - const dl = await axios.get(img.url, { responseType: 'arraybuffer' }); - buffer = Buffer.from(dl.data); - // derive extension from URL if possible - const parsed = path.extname(img.url.split('?')[0]).replace(/^[.]/, ''); - if (parsed) ext = parsed; - outputs.push({ url: img.url }); - } else { - throw new Error('No image data'); - } - const filename = `${message.author.id}-${Date.now()}-${i}.${ext}`; - const filePath = path.join(dir, filename); - await fs.writeFile(filePath, buffer); - client.logger.info(`Saved image: ${filePath}`); - attachments.push(new AttachmentBuilder(buffer, { name: filename })); - } - // Award output points based on token usage for image generation - const tokens = imgRes.usage?.total_tokens ?? count; - if (client.scorekeeper && tokens > 0) { - client.scorekeeper.addOutput(message.guild.id, message.author.id, tokens, 'image_generation') - .catch(err => client.logger.error(`Scorekeeper error: ${err.message}`)); - } - // Reply with attachments - await message.reply({ content: promptText, files: attachments }); - } catch (err) { - client.logger.error(`Image error: ${err.message}`); - await message.reply(`Image generation error: ${err.message}`); - } - return true; + return true; } /** @@ -244,213 +245,213 @@ async function handleImage(client, message, resp, cfg) { * @param {Message} message - Incoming Discord message. */ async function onMessage(client, cfg, message) { - const logger = client.logger; - const botId = client.user?.id; - client.logger.debug(`[onMessage] Received message ${message.id} from ${message.author.id}`); - // Check if bot should respond, based on config (mentions/replies) - if (!(await shouldRespond(message, botId, cfg, logger))) return; + const logger = client.logger; + const botId = client.user?.id; + client.logger.debug(`[onMessage] Received message ${message.id} from ${message.author.id}`); + // Check if bot should respond, based on config (mentions/replies) + if (!(await shouldRespond(message, botId, cfg, logger))) return; - // Determine channel/thread key for context - const key = message.thread?.id || message.channel.id; - // Initialize per-channel lock map - const lockMap = client._responseLockMap || (client._responseLockMap = new Map()); - // Get last pending promise for this key - const last = lockMap.get(key) || Promise.resolve(); - // Handler to run in sequence - const handler = async () => { + // Determine channel/thread key for context + const key = message.thread?.id || message.channel.id; + // Initialize per-channel lock map + const lockMap = client._responseLockMap || (client._responseLockMap = new Map()); + // Get last pending promise for this key + const last = lockMap.get(key) || Promise.resolve(); + // Handler to run in sequence + const handler = async () => { // Start typing indicator loop every 9 seconds - const typingInterval = setInterval(() => { - message.channel.sendTyping().catch(() => {}); - }, 9000); - // Initial typing - message.channel.sendTyping().catch(() => {}); - try { - // Previous response ID for context continuity - const prev = client.pb?.cache?.get(key); - // Enforce minimum score to use AI responses - // Enforce minimum score to use AI responses if scorekeeper is enabled - if (client.scorekeeper) { + const typingInterval = setInterval(() => { + message.channel.sendTyping().catch(() => {}); + }, 9000); + // Initial typing + message.channel.sendTyping().catch(() => {}); try { - const isAdmin = message.member?.permissions?.has(PermissionFlagsBits.Administrator); - const scoreData = await client.scorekeeper.getScore(message.guild.id, message.author.id); - if (!isAdmin && scoreData.totalScore < cfg.minScore) { - await message.reply( - `You need an I/O score of at least ${cfg.minScore} to use AI responses. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.` - ); - return; - } + // Previous response ID for context continuity + const prev = client.pb?.cache?.get(key); + // Enforce minimum score to use AI responses + // Enforce minimum score to use AI responses if scorekeeper is enabled + if (client.scorekeeper) { + try { + const isAdmin = message.member?.permissions?.has(PermissionFlagsBits.Administrator); + const scoreData = await client.scorekeeper.getScore(message.guild.id, message.author.id); + if (!isAdmin && scoreData.totalScore < cfg.minScore) { + await message.reply( + `You need an I/O score of at least ${cfg.minScore} to use AI responses. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.` + ); + return; + } + } catch (err) { + client.logger.error(`Error checking score: ${err.message}`); + } + } + // Build request body, including replied-to message context and mention of who spoke + let referencePrefix = ''; + let referenceMessage = null; + if (message.reference?.messageId) { + try { + const ref = await message.channel.messages.fetch(message.reference.messageId); + referenceMessage = ref; + const refContent = ref.content || ''; + if (ref.author.id === botId) { + referencePrefix = `You said: ${refContent}`; + } else { + referencePrefix = `<@${ref.author.id}> said: ${refContent}`; + } + } catch { + // ignore fetch errors + } + } + const speakerMention = `<@${message.author.id}>`; + const userInput = referencePrefix + ? `${referencePrefix}\n${speakerMention} said to you: ${message.content}` + : `${speakerMention} said to you: ${message.content}`; + // Prepare template context + const locationName = message.thread?.name || message.channel.name; + const locationId = message.thread?.id || message.channel.id; + const now = new Date(); + const date = now.toISOString().split('T')[0]; + const time = now.toTimeString().split(' ')[0]; + const datetime = now.toISOString().replace('T',' ').replace(/\..+$/,''); + const ctx = { + clientId: client.config.id, + userName: message.author.username, + userId: message.author.id, + userTag: message.author.tag, + // add guild context + guildName: message.guild?.name || '', + guildId: message.guild?.id || '', + input: userInput, + locationName, locationId, + date, time, datetime + }; + const instructions = expandTemplate(client.responsesPrompt, ctx); + const body = { + model: cfg.defaultModel, + instructions, + input: userInput, + previous_response_id: prev, + max_output_tokens: cfg.defaultMaxTokens, + temperature: cfg.defaultTemperature + }; + // Assemble any enabled tools + const tools = []; + if (cfg.tools?.imageGeneration) { + const model = cfg.imageGeneration.defaultModel; + // Configure allowed sizes per model + let sizeEnum; + switch (model) { + case 'gpt-image-1': sizeEnum = ['auto','1024x1024','1536x1024','1024x1536']; break; + case 'dall-e-2': sizeEnum = ['256x256','512x512','1024x1024']; break; + case 'dall-e-3': sizeEnum = ['auto','1024x1024','1792x1024','1024x1792']; break; + default: sizeEnum = ['auto','1024x1024']; + } + // Configure quality options per model + let qualityEnum; + switch (model) { + case 'gpt-image-1': qualityEnum = ['auto','low','medium','high']; break; + case 'dall-e-2': qualityEnum = ['standard']; break; + case 'dall-e-3': qualityEnum = ['auto','standard','hd']; break; + default: qualityEnum = ['auto','standard']; + } + // Build schema properties dynamically + const properties = { + prompt: { type: 'string', description: 'Text description of desired image(s).' }, + n: { type: 'number', description: 'Number of images to generate.' }, + size: { type: 'string', enum: sizeEnum, description: 'Image size.' }, + quality: { type: 'string', enum: qualityEnum, description: 'Image quality.' }, + user: { type: 'string', description: 'Unique end-user identifier.' } + }; + if (model !== 'gpt-image-1') { + properties.response_format = { type: 'string', enum: ['url','b64_json'], description: 'Format of returned images.' }; + } + if (model === 'gpt-image-1') { + properties.background = { type: 'string', enum: ['transparent','opaque','auto'], description: 'Background transparency.' }; + properties.moderation = { type: 'string', enum: ['low','auto'], description: 'Content moderation level.' }; + properties.output_format = { type: 'string', enum: ['png','jpeg','webp'], description: 'Output image format.' }; + properties.output_compression = { type: 'number', description: 'Compression level (0-100).' }; + } + if (model === 'dall-e-3') { + properties.style = { type: 'string', enum: ['vivid','natural'], description: 'Style option for dall-e-3.' }; + } + // Determine required fields + const required = ['prompt','n','size','quality','user']; + if (model !== 'gpt-image-1') required.push('response_format'); + if (model === 'gpt-image-1') required.push('background','moderation','output_format','output_compression'); + if (model === 'dall-e-3') required.push('style'); + // Register the function tool + tools.push({ + type: 'function', + name: 'generate_image', + description: `Generate images using model ${model} with requested parameters.`, + parameters: { + type: 'object', + properties, + required, + additionalProperties: false + }, + strict: true + }); + } + if (cfg.tools?.webSearch) { + tools.push({ type: 'web_search_preview' }); + } + if (tools.length) { + body.tools = tools; + } + + // If there are image attachments in the referenced or current message, wrap text and images into a multimodal message + const refImages = referenceMessage + ? referenceMessage.attachments.filter(att => /\.(png|jpe?g|gif|webp)$/i.test(att.name || att.url)) + : new Map(); + const currImages = message.attachments.filter(att => /\.(png|jpe?g|gif|webp)$/i.test(att.name || att.url)); + if (refImages.size > 0 || currImages.size > 0) { + // build ordered content items: text first, then referenced images, then current images + const contentItems = [{ type: 'input_text', text: userInput }]; + for (const att of refImages.values()) { + contentItems.push({ type: 'input_image', detail: 'auto', image_url: att.url }); + } + for (const att of currImages.values()) { + contentItems.push({ type: 'input_image', detail: 'auto', image_url: att.url }); + } + body.input = [{ type: 'message', role: 'user', content: contentItems }]; + } + + // Call OpenAI Responses + logger.debug(`Calling AI with body: ${JSON.stringify(body)}`); + const resp = await client.openai.responses.create(body); + logger.info(`AI response id=${resp.id}`); + // Award tokens for the AI chat response + const chatTokens = resp.usage?.total_tokens ?? resp.usage?.completion_tokens ?? 0; + awardOutput(client, message.guild.id, message.author.id, chatTokens); + + // Cache response ID if not a function call + const isFuncCall = Array.isArray(resp.output) && resp.output.some(o => o.type === 'function_call'); + if (!isFuncCall && resp.id && cfg.conversationExpiry) { + cacheResponse(client, key, resp.id, Math.floor(cfg.conversationExpiry / 1000)); + } + + // Handle image function call if present + if (await handleImage(client, message, resp, cfg)) return; + + // Otherwise reply with text + const text = resp.output_text?.trim(); + if (text) { + const parts = splitMessage(text, MAX_DISCORD_MSG_LENGTH); + for (const part of parts) { + await message.reply(part); + } + } } catch (err) { - client.logger.error(`Error checking score: ${err.message}`); + logger.error(`Queued onMessage error for ${key}: ${err.message}`); + } finally { + clearInterval(typingInterval); } - } - // Build request body, including replied-to message context and mention of who spoke - let referencePrefix = ''; - let referenceMessage = null; - if (message.reference?.messageId) { - try { - const ref = await message.channel.messages.fetch(message.reference.messageId); - referenceMessage = ref; - const refContent = ref.content || ''; - if (ref.author.id === botId) { - referencePrefix = `You said: ${refContent}`; - } else { - referencePrefix = `<@${ref.author.id}> said: ${refContent}`; - } - } catch { - // ignore fetch errors - } - } - const speakerMention = `<@${message.author.id}>`; - const userInput = referencePrefix - ? `${referencePrefix}\n${speakerMention} said to you: ${message.content}` - : `${speakerMention} said to you: ${message.content}`; - // Prepare template context - const locationName = message.thread?.name || message.channel.name; - const locationId = message.thread?.id || message.channel.id; - const now = new Date(); - const date = now.toISOString().split('T')[0]; - const time = now.toTimeString().split(' ')[0]; - const datetime = now.toISOString().replace('T',' ').replace(/\..+$/,''); - const ctx = { - clientId: client.config.id, - userName: message.author.username, - userId: message.author.id, - userTag: message.author.tag, - // add guild context - guildName: message.guild?.name || '', - guildId: message.guild?.id || '', - input: userInput, - locationName, locationId, - date, time, datetime - }; - const instructions = expandTemplate(client.responsesPrompt, ctx); - const body = { - model: cfg.defaultModel, - instructions, - input: userInput, - previous_response_id: prev, - max_output_tokens: cfg.defaultMaxTokens, - temperature: cfg.defaultTemperature - }; - // Assemble any enabled tools - const tools = []; - if (cfg.tools?.imageGeneration) { - const model = cfg.imageGeneration.defaultModel; - // Configure allowed sizes per model - let sizeEnum; - switch (model) { - case 'gpt-image-1': sizeEnum = ['auto','1024x1024','1536x1024','1024x1536']; break; - case 'dall-e-2': sizeEnum = ['256x256','512x512','1024x1024']; break; - case 'dall-e-3': sizeEnum = ['auto','1024x1024','1792x1024','1024x1792']; break; - default: sizeEnum = ['auto','1024x1024']; - } - // Configure quality options per model - let qualityEnum; - switch (model) { - case 'gpt-image-1': qualityEnum = ['auto','low','medium','high']; break; - case 'dall-e-2': qualityEnum = ['standard']; break; - case 'dall-e-3': qualityEnum = ['auto','standard','hd']; break; - default: qualityEnum = ['auto','standard']; - } - // Build schema properties dynamically - const properties = { - prompt: { type: 'string', description: 'Text description of desired image(s).' }, - n: { type: 'number', description: 'Number of images to generate.' }, - size: { type: 'string', enum: sizeEnum, description: 'Image size.' }, - quality: { type: 'string', enum: qualityEnum, description: 'Image quality.' }, - user: { type: 'string', description: 'Unique end-user identifier.' } - }; - if (model !== 'gpt-image-1') { - properties.response_format = { type: 'string', enum: ['url','b64_json'], description: 'Format of returned images.' }; - } - if (model === 'gpt-image-1') { - properties.background = { type: 'string', enum: ['transparent','opaque','auto'], description: 'Background transparency.' }; - properties.moderation = { type: 'string', enum: ['low','auto'], description: 'Content moderation level.' }; - properties.output_format = { type: 'string', enum: ['png','jpeg','webp'], description: 'Output image format.' }; - properties.output_compression = { type: 'number', description: 'Compression level (0-100).' }; - } - if (model === 'dall-e-3') { - properties.style = { type: 'string', enum: ['vivid','natural'], description: 'Style option for dall-e-3.' }; - } - // Determine required fields - const required = ['prompt','n','size','quality','user']; - if (model !== 'gpt-image-1') required.push('response_format'); - if (model === 'gpt-image-1') required.push('background','moderation','output_format','output_compression'); - if (model === 'dall-e-3') required.push('style'); - // Register the function tool - tools.push({ - type: 'function', - name: 'generate_image', - description: `Generate images using model ${model} with requested parameters.`, - parameters: { - type: 'object', - properties, - required, - additionalProperties: false - }, - strict: true - }); - } - if (cfg.tools?.webSearch) { - tools.push({ type: 'web_search_preview' }); - } - if (tools.length) { - body.tools = tools; - } - - // If there are image attachments in the referenced or current message, wrap text and images into a multimodal message - const refImages = referenceMessage - ? referenceMessage.attachments.filter(att => /\.(png|jpe?g|gif|webp)$/i.test(att.name || att.url)) - : new Map(); - const currImages = message.attachments.filter(att => /\.(png|jpe?g|gif|webp)$/i.test(att.name || att.url)); - if (refImages.size > 0 || currImages.size > 0) { - // build ordered content items: text first, then referenced images, then current images - const contentItems = [{ type: 'input_text', text: userInput }]; - for (const att of refImages.values()) { - contentItems.push({ type: 'input_image', detail: 'auto', image_url: att.url }); - } - for (const att of currImages.values()) { - contentItems.push({ type: 'input_image', detail: 'auto', image_url: att.url }); - } - body.input = [{ type: 'message', role: 'user', content: contentItems }]; - } - - // Call OpenAI Responses - logger.debug(`Calling AI with body: ${JSON.stringify(body)}`); - const resp = await client.openai.responses.create(body); - logger.info(`AI response id=${resp.id}`); - // Award tokens for the AI chat response - const chatTokens = resp.usage?.total_tokens ?? resp.usage?.completion_tokens ?? 0; - awardOutput(client, message.guild.id, message.author.id, chatTokens); - - // Cache response ID if not a function call - const isFuncCall = Array.isArray(resp.output) && resp.output.some(o => o.type === 'function_call'); - if (!isFuncCall && resp.id && cfg.conversationExpiry) { - cacheResponse(client, key, resp.id, Math.floor(cfg.conversationExpiry / 1000)); - } - - // Handle image function call if present - if (await handleImage(client, message, resp, cfg)) return; - - // Otherwise reply with text - const text = resp.output_text?.trim(); - if (text) { - const parts = splitMessage(text, MAX_DISCORD_MSG_LENGTH); - for (const part of parts) { - await message.reply(part); - } - } - } catch (err) { - logger.error(`Queued onMessage error for ${key}: ${err.message}`); - } finally { - clearInterval(typingInterval); - } - }; - // Chain the handler to the last promise - const next = last.then(handler).catch(err => logger.error(`[onMessage] Handler error: ${err.message}`)); - lockMap.set(key, next); - // Queue enqueued; handler will send response when its turn arrives - return; + }; + // Chain the handler to the last promise + const next = last.then(handler).catch(err => logger.error(`[onMessage] Handler error: ${err.message}`)); + lockMap.set(key, next); + // Queue enqueued; handler will send response when its turn arrives + return; } /** @@ -461,52 +462,52 @@ async function onMessage(client, cfg, message) { * @param {string} text - Narrative prompt text. */ export async function sendNarrative(client, cfg, channelId, text) { - const logger = client.logger; - try { + const logger = client.logger; + try { // Build the narrative instructions // Expand template for sendNarrative - const now = new Date(); - const date = now.toISOString().split('T')[0]; - const time = now.toTimeString().split(' ')[0]; - const datetime = now.toISOString().replace('T',' ').replace(/\..+$/,''); - const ctx = { - clientId: client.config.id, - userName: client.user.username, - userId: client.user.id, - input: text, - locationName: channel.name, - locationId: channel.id, - date, time, datetime - }; - const raw = `${client.responsesPrompt}\n\nGenerate the following as an engaging narrative:`; - const instructions = expandTemplate(raw, ctx); - const body = { - model: cfg.defaultModel, - instructions, - input: text, - max_output_tokens: cfg.defaultMaxTokens, - temperature: cfg.defaultTemperature - }; - logger.debug(`[sendNarrative] Calling AI with body: ${JSON.stringify(body).slice(0,1000)}`); - const resp = await client.openai.responses.create(body); - logger.info(`[sendNarrative] Received AI response id=${resp.id}`); - // Fetch the target channel or thread - const channel = await client.channels.fetch(channelId); - if (!channel || typeof channel.send !== 'function') { - logger.error(`[sendNarrative] Cannot send to channel ID ${channelId}`); - return; + const now = new Date(); + const date = now.toISOString().split('T')[0]; + const time = now.toTimeString().split(' ')[0]; + const datetime = now.toISOString().replace('T',' ').replace(/\..+$/,''); + const ctx = { + clientId: client.config.id, + userName: client.user.username, + userId: client.user.id, + input: text, + locationName: channel.name, + locationId: channel.id, + date, time, datetime + }; + const raw = `${client.responsesPrompt}\n\nGenerate the following as an engaging narrative:`; + const instructions = expandTemplate(raw, ctx); + const body = { + model: cfg.defaultModel, + instructions, + input: text, + max_output_tokens: cfg.defaultMaxTokens, + temperature: cfg.defaultTemperature + }; + logger.debug(`[sendNarrative] Calling AI with body: ${JSON.stringify(body).slice(0,1000)}`); + const resp = await client.openai.responses.create(body); + logger.info(`[sendNarrative] Received AI response id=${resp.id}`); + // Fetch the target channel or thread + const channel = await client.channels.fetch(channelId); + if (!channel || typeof channel.send !== 'function') { + logger.error(`[sendNarrative] Cannot send to channel ID ${channelId}`); + return; + } + // Split the output and send + const content = resp.output_text?.trim(); + if (content) { + const parts = splitMessage(content, MAX_DISCORD_MSG_LENGTH); + for (const part of parts) { + await channel.send(part); + } + } + } catch (err) { + client.logger.error(`[sendNarrative] Error: ${err.message}`); } - // Split the output and send - const content = resp.output_text?.trim(); - if (content) { - const parts = splitMessage(content, MAX_DISCORD_MSG_LENGTH); - for (const part of parts) { - await channel.send(part); - } - } - } catch (err) { - client.logger.error(`[sendNarrative] Error: ${err.message}`); - } } /** @@ -518,11 +519,11 @@ export async function sendNarrative(client, cfg, channelId, text) { * @param {object} clientConfig - Full client configuration object. */ export async function init(client, clientConfig) { - const cfg = clientConfig.responses; - client.logger.info('[module:responses] Initializing Responses module'); - // Initialize prompt from responsesPrompt module (must be loaded before this) - client.responsesPrompt = client.responsesPrompt ?? ''; - client.openai = new OpenAI({ apiKey: cfg.apiKey }); - client.on('messageCreate', m => onMessage(client, cfg, m)); - client.logger.info('[module:responses] Responses module ready'); + const cfg = clientConfig.responses; + client.logger.info('[module:responses] Initializing Responses module'); + // Initialize prompt from responsesPrompt module (must be loaded before this) + client.responsesPrompt = client.responsesPrompt ?? ''; + client.openai = new OpenAI({ apiKey: cfg.apiKey }); + client.on('messageCreate', m => onMessage(client, cfg, m)); + client.logger.info('[module:responses] Responses module ready'); } diff --git a/_opt/responsesPrompt.js b/_opt/responsesPrompt.js index 3ca215d..60986b1 100644 --- a/_opt/responsesPrompt.js +++ b/_opt/responsesPrompt.js @@ -1,6 +1,8 @@ +import { _fs } from 'fs'; +import { _path } from 'path'; + +import { _MessageFlags } from 'discord-api-types/v10'; import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js'; -import fs from 'fs'; -import path from 'path'; // Placeholder info for template variables const TEMPLATE_KEYS_INFO = 'Available keys: userName, userId, locationName, locationId, date, time, datetime, clientId'; @@ -14,140 +16,140 @@ const MAX_FIELDS = 5; * responses_prompts collection holds all versions; newest record per client is the live prompt. */ export const commands = [ - { - data: new SlashCommandBuilder() - .setName('prompt') - .setDescription('Edit the AI response prompt (current or past version)') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .setDMPermission(false) - .addStringOption(opt => - opt.setName('version') - .setDescription('ID of a past prompt version to load') - .setRequired(false) - .setAutocomplete(true) - ), - async execute(interaction, client) { - const clientId = client.config.id; - const versionId = interaction.options.getString('version'); - // Fetch prompt: live latest or selected historic - let promptText = client.responsesPrompt || ''; - if (versionId) { - try { - const rec = await client.pb.getOne('responses_prompts', versionId); - if (rec?.prompt) promptText = rec.prompt; - } catch (err) { - client.logger.error(`Failed to load prompt version ${versionId}: ${err.message}`); + { + data: new SlashCommandBuilder() + .setName('prompt') + .setDescription('Edit the AI response prompt (current or past version)') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false) + .addStringOption(opt => + opt.setName('version') + .setDescription('ID of a past prompt version to load') + .setRequired(false) + .setAutocomplete(true) + ), + async execute(interaction, client) { + const _clientId = client.config.id; + const versionId = interaction.options.getString('version'); + // Fetch prompt: live latest or selected historic + let promptText = client.responsesPrompt || ''; + if (versionId) { + try { + const rec = await client.pb.getOne('responses_prompts', versionId); + if (rec?.prompt) promptText = rec.prompt; + } catch (err) { + client.logger.error(`Failed to load prompt version ${versionId}: ${err.message}`); + } + } + // Prepare modal fields: one SHORT help, then paragraph chunks + // Help field + const helpField = new TextInputBuilder() + .setCustomId('template_help') + .setLabel('Template variables (no edits)') + .setStyle(TextInputStyle.Short) + .setRequired(false) + // prefill with the list of usable keys + .setValue(TEMPLATE_KEYS_INFO); + const modal = new ModalBuilder() + .setCustomId(`promptModal-${versionId || 'current'}`) + .setTitle('Edit AI Prompt') + .addComponents(new ActionRowBuilder().addComponents(helpField)); + // Prompt chunks + const chunks = []; + for (let off = 0; off < promptText.length && chunks.length < MAX_FIELDS - 1; off += MAX_LEN) { + chunks.push(promptText.slice(off, off + MAX_LEN)); + } + chunks.forEach((text, idx) => { + const input = new TextInputBuilder() + .setCustomId(`prompt_${idx}`) + .setLabel(`Part ${idx + 1}`) + .setStyle(TextInputStyle.Paragraph) + .setRequired(idx === 0) + .setMaxLength(MAX_LEN) + .setValue(text); + modal.addComponents(new ActionRowBuilder().addComponents(input)); + }); + // Empty fields to fill out to MAX_FIELDS + for (let i = chunks.length; i < MAX_FIELDS - 1; i++) { + modal.addComponents(new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId(`prompt_${i}`) + .setLabel(`Part ${i + 1}`) + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + .setMaxLength(MAX_LEN) + )); + } + await interaction.showModal(modal); } - } - // Prepare modal fields: one SHORT help, then paragraph chunks - // Help field - const helpField = new TextInputBuilder() - .setCustomId('template_help') - .setLabel('Template variables (no edits)') - .setStyle(TextInputStyle.Short) - .setRequired(false) - // prefill with the list of usable keys - .setValue(TEMPLATE_KEYS_INFO); - const modal = new ModalBuilder() - .setCustomId(`promptModal-${versionId || 'current'}`) - .setTitle('Edit AI Prompt') - .addComponents(new ActionRowBuilder().addComponents(helpField)); - // Prompt chunks - const chunks = []; - for (let off = 0; off < promptText.length && chunks.length < MAX_FIELDS - 1; off += MAX_LEN) { - chunks.push(promptText.slice(off, off + MAX_LEN)); - } - chunks.forEach((text, idx) => { - const input = new TextInputBuilder() - .setCustomId(`prompt_${idx}`) - .setLabel(`Part ${idx + 1}`) - .setStyle(TextInputStyle.Paragraph) - .setRequired(idx === 0) - .setMaxLength(MAX_LEN) - .setValue(text); - modal.addComponents(new ActionRowBuilder().addComponents(input)); - }); - // Empty fields to fill out to MAX_FIELDS - for (let i = chunks.length; i < MAX_FIELDS - 1; i++) { - modal.addComponents(new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId(`prompt_${i}`) - .setLabel(`Part ${i + 1}`) - .setStyle(TextInputStyle.Paragraph) - .setRequired(false) - .setMaxLength(MAX_LEN) - )); - } - await interaction.showModal(modal); } - } ]; // Store clients for event hooks const _clients = []; export async function init(client, clientConfig) { - const clientId = clientConfig.id; - client.logger.info('[module:responsesPrompt] initialized'); - // Load live prompt (latest version) - try { - const { items } = await client.pb.collection('responses_prompts') - .getList(1, 1, { filter: `clientId="${clientId}"`, sort: '-created' }); - client.responsesPrompt = items[0]?.prompt || ''; - } catch (err) { - client.logger.error(`Error loading current prompt: ${err.message}`); - client.responsesPrompt = ''; - } - _clients.push({ client, clientConfig }); - // Autocomplete versions - client.on('interactionCreate', async interaction => { - if (!interaction.isAutocomplete() || interaction.commandName !== 'prompt') return; - const focused = interaction.options.getFocused(true); - if (focused.name === 'version') { - try { + const _clientId = client.config.id; + client.logger.info('[module:responsesPrompt] initialized'); + // Load live prompt (latest version) + try { const { items } = await client.pb.collection('responses_prompts') - .getList(1, 25, { filter: `clientId="${clientId}"`, sort: '-created' }); - const choices = items.map(r => ({ name: new Date(r.created).toLocaleString(), value: r.id })); - await interaction.respond(choices); - } catch (err) { - client.logger.error(`Prompt autocomplete error: ${err.message}`); - await interaction.respond([]); - } - } - }); - // Modal submission: save new version & prune old - client.on('interactionCreate', async interaction => { - if (!interaction.isModalSubmit()) return; - const id = interaction.customId; - if (!id.startsWith('promptModal-')) return; - const parts = []; - for (let i = 0; i < MAX_FIELDS; i++) { - try { - const v = interaction.fields.getTextInputValue(`prompt_${i}`) || ''; - if (v.trim()) parts.push(v); - } catch {} - } - const newPrompt = parts.join('\n'); - // Persist new version - let newRec; - try { - newRec = await client.pb.createOne('responses_prompts', { clientId, prompt: newPrompt, updatedBy: interaction.user.id }); - client.responsesPrompt = newPrompt; + .getList(1, 1, { filter: `clientId="${_clientId}"`, sort: '-created' }); + client.responsesPrompt = items[0]?.prompt || ''; } catch (err) { - client.logger.error(`Failed to save prompt: ${err.message}`); - return interaction.reply({ content: `Error saving prompt: ${err.message}`, ephemeral: true }); + client.logger.error(`Error loading current prompt: ${err.message}`); + client.responsesPrompt = ''; } - // Prune older versions beyond the 10 most recent - try { - const { items } = await client.pb.collection('responses_prompts') - .getList(1, 100, { filter: `clientId="${clientId}"`, sort: '-created' }); - const toDelete = items.map(r => r.id).slice(10); - for (const id of toDelete) { - await client.pb.deleteOne('responses_prompts', id); - } - } catch (err) { - client.logger.error(`Failed to prune old prompts: ${err.message}`); - } - await interaction.reply({ content: 'Prompt saved!', ephemeral: true }); - }); -} \ No newline at end of file + _clients.push({ client, clientConfig }); + // Autocomplete versions + client.on('interactionCreate', async interaction => { + if (!interaction.isAutocomplete() || interaction.commandName !== 'prompt') return; + const focused = interaction.options.getFocused(true); + if (focused.name === 'version') { + try { + const { items } = await client.pb.collection('responses_prompts') + .getList(1, 25, { filter: `clientId="${_clientId}"`, sort: '-created' }); + const choices = items.map(r => ({ name: new Date(r.created).toLocaleString(), value: r.id })); + await interaction.respond(choices); + } catch (err) { + client.logger.error(`Prompt autocomplete error: ${err.message}`); + await interaction.respond([]); + } + } + }); + // Modal submission: save new version & prune old + client.on('interactionCreate', async interaction => { + if (!interaction.isModalSubmit()) return; + const id = interaction.customId; + if (!id.startsWith('promptModal-')) return; + const parts = []; + for (let i = 0; i < MAX_FIELDS; i++) { + try { + const v = interaction.fields.getTextInputValue(`prompt_${i}`) || ''; + if (v.trim()) parts.push(v); + } catch {} + } + const newPrompt = parts.join('\n'); + // Persist new version + let _newRec; + try { + _newRec = await client.pb.createOne('responses_prompts', { clientId: _clientId, prompt: newPrompt, updatedBy: interaction.user.id }); + client.responsesPrompt = newPrompt; + } catch (err) { + client.logger.error(`Failed to save prompt: ${err.message}`); + return interaction.reply({ content: `Error saving prompt: ${err.message}`, ephemeral: true }); + } + // Prune older versions beyond the 10 most recent + try { + const { items } = await client.pb.collection('responses_prompts') + .getList(1, 100, { filter: `clientId="${_clientId}"`, sort: '-created' }); + const toDelete = items.map(r => r.id).slice(10); + for (const id of toDelete) { + await client.pb.deleteOne('responses_prompts', id); + } + } catch (err) { + client.logger.error(`Failed to prune old prompts: ${err.message}`); + } + await interaction.reply({ content: 'Prompt saved!', ephemeral: true }); + }); +} diff --git a/_opt/responsesQuery.js b/_opt/responsesQuery.js index 6c4a364..a66385c 100644 --- a/_opt/responsesQuery.js +++ b/_opt/responsesQuery.js @@ -1,3 +1,7 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import axios from 'axios'; import { MessageFlags } from 'discord-api-types/v10'; /** * Slash command module for '/query'. @@ -5,10 +9,8 @@ import { MessageFlags } from 'discord-api-types/v10'; * including optional image generation function calls. */ import { SlashCommandBuilder, AttachmentBuilder, PermissionFlagsBits } from 'discord.js'; + import { expandTemplate } from '../_src/template.js'; -import fs from 'fs/promises'; -import path from 'path'; -import axios from 'axios'; /** * Split long text into chunks safe for Discord messaging. @@ -17,19 +19,19 @@ import axios from 'axios'; * @returns {string[]} Array of message chunks. */ function splitLongMessage(text, max = 2000) { - const lines = text.split('\n'); - const chunks = []; - let chunk = ''; - for (const line of lines) { - const next = line + '\n'; - if (chunk.length + next.length > max) { - chunks.push(chunk); - chunk = ''; + const lines = text.split('\n'); + const chunks = []; + let chunk = ''; + for (const line of lines) { + const next = line + '\n'; + if (chunk.length + next.length > max) { + chunks.push(chunk); + chunk = ''; + } + chunk += next; } - chunk += next; - } - if (chunk) chunks.push(chunk); - return chunks; + if (chunk) chunks.push(chunk); + return chunks; } /** @@ -43,112 +45,112 @@ function splitLongMessage(text, max = 2000) { * @returns {Promise} True if a function call was handled. */ async function handleImageInteraction(client, interaction, resp, cfg, ephemeral) { - const calls = Array.isArray(resp.output) ? resp.output : []; - const fn = calls.find(o => o.type === 'function_call' && o.name === 'generate_image'); - if (!fn?.arguments) return false; - client.logger.debug(`Image function args: ${fn.arguments}`); - let args; - try { args = JSON.parse(fn.arguments); } catch (e) { return false; } - if (!args.prompt?.trim()) { - await interaction.editReply({ content: 'Cannot generate image: empty prompt.', ephemeral }); - return true; - } - // Always use image model defined in config - const model = cfg.imageGeneration.defaultModel; - const promptText = args.prompt; - // Determine number of images (1-10); DALL·E-3 only supports 1 - let count = 1; - if (args.n != null) { - const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10); - if (!Number.isNaN(nVal)) count = nVal; - } - // clamp - count = Math.max(1, Math.min(10, count)); - if (model === 'dall-e-3') count = 1; - const size = args.size || 'auto'; - // Determine quality based on config and model constraints - let quality = args.quality || cfg.imageGeneration.defaultQuality; - if (model === 'gpt-image-1') { - if (!['low', 'medium', 'high', 'auto'].includes(quality)) quality = 'auto'; - } else if (model === 'dall-e-2') { - quality = 'standard'; - } else if (model === 'dall-e-3') { - if (!['standard', 'hd', 'auto'].includes(quality)) quality = 'standard'; - } - const background = args.background; - const moderation = args.moderation; - const outputFormat = args.output_format; - const compression = args.output_compression; - const style = args.style; - const user = args.user || interaction.user.id; - try { - // Build generate parameters - const genParams = { model, prompt: promptText, n: count, size, quality, user }; - // response_format supported for DALL·E models (not gpt-image-1) - if (model !== 'gpt-image-1' && args.response_format) { - genParams['response_format'] = args.response_format; + const calls = Array.isArray(resp.output) ? resp.output : []; + const fn = calls.find(o => o.type === 'function_call' && o.name === 'generate_image'); + if (!fn?.arguments) return false; + client.logger.debug(`Image function args: ${fn.arguments}`); + let args; + try { args = JSON.parse(fn.arguments); } catch (e) { return false; } + if (!args.prompt?.trim()) { + await interaction.editReply({ content: 'Cannot generate image: empty prompt.', ephemeral }); + return true; } - // gpt-image-1 supports background, moderation, output_format, and output_compression + // Always use image model defined in config + const model = cfg.imageGeneration.defaultModel; + const promptText = args.prompt; + // Determine number of images (1-10); DALL·E-3 only supports 1 + let count = 1; + if (args.n !== null) { + const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10); + if (!Number.isNaN(nVal)) count = nVal; + } + // clamp + count = Math.max(1, Math.min(10, count)); + if (model === 'dall-e-3') count = 1; + const size = args.size || 'auto'; + // Determine quality based on config and model constraints + let quality = args.quality || cfg.imageGeneration.defaultQuality; if (model === 'gpt-image-1') { - if (background) genParams['background'] = background; - if (moderation) genParams['moderation'] = moderation; - if (outputFormat) { - genParams['output_format'] = outputFormat; - // only support compression for JPEG or WEBP formats - if (['jpeg','webp'].includes(outputFormat) && typeof compression === 'number') { - genParams['output_compression'] = compression; + if (!['low', 'medium', 'high', 'auto'].includes(quality)) quality = 'auto'; + } else if (model === 'dall-e-2') { + quality = 'standard'; + } else if (model === 'dall-e-3') { + if (!['standard', 'hd', 'auto'].includes(quality)) quality = 'standard'; + } + const background = args.background; + const moderation = args.moderation; + const outputFormat = args.output_format; + const compression = args.output_compression; + const style = args.style; + const user = args.user || interaction.user.id; + try { + // Build generate parameters + const genParams = { model, prompt: promptText, n: count, size, quality, user }; + // response_format supported for DALL·E models (not gpt-image-1) + if (model !== 'gpt-image-1' && args.response_format) { + genParams['response_format'] = args.response_format; } - } + // gpt-image-1 supports background, moderation, output_format, and output_compression + if (model === 'gpt-image-1') { + if (background) genParams['background'] = background; + if (moderation) genParams['moderation'] = moderation; + if (outputFormat) { + genParams['output_format'] = outputFormat; + // only support compression for JPEG or WEBP formats + if (['jpeg','webp'].includes(outputFormat) && typeof compression === 'number') { + genParams['output_compression'] = compression; + } + } + } + // dall-e-3 supports style + if (model === 'dall-e-3' && style) { + genParams['style'] = style; + } + // Generate images via OpenAI Images API + const imgRes = await client.openai.images.generate(genParams); + const images = imgRes.data || []; + if (!images.length) throw new Error('No images generated'); + // Ensure save directory exists + const dir = cfg.imageGeneration?.imageSavePath || './images'; + await fs.mkdir(dir, { recursive: true }); + const attachments = []; + const outputs = []; + // Process each generated image + for (let i = 0; i < images.length; i++) { + const img = images[i]; + let buffer, ext = outputFormat || 'png'; + if (img.b64_json) { + buffer = Buffer.from(img.b64_json, 'base64'); + outputs.push({ b64_json: img.b64_json }); + } else if (img.url) { + const dl = await axios.get(img.url, { responseType: 'arraybuffer' }); + buffer = Buffer.from(dl.data); + const parsed = path.extname(img.url.split('?')[0]).replace(/^[.]/, ''); + if (parsed) ext = parsed; + outputs.push({ url: img.url }); + } else { + throw new Error('No image data'); + } + const filename = `${interaction.user.id}-${Date.now()}-${i}.${ext}`; + const filePath = path.join(dir, filename); + await fs.writeFile(filePath, buffer); + client.logger.info(`Saved image: ${filePath}`); + attachments.push(new AttachmentBuilder(buffer, { name: filename })); + } + // Award output points based on token usage for image generation + const tokens = imgRes.usage?.total_tokens ?? count; + if (client.scorekeeper && tokens > 0) { + client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, tokens, 'image_generation') + .catch(err => client.logger.error(`Scorekeeper error: ${err.message}`)); + } + // Reply with attachments + await interaction.editReply({ content: promptText, files: attachments }); + return true; + } catch (err) { + client.logger.error(`Image generation error: ${err.message}`); + await interaction.editReply({ content: `Image generation error: ${err.message}`, ephemeral }); + return true; } - // dall-e-3 supports style - if (model === 'dall-e-3' && style) { - genParams['style'] = style; - } - // Generate images via OpenAI Images API - const imgRes = await client.openai.images.generate(genParams); - const images = imgRes.data || []; - if (!images.length) throw new Error('No images generated'); - // Ensure save directory exists - const dir = cfg.imageGeneration?.imageSavePath || './images'; - await fs.mkdir(dir, { recursive: true }); - const attachments = []; - const outputs = []; - // Process each generated image - for (let i = 0; i < images.length; i++) { - const img = images[i]; - let buffer, ext = outputFormat || 'png'; - if (img.b64_json) { - buffer = Buffer.from(img.b64_json, 'base64'); - outputs.push({ b64_json: img.b64_json }); - } else if (img.url) { - const dl = await axios.get(img.url, { responseType: 'arraybuffer' }); - buffer = Buffer.from(dl.data); - const parsed = path.extname(img.url.split('?')[0]).replace(/^[.]/, ''); - if (parsed) ext = parsed; - outputs.push({ url: img.url }); - } else { - throw new Error('No image data'); - } - const filename = `${interaction.user.id}-${Date.now()}-${i}.${ext}`; - const filePath = path.join(dir, filename); - await fs.writeFile(filePath, buffer); - client.logger.info(`Saved image: ${filePath}`); - attachments.push(new AttachmentBuilder(buffer, { name: filename })); - } - // Award output points based on token usage for image generation - const tokens = imgRes.usage?.total_tokens ?? count; - if (client.scorekeeper && tokens > 0) { - client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, tokens, 'image_generation') - .catch(err => client.logger.error(`Scorekeeper error: ${err.message}`)); - } - // Reply with attachments - await interaction.editReply({ content: promptText, files: attachments }); - return true; - } catch (err) { - client.logger.error(`Image generation error: ${err.message}`); - await interaction.editReply({ content: `Image generation error: ${err.message}`, ephemeral }); - return true; - } } /** @@ -161,198 +163,198 @@ async function handleImageInteraction(client, interaction, resp, cfg, ephemeral) * Slash command definitions and handlers for the '/query' command. */ export const commands = [ - { - data: new SlashCommandBuilder() - .setName('query') - .setDescription('Send a custom AI query') - .addStringOption(opt => - opt.setName('prompt') - .setDescription('Your query text') - .setRequired(true) - ) - .addBooleanOption(opt => - opt.setName('ephemeral') - .setDescription('Receive an ephemeral response') - .setRequired(false) - ), - async execute(interaction, client) { - const cfg = client.config.responses; - // Enforce minimum score to use /query if scorekeeper is enabled - if (client.scorekeeper) { - try { - const isAdmin = interaction.member?.permissions?.has(PermissionFlagsBits.Administrator); - const scoreData = await client.scorekeeper.getScore(interaction.guildId, interaction.user.id); - if (!isAdmin && scoreData.totalScore < cfg.minScore) { - return interaction.reply({ - content: `You need an I/O score of at least ${cfg.minScore} to use /query. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.`, - ephemeral: true - }); - } - } catch (err) { - client.logger.error(`[cmd:query] Error checking score: ${err.message}`); - return interaction.reply({ content: 'Error verifying your score. Please try again later.', flags: MessageFlags.Ephemeral}); - } - } - const prompt = interaction.options.getString('prompt'); - const flag = interaction.options.getBoolean('ephemeral'); - client.logger.info(`[cmd:query] Prompt received from ${interaction.user.id}, length=${prompt.length}`); - const ephemeral = flag !== null ? flag : true; - await interaction.deferReply({ ephemeral }); + { + data: new SlashCommandBuilder() + .setName('query') + .setDescription('Send a custom AI query') + .addStringOption(opt => + opt.setName('prompt') + .setDescription('Your query text') + .setRequired(true) + ) + .addBooleanOption(opt => + opt.setName('ephemeral') + .setDescription('Receive an ephemeral response') + .setRequired(false) + ), + async execute(interaction, client) { + const cfg = client.config.responses; + // Enforce minimum score to use /query if scorekeeper is enabled + if (client.scorekeeper) { + try { + const isAdmin = interaction.member?.permissions?.has(PermissionFlagsBits.Administrator); + const scoreData = await client.scorekeeper.getScore(interaction.guildId, interaction.user.id); + if (!isAdmin && scoreData.totalScore < cfg.minScore) { + return interaction.reply({ + content: `You need an I/O score of at least ${cfg.minScore} to use /query. Your current I/O score is ${scoreData.totalScore.toFixed(2)}.`, + ephemeral: true + }); + } + } catch (err) { + client.logger.error(`[cmd:query] Error checking score: ${err.message}`); + return interaction.reply({ content: 'Error verifying your score. Please try again later.', flags: MessageFlags.Ephemeral }); + } + } + const prompt = interaction.options.getString('prompt'); + const flag = interaction.options.getBoolean('ephemeral'); + client.logger.info(`[cmd:query] Prompt received from ${interaction.user.id}, length=${prompt.length}`); + const ephemeral = flag !== null ? flag : true; + await interaction.deferReply({ ephemeral }); - // Determine channel/thread key for context - const key = interaction.channelId; - // Initialize per-channel lock map - const lockMap = client._responseLockMap || (client._responseLockMap = new Map()); - // Get last pending promise for this key - const last = lockMap.get(key) || Promise.resolve(); - // Handler to run in sequence - const handler = async () => { - // Kick off a repeated typing indicator during processing - const typingInterval = setInterval(() => interaction.channel.sendTyping().catch(() => {}), 9000); - // initial typing - interaction.channel.sendTyping().catch(() => {}); - // Read previous response ID - const previous = client.pb?.cache?.get(key); - // Build request body - // Expand template for query - const now = new Date(); - const date = now.toISOString().split('T')[0]; - const time = now.toTimeString().split(' ')[0]; - const datetime = now.toISOString().replace('T',' ').replace(/\..+$/,''); - const channel = await client.channels.fetch(interaction.channelId); - const locationName = channel.name; - const locationId = channel.id; - const ctx = { - clientId: client.config.id, - userName: interaction.user.username, - userId: interaction.user.id, - userTag: interaction.user.tag, - // add guild context - guildName: interaction.guild?.name || '', - guildId: interaction.guild?.id || '', - input: prompt, - locationName, locationId, - date, time, datetime - }; - const instructions = expandTemplate(client.responsesPrompt, ctx); - const body = { - model: cfg.defaultModel, - instructions, - input: prompt, - previous_response_id: previous, - max_output_tokens: cfg.defaultMaxTokens, - temperature: cfg.defaultTemperature - }; - // Assemble enabled tools - const tools = []; - if (cfg.tools?.imageGeneration) { - const model = cfg.imageGeneration.defaultModel; - // Configure allowed sizes per model - let sizeEnum; - switch (model) { - case 'gpt-image-1': sizeEnum = ['auto','1024x1024','1536x1024','1024x1536']; break; - case 'dall-e-2': sizeEnum = ['256x256','512x512','1024x1024']; break; - case 'dall-e-3': sizeEnum = ['auto','1024x1024','1792x1024','1024x1792']; break; - default: sizeEnum = ['auto','1024x1024']; - } - // Configure quality options per model - let qualityEnum; - switch (model) { - case 'gpt-image-1': qualityEnum = ['auto','low','medium','high']; break; - case 'dall-e-2': qualityEnum = ['standard']; break; - case 'dall-e-3': qualityEnum = ['auto','standard','hd']; break; - default: qualityEnum = ['auto','standard']; - } - // Build schema properties dynamically - const properties = { - prompt: { type: 'string', description: 'Text description of desired image(s).' }, - n: { type: 'number', description: 'Number of images to generate.' }, - size: { type: 'string', enum: sizeEnum, description: 'Image size.' }, - quality: { type: 'string', enum: qualityEnum, description: 'Image quality.' }, - user: { type: 'string', description: 'Unique end-user identifier.' } - }; - if (model !== 'gpt-image-1') { - properties.response_format = { type: 'string', enum: ['url','b64_json'], description: 'Format of returned images.' }; - } - if (model === 'gpt-image-1') { - properties.background = { type: 'string', enum: ['transparent','opaque','auto'], description: 'Background transparency.' }; - properties.moderation = { type: 'string', enum: ['low','auto'], description: 'Content moderation level.' }; - properties.output_format = { type: 'string', enum: ['png','jpeg','webp'], description: 'Output image format.' }; - properties.output_compression = { type: 'number', description: 'Compression level (0-100).' }; - } - if (model === 'dall-e-3') { - properties.style = { type: 'string', enum: ['vivid','natural'], description: 'Style option for dall-e-3.' }; - } - // Determine required fields - const required = ['prompt','n','size','quality','user']; - if (model !== 'gpt-image-1') required.push('response_format'); - if (model === 'gpt-image-1') required.push('background','moderation','output_format','output_compression'); - if (model === 'dall-e-3') required.push('style'); - tools.push({ - type: 'function', - name: 'generate_image', - description: `Generate images using model ${model} with requested parameters.`, - parameters: { - type: 'object', - properties, - required, - additionalProperties: false - }, - strict: true, - }); - } - if (cfg.tools?.webSearch) { - tools.push({ type: 'web_search_preview' }); - } - if (tools.length) body.tools = tools; + // Determine channel/thread key for context + const key = interaction.channelId; + // Initialize per-channel lock map + const lockMap = client._responseLockMap || (client._responseLockMap = new Map()); + // Get last pending promise for this key + const last = lockMap.get(key) || Promise.resolve(); + // Handler to run in sequence + const handler = async () => { + // Kick off a repeated typing indicator during processing + const typingInterval = setInterval(() => interaction.channel.sendTyping().catch(() => {}), 9000); + // initial typing + interaction.channel.sendTyping().catch(() => {}); + // Read previous response ID + const previous = client.pb?.cache?.get(key); + // Build request body + // Expand template for query + const now = new Date(); + const date = now.toISOString().split('T')[0]; + const time = now.toTimeString().split(' ')[0]; + const datetime = now.toISOString().replace('T',' ').replace(/\..+$/,''); + const channel = await client.channels.fetch(interaction.channelId); + const locationName = channel.name; + const locationId = channel.id; + const ctx = { + clientId: client.config.id, + userName: interaction.user.username, + userId: interaction.user.id, + userTag: interaction.user.tag, + // add guild context + guildName: interaction.guild?.name || '', + guildId: interaction.guild?.id || '', + input: prompt, + locationName, locationId, + date, time, datetime + }; + const instructions = expandTemplate(client.responsesPrompt, ctx); + const body = { + model: cfg.defaultModel, + instructions, + input: prompt, + previous_response_id: previous, + max_output_tokens: cfg.defaultMaxTokens, + temperature: cfg.defaultTemperature + }; + // Assemble enabled tools + const tools = []; + if (cfg.tools?.imageGeneration) { + const model = cfg.imageGeneration.defaultModel; + // Configure allowed sizes per model + let sizeEnum; + switch (model) { + case 'gpt-image-1': sizeEnum = ['auto','1024x1024','1536x1024','1024x1536']; break; + case 'dall-e-2': sizeEnum = ['256x256','512x512','1024x1024']; break; + case 'dall-e-3': sizeEnum = ['auto','1024x1024','1792x1024','1024x1792']; break; + default: sizeEnum = ['auto','1024x1024']; + } + // Configure quality options per model + let qualityEnum; + switch (model) { + case 'gpt-image-1': qualityEnum = ['auto','low','medium','high']; break; + case 'dall-e-2': qualityEnum = ['standard']; break; + case 'dall-e-3': qualityEnum = ['auto','standard','hd']; break; + default: qualityEnum = ['auto','standard']; + } + // Build schema properties dynamically + const properties = { + prompt: { type: 'string', description: 'Text description of desired image(s).' }, + n: { type: 'number', description: 'Number of images to generate.' }, + size: { type: 'string', enum: sizeEnum, description: 'Image size.' }, + quality: { type: 'string', enum: qualityEnum, description: 'Image quality.' }, + user: { type: 'string', description: 'Unique end-user identifier.' } + }; + if (model !== 'gpt-image-1') { + properties.response_format = { type: 'string', enum: ['url','b64_json'], description: 'Format of returned images.' }; + } + if (model === 'gpt-image-1') { + properties.background = { type: 'string', enum: ['transparent','opaque','auto'], description: 'Background transparency.' }; + properties.moderation = { type: 'string', enum: ['low','auto'], description: 'Content moderation level.' }; + properties.output_format = { type: 'string', enum: ['png','jpeg','webp'], description: 'Output image format.' }; + properties.output_compression = { type: 'number', description: 'Compression level (0-100).' }; + } + if (model === 'dall-e-3') { + properties.style = { type: 'string', enum: ['vivid','natural'], description: 'Style option for dall-e-3.' }; + } + // Determine required fields + const required = ['prompt','n','size','quality','user']; + if (model !== 'gpt-image-1') required.push('response_format'); + if (model === 'gpt-image-1') required.push('background','moderation','output_format','output_compression'); + if (model === 'dall-e-3') required.push('style'); + tools.push({ + type: 'function', + name: 'generate_image', + description: `Generate images using model ${model} with requested parameters.`, + parameters: { + type: 'object', + properties, + required, + additionalProperties: false + }, + strict: true + }); + } + if (cfg.tools?.webSearch) { + tools.push({ type: 'web_search_preview' }); + } + if (tools.length) body.tools = tools; - // Call AI - let resp; - try { - resp = await client.openai.responses.create(body); - // Award output tokens - const tokens = resp.usage?.total_tokens ?? resp.usage?.completion_tokens ?? 0; - if (client.scorekeeper && tokens > 0) { - client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, tokens, 'AI_query') - .catch(e => client.logger.error(`Scorekeeper error: ${e.message}`)); - } - } catch (err) { - client.logger.error(`AI error in /query: ${err.message}`); - clearInterval(typingInterval); - return interaction.editReply({ content: 'Error generating response.', ephemeral }); - } + // Call AI + let resp; + try { + resp = await client.openai.responses.create(body); + // Award output tokens + const tokens = resp.usage?.total_tokens ?? resp.usage?.completion_tokens ?? 0; + if (client.scorekeeper && tokens > 0) { + client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, tokens, 'AI_query') + .catch(e => client.logger.error(`Scorekeeper error: ${e.message}`)); + } + } catch (err) { + client.logger.error(`AI error in /query: ${err.message}`); + clearInterval(typingInterval); + return interaction.editReply({ content: 'Error generating response.', ephemeral }); + } - // Cache response ID if not a function call - const isFuncCall = Array.isArray(resp.output) && resp.output.some(o => o.type === 'function_call'); - if (!isFuncCall && resp.id && cfg.conversationExpiry) { - client.pb?.cache?.set(key, resp.id, Math.floor(cfg.conversationExpiry / 1000)); - } + // Cache response ID if not a function call + const isFuncCall = Array.isArray(resp.output) && resp.output.some(o => o.type === 'function_call'); + if (!isFuncCall && resp.id && cfg.conversationExpiry) { + client.pb?.cache?.set(key, resp.id, Math.floor(cfg.conversationExpiry / 1000)); + } - // Handle image function call if present - if (await handleImageInteraction(client, interaction, resp, cfg, ephemeral)) { - clearInterval(typingInterval); - return; + // Handle image function call if present + if (await handleImageInteraction(client, interaction, resp, cfg, ephemeral)) { + clearInterval(typingInterval); + return; + } + // Send text reply chunks + const text = resp.output_text?.trim() || ''; + if (!text) { + clearInterval(typingInterval); + return interaction.editReply({ content: 'No response generated.', ephemeral }); + } + const chunks = splitLongMessage(text, 2000); + for (let i = 0; i < chunks.length; i++) { + if (i === 0) { + await interaction.editReply({ content: chunks[i] }); + } else { + await interaction.followUp({ content: chunks[i], ephemeral }); + } + } + clearInterval(typingInterval); + }; + // Chain handler after last and await + const next = last.then(handler).catch(err => client.logger.error(`Queued /query error for ${key}: ${err.message}`)); + lockMap.set(key, next); + await next; } - // Send text reply chunks - const text = resp.output_text?.trim() || ''; - if (!text) { - clearInterval(typingInterval); - return interaction.editReply({ content: 'No response generated.', ephemeral }); - } - const chunks = splitLongMessage(text, 2000); - for (let i = 0; i < chunks.length; i++) { - if (i === 0) { - await interaction.editReply({ content: chunks[i] }); - } else { - await interaction.followUp({ content: chunks[i], ephemeral }); - } - } - clearInterval(typingInterval); - }; - // Chain handler after last and await - const next = last.then(handler).catch(err => client.logger.error(`Queued /query error for ${key}: ${err.message}`)); - lockMap.set(key, next); - await next; } - } -]; \ No newline at end of file +]; diff --git a/_opt/responsesRandomizer.js b/_opt/responsesRandomizer.js index 85a97e5..13cbf39 100644 --- a/_opt/responsesRandomizer.js +++ b/_opt/responsesRandomizer.js @@ -12,26 +12,26 @@ import { sendNarrative } from './responses.js'; * @param {object} clientConfig - Full client configuration object. */ export async function init(client, clientConfig) { - const cfg = clientConfig.responsesRandomizer; - const chance = Number(cfg.chance); - if (isNaN(chance) || chance <= 0) { - client.logger.warn(`[module:responsesRandomizer] Invalid chance value: ${cfg.chance}. Module disabled.`); - return; - } - client.logger.info(`[module:responsesRandomizer] Enabled with chance=${chance}`); - - client.on('messageCreate', async (message) => { - try { - // Skip bot messages or non-guild messages - if (message.author.bot || !message.guild) return; - const content = message.content?.trim(); - if (!content) return; - // Roll the dice - if (Math.random() > chance) return; - // Generate and send narrative - await sendNarrative(client, clientConfig.responses, message.channel.id, content); - } catch (err) { - client.logger.error(`[module:responsesRandomizer] Error processing message: ${err.message}`); + const cfg = clientConfig.responsesRandomizer; + const chance = Number(cfg.chance); + if (isNaN(chance) || chance <= 0) { + client.logger.warn(`[module:responsesRandomizer] Invalid chance value: ${cfg.chance}. Module disabled.`); + return; } - }); + client.logger.info(`[module:responsesRandomizer] Enabled with chance=${chance}`); + + client.on('messageCreate', async (message) => { + try { + // Skip bot messages or non-guild messages + if (message.author.bot || !message.guild) return; + const content = message.content?.trim(); + if (!content) return; + // Roll the dice + if (Math.random() > chance) return; + // Generate and send narrative + await sendNarrative(client, clientConfig.responses, message.channel.id, content); + } catch (err) { + client.logger.error(`[module:responsesRandomizer] Error processing message: ${err.message}`); + } + }); } diff --git a/_opt/scExecHangarStatus.js b/_opt/scExecHangarStatus.js index c6d17f2..df521ff 100644 --- a/_opt/scExecHangarStatus.js +++ b/_opt/scExecHangarStatus.js @@ -1,364 +1,364 @@ -import { MessageFlags } from 'discord-api-types/v10'; +import { _MessageFlags } from 'discord-api-types/v10'; // _opt/schangar.js import { SlashCommandBuilder } from 'discord.js'; // Export commands array for the centralized handler export const commands = [ - { - data: new SlashCommandBuilder() - .setName('hangarsync') - .setDescription('Mark the moment all five lights turn green, for use with hangarstatus') - .addStringOption(option => - option.setName('timestamp') - .setDescription('Custom timestamp (Unix time in seconds or ISO 8601 format). Leave empty for current time.') - .setRequired(false)), + { + data: new SlashCommandBuilder() + .setName('hangarsync') + .setDescription('Mark the moment all five lights turn green, for use with hangarstatus') + .addStringOption(option => + option.setName('timestamp') + .setDescription('Custom timestamp (Unix time in seconds or ISO 8601 format). Leave empty for current time.') + .setRequired(false)), - execute: async (interaction, client) => { - const customTimestamp = interaction.options.getString('timestamp'); - let syncEpoch; + execute: async (interaction, client) => { + const customTimestamp = interaction.options.getString('timestamp'); + let syncEpoch; - // Attempt to validate custom timestamp - if (customTimestamp) { - try { - if (/^\d+$/.test(customTimestamp)) { - const timestampInSeconds = parseInt(customTimestamp); - if (timestampInSeconds < 0 || timestampInSeconds > Math.floor(Date.now() / 1000)) { - return interaction.reply({ - content: 'Invalid timestamp. Please provide a Unix time in seconds that is not in the future.', - ephemeral: true - }); - } - syncEpoch = timestampInSeconds * 1000; - } else { - const date = new Date(customTimestamp); - syncEpoch = date.getTime(); - if (isNaN(syncEpoch) || syncEpoch < 0) { - return interaction.reply({ - content: 'Invalid timestamp format. Please use Unix time in seconds or a valid ISO 8601 string.', - ephemeral: true - }); - } - } - } catch (error) { - client.logger.error(`[cmd:hangarsync] Failed to parse timestamp: ${error.message}`); - return interaction.reply({ - content: 'Failed to parse timestamp. Please use Unix time in seconds or a valid ISO 8601 string.', - ephemeral: true - }); - } - } else { - syncEpoch = Date.now(); - } + // Attempt to validate custom timestamp + if (customTimestamp) { + try { + if (/^\d+$/.test(customTimestamp)) { + const timestampInSeconds = parseInt(customTimestamp); + if (timestampInSeconds < 0 || timestampInSeconds > Math.floor(Date.now() / 1000)) { + return interaction.reply({ + content: 'Invalid timestamp. Please provide a Unix time in seconds that is not in the future.', + ephemeral: true + }); + } + syncEpoch = timestampInSeconds * 1000; + } else { + const date = new Date(customTimestamp); + syncEpoch = date.getTime(); + if (isNaN(syncEpoch) || syncEpoch < 0) { + return interaction.reply({ + content: 'Invalid timestamp format. Please use Unix time in seconds or a valid ISO 8601 string.', + ephemeral: true + }); + } + } + } catch (error) { + client.logger.error(`[cmd:hangarsync] Failed to parse timestamp: ${error.message}`); + return interaction.reply({ + content: 'Failed to parse timestamp. Please use Unix time in seconds or a valid ISO 8601 string.', + ephemeral: true + }); + } + } else { + syncEpoch = Date.now(); + } - // Check PocketBase connection status - if (!isPocketBaseConnected(client)) { + // Check PocketBase connection status + if (!isPocketBaseConnected(client)) { client.logger.error('[cmd:hangarsync] PocketBase not connected'); - // Try to reconnect if available - if (typeof client.pb.ensureConnection === 'function') { - await client.pb.ensureConnection(); + // Try to reconnect if available + if (typeof client.pb.ensureConnection === 'function') { + await client.pb.ensureConnection(); - // Check if reconnection worked - if (!isPocketBaseConnected(client)) { - return interaction.reply({ - content: 'Database connection unavailable. Please try again later.', - ephemeral: true - }); - } - } else { - return interaction.reply({ - content: 'Database connection unavailable. Please try again later.', - ephemeral: true - }); - } - } + // Check if reconnection worked + if (!isPocketBaseConnected(client)) { + return interaction.reply({ + content: 'Database connection unavailable. Please try again later.', + ephemeral: true + }); + } + } else { + return interaction.reply({ + content: 'Database connection unavailable. Please try again later.', + ephemeral: true + }); + } + } - // Create or update timestamp for guild - try { - let record = null; + // Create or update timestamp for guild + try { + let record = null; - try { - // First try the enhanced method if available - if (typeof client.pb.getFirst === 'function') { - record = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`); - } else { - // Fall back to standard PocketBase method - const records = await client.pb.collection('command_hangarsync').getList(1, 1, { - filter: `guildId = "${interaction.guildId}"` - }); - if (records.items.length > 0) { - record = records.items[0]; - } - } - } catch (error) { - // Handle case where collection might not exist - client.logger.warn(`Error retrieving hangarsync record: ${error.message}`); - } + try { + // First try the enhanced method if available + if (typeof client.pb.getFirst === 'function') { + record = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`); + } else { + // Fall back to standard PocketBase method + const records = await client.pb.collection('command_hangarsync').getList(1, 1, { + filter: `guildId = "${interaction.guildId}"` + }); + if (records.items.length > 0) { + record = records.items[0]; + } + } + } catch (error) { + // Handle case where collection might not exist + client.logger.warn(`Error retrieving hangarsync record: ${error.message}`); + } - if (record) { - // Update existing record - if (typeof client.pb.updateOne === 'function') { - await client.pb.updateOne('command_hangarsync', record.id, { - userId: `${interaction.user.id}`, - epoch: `${syncEpoch}`, - }); - } else { - await client.pb.collection('command_hangarsync').update(record.id, { - userId: `${interaction.user.id}`, - epoch: `${syncEpoch}`, - }); - } - client.logger.info(`[cmd:hangarsync] Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); - } else { - // Create new record - if (typeof client.pb.createOne === 'function') { - await client.pb.createOne('command_hangarsync', { - guildId: `${interaction.guildId}`, - userId: `${interaction.user.id}`, - epoch: `${syncEpoch}`, - }); - } else { - await client.pb.collection('command_hangarsync').create({ - guildId: `${interaction.guildId}`, - userId: `${interaction.user.id}`, - epoch: `${syncEpoch}`, - }); - } - client.logger.info(`[cmd:hangarsync] Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); - } + if (record) { + // Update existing record + if (typeof client.pb.updateOne === 'function') { + await client.pb.updateOne('command_hangarsync', record.id, { + userId: `${interaction.user.id}`, + epoch: `${syncEpoch}` + }); + } else { + await client.pb.collection('command_hangarsync').update(record.id, { + userId: `${interaction.user.id}`, + epoch: `${syncEpoch}` + }); + } + client.logger.info(`[cmd:hangarsync] Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); + } else { + // Create new record + if (typeof client.pb.createOne === 'function') { + await client.pb.createOne('command_hangarsync', { + guildId: `${interaction.guildId}`, + userId: `${interaction.user.id}`, + epoch: `${syncEpoch}` + }); + } else { + await client.pb.collection('command_hangarsync').create({ + guildId: `${interaction.guildId}`, + userId: `${interaction.user.id}`, + epoch: `${syncEpoch}` + }); + } + client.logger.info(`[cmd:hangarsync] Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); + } - await interaction.reply(`Executive hangar status has been synced: `); + await interaction.reply(`Executive hangar status has been synced: `); } catch (error) { client.logger.error(`[cmd:hangarsync] Error: ${error.message}`); - await interaction.reply({ - content: `Error syncing hangar status. Please try again later.`, - ephemeral: true - }); - } - } - }, - { - data: new SlashCommandBuilder() - .setName('hangarstatus') - .setDescription('Check the status of contested zone executive hangars') - .addBooleanOption(option => - option.setName('verbose') - .setDescription('Extra output, mainly for debugging.') - .setRequired(false)), + await interaction.reply({ + content: 'Error syncing hangar status. Please try again later.', + ephemeral: true + }); + } + } + }, + { + data: new SlashCommandBuilder() + .setName('hangarstatus') + .setDescription('Check the status of contested zone executive hangars') + .addBooleanOption(option => + option.setName('verbose') + .setDescription('Extra output, mainly for debugging.') + .setRequired(false)), - execute: async (interaction, client) => { - const verbose = interaction.options.getBoolean('verbose'); + execute: async (interaction, client) => { + const verbose = interaction.options.getBoolean('verbose'); - // Check PocketBase connection status - if (!isPocketBaseConnected(client)) { + // Check PocketBase connection status + if (!isPocketBaseConnected(client)) { client.logger.error('[cmd:hangarstatus] PocketBase not connected'); - // Try to reconnect if available - if (typeof client.pb.ensureConnection === 'function') { - await client.pb.ensureConnection(); + // Try to reconnect if available + if (typeof client.pb.ensureConnection === 'function') { + await client.pb.ensureConnection(); - // Check if reconnection worked - if (!isPocketBaseConnected(client)) { - return interaction.reply({ - content: 'Database connection unavailable. Please try again later.', - ephemeral: true - }); - } - } else { - return interaction.reply({ - content: 'Database connection unavailable. Please try again later.', - ephemeral: true - }); - } - } + // Check if reconnection worked + if (!isPocketBaseConnected(client)) { + return interaction.reply({ + content: 'Database connection unavailable. Please try again later.', + ephemeral: true + }); + } + } else { + return interaction.reply({ + content: 'Database connection unavailable. Please try again later.', + ephemeral: true + }); + } + } - try { - // Get hangarsync data for guild - let hangarSync = null; + try { + // Get hangarsync data for guild + let hangarSync = null; - try { - // First try the enhanced method if available - if (typeof client.pb.getFirst === 'function') { - hangarSync = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`); - } else { - // Fall back to standard PocketBase methods - try { - hangarSync = await client.pb.collection('command_hangarsync').getFirstListItem(`guildId = "${interaction.guildId}"`); - } catch (error) { - // getFirstListItem throws if no items found - if (error.status !== 404) throw error; - } - } + try { + // First try the enhanced method if available + if (typeof client.pb.getFirst === 'function') { + hangarSync = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`); + } else { + // Fall back to standard PocketBase methods + try { + hangarSync = await client.pb.collection('command_hangarsync').getFirstListItem(`guildId = "${interaction.guildId}"`); + } catch (error) { + // getFirstListItem throws if no items found + if (error.status !== 404) throw error; + } + } - if (!hangarSync) { - client.logger.info(`[cmd:hangarstatus] No sync data found for guild ${interaction.guildId}`); - return interaction.reply({ - content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', - ephemeral: true - }); - } - } catch (error) { - client.logger.error(`[cmd:hangarstatus] Error retrieving sync data for guild ${interaction.guildId}: ${error.message}`); - return interaction.reply({ - content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', - ephemeral: true - }); - } + if (!hangarSync) { + client.logger.info(`[cmd:hangarstatus] No sync data found for guild ${interaction.guildId}`); + return interaction.reply({ + content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', + ephemeral: true + }); + } + } catch (error) { + client.logger.error(`[cmd:hangarstatus] Error retrieving sync data for guild ${interaction.guildId}: ${error.message}`); + return interaction.reply({ + content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', + ephemeral: true + }); + } - const currentTime = Date.now(); + const currentTime = Date.now(); - // 5 minutes (all off) + 5*24 minutes (turning green) + 5*12 minutes (turning off) = 185 minutes - const cycleDuration = 5 + (5 * 24) + (5 * 12); + // 5 minutes (all off) + 5*24 minutes (turning green) + 5*12 minutes (turning off) = 185 minutes + const cycleDuration = 5 + (5 * 24) + (5 * 12); - // Key positions in the cycle - const allOffDuration = 5; - const turningGreenDuration = 5 * 24; - const turningOffDuration = 5 * 12; + // Key positions in the cycle + const allOffDuration = 5; + const _turningGreenDuration = 5 * 24 * 1000; + const turningOffDuration = 5 * 12 * 1000; - // Calculate how much time has passed since the epoch - const timeSinceEpoch = (currentTime - hangarSync.epoch) / (60 * 1000); + // Calculate how much time has passed since the epoch + const timeSinceEpoch = (currentTime - hangarSync.epoch) / (60 * 1000); - // Calculate where we are in the full-cycle relative to the epoch - const cyclePosition = ((timeSinceEpoch % cycleDuration) + cycleDuration) % cycleDuration; + // Calculate where we are in the full-cycle relative to the epoch + const cyclePosition = ((timeSinceEpoch % cycleDuration) + cycleDuration) % cycleDuration; - // Initialize stuff and things - const lights = [":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:"]; - let minutesUntilNextPhase = 0; - let currentPhase = ""; + // Initialize stuff and things + const lights = [':black_circle:', ':black_circle:', ':black_circle:', ':black_circle:', ':black_circle:']; + let minutesUntilNextPhase = 0; + let currentPhase = ''; - // If the epoch is now, we should be at the all-green position. - // From there, we need to determine where we are in the cycle. + // If the epoch is now, we should be at the all-green position. + // From there, we need to determine where we are in the cycle. - // Case 1: We're in the unlocked phase, right after epoch - if (cyclePosition < turningOffDuration) { - currentPhase = "Unlocked"; + // Case 1: We're in the unlocked phase, right after epoch + if (cyclePosition < turningOffDuration) { + currentPhase = 'Unlocked'; - // All lights start as green - lights.fill(":green_circle:"); + // All lights start as green + lights.fill(':green_circle:'); - // Calculate how many lights have turned off - const offLights = Math.floor(cyclePosition / 12); + // Calculate how many lights have turned off + const offLights = Math.floor(cyclePosition / 12); - // Set the appropriate number of lights to off - for (let i = 0; i < offLights; i++) { - lights[i] = ":black_circle:"; - } + // Set the appropriate number of lights to off + for (let i = 0; i < offLights; i++) { + lights[i] = ':black_circle:'; + } - // Calculate time until next light turns off - const timeUntilNextLight = 12 - (cyclePosition % 12); - minutesUntilNextPhase = timeUntilNextLight; - } + // Calculate time until next light turns off + const timeUntilNextLight = 12 - (cyclePosition % 12); + minutesUntilNextPhase = timeUntilNextLight; + } - // Case 2: We're in the reset phase - else if (cyclePosition < turningOffDuration + allOffDuration) { - currentPhase = "Resetting"; + // Case 2: We're in the reset phase + else if (cyclePosition < turningOffDuration + allOffDuration) { + currentPhase = 'Resetting'; - // Lights are initialized "off", so do nothing with them + // Lights are initialized "off", so do nothing with them - // Calculate time until all lights turn red - const timeIntoPhase = cyclePosition - turningOffDuration; - minutesUntilNextPhase = allOffDuration - timeIntoPhase; - } + // Calculate time until all lights turn red + const timeIntoPhase = cyclePosition - turningOffDuration; + minutesUntilNextPhase = allOffDuration - timeIntoPhase; + } - // Case 3: We're in the locked phase - else { - currentPhase = "Locked"; + // Case 3: We're in the locked phase + else { + currentPhase = 'Locked'; - // All lights start as red - lights.fill(":red_circle:"); + // All lights start as red + lights.fill(':red_circle:'); - // Calculate how many lights have turned green - const timeIntoPhase = cyclePosition - (turningOffDuration + allOffDuration); - const greenLights = Math.floor(timeIntoPhase / 24); + // Calculate how many lights have turned green + const timeIntoPhase = cyclePosition - (turningOffDuration + allOffDuration); + const greenLights = Math.floor(timeIntoPhase / 24); - // Set the appropriate number of lights to green - for (let i = 0; i < greenLights; i++) { - lights[i] = ":green_circle:"; - } + // Set the appropriate number of lights to green + for (let i = 0; i < greenLights; i++) { + lights[i] = ':green_circle:'; + } - // Calculate time until next light turns green - const timeUntilNextLight = 24 - (timeIntoPhase % 24); - minutesUntilNextPhase = timeUntilNextLight; - } + // Calculate time until next light turns green + const timeUntilNextLight = 24 - (timeIntoPhase % 24); + minutesUntilNextPhase = timeUntilNextLight; + } - // Calculate a timestamp for Discord's formatting and reply - const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60)); - // Determine time to next Lock/Unlock phase for inline display - const isUnlocked = currentPhase === 'Unlocked'; - const label = isUnlocked ? 'Lock' : 'Unlock'; - const minutesToPhase = isUnlocked - ? (turningOffDuration + allOffDuration) - cyclePosition - : cycleDuration - cyclePosition; - const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60)); - // Reply with lights and inline time to phase - await interaction.reply( - `### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]} — Time to ${label}: ` - ); + // Calculate a timestamp for Discord's formatting and reply + const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60)); + // Determine time to next Lock/Unlock phase for inline display + const isUnlocked = currentPhase === 'Unlocked'; + const label = isUnlocked ? 'Lock' : 'Unlock'; + const minutesToPhase = isUnlocked + ? (turningOffDuration + allOffDuration) - cyclePosition + : cycleDuration - cyclePosition; + const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60)); + // Reply with lights and inline time to phase + await interaction.reply( + `### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]} — Time to ${label}: ` + ); - if (verbose) { - // Replace user mention with displayName for last sync - const syncMember = await interaction.guild.members.fetch(hangarSync.userId).catch(() => null); - const syncName = syncMember ? syncMember.displayName : `<@${hangarSync.userId}>`; + if (verbose) { + // Replace user mention with displayName for last sync + const syncMember = await interaction.guild.members.fetch(hangarSync.userId).catch(() => null); + const syncName = syncMember ? syncMember.displayName : `<@${hangarSync.userId}>`; - // Calculate time until next Lock/Unlock phase - const isUnlocked = currentPhase === 'Unlocked'; - const label = isUnlocked ? 'Lock' : 'Unlock'; - const minutesToPhase = isUnlocked - ? (turningOffDuration + allOffDuration) - cyclePosition - : cycleDuration - cyclePosition; - const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60)); + // Calculate time until next Lock/Unlock phase + const isUnlocked = currentPhase === 'Unlocked'; + const label = isUnlocked ? 'Lock' : 'Unlock'; + const minutesToPhase = isUnlocked + ? (turningOffDuration + allOffDuration) - cyclePosition + : cycleDuration - cyclePosition; + const phaseEpoch = Math.ceil(Date.now() / 1000 + (minutesToPhase * 60)); - await interaction.followUp( - `- **Phase**: ${currentPhase}\n` + + await interaction.followUp( + `- **Phase**: ${currentPhase}\n` + `- **Time to ${label}**: \n` + `- **Status Expiration**: \n` + `- **Epoch**: \n` + `- **Sync**: by ${syncName}` - ); + ); - // Add additional debug info to logs - client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`); - } + // Add additional debug info to logs + client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`); + } - } catch (error) { - client.logger.error(`Error in hangarstatus command: ${error.message}`); - await interaction.reply({ - content: `Error retrieving hangar status. Please try again later.`, - ephemeral: true - }); - } - } - } + } catch (error) { + client.logger.error(`Error in hangarstatus command: ${error.message}`); + await interaction.reply({ + content: 'Error retrieving hangar status. Please try again later.', + ephemeral: true + }); + } + } + } ]; // Function to check PocketBase connection status function isPocketBaseConnected(client) { - // Check multiple possible status indicators to be safe - return client.pb && ( - // Check status object (original code style) - (client.pb.status && client.pb.status.connected) || + // Check multiple possible status indicators to be safe + return client.pb && ( + // Check status object (original code style) + (client.pb.status && client.pb.status.connected) || // Check isConnected property (pbutils module style) - client.pb.isConnected === true || + client.pb.isConnected === true || // Last resort: check if authStore is valid client.pb.authStore?.isValid === true - ); + ); } // Initialize module -export const init = async (client, config) => { - client.logger.info('Initializing Star Citizen Hangar Status module'); +export async function init(client, _config) { + client.logger.info('Initializing Star Citizen Hangar Status module'); - // Check PocketBase connection - if (!isPocketBaseConnected(client)) { - client.logger.warn('PocketBase not connected at initialization'); + // Check PocketBase connection + if (!isPocketBaseConnected(client)) { + client.logger.warn('PocketBase not connected at initialization'); - // Try to reconnect if available - if (typeof client.pb.ensureConnection === 'function') { - await client.pb.ensureConnection(); - } - } else { - client.logger.info('PocketBase connection confirmed'); - } + // Try to reconnect if available + if (typeof client.pb.ensureConnection === 'function') { + await client.pb.ensureConnection(); + } + } else { + client.logger.info('PocketBase connection confirmed'); + } - client.logger.info('Star Citizen Hangar Status module initialized'); -}; \ No newline at end of file + client.logger.info('Star Citizen Hangar Status module initialized'); +} diff --git a/_opt/scorekeeper-example.js b/_opt/scorekeeper-example.js index f25c3d3..755cbab 100644 --- a/_opt/scorekeeper-example.js +++ b/_opt/scorekeeper-example.js @@ -1,150 +1,150 @@ // Example of another module using scorekeeper -export const init = async (client, config) => { - // Set up message listener that adds input points when users chat - client.on('messageCreate', async (message) => { - if (message.author.bot) return; +export async function init(client, _config) { + // Set up message listener that adds input points when users chat + client.on('messageCreate', async (message) => { + if (message.author.bot) return; - // Skip if not in a guild - if (!message.guild) return; + // Skip if not in a guild + if (!message.guild) return; - // Calculate input points: 1 point per character, plus 10 points per attachment - const textPoints = message.content.length; - const attachmentPoints = message.attachments.size * 10; - const points = textPoints + attachmentPoints; - // Do not award zero or negative points - if (points <= 0) return; - try { - await client.scorekeeper.addInput(message.guild.id, message.author.id, points, 'message'); - } catch (error) { - client.logger.error(`Error adding input points: ${error.message}`); - } - }); + // Calculate input points: 1 point per character, plus 10 points per attachment + const textPoints = message.content.length; + const attachmentPoints = message.attachments.size * 10; + const points = textPoints + attachmentPoints; + // Do not award zero or negative points + if (points <= 0) return; + try { + await client.scorekeeper.addInput(message.guild.id, message.author.id, points, 'message'); + } catch (error) { + client.logger.error(`Error adding input points: ${error.message}`); + } + }); - // Initialize voice tracking state - client.voiceTracker = { - joinTimes: new Map(), // Tracks when users joined voice - activeUsers: new Map() // Tracks users currently earning points - }; + // Initialize voice tracking state + client.voiceTracker = { + joinTimes: new Map(), // Tracks when users joined voice + activeUsers: new Map() // Tracks users currently earning points + }; - // Set up a voice state listener that adds input for voice activity - client.on('voiceStateUpdate', async (oldState, newState) => { - // Skip if not in a guild - if (!oldState.guild && !newState.guild) return; + // Set up a voice state listener that adds input for voice activity + client.on('voiceStateUpdate', async (oldState, newState) => { + // Skip if not in a guild + if (!oldState.guild && !newState.guild) return; - const guild = oldState.guild || newState.guild; - const member = oldState.member || newState.member; + const guild = oldState.guild || newState.guild; + const member = oldState.member || newState.member; - // User joined a voice channel - if (!oldState.channelId && newState.channelId) { - // Check if the channel has other non-bot users - const channel = newState.channel; - const otherUsers = channel.members.filter(m => - m.id !== member.id && !m.user.bot - ); + // User joined a voice channel + if (!oldState.channelId && newState.channelId) { + // Check if the channel has other non-bot users + const channel = newState.channel; + const otherUsers = channel.members.filter(m => + m.id !== member.id && !m.user.bot + ); - // Store join time if there's at least one other non-bot user - if (otherUsers.size > 0) { - client.voiceTracker.joinTimes.set(member.id, Date.now()); - client.voiceTracker.activeUsers.set(member.id, newState.channelId); - client.logger.debug(`${member.user.tag} joined voice with others - tracking time`); - } else { - client.logger.debug(`${member.user.tag} joined voice alone or with bots - not tracking time`); - } - } - // User left a voice channel - else if (oldState.channelId && !newState.channelId) { - processVoiceLeave(client, guild, member, oldState.channelId); - } - // User switched voice channels - else if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) { - // Process leaving the old channel - processVoiceLeave(client, guild, member, oldState.channelId); + // Store join time if there's at least one other non-bot user + if (otherUsers.size > 0) { + client.voiceTracker.joinTimes.set(member.id, Date.now()); + client.voiceTracker.activeUsers.set(member.id, newState.channelId); + client.logger.debug(`${member.user.tag} joined voice with others - tracking time`); + } else { + client.logger.debug(`${member.user.tag} joined voice alone or with bots - not tracking time`); + } + } + // User left a voice channel + else if (oldState.channelId && !newState.channelId) { + processVoiceLeave(client, guild, member, oldState.channelId); + } + // User switched voice channels + else if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) { + // Process leaving the old channel + processVoiceLeave(client, guild, member, oldState.channelId); - // Check if the new channel has other non-bot users - const channel = newState.channel; - const otherUsers = channel.members.filter(m => - m.id !== member.id && !m.user.bot - ); + // Check if the new channel has other non-bot users + const channel = newState.channel; + const otherUsers = channel.members.filter(m => + m.id !== member.id && !m.user.bot + ); - // Start tracking in the new channel if there are other non-bot users - if (otherUsers.size > 0) { - client.voiceTracker.joinTimes.set(member.id, Date.now()); - client.voiceTracker.activeUsers.set(member.id, newState.channelId); - } - } + // Start tracking in the new channel if there are other non-bot users + if (otherUsers.size > 0) { + client.voiceTracker.joinTimes.set(member.id, Date.now()); + client.voiceTracker.activeUsers.set(member.id, newState.channelId); + } + } - // If someone joined or left a channel, update tracking for everyone in that channel - updateChannelUserTracking(client, oldState, newState); - }); -}; + // If someone joined or left a channel, update tracking for everyone in that channel + updateChannelUserTracking(client, oldState, newState); + }); +} /** * Process when a user leaves a voice channel */ function processVoiceLeave(client, guild, member, channelId) { - if (client.voiceTracker.activeUsers.get(member.id) === channelId) { - const joinTime = client.voiceTracker.joinTimes.get(member.id); + if (client.voiceTracker.activeUsers.get(member.id) === channelId) { + const joinTime = client.voiceTracker.joinTimes.get(member.id); - if (joinTime) { - const duration = (Date.now() - joinTime) / 1000 / 60; // Duration in minutes + if (joinTime) { + const duration = (Date.now() - joinTime) / 1000 / 60; // Duration in minutes - // Award 1 point per minute, up to 30 per session - const points = Math.min(Math.floor(duration), 30); - if (points > 0) { - try { - client.scorekeeper.addInput(guild.id, member.id, points, 'voice_activity') - .then(() => { - client.logger.debug(`Added ${points} voice activity points for ${member.user.tag}`); - }) - .catch(error => { - client.logger.error(`Error adding voice points: ${error.message}`); - }); - } catch (error) { - client.logger.error(`Error adding voice points: ${error.message}`); - } - } - } + // Award 1 point per minute, up to 30 per session + const points = Math.min(Math.floor(duration), 30); + if (points > 0) { + try { + client.scorekeeper.addInput(guild.id, member.id, points, 'voice_activity') + .then(() => { + client.logger.debug(`Added ${points} voice activity points for ${member.user.tag}`); + }) + .catch(error => { + client.logger.error(`Error adding voice points: ${error.message}`); + }); + } catch (error) { + client.logger.error(`Error adding voice points: ${error.message}`); + } + } + } - client.voiceTracker.joinTimes.delete(member.id); - client.voiceTracker.activeUsers.delete(member.id); - } + client.voiceTracker.joinTimes.delete(member.id); + client.voiceTracker.activeUsers.delete(member.id); + } } /** * Updates tracking for all users in affected channels */ function updateChannelUserTracking(client, oldState, newState) { - // Get the affected channels - const affectedChannels = new Set(); - if (oldState.channelId) affectedChannels.add(oldState.channelId); - if (newState.channelId) affectedChannels.add(newState.channelId); + // Get the affected channels + const affectedChannels = new Set(); + if (oldState.channelId) affectedChannels.add(oldState.channelId); + if (newState.channelId) affectedChannels.add(newState.channelId); - for (const channelId of affectedChannels) { - const channel = oldState.guild.channels.cache.get(channelId); - if (!channel) continue; + for (const channelId of affectedChannels) { + const channel = oldState.guild.channels.cache.get(channelId); + if (!channel) continue; - // Check if the channel has at least 2 non-bot users - const nonBotMembers = channel.members.filter(m => !m.user.bot); - const hasMultipleUsers = nonBotMembers.size >= 2; + // Check if the channel has at least 2 non-bot users + const nonBotMembers = channel.members.filter(m => !m.user.bot); + const hasMultipleUsers = nonBotMembers.size >= 2; - // For each user in the channel - channel.members.forEach(channelMember => { - if (channelMember.user.bot) return; // Skip bots + // For each user in the channel + channel.members.forEach(channelMember => { + if (channelMember.user.bot) return; // Skip bots - const userId = channelMember.id; - const isActive = client.voiceTracker.activeUsers.get(userId) === channelId; + const userId = channelMember.id; + const isActive = client.voiceTracker.activeUsers.get(userId) === channelId; - // Should be active but isn't yet - if (hasMultipleUsers && !isActive) { - client.voiceTracker.joinTimes.set(userId, Date.now()); - client.voiceTracker.activeUsers.set(userId, channelId); - client.logger.debug(`Starting tracking for ${channelMember.user.tag} in ${channel.name}`); - } - // Should not be active but is - else if (!hasMultipleUsers && isActive) { - processVoiceLeave(client, oldState.guild, channelMember, channelId); - client.logger.debug(`Stopping tracking for ${channelMember.user.tag} - not enough users`); - } - }); - } + // Should be active but isn't yet + if (hasMultipleUsers && !isActive) { + client.voiceTracker.joinTimes.set(userId, Date.now()); + client.voiceTracker.activeUsers.set(userId, channelId); + client.logger.debug(`Starting tracking for ${channelMember.user.tag} in ${channel.name}`); + } + // Should not be active but is + else if (!hasMultipleUsers && isActive) { + processVoiceLeave(client, oldState.guild, channelMember, channelId); + client.logger.debug(`Stopping tracking for ${channelMember.user.tag} - not enough users`); + } + }); + } } diff --git a/_opt/scorekeeper.js b/_opt/scorekeeper.js index bc57995..b6e2ca2 100644 --- a/_opt/scorekeeper.js +++ b/_opt/scorekeeper.js @@ -1,90 +1,90 @@ import { MessageFlags } from 'discord-api-types/v10'; // opt/scorekeeper.js -import cron from 'node-cron'; import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; +import cron from 'node-cron'; // Module state container const moduleState = { - cronJobs: new Map(), // Store cron jobs by client ID + cronJobs: new Map() // Store cron jobs by client ID }; /** * Initialize the scorekeeper module */ export const init = async (client, config) => { - client.logger.info('[module:scorekeeper] Initializing Scorekeeper module'); + client.logger.info('[module:scorekeeper] Initializing Scorekeeper module'); - // Check if configuration exists - if (!config.scorekeeper) { - client.logger.warn('Scorekeeper configuration missing, using defaults'); - config.scorekeeper = { - baseOutput: 1000, - commendationValue: 1.0, - citationValue: 1.2, - decay: 90, - schedule: '0 0 * * 0' // Default: weekly at midnight on Sunday - }; - } + // Check if configuration exists + if (!config.scorekeeper) { + client.logger.warn('Scorekeeper configuration missing, using defaults'); + config.scorekeeper = { + baseOutput: 1000, + commendationValue: 1.0, + citationValue: 1.2, + decay: 90, + schedule: '0 0 * * 0' // Default: weekly at midnight on Sunday + }; + } - // Check if scorekeeper collection exists - await checkCollection(client); + // Check if scorekeeper collection exists + await checkCollection(client); // Ensure auxiliary collections exist (categories & events) await checkCategoriesCollection(client); await checkEventsCollection(client); // Create scorekeeper interface on client - client.scorekeeper = { - /** + client.scorekeeper = { + /** * Add input points with optional reason for audit * @param {string} guildId * @param {string} userId * @param {number} amount * @param {string} [reason] */ - addInput: (guildId, userId, amount, reason) => addInput(client, guildId, userId, amount, reason), - /** + addInput: (guildId, userId, amount, reason) => addInput(client, guildId, userId, amount, reason), + /** * Add output points with optional reason for audit * @param {string} guildId * @param {string} userId * @param {number} amount * @param {string} [reason] */ - addOutput: (guildId, userId, amount, reason) => addOutput(client, guildId, userId, amount, reason), - addCommendation: (guildId, userId, amount = 1) => addCommendation(client, guildId, userId, amount), - addCitation: (guildId, userId, amount = 1) => addCitation(client, guildId, userId, amount), - getScore: (guildId, userId) => getScore(client, guildId, userId), - getScores: (guildId, limit = 10) => getScores(client, guildId, limit), - runDecay: (guildId) => runDecay(client, guildId) - }; + addOutput: (guildId, userId, amount, reason) => addOutput(client, guildId, userId, amount, reason), + addCommendation: (guildId, userId, amount = 1) => addCommendation(client, guildId, userId, amount), + addCitation: (guildId, userId, amount = 1) => addCitation(client, guildId, userId, amount), + getScore: (guildId, userId) => getScore(client, guildId, userId), + getScores: (guildId, limit = 10) => getScores(client, guildId, limit), + runDecay: (guildId) => runDecay(client, guildId) + }; // Set up cron job for decay setupDecayCron(client, config.scorekeeper.schedule); // Enable autocomplete for category options in commend/cite registerCategoryAutocomplete(client); - client.logger.info('Scorekeeper module initialized'); + client.logger.info('Scorekeeper module initialized'); }; /** * Check if the scorekeeper collection exists in PocketBase */ async function checkCollection(client) { - try { - // Check if collection exists by trying to list records - await client.pb.collection('scorekeeper').getList(1, 1); - client.logger.info('[module:scorekeeper] Scorekeeper collection exists in PocketBase'); - } catch (error) { - // If collection doesn't exist, log warning - client.logger.warn('[module:scorekeeper] Scorekeeper collection does not exist in PocketBase'); - client.logger.warn('Please create a "scorekeeper" collection with fields:'); - client.logger.warn('- guildId (text, required)'); - client.logger.warn('- userId (text, required)'); - client.logger.warn('- input (number, default: 0)'); - client.logger.warn('- output (number, default: 0)'); - client.logger.warn('- commendations (number, default: 0)'); - client.logger.warn('- citations (number, default: 0)'); - client.logger.warn('- lastDecay (date, required)'); - } + try { + // Check if collection exists by trying to list records + await client.pb.collection('scorekeeper').getList(1, 1); + client.logger.info('[module:scorekeeper] Scorekeeper collection exists in PocketBase'); + } catch (error) { + // If collection doesn't exist, log warning + client.logger.warn('[module:scorekeeper] Scorekeeper collection does not exist in PocketBase'); + client.logger.warn('Please create a "scorekeeper" collection with fields:'); + client.logger.warn('- guildId (text, required)'); + client.logger.warn('- userId (text, required)'); + client.logger.warn('- input (number, default: 0)'); + client.logger.warn('- output (number, default: 0)'); + client.logger.warn('- commendations (number, default: 0)'); + client.logger.warn('- citations (number, default: 0)'); + client.logger.warn('- lastDecay (date, required)'); + } } /** * Ensure the "scorekeeper_categories" collection exists in PocketBase. @@ -92,17 +92,17 @@ async function checkCollection(client) { * @param {import('discord.js').Client} client - The Discord client with PocketBase attached. */ async function checkCategoriesCollection(client) { - try { - await client.pb.collection('scorekeeper_categories').getList(1, 1); - client.logger.info('[module:scorekeeper] scorekeeper_categories collection exists'); - } catch (error) { - client.logger.warn('[module:scorekeeper] scorekeeper_categories collection does not exist in PocketBase'); - client.logger.warn('Please create a "scorekeeper_categories" collection with fields:'); - client.logger.warn('- guildId (text, required)'); - client.logger.warn('- name (text, required, unique per guild)'); - client.logger.warn('- createdBy (text, required)'); - client.logger.warn('- createdAt (date, required)'); - } + try { + await client.pb.collection('scorekeeper_categories').getList(1, 1); + client.logger.info('[module:scorekeeper] scorekeeper_categories collection exists'); + } catch (error) { + client.logger.warn('[module:scorekeeper] scorekeeper_categories collection does not exist in PocketBase'); + client.logger.warn('Please create a "scorekeeper_categories" collection with fields:'); + client.logger.warn('- guildId (text, required)'); + client.logger.warn('- name (text, required, unique per guild)'); + client.logger.warn('- createdBy (text, required)'); + client.logger.warn('- createdAt (date, required)'); + } } /** @@ -111,59 +111,59 @@ async function checkCategoriesCollection(client) { * @param {import('discord.js').Client} client - The Discord client with PocketBase attached. */ async function checkEventsCollection(client) { - try { - await client.pb.collection('scorekeeper_events').getList(1, 1); - client.logger.info('[module:scorekeeper] scorekeeper_events collection exists'); - } catch (error) { - client.logger.warn('[module:scorekeeper] scorekeeper_events collection does not exist in PocketBase'); - client.logger.warn('Please create a "scorekeeper_events" collection with fields:'); - client.logger.warn('- guildId (text, required)'); - client.logger.warn('- userId (text, required)'); - client.logger.warn('- type (text, required) // "commendation" or "citation"'); - client.logger.warn('- categoryId (text, required)'); - client.logger.warn('- amount (number, required)'); - client.logger.warn('- awardedBy (text, required)'); - client.logger.warn('- timestamp (date, required)'); - } + try { + await client.pb.collection('scorekeeper_events').getList(1, 1); + client.logger.info('[module:scorekeeper] scorekeeper_events collection exists'); + } catch (error) { + client.logger.warn('[module:scorekeeper] scorekeeper_events collection does not exist in PocketBase'); + client.logger.warn('Please create a "scorekeeper_events" collection with fields:'); + client.logger.warn('- guildId (text, required)'); + client.logger.warn('- userId (text, required)'); + client.logger.warn('- type (text, required) // "commendation" or "citation"'); + client.logger.warn('- categoryId (text, required)'); + client.logger.warn('- amount (number, required)'); + client.logger.warn('- awardedBy (text, required)'); + client.logger.warn('- timestamp (date, required)'); + } } /** * Set up cron job for decay */ function setupDecayCron(client, schedule) { - try { - // Validate cron expression - if (!cron.validate(schedule)) { - client.logger.error(`Invalid cron schedule: ${schedule}, using default`); - schedule = '0 0 * * 0'; // Default: weekly at midnight on Sunday - } + try { + // Validate cron expression + if (!cron.validate(schedule)) { + client.logger.error(`Invalid cron schedule: ${schedule}, using default`); + schedule = '0 0 * * 0'; // Default: weekly at midnight on Sunday + } - // Create and start the cron job - const job = cron.schedule(schedule, async () => { - client.logger.info('Running scheduled score decay'); + // Create and start the cron job + const job = cron.schedule(schedule, async () => { + client.logger.info('Running scheduled score decay'); - try { - // Get all guilds the bot is in - const guilds = client.guilds.cache.map(guild => guild.id); + try { + // Get all guilds the bot is in + const guilds = client.guilds.cache.map(guild => guild.id); - // Run decay for each guild - for (const guildId of guilds) { - await runDecay(client, guildId); - } + // Run decay for each guild + for (const guildId of guilds) { + await runDecay(client, guildId); + } - client.logger.info('Score decay completed'); - } catch (error) { - client.logger.error(`Error during scheduled score decay: ${error.message}`); - } - }); + client.logger.info('Score decay completed'); + } catch (error) { + client.logger.error(`Error during scheduled score decay: ${error.message}`); + } + }); - // Store the job in module state - moduleState.cronJobs.set(client.config.id, job); + // Store the job in module state + moduleState.cronJobs.set(client.config.id, job); - client.logger.info(`Score decay scheduled with cron: ${schedule}`); - } catch (error) { - client.logger.error(`Failed to set up decay cron job: ${error.message}`); - } + client.logger.info(`Score decay scheduled with cron: ${schedule}`); + } catch (error) { + client.logger.error(`Failed to set up decay cron job: ${error.message}`); + } } /** @@ -178,43 +178,43 @@ function setupDecayCron(client, schedule) { * @param {string} [reason] */ async function addInput(client, guildId, userId, amount, reason = '') { - if (!guildId || !userId || !amount || amount <= 0) { - throw new Error(`Invalid parameters for addInput - guildId: ${guildId}, userId: ${userId}, amount: ${amount}`); - } + if (!guildId || !userId || !amount || amount <= 0) { + throw new Error(`Invalid parameters for addInput - guildId: ${guildId}, userId: ${userId}, amount: ${amount}`); + } - try { - // Get or create user record - const scoreData = await getOrCreateScoreData(client, guildId, userId); + try { + // Get or create user record + const scoreData = await getOrCreateScoreData(client, guildId, userId); - // Calculate new input score - const newInput = scoreData.input + amount; + // Calculate new input score + const newInput = scoreData.input + amount; - client.logger.debug(`Updating record ${scoreData.id} - input from ${scoreData.input} to ${newInput}`); + client.logger.debug(`Updating record ${scoreData.id} - input from ${scoreData.input} to ${newInput}`); - // Use direct update with ID to avoid duplicate records - const updatedRecord = await client.pb.collection('scorekeeper').update(scoreData.id, { - input: newInput - }); - // Log input change at info level - client.logger.info(`[module:scorekeeper][addInput] guildId=${guildId}, userId=${userId}, recordId=${scoreData.id}, previousInput=${scoreData.input}, newInput=${newInput}, amount=${amount}, reason=${reason}`); - // Audit event: log input change - try { - await client.pb.collection('scorekeeper_events').create({ - guildId, - userId, - type: 'input', - amount, - reason, - awardedBy: client.user?.id - }); - } catch (eventError) { - client.logger.error(`[module:scorekeeper] Failed to log input event: ${eventError.message}`); - } - return updatedRecord; - } catch (error) { - client.logger.error(`Error adding input points: ${error.message}`); - throw error; - } + // Use direct update with ID to avoid duplicate records + const updatedRecord = await client.pb.collection('scorekeeper').update(scoreData.id, { + input: newInput + }); + // Log input change at info level + client.logger.info(`[module:scorekeeper][addInput] guildId=${guildId}, userId=${userId}, recordId=${scoreData.id}, previousInput=${scoreData.input}, newInput=${newInput}, amount=${amount}, reason=${reason}`); + // Audit event: log input change + try { + await client.pb.collection('scorekeeper_events').create({ + guildId, + userId, + type: 'input', + amount, + reason, + awardedBy: client.user?.id + }); + } catch (eventError) { + client.logger.error(`[module:scorekeeper] Failed to log input event: ${eventError.message}`); + } + return updatedRecord; + } catch (error) { + client.logger.error(`Error adding input points: ${error.message}`); + throw error; + } } /** @@ -229,143 +229,143 @@ async function addInput(client, guildId, userId, amount, reason = '') { * @param {string} [reason] */ async function addOutput(client, guildId, userId, amount, reason = '') { - if (!guildId || !userId || !amount || amount <= 0) { - throw new Error('Invalid parameters for addOutput'); - } + if (!guildId || !userId || !amount || amount <= 0) { + throw new Error('Invalid parameters for addOutput'); + } - try { - // Get or create user record - const scoreData = await getOrCreateScoreData(client, guildId, userId); + try { + // Get or create user record + const scoreData = await getOrCreateScoreData(client, guildId, userId); - // Calculate new output score - const newOutput = scoreData.output + amount; + // Calculate new output score + const newOutput = scoreData.output + amount; - client.logger.debug(`Updating record ${scoreData.id} - output from ${scoreData.output} to ${newOutput}`); + client.logger.debug(`Updating record ${scoreData.id} - output from ${scoreData.output} to ${newOutput}`); - // Use direct update with ID to avoid duplicate records - const updatedRecord = await client.pb.collection('scorekeeper').update(scoreData.id, { - output: newOutput - }); - // Log output change at info level - client.logger.info(`[module:scorekeeper][addOutput] guildId=${guildId}, userId=${userId}, recordId=${scoreData.id}, previousOutput=${scoreData.output}, newOutput=${newOutput}, amount=${amount}, reason=${reason}`); - // Audit event: log output change - try { - await client.pb.collection('scorekeeper_events').create({ - guildId, - userId, - type: 'output', - amount, - reason, - awardedBy: client.user?.id - }); - } catch (eventError) { - client.logger.error(`[module:scorekeeper] Failed to log output event: ${eventError.message}`); - } - return updatedRecord; - } catch (error) { - client.logger.error(`Error adding output points: ${error.message}`); - throw error; - } + // Use direct update with ID to avoid duplicate records + const updatedRecord = await client.pb.collection('scorekeeper').update(scoreData.id, { + output: newOutput + }); + // Log output change at info level + client.logger.info(`[module:scorekeeper][addOutput] guildId=${guildId}, userId=${userId}, recordId=${scoreData.id}, previousOutput=${scoreData.output}, newOutput=${newOutput}, amount=${amount}, reason=${reason}`); + // Audit event: log output change + try { + await client.pb.collection('scorekeeper_events').create({ + guildId, + userId, + type: 'output', + amount, + reason, + awardedBy: client.user?.id + }); + } catch (eventError) { + client.logger.error(`[module:scorekeeper] Failed to log output event: ${eventError.message}`); + } + return updatedRecord; + } catch (error) { + client.logger.error(`Error adding output points: ${error.message}`); + throw error; + } } /** * Add commendations for a user */ async function addCommendation(client, guildId, userId, amount = 1) { - if (!guildId || !userId || amount <= 0) { - throw new Error('Invalid parameters for addCommendation'); - } + if (!guildId || !userId || amount <= 0) { + throw new Error('Invalid parameters for addCommendation'); + } - try { - // Log the search to debug - client.logger.debug(`Looking for record with guildId=${guildId} and userId=${userId}`); + try { + // Log the search to debug + client.logger.debug(`Looking for record with guildId=${guildId} and userId=${userId}`); - // Get or create user record - const scoreData = await getOrCreateScoreData(client, guildId, userId); + // Get or create user record + const scoreData = await getOrCreateScoreData(client, guildId, userId); - // Log the found/created record - client.logger.debug(`Found/created record with ID: ${scoreData.id}`); + // Log the found/created record + client.logger.debug(`Found/created record with ID: ${scoreData.id}`); - // Calculate new commendations value - const newCommendations = scoreData.commendations + amount; + // Calculate new commendations value + const newCommendations = scoreData.commendations + amount; - // Log the update attempt - client.logger.debug(`Updating record ${scoreData.id} - commendations from ${scoreData.commendations} to ${newCommendations}`); + // Log the update attempt + client.logger.debug(`Updating record ${scoreData.id} - commendations from ${scoreData.commendations} to ${newCommendations}`); - // Use direct update with ID to avoid duplicate records - return await client.pb.collection('scorekeeper').update(scoreData.id, { - commendations: newCommendations - }); - } catch (error) { - client.logger.error(`Error adding commendations: ${error.message}`); - throw error; - } + // Use direct update with ID to avoid duplicate records + return await client.pb.collection('scorekeeper').update(scoreData.id, { + commendations: newCommendations + }); + } catch (error) { + client.logger.error(`Error adding commendations: ${error.message}`); + throw error; + } } /** * Add citations for a user */ async function addCitation(client, guildId, userId, amount = 1) { - if (!guildId || !userId || amount <= 0) { - throw new Error('Invalid parameters for addCitation'); - } + if (!guildId || !userId || amount <= 0) { + throw new Error('Invalid parameters for addCitation'); + } - try { - // Get or create user record - const scoreData = await getOrCreateScoreData(client, guildId, userId); + try { + // Get or create user record + const scoreData = await getOrCreateScoreData(client, guildId, userId); - // Calculate new citations value - const newCitations = scoreData.citations + amount; + // Calculate new citations value + const newCitations = scoreData.citations + amount; - client.logger.debug(`Updating record ${scoreData.id} - citations from ${scoreData.citations} to ${newCitations}`); + client.logger.debug(`Updating record ${scoreData.id} - citations from ${scoreData.citations} to ${newCitations}`); - // Use direct update with ID to avoid duplicate records - return await client.pb.collection('scorekeeper').update(scoreData.id, { - citations: newCitations - }); - } catch (error) { - client.logger.error(`Error adding citations: ${error.message}`); - throw error; - } + // Use direct update with ID to avoid duplicate records + return await client.pb.collection('scorekeeper').update(scoreData.id, { + citations: newCitations + }); + } catch (error) { + client.logger.error(`Error adding citations: ${error.message}`); + throw error; + } } /** * Get a user's score */ async function getScore(client, guildId, userId) { - if (!guildId || !userId) { - throw new Error('Guild ID and User ID are required'); - } + if (!guildId || !userId) { + throw new Error('Guild ID and User ID are required'); + } - try { - // Get score data, create if not exists - const scoreData = await getOrCreateScoreData(client, guildId, userId); + try { + // Get score data, create if not exists + const scoreData = await getOrCreateScoreData(client, guildId, userId); - // Calculate total score - const totalScore = calculateScore( - scoreData, - client.config.scorekeeper.baseOutput, - client.config.scorekeeper.commendationValue, - client.config.scorekeeper.citationValue - ); + // Calculate total score + const totalScore = calculateScore( + scoreData, + client.config.scorekeeper.baseOutput, + client.config.scorekeeper.commendationValue, + client.config.scorekeeper.citationValue + ); - return { - ...scoreData, - totalScore - }; - } catch (error) { - client.logger.error(`Error getting score: ${error.message}`); - throw error; - } + return { + ...scoreData, + totalScore + }; + } catch (error) { + client.logger.error(`Error getting score: ${error.message}`); + throw error; + } } /** * Get scores for a guild */ async function getScores(client, guildId, limit = 10) { - if (!guildId) { - throw new Error('Guild ID is required'); - } + if (!guildId) { + throw new Error('Guild ID is required'); + } try { // Fetch all score records for this guild @@ -396,51 +396,52 @@ async function getScores(client, guildId, limit = 10) { * Run decay process for a guild */ async function runDecay(client, guildId) { - if (!guildId) { - throw new Error('Guild ID is required'); - } + if (!guildId) { + throw new Error('Guild ID is required'); + } - try { - const decayFactor = client.config.scorekeeper.decay / 100; - const baseOutput = client.config.scorekeeper.baseOutput; + try { + const decayFactor = client.config.scorekeeper.decay / 100; + const baseOutput = client.config.scorekeeper.baseOutput; - // Get all records for this guild - const records = await client.pb.collection('scorekeeper').getFullList({ - filter: `guildId = "${guildId}"` - }); + // Get all records for this guild + const records = await client.pb.collection('scorekeeper').getFullList({ + filter: `guildId = "${guildId}"` + }); - // Update each record with decay - let updatedCount = 0; - for (const record of records) { - try { - const newInput = Math.floor(record.input * decayFactor); + // Update each record with decay + let updatedCount = 0; + for (const record of records) { + try { + const newInput = Math.floor(record.input * decayFactor); - // Calculate new output, but ensure it never falls below baseOutput - let newOutput = Math.floor(record.output * decayFactor); - if (newOutput < baseOutput) { - newOutput = baseOutput; - client.logger.debug(`Output for record ${record.id} would fall below BaseOutput - setting to ${baseOutput}`); - } + // Calculate new output, but ensure it never falls below baseOutput + let newOutput = Math.floor(record.output * decayFactor); + if (newOutput < baseOutput) { + newOutput = baseOutput; + client.logger.debug(`Output for record ${record.id} would fall below BaseOutput - setting to ${baseOutput}`); + } - // Update record directly - await client.pb.collection('scorekeeper').update(record.id, { - input: newInput, - output: newOutput, - lastDecay: new Date().toISOString() - }); + // Update record directly + await client.pb.collection('scorekeeper').update(record.id, { + input: newInput, + output: newOutput, + lastDecay: new Date().toISOString() + }); - updatedCount++; - } catch (updateError) { - client.logger.error(`Error updating record ${record.id} during decay: ${updateError.message}`); - } - } + updatedCount++; + } catch (updateError) { + client.logger.error(`Error updating record ${record.id} during decay: ${updateError.message}`); + } + } - client.logger.info(`Decay completed for guild ${guildId}: ${updatedCount} records updated`); - return updatedCount; - } catch (error) { - client.logger.error(`Error running decay: ${error.message}`); - throw error; - } + const _reason = 'Automated decay'; + client.logger.info(`[module:scorekeeper] Decayed ${updatedCount} records by ${client.config.scorekeeper.decay}% (${_reason})`); + return updatedCount; + } catch (error) { + client.logger.error(`Error running decay: ${error.message}`); + throw error; + } } /** @@ -459,516 +460,516 @@ function calculateScore(data, baseOutput, commendationValue, citationValue) { * Get or create score data for a user in a guild */ async function getOrCreateScoreData(client, guildId, userId) { - try { - // Always try to get existing record first - let existingRecord = null; - const baseOutput = client.config.scorekeeper.baseOutput; + try { + // Always try to get existing record first + let existingRecord = null; + const baseOutput = client.config.scorekeeper.baseOutput; - // Try to find the record using filter - try { - existingRecord = await client.pb.collection('scorekeeper').getFirstListItem( - `guildId = "${guildId}" && userId = "${userId}"` - ); + // Try to find the record using filter + try { + existingRecord = await client.pb.collection('scorekeeper').getFirstListItem( + `guildId = "${guildId}" && userId = "${userId}"` + ); - client.logger.debug(`Found existing score record for ${userId} in guild ${guildId}`); - return existingRecord; - } catch (error) { - // Only create new record if specifically not found (404) - if (error.status === 404) { - client.logger.debug(`No existing score record found, creating new one for ${userId} in guild ${guildId}`); + client.logger.debug(`Found existing score record for ${userId} in guild ${guildId}`); + return existingRecord; + } catch (error) { + // Only create new record if specifically not found (404) + if (error.status === 404) { + client.logger.debug(`No existing score record found, creating new one for ${userId} in guild ${guildId}`); - // Create new record with default values - // Note: output is now initialized to baseOutput instead of 0 - const newData = { - guildId, - userId, - input: 0, - output: baseOutput, // Initialize to baseOutput, not 0 - commendations: 0, - citations: 0, - lastDecay: new Date().toISOString() - }; + // Create new record with default values + // Note: output is now initialized to baseOutput instead of 0 + const newData = { + guildId, + userId, + input: 0, + output: baseOutput, // Initialize to baseOutput, not 0 + commendations: 0, + citations: 0, + lastDecay: new Date().toISOString() + }; - return await client.pb.collection('scorekeeper').create(newData); - } + return await client.pb.collection('scorekeeper').create(newData); + } - // For any other error, rethrow - client.logger.error(`Error searching for score record: ${error.message}`); - throw error; - } - } catch (error) { - client.logger.error(`Error in getOrCreateScoreData: ${error.message}`); - throw error; - } + // For any other error, rethrow + client.logger.error(`Error searching for score record: ${error.message}`); + throw error; + } + } catch (error) { + client.logger.error(`Error in getOrCreateScoreData: ${error.message}`); + throw error; + } } // Define slash commands for the module export const commands = [ - // Command to view a user's score - { - data: new SlashCommandBuilder() - .setName('score') - .setDescription('View your I/O score or another user\'s I/O score') - .addUserOption(option => - option.setName('user') - .setDescription('User to check I/O score for (defaults to you)') - .setRequired(false) - ) - .addBooleanOption(option => - option.setName('ephemeral') - .setDescription('Whether the response should be ephemeral') - .setRequired(false) - ), - - execute: async (interaction, client) => { - const targetUser = interaction.options.getUser('user') || interaction.user; - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; - // Acknowledge early to avoid interaction timeout - await interaction.deferReply({ ephemeral }); - client.logger.info(`[cmd:score] Processing score for user ${targetUser.id}`); - // Wrap score retrieval and embed generation in try/catch to handle errors gracefully - try { - - // Fetch score data and compute multiplier - const baseOutput = client.config.scorekeeper.baseOutput; - const commendationValue = client.config.scorekeeper.commendationValue; - const citationValue = client.config.scorekeeper.citationValue; - const scoreData = await client.scorekeeper.getScore(interaction.guildId, targetUser.id); - const multiplierValue = 1 + (scoreData.commendations * commendationValue) - (scoreData.citations * citationValue); - // Load categories for breakdown - const categories = await client.pb.collection('scorekeeper_categories').getFullList({ - filter: `guildId = "${interaction.guildId}"` - }); - const catMap = new Map(categories.map(c => [c.id, c.name])); - // Commendations grouped by category with reasons - const commendEvents = await client.pb.collection('scorekeeper_events').getFullList({ - filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "commendation"` - }); - let commendBreakdown = 'None'; - if (commendEvents.length > 0) { - // Group events by category - const eventsByCat = new Map(); - for (const e of commendEvents) { - const arr = eventsByCat.get(e.categoryId) || []; - arr.push(e); - eventsByCat.set(e.categoryId, arr); - } - // Build breakdown string - const parts = []; - for (const [cid, events] of eventsByCat.entries()) { - const catName = catMap.get(cid) || 'Unknown'; - parts.push(`__${catName}__`); - // List each event as bullet with date and reason - for (const ev of events) { - const date = new Date(ev.created || ev.timestamp); - const shortDate = date.toLocaleDateString(); - const reason = ev.reason || ''; - parts.push(`• ${shortDate}: ${reason}`); - } - } - commendBreakdown = parts.join('\n'); - } - // Citations grouped by category with reasons - const citeEvents = await client.pb.collection('scorekeeper_events').getFullList({ - filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "citation"` - }); - let citeBreakdown = 'None'; - if (citeEvents.length > 0) { - const eventsByCat2 = new Map(); - for (const e of citeEvents) { - const arr = eventsByCat2.get(e.categoryId) || []; - arr.push(e); - eventsByCat2.set(e.categoryId, arr); - } - const parts2 = []; - for (const [cid, events] of eventsByCat2.entries()) { - const catName = catMap.get(cid) || 'Unknown'; - parts2.push(`__${catName}__`); - for (const ev of events) { - const date = new Date(ev.created || ev.timestamp); - const shortDate = date.toLocaleDateString(); - const reason = ev.reason || ''; - parts2.push(`• ${shortDate}: ${reason}`); - } - } - citeBreakdown = parts2.join('\n'); - } - const embed = new EmbedBuilder() - .setAuthor({ name: `${client.user.username}: Scorekeeper Module`, iconURL: client.user.displayAvatarURL() }) - .setTitle(`I/O Score for ${(await interaction.guild.members.fetch(targetUser.id).catch(() => null))?.displayName || targetUser.username}`) - .setColor(0x00AE86) - .setThumbnail(targetUser.displayAvatarURL()) - .setDescription('I/O is a mechanism to rank the users within a system based on the resources they put into the system and the resources they take from it. The resulting priority score can be used for a variety of dystopian purposes.') - .addFields( - { name: 'Commendations', value: commendBreakdown, inline: false }, - { name: 'Citations', value: citeBreakdown, inline: false }, - { name: 'Input', value: `${scoreData.input}`, inline: true }, - { name: 'Output', value: `${scoreData.output}`, inline: true }, - { name: 'Priority Score', value: `${scoreData.totalScore.toFixed(2)}`, inline: true }, - { name: 'Base Output', value: `-# ${baseOutput}`, inline: true }, - { name: 'Commendation Value', value: `-# ${commendationValue}`, inline: true }, - { name: 'Citation Value', value: `-# ${citationValue}`, inline: true }, - { name: 'Multiplier Formula', value: `-# 1 + (${scoreData.commendations} * ${commendationValue}) - (${scoreData.citations} * ${citationValue}) = ${multiplierValue.toFixed(2)}`, inline: false }, - { name: 'Priority Score Formula', value: `-# ${multiplierValue.toFixed(2)} × ${scoreData.input} / (${scoreData.output} + ${baseOutput}) = ${scoreData.totalScore.toFixed(2)}`, inline: false }, + // Command to view a user's score + { + data: new SlashCommandBuilder() + .setName('score') + .setDescription('View your I/O score or another user\'s I/O score') + .addUserOption(option => + option.setName('user') + .setDescription('User to check I/O score for (defaults to you)') + .setRequired(false) ) - .setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() }) - .setTimestamp(); + .addBooleanOption(option => + option.setName('ephemeral') + .setDescription('Whether the response should be ephemeral') + .setRequired(false) + ), - await interaction.editReply({ embeds: [embed] }); + execute: async (interaction, client) => { + const targetUser = interaction.options.getUser('user') || interaction.user; + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + // Acknowledge early to avoid interaction timeout + await interaction.deferReply({ ephemeral }); + client.logger.info(`[cmd:score] Processing score for user ${targetUser.id}`); + // Wrap score retrieval and embed generation in try/catch to handle errors gracefully + try { + + // Fetch score data and compute multiplier + const baseOutput = client.config.scorekeeper.baseOutput; + const commendationValue = client.config.scorekeeper.commendationValue; + const citationValue = client.config.scorekeeper.citationValue; + const scoreData = await client.scorekeeper.getScore(interaction.guildId, targetUser.id); + const multiplierValue = 1 + (scoreData.commendations * commendationValue) - (scoreData.citations * citationValue); + // Load categories for breakdown + const categories = await client.pb.collection('scorekeeper_categories').getFullList({ + filter: `guildId = "${interaction.guildId}"` + }); + const catMap = new Map(categories.map(c => [c.id, c.name])); + // Commendations grouped by category with reasons + const commendEvents = await client.pb.collection('scorekeeper_events').getFullList({ + filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "commendation"` + }); + let commendBreakdown = 'None'; + if (commendEvents.length > 0) { + // Group events by category + const eventsByCat = new Map(); + for (const e of commendEvents) { + const arr = eventsByCat.get(e.categoryId) || []; + arr.push(e); + eventsByCat.set(e.categoryId, arr); + } + // Build breakdown string + const parts = []; + for (const [cid, events] of eventsByCat.entries()) { + const catName = catMap.get(cid) || 'Unknown'; + parts.push(`__${catName}__`); + // List each event as bullet with date and reason + for (const ev of events) { + const date = new Date(ev.created || ev.timestamp); + const shortDate = date.toLocaleDateString(); + const reason = ev.reason || ''; + parts.push(`• ${shortDate}: ${reason}`); + } + } + commendBreakdown = parts.join('\n'); + } + // Citations grouped by category with reasons + const citeEvents = await client.pb.collection('scorekeeper_events').getFullList({ + filter: `guildId = "${interaction.guildId}" && userId = "${targetUser.id}" && type = "citation"` + }); + let citeBreakdown = 'None'; + if (citeEvents.length > 0) { + const eventsByCat2 = new Map(); + for (const e of citeEvents) { + const arr = eventsByCat2.get(e.categoryId) || []; + arr.push(e); + eventsByCat2.set(e.categoryId, arr); + } + const parts2 = []; + for (const [cid, events] of eventsByCat2.entries()) { + const catName = catMap.get(cid) || 'Unknown'; + parts2.push(`__${catName}__`); + for (const ev of events) { + const date = new Date(ev.created || ev.timestamp); + const shortDate = date.toLocaleDateString(); + const reason = ev.reason || ''; + parts2.push(`• ${shortDate}: ${reason}`); + } + } + citeBreakdown = parts2.join('\n'); + } + const embed = new EmbedBuilder() + .setAuthor({ name: `${client.user.username}: Scorekeeper Module`, iconURL: client.user.displayAvatarURL() }) + .setTitle(`I/O Score for ${(await interaction.guild.members.fetch(targetUser.id).catch(() => null))?.displayName || targetUser.username}`) + .setColor(0x00AE86) + .setThumbnail(targetUser.displayAvatarURL()) + .setDescription('I/O is a mechanism to rank the users within a system based on the resources they put into the system and the resources they take from it. The resulting priority score can be used for a variety of dystopian purposes.') + .addFields( + { name: 'Commendations', value: commendBreakdown, inline: false }, + { name: 'Citations', value: citeBreakdown, inline: false }, + { name: 'Input', value: `${scoreData.input}`, inline: true }, + { name: 'Output', value: `${scoreData.output}`, inline: true }, + { name: 'Priority Score', value: `${scoreData.totalScore.toFixed(2)}`, inline: true }, + { name: 'Base Output', value: `-# ${baseOutput}`, inline: true }, + { name: 'Commendation Value', value: `-# ${commendationValue}`, inline: true }, + { name: 'Citation Value', value: `-# ${citationValue}`, inline: true }, + { name: 'Multiplier Formula', value: `-# 1 + (${scoreData.commendations} * ${commendationValue}) - (${scoreData.citations} * ${citationValue}) = ${multiplierValue.toFixed(2)}`, inline: false }, + { name: 'Priority Score Formula', value: `-# ${multiplierValue.toFixed(2)} × ${scoreData.input} / (${scoreData.output} + ${baseOutput}) = ${scoreData.totalScore.toFixed(2)}`, inline: false } + ) + .setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); } catch (error) { - client.logger.error(`[cmd:score] Error: ${error.message}`); - try { - await interaction.editReply({ content: 'Failed to retrieve I/O score.' }); - } catch {} + client.logger.error(`[cmd:score] Error: ${error.message}`); + try { + await interaction.editReply({ content: 'Failed to retrieve I/O score.' }); + } catch {} } - } - }, + } + }, - // Command to view top scores - { - data: new SlashCommandBuilder() - .setName('leaderboard') - .setDescription('View the server\'s I/O score leaderboard') - .addBooleanOption(option => - option.setName('ephemeral') - .setDescription('Whether the response should be ephemeral') - .setRequired(false) - ), + // Command to view top scores + { + data: new SlashCommandBuilder() + .setName('leaderboard') + .setDescription('View the server\'s I/O score leaderboard') + .addBooleanOption(option => + option.setName('ephemeral') + .setDescription('Whether the response should be ephemeral') + .setRequired(false) + ), - execute: async (interaction, client) => { - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; - await interaction.deferReply({ ephemeral }); + execute: async (interaction, client) => { + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + await interaction.deferReply({ ephemeral }); - const limit = 10; + const limit = 10; - try { - const scores = await client.scorekeeper.getScores(interaction.guildId, limit); + try { + const scores = await client.scorekeeper.getScores(interaction.guildId, limit); - if (scores.length === 0) { - return interaction.editReply('No scores found for this server.'); - } + if (scores.length === 0) { + return interaction.editReply('No scores found for this server.'); + } - // Format leaderboard - const guild = interaction.guild; - let leaderboardText = ''; + // Format leaderboard + const guild = interaction.guild; + let leaderboardText = ''; - for (let i = 0; i < scores.length; i++) { - const score = scores[i]; - const user = await guild.members.fetch(score.userId).catch(() => null); - const displayName = user ? user.displayName : 'Unknown User'; + for (let i = 0; i < scores.length; i++) { + const score = scores[i]; + const user = await guild.members.fetch(score.userId).catch(() => null); + const displayName = user ? user.displayName : 'Unknown User'; - leaderboardText += `${i + 1}. **${displayName}**: ${score.totalScore.toFixed(2)}\n`; - } + leaderboardText += `${i + 1}. **${displayName}**: ${score.totalScore.toFixed(2)}\n`; + } - const embed = new EmbedBuilder() - .setAuthor({ name: `${client.user.username}: Scorekeeper Module`, iconURL: client.user.displayAvatarURL() }) - .setTitle('I/O Score Leaderboard') - .setColor(0x00AE86) - .setDescription(leaderboardText) - .setFooter({ text: `Showing top ${scores.length} users` }) - .setTimestamp(); + const embed = new EmbedBuilder() + .setAuthor({ name: `${client.user.username}: Scorekeeper Module`, iconURL: client.user.displayAvatarURL() }) + .setTitle('I/O Score Leaderboard') + .setColor(0x00AE86) + .setDescription(leaderboardText) + .setFooter({ text: `Showing top ${scores.length} users` }) + .setTimestamp(); - await interaction.editReply({ embeds: [embed] }); - } catch (error) { - client.logger.error(`Error in leaderboard command: ${error.message}`); - await interaction.editReply('Failed to retrieve leaderboard data.'); - } - } - }, + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + client.logger.error(`Error in leaderboard command: ${error.message}`); + await interaction.editReply('Failed to retrieve leaderboard data.'); + } + } + }, - // Command to give a commendation (admin only) - { - data: new SlashCommandBuilder() - .setName('commend') - .setDescription('Give a commendation to a user (Admin only)') - .addUserOption(option => - option.setName('user') - .setDescription('User to commend') - .setRequired(true)) - .addStringOption(option => - option.setName('category') - .setDescription('Category to award') - .setRequired(true) - .setAutocomplete(true) - ) - .addStringOption(option => - option.setName('reason') - .setDescription('Reason for commendation') - .setRequired(true) - ) + // Command to give a commendation (admin only) + { + data: new SlashCommandBuilder() + .setName('commend') + .setDescription('Give a commendation to a user (Admin only)') + .addUserOption(option => + option.setName('user') + .setDescription('User to commend') + .setRequired(true)) + .addStringOption(option => + option.setName('category') + .setDescription('Category to award') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for commendation') + .setRequired(true) + ) .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - execute: async (interaction, client) => { - const guildId = interaction.guildId; - // Ensure categories exist before proceeding - let catList = []; - try { - catList = await client.pb.collection('scorekeeper_categories').getFullList({ - filter: `guildId = "${guildId}"` - }); - } catch {} - if (catList.length === 0) { - return interaction.reply({ - content: 'No categories defined for this server. Ask an admin to create one with /addcategory.', - ephemeral: true - }); - } - const targetUser = interaction.options.getUser('user'); - const categoryId = interaction.options.getString('category'); - const reason = interaction.options.getString('reason'); - const amount = 1; - // Enforce per-category cooldown - const cooldown = client.config.scorekeeper.cooldown || 0; - if (cooldown > 0) { - const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, { - filter: `guildId = \"${guildId}\" && userId = \"${targetUser.id}\" && type = \"commendation\" && categoryId = \"${categoryId}\"`, - sort: '-created' - }); - const lastItem = recent.items?.[0]; - if (lastItem) { - const lastTs = new Date(lastItem.created).getTime(); - const elapsed = Date.now() - lastTs; - if (elapsed < cooldown) { - const expireTs = lastTs + cooldown; - const expireSec = Math.ceil(expireTs / 1000); - const categoryRecord = catList.find(c => c.id === categoryId); - const categoryName = categoryRecord ? categoryRecord.name : categoryId; - return interaction.reply({ - content: `${targetUser} cannot receive another commendation in the ${categoryName} category for .`, - ephemeral: true - }); - } - } - } + execute: async (interaction, client) => { + const guildId = interaction.guildId; + // Ensure categories exist before proceeding + let catList = []; + try { + catList = await client.pb.collection('scorekeeper_categories').getFullList({ + filter: `guildId = "${guildId}"` + }); + } catch {} + if (catList.length === 0) { + return interaction.reply({ + content: 'No categories defined for this server. Ask an admin to create one with /addcategory.', + ephemeral: true + }); + } + const targetUser = interaction.options.getUser('user'); + const categoryId = interaction.options.getString('category'); + const reason = interaction.options.getString('reason'); + const amount = 1; + // Enforce per-category cooldown + const cooldown = client.config.scorekeeper.cooldown || 0; + if (cooldown > 0) { + const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, { + filter: `guildId = "${guildId}" && userId = "${targetUser.id}" && type = "commendation" && categoryId = "${categoryId}"`, + sort: '-created' + }); + const lastItem = recent.items?.[0]; + if (lastItem) { + const lastTs = new Date(lastItem.created).getTime(); + const elapsed = Date.now() - lastTs; + if (elapsed < cooldown) { + const expireTs = lastTs + cooldown; + const expireSec = Math.ceil(expireTs / 1000); + const categoryRecord = catList.find(c => c.id === categoryId); + const categoryName = categoryRecord ? categoryRecord.name : categoryId; + return interaction.reply({ + content: `${targetUser} cannot receive another commendation in the ${categoryName} category for .`, + ephemeral: true + }); + } + } + } - try { - await client.scorekeeper.addCommendation(interaction.guildId, targetUser.id, amount); - // Log event - // Log event (timestamp managed by PocketBase "created" field) - await client.pb.collection('scorekeeper_events').create({ - guildId: interaction.guildId, - userId: targetUser.id, - type: 'commendation', - categoryId, - amount, - reason, - awardedBy: interaction.user.id - }); + try { + await client.scorekeeper.addCommendation(interaction.guildId, targetUser.id, amount); + // Log event + // Log event (timestamp managed by PocketBase "created" field) + await client.pb.collection('scorekeeper_events').create({ + guildId: interaction.guildId, + userId: targetUser.id, + type: 'commendation', + categoryId, + amount, + reason, + awardedBy: interaction.user.id + }); - client.logger.info(`[cmd:commend] Added commendation to ${targetUser.id} in category ${categoryId} with reason: ${reason}`); - await interaction.reply(`Added commendation to ${targetUser}.`); - } catch (error) { - client.logger.error(`Error in commend command: ${error.message}`); - await interaction.reply({ - content: 'Failed to add commendation.', - ephemeral: true - }); - } - } - }, - - // Command to give a citation (admin only) - { - data: new SlashCommandBuilder() - .setName('cite') - .setDescription('Give a citation to a user (Admin only)') - .addUserOption(option => - option.setName('user') - .setDescription('User to cite') - .setRequired(true)) - .addStringOption(option => - option.setName('category') - .setDescription('Category to award') - .setRequired(true) - .setAutocomplete(true) - ) - .addStringOption(option => - option.setName('reason') - .setDescription('Reason for citation') - .setRequired(true) - ) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - - execute: async (interaction, client) => { - const guildId = interaction.guildId; - // Ensure categories exist before proceeding - let catList = []; - try { - catList = await client.pb.collection('scorekeeper_categories').getFullList({ - filter: `guildId = "${guildId}"` - }); - } catch {} - if (catList.length === 0) { - return interaction.reply({ - content: 'No categories defined for this server. Ask an admin to create one with /addcategory.', - ephemeral: true - }); - } - const targetUser = interaction.options.getUser('user'); - const categoryId = interaction.options.getString('category'); - const amount = 1; - // Enforce per-category cooldown - const cooldown = client.config.scorekeeper.cooldown || 0; - if (cooldown > 0) { - const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, { - filter: `guildId = \"${guildId}\" && userId = \"${targetUser.id}\" && type = \"citation\" && categoryId = \"${categoryId}\"`, - sort: '-created' - }); - const lastItem = recent.items?.[0]; - if (lastItem) { - const lastTs = new Date(lastItem.created).getTime(); - const elapsed = Date.now() - lastTs; - if (elapsed < cooldown) { - const expireTs = lastTs + cooldown; - const expireSec = Math.ceil(expireTs / 1000); - const categoryRecord = catList.find(c => c.id === categoryId); - const categoryName = categoryRecord ? categoryRecord.name : categoryId; - return interaction.reply({ - content: `${targetUser} cannot receive another citation in the ${categoryName} category for .`, - ephemeral: true - }); - } - } - } - - try { - await client.scorekeeper.addCitation(interaction.guildId, targetUser.id, amount); - // Log event - // Log event (timestamp managed by PocketBase "created" field) - await client.pb.collection('scorekeeper_events').create({ - guildId: interaction.guildId, - userId: targetUser.id, - type: 'citation', - categoryId, - amount, - reason, - awardedBy: interaction.user.id - }); - - client.logger.info(`[cmd:cite] Added citation to ${targetUser.id} in category ${categoryId} with reason: ${reason}`); - await interaction.reply(`Added citation to ${targetUser}.`); - } catch (error) { - client.logger.error(`Error in cite command: ${error.message}`); - await interaction.reply({ - content: 'Failed to add citation.', - ephemeral: true - }); - } - } - }, - - // Command to manually run decay (admin only) - { - data: new SlashCommandBuilder() - .setName('run-decay') - .setDescription('Manually run score decay (Admin only)') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - - execute: async (interaction, client) => { - await interaction.deferReply(); - - try { - const updatedCount = await client.scorekeeper.runDecay(interaction.guildId); - - await interaction.editReply(`Score decay completed. Updated ${updatedCount} user records.`); - } catch (error) { - client.logger.error(`Error in run-decay command: ${error.message}`); - await interaction.editReply('Failed to run score decay.'); - } - } - } - // Admin command: add a new category - ,{ - data: new SlashCommandBuilder() - .setName('addcategory') - .setDescription('Create a new commendation/citation category (Admin only)') - .addStringOption(opt => - opt.setName('name') - .setDescription('Name of the new category') - .setRequired(true) - ) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - execute: async (interaction, client) => { - const name = interaction.options.getString('name').trim(); - const guildId = interaction.guildId; - try { - // Check for existing - const existing = await client.pb.collection('scorekeeper_categories').getFirstListItem( - `guildId = "${guildId}" && name = "${name}"` - ).catch(() => null); - if (existing) { - return interaction.reply({ content: `Category '${name}' already exists.`, ephemeral: true }); + client.logger.info(`[cmd:commend] Added commendation to ${targetUser.id} in category ${categoryId} with reason: ${reason}`); + await interaction.reply(`Added commendation to ${targetUser}.`); + } catch (error) { + client.logger.error(`Error in commend command: ${error.message}`); + await interaction.reply({ + content: 'Failed to add commendation.', + ephemeral: true + }); + } } - // Create new category - await client.pb.collection('scorekeeper_categories').create({ - guildId, - name, - createdBy: interaction.user.id, - createdAt: new Date().toISOString() - }); - await interaction.reply({ content: `Category '${name}' created.`, ephemeral: true }); - } catch (err) { - client.logger.error(`Error in addcategory: ${err.message}`); - await interaction.reply({ content: 'Failed to create category.', flags: MessageFlags.Ephemeral}); - } - } - } - // Admin command: remove a category - ,{ - data: new SlashCommandBuilder() - .setName('removecategory') - .setDescription('Delete a commendation/citation category (Admin only)') - .addStringOption(opt => - opt.setName('name') - .setDescription('Name of the category to remove') - .setRequired(true) - ) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - execute: async (interaction, client) => { - const name = interaction.options.getString('name').trim(); - const guildId = interaction.guildId; - try { - const record = await client.pb.collection('scorekeeper_categories').getFirstListItem( - `guildId = "${guildId}" && name = "${name}"` - ).catch(() => null); - if (!record) { - return interaction.reply({ content: `Category '${name}' not found.`, ephemeral: true }); + }, + + // Command to give a citation (admin only) + { + data: new SlashCommandBuilder() + .setName('cite') + .setDescription('Give a citation to a user (Admin only)') + .addUserOption(option => + option.setName('user') + .setDescription('User to cite') + .setRequired(true)) + .addStringOption(option => + option.setName('category') + .setDescription('Category to award') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for citation') + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction, client) => { + const guildId = interaction.guildId; + // Ensure categories exist before proceeding + let catList = []; + try { + catList = await client.pb.collection('scorekeeper_categories').getFullList({ + filter: `guildId = "${guildId}"` + }); + } catch {} + if (catList.length === 0) { + return interaction.reply({ + content: 'No categories defined for this server. Ask an admin to create one with /addcategory.', + ephemeral: true + }); + } + const targetUser = interaction.options.getUser('user'); + const categoryId = interaction.options.getString('category'); + const reason = interaction.options.getString('reason'); + const amount = 1; + // Enforce per-category cooldown + const cooldown = client.config.scorekeeper.cooldown || 0; + if (cooldown > 0) { + const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, { + filter: `guildId = "${guildId}" && userId = "${targetUser.id}" && type = "citation" && categoryId = "${categoryId}"`, + sort: '-created' + }); + const lastItem = recent.items?.[0]; + if (lastItem) { + const lastTs = new Date(lastItem.created).getTime(); + const elapsed = Date.now() - lastTs; + if (elapsed < cooldown) { + const expireTs = lastTs + cooldown; + const expireSec = Math.ceil(expireTs / 1000); + const categoryRecord = catList.find(c => c.id === categoryId); + const categoryName = categoryRecord ? categoryRecord.name : categoryId; + return interaction.reply({ + content: `${targetUser} cannot receive another citation in the ${categoryName} category for .`, + ephemeral: true + }); + } + } + } + + try { + await client.scorekeeper.addCitation(interaction.guildId, targetUser.id, amount); + // Log event + await client.pb.collection('scorekeeper_events').create({ + guildId: interaction.guildId, + userId: targetUser.id, + type: 'citation', + categoryId, + amount, + reason, + awardedBy: interaction.user.id + }); + + client.logger.info(`[cmd:cite] Added citation to ${targetUser.id} in category ${categoryId} with reason: ${reason}`); + await interaction.reply(`Added citation to ${targetUser}.`); + } catch (error) { + client.logger.error(`Error in cite command: ${error.message}`); + await interaction.reply({ + content: 'Failed to add citation.', + ephemeral: true + }); + } } - await client.pb.collection('scorekeeper_categories').delete(record.id); - await interaction.reply({ content: `Category '${name}' removed.`, ephemeral: true }); - } catch (err) { - client.logger.error(`Error in removecategory: ${err.message}`); - await interaction.reply({ content: 'Failed to remove category.', flags: MessageFlags.Ephemeral}); - } - } - } - // Public command: list categories (admin-only) - ,{ - data: new SlashCommandBuilder() - .setName('listcategories') - .setDescription('List all commendation/citation categories (Admin only)') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addBooleanOption(opt => - opt.setName('ephemeral') - .setDescription('Whether the result should be ephemeral') - .setRequired(false) - ), - execute: async (interaction, client) => { - const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; - const guildId = interaction.guildId; - try { - const records = await client.pb.collection('scorekeeper_categories').getFullList({ filter: `guildId = "${guildId}"` }); - if (records.length === 0) { - return interaction.reply({ content: 'No categories defined for this guild.', ephemeral }); + }, + + // Command to manually run decay (admin only) + { + data: new SlashCommandBuilder() + .setName('run-decay') + .setDescription('Manually run score decay (Admin only)') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction, client) => { + await interaction.deferReply(); + + try { + const updatedCount = await client.scorekeeper.runDecay(interaction.guildId); + + await interaction.editReply(`Score decay completed. Updated ${updatedCount} user records.`); + } catch (error) { + client.logger.error(`Error in run-decay command: ${error.message}`); + await interaction.editReply('Failed to run score decay.'); + } + } + } + // Admin command: add a new category + ,{ + data: new SlashCommandBuilder() + .setName('addcategory') + .setDescription('Create a new commendation/citation category (Admin only)') + .addStringOption(opt => + opt.setName('name') + .setDescription('Name of the new category') + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction, client) => { + const name = interaction.options.getString('name').trim(); + const guildId = interaction.guildId; + try { + // Check for existing + const existing = await client.pb.collection('scorekeeper_categories').getFirstListItem( + `guildId = "${guildId}" && name = "${name}"` + ).catch(() => null); + if (existing) { + return interaction.reply({ content: `Category '${name}' already exists.`, ephemeral: true }); + } + // Create new category + await client.pb.collection('scorekeeper_categories').create({ + guildId, + name, + createdBy: interaction.user.id, + createdAt: new Date().toISOString() + }); + await interaction.reply({ content: `Category '${name}' created.`, ephemeral: true }); + } catch (err) { + client.logger.error(`Error in addcategory: ${err.message}`); + await interaction.reply({ content: 'Failed to create category.', flags: MessageFlags.Ephemeral }); + } + } + } + // Admin command: remove a category + ,{ + data: new SlashCommandBuilder() + .setName('removecategory') + .setDescription('Delete a commendation/citation category (Admin only)') + .addStringOption(opt => + opt.setName('name') + .setDescription('Name of the category to remove') + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction, client) => { + const name = interaction.options.getString('name').trim(); + const guildId = interaction.guildId; + try { + const record = await client.pb.collection('scorekeeper_categories').getFirstListItem( + `guildId = "${guildId}" && name = "${name}"` + ).catch(() => null); + if (!record) { + return interaction.reply({ content: `Category '${name}' not found.`, ephemeral: true }); + } + await client.pb.collection('scorekeeper_categories').delete(record.id); + await interaction.reply({ content: `Category '${name}' removed.`, ephemeral: true }); + } catch (err) { + client.logger.error(`Error in removecategory: ${err.message}`); + await interaction.reply({ content: 'Failed to remove category.', flags: MessageFlags.Ephemeral }); + } + } + } + // Public command: list categories (admin-only) + ,{ + data: new SlashCommandBuilder() + .setName('listcategories') + .setDescription('List all commendation/citation categories (Admin only)') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addBooleanOption(opt => + opt.setName('ephemeral') + .setDescription('Whether the result should be ephemeral') + .setRequired(false) + ), + execute: async (interaction, client) => { + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + const guildId = interaction.guildId; + try { + const records = await client.pb.collection('scorekeeper_categories').getFullList({ filter: `guildId = "${guildId}"` }); + if (records.length === 0) { + return interaction.reply({ content: 'No categories defined for this guild.', ephemeral }); + } + const list = records.map(r => r.name).join(', '); + await interaction.reply({ content: `Categories: ${list}`, ephemeral }); + } catch (err) { + client.logger.error(`Error in listcategories: ${err.message}`); + await interaction.reply({ content: 'Failed to list categories.', ephemeral }); + } } - const list = records.map(r => r.name).join(', '); - await interaction.reply({ content: `Categories: ${list}`, ephemeral }); - } catch (err) { - client.logger.error(`Error in listcategories: ${err.message}`); - await interaction.reply({ content: 'Failed to list categories.', ephemeral }); - } } - } ]; /** * Attach autocomplete handlers for category options in commend and cite commands. @@ -979,25 +980,25 @@ export const commands = [ * @param {import('discord.js').Client} client - The Discord client. */ export function registerCategoryAutocomplete(client) { - client.on('interactionCreate', async interaction => { - if (!interaction.isAutocomplete()) return; - const cmd = interaction.commandName; - if (cmd !== 'commend' && cmd !== 'cite') return; - const focused = interaction.options.getFocused(true); - if (focused.name !== 'category') return; - const guildId = interaction.guildId; - try { - const records = await client.pb.collection('scorekeeper_categories').getFullList({ - filter: `guildId = "${guildId}"` - }); - const choices = records - .filter(r => r.name.toLowerCase().startsWith(focused.value.toLowerCase())) - .slice(0, 25) - .map(r => ({ name: r.name, value: r.id })); - await interaction.respond(choices); - } catch (error) { - client.logger.error(`Category autocomplete error: ${error.message}`); - await interaction.respond([]); - } - }); + client.on('interactionCreate', async interaction => { + if (!interaction.isAutocomplete()) return; + const cmd = interaction.commandName; + if (cmd !== 'commend' && cmd !== 'cite') return; + const focused = interaction.options.getFocused(true); + if (focused.name !== 'category') return; + const guildId = interaction.guildId; + try { + const records = await client.pb.collection('scorekeeper_categories').getFullList({ + filter: `guildId = "${guildId}"` + }); + const choices = records + .filter(r => r.name.toLowerCase().startsWith(focused.value.toLowerCase())) + .slice(0, 25) + .map(r => ({ name: r.name, value: r.id })); + await interaction.respond(choices); + } catch (error) { + client.logger.error(`Category autocomplete error: ${error.message}`); + await interaction.respond([]); + } + }); } diff --git a/_opt/tempvc.js b/_opt/tempvc.js index 7d2b9a6..ac8ee6f 100644 --- a/_opt/tempvc.js +++ b/_opt/tempvc.js @@ -1,5 +1,5 @@ -import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js'; import { MessageFlags } from 'discord-api-types/v10'; +import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js'; // Init function to handle autocomplete for /vc invite /** * tempvc module: temporary voice channel manager @@ -39,639 +39,639 @@ import { MessageFlags } from 'discord-api-types/v10'; * Slash commands for vcadmin and vc */ export const commands = [ - // Administrator: manage spawn points - { - data: new SlashCommandBuilder() - .setName('vcadmin') - .setDescription('Configure temporary voice-channel spawn points (Admin only)') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .setDMPermission(false) - .addSubcommand(sub => - sub.setName('add') - .setDescription('Add a spawn voice channel and its temp category') - .addChannelOption(opt => - opt.setName('voice_channel') - .setDescription('Voice channel to spawn from') - .setRequired(true) - .addChannelTypes(ChannelType.GuildVoice) - ) - .addChannelOption(opt => - opt.setName('category') - .setDescription('Category for new temp channels') - .setRequired(true) - .addChannelTypes(ChannelType.GuildCategory) - ) - ) - .addSubcommand(sub => - sub.setName('remove') - .setDescription('Remove a spawn voice channel') - .addChannelOption(opt => - opt.setName('voice_channel') - .setDescription('Voice channel to remove') - .setRequired(true) - .addChannelTypes(ChannelType.GuildVoice) - ) - ) - .addSubcommand(sub => - sub.setName('list') - .setDescription('List all spawn voice channels and categories') - ), - async execute(interaction, client) { - const guildId = interaction.guildId; - const sub = interaction.options.getSubcommand(); - // ensure in-guild - if (!guildId) { - return interaction.reply({ content: 'This command can only be used in a server.', flags: MessageFlags.Ephemeral }); - } - // init memory map for this guild - client.tempvc = client.tempvc || { masters: new Map(), sessions: new Map() }; - if (!client.tempvc.masters.has(guildId)) { - client.tempvc.masters.set(guildId, new Map()); - } - const guildMasters = client.tempvc.masters.get(guildId); - try { - if (sub === 'add') { - const vc = interaction.options.getChannel('voice_channel', true); - const cat = interaction.options.getChannel('category', true); - // persist - const existing = await client.pb.getFirst( - 'tempvc_masters', - `guildId = "${guildId}" && masterChannelId = "${vc.id}"` - ); - if (existing) { - await client.pb.updateOne('tempvc_masters', existing.id, { - guildId, masterChannelId: vc.id, categoryId: cat.id - }); - } else { - await client.pb.createOne('tempvc_masters', { - guildId, masterChannelId: vc.id, categoryId: cat.id - }); - } - // update memory - guildMasters.set(vc.id, cat.id); - await interaction.reply({ - content: `Spawn channel <#${vc.id}> will now create temp VCs in <#${cat.id}>.`, - flags: MessageFlags.Ephemeral - }); - } else if (sub === 'remove') { - const vc = interaction.options.getChannel('voice_channel', true); - if (!guildMasters.has(vc.id)) { - return interaction.reply({ content: 'That channel is not configured as a spawn point.', flags: MessageFlags.Ephemeral }); - } - // remove from PB - const existing = await client.pb.getFirst( - 'tempvc_masters', - `guildId = "${guildId}" && masterChannelId = "${vc.id}"` - ); - if (existing) { - await client.pb.deleteOne('tempvc_masters', existing.id); - } - // update memory - guildMasters.delete(vc.id); - await interaction.reply({ content: `Removed spawn channel <#${vc.id}>.`, flags: MessageFlags.Ephemeral }); - } else if (sub === 'list') { - if (guildMasters.size === 0) { - return interaction.reply({ content: 'No spawn channels configured.', flags: MessageFlags.Ephemeral }); - } - const lines = []; - for (const [mId, cId] of guildMasters.entries()) { - lines.push(`<#${mId}> → <#${cId}>`); - } - await interaction.reply({ content: '**Spawn channels:**\n' + lines.join('\n'), flags: MessageFlags.Ephemeral }); - } - } catch (err) { - client.logger.error(`[module:tempvc][vcadmin] ${err.message}`); - await interaction.reply({ content: 'Operation failed, see logs.', flags: MessageFlags.Ephemeral }); - } - } - }, - // User: manage own temp VC and presets - { - data: new SlashCommandBuilder() - .setName('vc') - .setDescription('Manage your temporary voice channel') - .setDMPermission(false) - // Access Control - .addSubcommand(sub => - sub.setName('invite') - .setDescription('Invite a user to this channel') - // Autocomplete string option for user ID - .addStringOption(opt => - opt.setName('user') - .setDescription('User to invite') - .setRequired(true) - .setAutocomplete(true) - ) - ) - .addSubcommand(sub => - sub.setName('kick') - .setDescription('Kick a user from this channel') - .addUserOption(opt => opt.setName('user').setDescription('User to kick').setRequired(true)) - ) - .addSubcommand(sub => - sub.setName('role') - .setDescription('Set role to allow/deny access') - .addRoleOption(opt => opt.setName('role').setDescription('Role to allow/deny').setRequired(true)) - ) - .addSubcommand(sub => - sub.setName('mode') - .setDescription('Switch role mode') - .addStringOption(opt => - opt.setName('mode') - .setDescription('Mode: whitelist or blacklist') - .setRequired(true) - .addChoices( - { name: 'whitelist', value: 'whitelist' }, - { name: 'blacklist', value: 'blacklist' } - ) - ) - ) - .addSubcommand(sub => - sub.setName('limit') - .setDescription('Set user limit (0–99)') - .addIntegerOption(opt => opt.setName('number').setDescription('Max users').setRequired(true)) - ) - // Presets - .addSubcommand(sub => - sub.setName('save') - .setDescription('Save current settings as a preset') - .addStringOption(opt => opt.setName('name').setDescription('Preset name').setRequired(true).setAutocomplete(true)) - ) - .addSubcommand(sub => - sub.setName('restore') - .setDescription('Restore settings from a preset') - .addStringOption(opt => opt.setName('name').setDescription('Preset name').setRequired(true).setAutocomplete(true)) - ) - .addSubcommand(sub => sub.setName('reset').setDescription('Reset channel to default settings')) - // Utilities - .addSubcommand(sub => sub.setName('rename').setDescription('Rename this channel').addStringOption(opt => opt.setName('new_name').setDescription('New channel name').setRequired(true))) - .addSubcommand(sub => sub.setName('info').setDescription('Show channel info')) - .addSubcommand(sub => sub.setName('delete').setDescription('Delete this channel')), - async execute(interaction, client) { - const guild = interaction.guild; - const member = interaction.member; - const sub = interaction.options.getSubcommand(); - // must be in guild and in voice - if (!guild || !member || !member.voice.channel) { - return interaction.reply({ content: 'You must be in a temp voice channel to use this.', flags: MessageFlags.Ephemeral }); - } - const voice = member.voice.channel; - client.tempvc = client.tempvc || { masters: new Map(), sessions: new Map() }; - const sess = client.tempvc.sessions.get(voice.id); - if (!sess) { - return interaction.reply({ content: 'This is not one of my temporary channels.', flags: MessageFlags.Ephemeral }); - } - if (sess.ownerId !== interaction.user.id) { - return interaction.reply({ content: 'Only the room owner can do that.', flags: MessageFlags.Ephemeral }); - } - try { - if (sub === 'rename') { - const name = interaction.options.getString('new_name', true); - await voice.setName(name); - await interaction.reply({ content: `Channel renamed to **${name}**.`, flags: MessageFlags.Ephemeral }); - } else if (sub === 'invite') { - // Invitation: support both string (autocomplete) and user option types - let userId; - let memberToInvite; - // Try string option first (autocomplete) - try { - userId = interaction.options.getString('user', true); - memberToInvite = await guild.members.fetch(userId); - } catch (e) { - // Fallback to user option + // Administrator: manage spawn points + { + data: new SlashCommandBuilder() + .setName('vcadmin') + .setDescription('Configure temporary voice-channel spawn points (Admin only)') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false) + .addSubcommand(sub => + sub.setName('add') + .setDescription('Add a spawn voice channel and its temp category') + .addChannelOption(opt => + opt.setName('voice_channel') + .setDescription('Voice channel to spawn from') + .setRequired(true) + .addChannelTypes(ChannelType.GuildVoice) + ) + .addChannelOption(opt => + opt.setName('category') + .setDescription('Category for new temp channels') + .setRequired(true) + .addChannelTypes(ChannelType.GuildCategory) + ) + ) + .addSubcommand(sub => + sub.setName('remove') + .setDescription('Remove a spawn voice channel') + .addChannelOption(opt => + opt.setName('voice_channel') + .setDescription('Voice channel to remove') + .setRequired(true) + .addChannelTypes(ChannelType.GuildVoice) + ) + ) + .addSubcommand(sub => + sub.setName('list') + .setDescription('List all spawn voice channels and categories') + ), + async execute(interaction, client) { + const guildId = interaction.guildId; + const sub = interaction.options.getSubcommand(); + // ensure in-guild + if (!guildId) { + return interaction.reply({ content: 'This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + } + // init memory map for this guild + client.tempvc = client.tempvc || { masters: new Map(), sessions: new Map() }; + if (!client.tempvc.masters.has(guildId)) { + client.tempvc.masters.set(guildId, new Map()); + } + const guildMasters = client.tempvc.masters.get(guildId); try { - const user = interaction.options.getUser('user', true); - userId = user.id; - memberToInvite = await guild.members.fetch(userId); - } catch { - memberToInvite = null; + if (sub === 'add') { + const vc = interaction.options.getChannel('voice_channel', true); + const cat = interaction.options.getChannel('category', true); + // persist + const existing = await client.pb.getFirst( + 'tempvc_masters', + `guildId = "${guildId}" && masterChannelId = "${vc.id}"` + ); + if (existing) { + await client.pb.updateOne('tempvc_masters', existing.id, { + guildId, masterChannelId: vc.id, categoryId: cat.id + }); + } else { + await client.pb.createOne('tempvc_masters', { + guildId, masterChannelId: vc.id, categoryId: cat.id + }); + } + // update memory + guildMasters.set(vc.id, cat.id); + await interaction.reply({ + content: `Spawn channel <#${vc.id}> will now create temp VCs in <#${cat.id}>.`, + flags: MessageFlags.Ephemeral + }); + } else if (sub === 'remove') { + const vc = interaction.options.getChannel('voice_channel', true); + if (!guildMasters.has(vc.id)) { + return interaction.reply({ content: 'That channel is not configured as a spawn point.', flags: MessageFlags.Ephemeral }); + } + // remove from PB + const existing = await client.pb.getFirst( + 'tempvc_masters', + `guildId = "${guildId}" && masterChannelId = "${vc.id}"` + ); + if (existing) { + await client.pb.deleteOne('tempvc_masters', existing.id); + } + // update memory + guildMasters.delete(vc.id); + await interaction.reply({ content: `Removed spawn channel <#${vc.id}>.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'list') { + if (guildMasters.size === 0) { + return interaction.reply({ content: 'No spawn channels configured.', flags: MessageFlags.Ephemeral }); + } + const lines = []; + for (const [mId, cId] of guildMasters.entries()) { + lines.push(`<#${mId}> → <#${cId}>`); + } + await interaction.reply({ content: '**Spawn channels:**\n' + lines.join('\n'), flags: MessageFlags.Ephemeral }); + } + } catch (err) { + client.logger.error(`[module:tempvc][vcadmin] ${err.message}`); + await interaction.reply({ content: 'Operation failed, see logs.', flags: MessageFlags.Ephemeral }); + } + } + }, + // User: manage own temp VC and presets + { + data: new SlashCommandBuilder() + .setName('vc') + .setDescription('Manage your temporary voice channel') + .setDMPermission(false) + // Access Control + .addSubcommand(sub => + sub.setName('invite') + .setDescription('Invite a user to this channel') + // Autocomplete string option for user ID + .addStringOption(opt => + opt.setName('user') + .setDescription('User to invite') + .setRequired(true) + .setAutocomplete(true) + ) + ) + .addSubcommand(sub => + sub.setName('kick') + .setDescription('Kick a user from this channel') + .addUserOption(opt => opt.setName('user').setDescription('User to kick').setRequired(true)) + ) + .addSubcommand(sub => + sub.setName('role') + .setDescription('Set role to allow/deny access') + .addRoleOption(opt => opt.setName('role').setDescription('Role to allow/deny').setRequired(true)) + ) + .addSubcommand(sub => + sub.setName('mode') + .setDescription('Switch role mode') + .addStringOption(opt => + opt.setName('mode') + .setDescription('Mode: whitelist or blacklist') + .setRequired(true) + .addChoices( + { name: 'whitelist', value: 'whitelist' }, + { name: 'blacklist', value: 'blacklist' } + ) + ) + ) + .addSubcommand(sub => + sub.setName('limit') + .setDescription('Set user limit (0–99)') + .addIntegerOption(opt => opt.setName('number').setDescription('Max users').setRequired(true)) + ) + // Presets + .addSubcommand(sub => + sub.setName('save') + .setDescription('Save current settings as a preset') + .addStringOption(opt => opt.setName('name').setDescription('Preset name').setRequired(true).setAutocomplete(true)) + ) + .addSubcommand(sub => + sub.setName('restore') + .setDescription('Restore settings from a preset') + .addStringOption(opt => opt.setName('name').setDescription('Preset name').setRequired(true).setAutocomplete(true)) + ) + .addSubcommand(sub => sub.setName('reset').setDescription('Reset channel to default settings')) + // Utilities + .addSubcommand(sub => sub.setName('rename').setDescription('Rename this channel').addStringOption(opt => opt.setName('new_name').setDescription('New channel name').setRequired(true))) + .addSubcommand(sub => sub.setName('info').setDescription('Show channel info')) + .addSubcommand(sub => sub.setName('delete').setDescription('Delete this channel')), + async execute(interaction, client) { + const guild = interaction.guild; + const member = interaction.member; + const sub = interaction.options.getSubcommand(); + // must be in guild and in voice + if (!guild || !member || !member.voice.channel) { + return interaction.reply({ content: 'You must be in a temp voice channel to use this.', flags: MessageFlags.Ephemeral }); + } + const voice = member.voice.channel; + client.tempvc = client.tempvc || { masters: new Map(), sessions: new Map() }; + const sess = client.tempvc.sessions.get(voice.id); + if (!sess) { + return interaction.reply({ content: 'This is not one of my temporary channels.', flags: MessageFlags.Ephemeral }); + } + if (sess.ownerId !== interaction.user.id) { + return interaction.reply({ content: 'Only the room owner can do that.', flags: MessageFlags.Ephemeral }); + } + try { + if (sub === 'rename') { + const name = interaction.options.getString('new_name', true); + await voice.setName(name); + await interaction.reply({ content: `Channel renamed to **${name}**.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'invite') { + // Invitation: support both string (autocomplete) and user option types + let userId; + let memberToInvite; + // Try string option first (autocomplete) + try { + userId = interaction.options.getString('user', true); + memberToInvite = await guild.members.fetch(userId); + } catch (e) { + // Fallback to user option + try { + const user = interaction.options.getUser('user', true); + userId = user.id; + memberToInvite = await guild.members.fetch(userId); + } catch { + memberToInvite = null; + } + } + if (!memberToInvite) { + return interaction.reply({ content: 'User not found in this server.', flags: MessageFlags.Ephemeral }); + } + // grant view and connect + await voice.permissionOverwrites.edit(userId, { ViewChannel: true, Connect: true }); + await interaction.reply({ content: `Invited <@${userId}>.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'kick') { + const u = interaction.options.getUser('user', true); + const gm = await guild.members.fetch(u.id); + // move them out if in this channel + if (gm.voice.channelId === voice.id) { + await gm.voice.setChannel(null); + } + // remove any previous invite allow + try { + await voice.permissionOverwrites.delete(u.id); + } catch {} + await interaction.reply({ content: `Kicked <@${u.id}>.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'limit') { + const num = interaction.options.getInteger('number', true); + // enforce range 0-99 + if (num < 0 || num > 99) { + return interaction.reply({ content: 'User limit must be between 0 (no limit) and 99.', flags: MessageFlags.Ephemeral }); + } + await voice.setUserLimit(num); + await interaction.reply({ content: `User limit set to ${num}.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'role') { + const newRole = interaction.options.getRole('role', true); + const oldRoleId = sess.roleId; + // remove old role overwrite if any + if (oldRoleId && oldRoleId !== guild.roles.everyone.id) { + await voice.permissionOverwrites.delete(oldRoleId).catch(() => {}); + } + // selecting @everyone resets all + if (newRole.id === guild.roles.everyone.id) { + // clear all overwrites + await voice.permissionOverwrites.set([ + { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.Connect] }, + { id: sess.ownerId, allow: [PermissionFlagsBits.Connect, PermissionFlagsBits.MoveMembers, PermissionFlagsBits.ManageChannels] } + ]); + sess.roleId = ''; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: '', mode: sess.mode }); + return interaction.reply({ content: '@everyone can now connect.', flags: MessageFlags.Ephemeral }); + } + if (sess.mode === 'whitelist') { + // whitelist: lock everyone, allow role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: false }); + await voice.permissionOverwrites.edit(newRole.id, { Connect: true }); + sess.roleId = newRole.id; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: newRole.id, mode: sess.mode }); + await interaction.reply({ content: `Whitelisted role <@&${newRole.id}>.`, flags: MessageFlags.Ephemeral }); + } else { + // blacklist: allow everyone, deny role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: true }); + await voice.permissionOverwrites.edit(newRole.id, { Connect: false }); + sess.roleId = newRole.id; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: newRole.id, mode: sess.mode }); + await interaction.reply({ content: `Blacklisted role <@&${newRole.id}>.`, flags: MessageFlags.Ephemeral }); + } + } else if (sub === 'delete') { + await interaction.reply({ content: 'Deleting your channel...', flags: MessageFlags.Ephemeral }); + await client.pb.deleteOne('tempvc_sessions', sess.pbId); + client.tempvc.sessions.delete(voice.id); + await voice.delete('Owner deleted temp VC'); + } else if (sub === 'info') { + const invites = voice.permissionOverwrites.cache + .filter(po => po.allow.has(PermissionFlagsBits.Connect) && ![guild.roles.everyone.id, sess.roleId].includes(po.id)) + .map(po => `<@${po.id}>`); + const everyoneId = guild.roles.everyone.id; + const roleLine = (!sess.roleId || sess.roleId === everyoneId) + ? '@everyone' + : `<@&${sess.roleId}>`; + const modeLine = sess.mode || 'whitelist'; + const lines = [ + `Owner: <@${sess.ownerId}>`, + `Name: ${voice.name}`, + `Role: ${roleLine} (${modeLine})`, + `User limit: ${voice.userLimit}`, + `Invites: ${invites.length ? invites.join(', ') : 'none'}` + ]; + await interaction.reply({ content: lines.join('\n'), flags: MessageFlags.Ephemeral }); + } else if (sub === 'save') { + const name = interaction.options.getString('name', true); + // gather invites + const invited = voice.permissionOverwrites.cache + .filter(po => po.allow.has(PermissionFlagsBits.Connect) && ![guild.roles.everyone.id, sess.roleId].includes(po.id)) + .map(po => po.id); + // upsert preset + const existing = await client.pb.getFirst( + 'tempvc_presets', + `guildId = "${guild.id}" && userId = "${interaction.user.id}" && name = "${name}"` + ); + const data = { + guildId: guild.id, + userId: interaction.user.id, + name, + channelName: voice.name, + userLimit: voice.userLimit, + roleId: sess.roleId || '', + invitedUserIds: invited, + mode: sess.mode || 'whitelist' + }; + if (existing) { + await client.pb.updateOne('tempvc_presets', existing.id, data); + } else { + await client.pb.createOne('tempvc_presets', data); + } + await interaction.reply({ content: `Preset **${name}** saved.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'reset') { + // Defer to avoid Discord interaction timeout during reset + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + // reset channel to default parameters + const owner = interaction.member; + const display = owner.displayName || owner.user.username; + const defaultName = `TempVC: ${display}`; + await voice.setName(defaultName); + await voice.setUserLimit(0); + // clear all overwrites: allow everyone, owner elevated perms + await voice.permissionOverwrites.set([ + { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect] }, + { id: sess.ownerId, allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.Connect, + PermissionFlagsBits.MoveMembers, + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.PrioritySpeaker, + PermissionFlagsBits.MuteMembers, + PermissionFlagsBits.DeafenMembers + ] + } + ]); + sess.roleId = guild.roles.everyone.id; + await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: guild.roles.everyone.id, invitedUserIds: [] }); + await interaction.editReply({ content: 'Channel has been reset to default settings.' }); + } else if (sub === 'mode') { + const mode = interaction.options.getString('mode', true); + sess.mode = mode; + // apply mode overwrites + if (mode === 'whitelist') { + // only allow whitelisted role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { ViewChannel: false }); + if (sess.roleId) await voice.permissionOverwrites.edit(sess.roleId, { ViewChannel: true }); + } else { + // blacklist: allow everyone, then deny the specified role + await voice.permissionOverwrites.edit(guild.roles.everyone.id, { ViewChannel: true }); + if (sess.roleId) await voice.permissionOverwrites.edit(sess.roleId, { ViewChannel: false }); + } + // persist mode + await client.pb.updateOne('tempvc_sessions', sess.pbId, { mode }); + await interaction.reply({ content: `Channel mode set to **${mode}**.`, flags: MessageFlags.Ephemeral }); + } else if (sub === 'restore') { + // Defer initial reply to extend Discord interaction window + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + const name = interaction.options.getString('name', true); + const preset = await client.pb.getFirst( + 'tempvc_presets', + `guildId = "${guild.id}" && userId = "${interaction.user.id}" && name = "${name}"` + ); + if (!preset) { + return interaction.editReply({ content: `Preset **${name}** not found.` }); + } + // apply settings + await voice.setName(preset.channelName); + await voice.setUserLimit(preset.userLimit); + // apply mode-based permissions + const mode = preset.mode || 'whitelist'; + sess.mode = mode; + // adjust view/connect for @everyone + await voice.permissionOverwrites.edit( + guild.roles.everyone.id, + { ViewChannel: mode === 'blacklist', Connect: mode === 'blacklist' } + ); + // adjust view/connect for role + if (preset.roleId) { + await voice.permissionOverwrites.edit( + preset.roleId, + { ViewChannel: mode === 'whitelist', Connect: mode === 'whitelist' } + ); + } + // invite users explicitly + for (const uid of preset.invitedUserIds || []) { + await voice.permissionOverwrites.edit(uid, { Connect: true }).catch(() => {}); + } + // persist session changes + await client.pb.updateOne( + 'tempvc_sessions', + sess.pbId, + { roleId: preset.roleId || '', mode } + ); + sess.roleId = preset.roleId || ''; + await interaction.editReply({ content: `Preset **${name}** restored (mode: ${mode}).` }); + } + } catch (err) { + client.logger.error(`[module:tempvc][vc] ${err.message}`); + await interaction.reply({ content: 'Operation failed, see logs.', flags: MessageFlags.Ephemeral }); } - } - if (!memberToInvite) { - return interaction.reply({ content: 'User not found in this server.', flags: MessageFlags.Ephemeral }); - } - // grant view and connect - await voice.permissionOverwrites.edit(userId, { ViewChannel: true, Connect: true }); - await interaction.reply({ content: `Invited <@${userId}>.`, flags: MessageFlags.Ephemeral }); - } else if (sub === 'kick') { - const u = interaction.options.getUser('user', true); - const gm = await guild.members.fetch(u.id); - // move them out if in this channel - if (gm.voice.channelId === voice.id) { - await gm.voice.setChannel(null); - } - // remove any previous invite allow - try { - await voice.permissionOverwrites.delete(u.id); - } catch {} - await interaction.reply({ content: `Kicked <@${u.id}>.`, flags: MessageFlags.Ephemeral }); - } else if (sub === 'limit') { - let num = interaction.options.getInteger('number', true); - // enforce range 0-99 - if (num < 0 || num > 99) { - return interaction.reply({ content: 'User limit must be between 0 (no limit) and 99.', flags: MessageFlags.Ephemeral }); - } - await voice.setUserLimit(num); - await interaction.reply({ content: `User limit set to ${num}.`, flags: MessageFlags.Ephemeral }); - } else if (sub === 'role') { - const newRole = interaction.options.getRole('role', true); - const oldRoleId = sess.roleId; - // remove old role overwrite if any - if (oldRoleId && oldRoleId !== guild.roles.everyone.id) { - await voice.permissionOverwrites.delete(oldRoleId).catch(() => {}); - } - // selecting @everyone resets all - if (newRole.id === guild.roles.everyone.id) { - // clear all overwrites - await voice.permissionOverwrites.set([ - { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.Connect] }, - { id: sess.ownerId, allow: [PermissionFlagsBits.Connect, PermissionFlagsBits.MoveMembers, PermissionFlagsBits.ManageChannels] } - ]); - sess.roleId = ''; - await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: '', mode: sess.mode }); - return interaction.reply({ content: '@everyone can now connect.', flags: MessageFlags.Ephemeral }); - } - if (sess.mode === 'whitelist') { - // whitelist: lock everyone, allow role - await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: false }); - await voice.permissionOverwrites.edit(newRole.id, { Connect: true }); - sess.roleId = newRole.id; - await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: newRole.id, mode: sess.mode }); - await interaction.reply({ content: `Whitelisted role <@&${newRole.id}>.`, flags: MessageFlags.Ephemeral }); - } else { - // blacklist: allow everyone, deny role - await voice.permissionOverwrites.edit(guild.roles.everyone.id, { Connect: true }); - await voice.permissionOverwrites.edit(newRole.id, { Connect: false }); - sess.roleId = newRole.id; - await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: newRole.id, mode: sess.mode }); - await interaction.reply({ content: `Blacklisted role <@&${newRole.id}>.`, flags: MessageFlags.Ephemeral }); - } - } else if (sub === 'delete') { - await interaction.reply({ content: 'Deleting your channel...', flags: MessageFlags.Ephemeral }); - await client.pb.deleteOne('tempvc_sessions', sess.pbId); - client.tempvc.sessions.delete(voice.id); - await voice.delete('Owner deleted temp VC'); - } else if (sub === 'info') { - const invites = voice.permissionOverwrites.cache - .filter(po => po.allow.has(PermissionFlagsBits.Connect) && ![guild.roles.everyone.id, sess.roleId].includes(po.id)) - .map(po => `<@${po.id}>`); - const everyoneId = guild.roles.everyone.id; - const roleLine = (!sess.roleId || sess.roleId === everyoneId) - ? '@everyone' - : `<@&${sess.roleId}>`; - const modeLine = sess.mode || 'whitelist'; - const lines = [ - `Owner: <@${sess.ownerId}>`, - `Name: ${voice.name}`, - `Role: ${roleLine} (${modeLine})`, - `User limit: ${voice.userLimit}`, - `Invites: ${invites.length ? invites.join(', ') : 'none'}` - ]; - await interaction.reply({ content: lines.join('\n'), flags: MessageFlags.Ephemeral }); - } else if (sub === 'save') { - const name = interaction.options.getString('name', true); - // gather invites - const invited = voice.permissionOverwrites.cache - .filter(po => po.allow.has(PermissionFlagsBits.Connect) && ![guild.roles.everyone.id, sess.roleId].includes(po.id)) - .map(po => po.id); - // upsert preset - const existing = await client.pb.getFirst( - 'tempvc_presets', - `guildId = "${guild.id}" && userId = "${interaction.user.id}" && name = "${name}"` - ); - const data = { - guildId: guild.id, - userId: interaction.user.id, - name, - channelName: voice.name, - userLimit: voice.userLimit, - roleId: sess.roleId || '', - invitedUserIds: invited, - mode: sess.mode || 'whitelist' - }; - if (existing) { - await client.pb.updateOne('tempvc_presets', existing.id, data); - } else { - await client.pb.createOne('tempvc_presets', data); - } - await interaction.reply({ content: `Preset **${name}** saved.`, flags: MessageFlags.Ephemeral }); - } else if (sub === 'reset') { - // Defer to avoid Discord interaction timeout during reset - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - // reset channel to default parameters - const owner = interaction.member; - const display = owner.displayName || owner.user.username; - const defaultName = `TempVC: ${display}`; - await voice.setName(defaultName); - await voice.setUserLimit(0); - // clear all overwrites: allow everyone, owner elevated perms - await voice.permissionOverwrites.set([ - { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect] }, - { id: sess.ownerId, allow: [ - PermissionFlagsBits.ViewChannel, - PermissionFlagsBits.Connect, - PermissionFlagsBits.MoveMembers, - PermissionFlagsBits.ManageChannels, - PermissionFlagsBits.PrioritySpeaker, - PermissionFlagsBits.MuteMembers, - PermissionFlagsBits.DeafenMembers - ] - } - ]); - sess.roleId = guild.roles.everyone.id; - await client.pb.updateOne('tempvc_sessions', sess.pbId, { roleId: guild.roles.everyone.id, invitedUserIds: [] }); - await interaction.editReply({ content: 'Channel has been reset to default settings.' }); - } else if (sub === 'mode') { - const mode = interaction.options.getString('mode', true); - sess.mode = mode; - // apply mode overwrites - if (mode === 'whitelist') { - // only allow whitelisted role - await voice.permissionOverwrites.edit(guild.roles.everyone.id, { ViewChannel: false }); - if (sess.roleId) await voice.permissionOverwrites.edit(sess.roleId, { ViewChannel: true }); - } else { - // blacklist: allow everyone, then deny the specified role - await voice.permissionOverwrites.edit(guild.roles.everyone.id, { ViewChannel: true }); - if (sess.roleId) await voice.permissionOverwrites.edit(sess.roleId, { ViewChannel: false }); - } - // persist mode - await client.pb.updateOne('tempvc_sessions', sess.pbId, { mode }); - await interaction.reply({ content: `Channel mode set to **${mode}**.`, flags: MessageFlags.Ephemeral }); - } else if (sub === 'restore') { - // Defer initial reply to extend Discord interaction window - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - const name = interaction.options.getString('name', true); - const preset = await client.pb.getFirst( - 'tempvc_presets', - `guildId = "${guild.id}" && userId = "${interaction.user.id}" && name = "${name}"` - ); - if (!preset) { - return interaction.editReply({ content: `Preset **${name}** not found.` }); - } - // apply settings - await voice.setName(preset.channelName); - await voice.setUserLimit(preset.userLimit); - // apply mode-based permissions - const mode = preset.mode || 'whitelist'; - sess.mode = mode; - // adjust view/connect for @everyone - await voice.permissionOverwrites.edit( - guild.roles.everyone.id, - { ViewChannel: mode === 'blacklist', Connect: mode === 'blacklist' } - ); - // adjust view/connect for role - if (preset.roleId) { - await voice.permissionOverwrites.edit( - preset.roleId, - { ViewChannel: mode === 'whitelist', Connect: mode === 'whitelist' } - ); - } - // invite users explicitly - for (const uid of preset.invitedUserIds || []) { - await voice.permissionOverwrites.edit(uid, { Connect: true }).catch(() => {}); - } - // persist session changes - await client.pb.updateOne( - 'tempvc_sessions', - sess.pbId, - { roleId: preset.roleId || '', mode } - ); - sess.roleId = preset.roleId || ''; - await interaction.editReply({ content: `Preset **${name}** restored (mode: ${mode}).` }); } - } catch (err) { - client.logger.error(`[module:tempvc][vc] ${err.message}`); - await interaction.reply({ content: 'Operation failed, see logs.', flags: MessageFlags.Ephemeral }); - } } - } ]; /** * Initialize module: load PB state and hook events */ export async function init(client) { - // autocomplete for /vc invite - client.on('interactionCreate', async interaction => { - if (!interaction.isAutocomplete()) return; - if (interaction.commandName !== 'vc') return; - // Only handle autocomplete for the 'invite' subcommand - let sub; - try { - sub = interaction.options.getSubcommand(); - } catch { - return; - } - if (sub !== 'invite') return; - const focused = interaction.options.getFocused(); - const guild = interaction.guild; - if (!guild) return; - // Perform guild member search for autocomplete suggestions (prefix match) - let choices = []; - try { - const members = await guild.members.search({ query: focused, limit: 25 }); - choices = members.map(m => ({ name: m.displayName || m.user.username, value: m.id })); - } catch (err) { - client.logger.error(`[module:tempvc] Autocomplete search failed: ${err.message}`); - } - // If no choices found or to support substring matching, fallback to cache filter - if (choices.length === 0) { - const str = String(focused).toLowerCase(); - choices = Array.from(guild.members.cache.values()) - .filter(m => (m.displayName || m.user.username).toLowerCase().includes(str)) - .slice(0, 25) - .map(m => ({ name: m.displayName || m.user.username, value: m.id })); - } - // Respond with suggestions (max 25) - await interaction.respond(choices); - }); - // tempvc state: masters per guild, sessions map - client.tempvc = { masters: new Map(), sessions: new Map() }; - // hook voice state updates - client.on('voiceStateUpdate', async (oldState, newState) => { - client.logger.debug( - `[module:tempvc] voiceStateUpdate: user=${newState.id} oldChannel=${oldState.channelId} newChannel=${newState.channelId}` - ); - // cleanup on leave - if (oldState.channelId && oldState.channelId !== newState.channelId) { - const sess = client.tempvc.sessions.get(oldState.channelId); - const ch = oldState.guild.channels.cache.get(oldState.channelId); - if (sess && (!ch || ch.members.size === 0)) { - await client.pb.deleteOne('tempvc_sessions', sess.pbId).catch(()=>{}); - client.tempvc.sessions.delete(oldState.channelId); - await ch?.delete('Empty temp VC cleanup').catch(()=>{}); - } - } - // spawn on join - if (newState.channelId && newState.channelId !== oldState.channelId) { - const masters = client.tempvc.masters.get(newState.guild.id) || new Map(); - client.logger.debug( - `[module:tempvc] Guild ${newState.guild.id} masters: ${[...masters.keys()].join(',')}` - ); - client.logger.debug( - `[module:tempvc] Checking spawn for channel ${newState.channelId}: ${masters.has(newState.channelId)}` - ); - if (masters.has(newState.channelId)) { - const catId = masters.get(newState.channelId); - const owner = newState.member; - const guild = newState.guild; - // default channel name - const displayName = owner.displayName || owner.user.username; - const name = `TempVC: ${displayName}`; - // create channel - // create voice channel, default permissions inherited from category (allow everyone) - // create voice channel; default allow everyone view/join, owner elevated perms - const ch = await guild.channels.create({ - name, - type: ChannelType.GuildVoice, - parent: catId, - permissionOverwrites: [ - { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect] }, - { id: owner.id, allow: [ - PermissionFlagsBits.ViewChannel, - PermissionFlagsBits.Connect, - PermissionFlagsBits.MoveMembers, - PermissionFlagsBits.ManageChannels, - PermissionFlagsBits.PrioritySpeaker, - PermissionFlagsBits.MuteMembers, - PermissionFlagsBits.DeafenMembers - ] - } - ] - }); - // move member - await owner.voice.setChannel(ch); - // persist session - const rec = await client.pb.createOne('tempvc_sessions', { - guildId: guild.id, - masterChannelId: newState.channelId, - channelId: ch.id, - ownerId: owner.id, - roleId: guild.roles.everyone.id, - mode: 'whitelist' - }); - client.tempvc.sessions.set(ch.id, { - pbId: rec.id, - guildId: guild.id, - masterChannelId: newState.channelId, - ownerId: owner.id, - roleId: guild.roles.everyone.id, - mode: 'whitelist' - }); - // send instructions to the voice channel itself + // autocomplete for /vc invite + client.on('interactionCreate', async interaction => { + if (!interaction.isAutocomplete()) return; + if (interaction.commandName !== 'vc') return; + // Only handle autocomplete for the 'invite' subcommand + let sub; try { - const helpEmbed = new EmbedBuilder() - .setTitle('👋 Welcome to Your Temporary Voice Channel!') - .setColor('Blue') - .addFields( - { - name: 'Access Control', - value: + sub = interaction.options.getSubcommand(); + } catch { + return; + } + if (sub !== 'invite') return; + const focused = interaction.options.getFocused(); + const guild = interaction.guild; + if (!guild) return; + // Perform guild member search for autocomplete suggestions (prefix match) + let choices = []; + try { + const members = await guild.members.search({ query: focused, limit: 25 }); + choices = members.map(m => ({ name: m.displayName || m.user.username, value: m.id })); + } catch (err) { + client.logger.error(`[module:tempvc] Autocomplete search failed: ${err.message}`); + } + // If no choices found or to support substring matching, fallback to cache filter + if (choices.length === 0) { + const str = String(focused).toLowerCase(); + choices = Array.from(guild.members.cache.values()) + .filter(m => (m.displayName || m.user.username).toLowerCase().includes(str)) + .slice(0, 25) + .map(m => ({ name: m.displayName || m.user.username, value: m.id })); + } + // Respond with suggestions (max 25) + await interaction.respond(choices); + }); + // tempvc state: masters per guild, sessions map + client.tempvc = { masters: new Map(), sessions: new Map() }; + // hook voice state updates + client.on('voiceStateUpdate', async (oldState, newState) => { + client.logger.debug( + `[module:tempvc] voiceStateUpdate: user=${newState.id} oldChannel=${oldState.channelId} newChannel=${newState.channelId}` + ); + // cleanup on leave + if (oldState.channelId && oldState.channelId !== newState.channelId) { + const sess = client.tempvc.sessions.get(oldState.channelId); + const ch = oldState.guild.channels.cache.get(oldState.channelId); + if (sess && (!ch || ch.members.size === 0)) { + await client.pb.deleteOne('tempvc_sessions', sess.pbId).catch(()=>{}); + client.tempvc.sessions.delete(oldState.channelId); + await ch?.delete('Empty temp VC cleanup').catch(()=>{}); + } + } + // spawn on join + if (newState.channelId && newState.channelId !== oldState.channelId) { + const masters = client.tempvc.masters.get(newState.guild.id) || new Map(); + client.logger.debug( + `[module:tempvc] Guild ${newState.guild.id} masters: ${[...masters.keys()].join(',')}` + ); + client.logger.debug( + `[module:tempvc] Checking spawn for channel ${newState.channelId}: ${masters.has(newState.channelId)}` + ); + if (masters.has(newState.channelId)) { + const catId = masters.get(newState.channelId); + const owner = newState.member; + const guild = newState.guild; + // default channel name + const displayName = owner.displayName || owner.user.username; + const name = `TempVC: ${displayName}`; + // create channel + // create voice channel, default permissions inherited from category (allow everyone) + // create voice channel; default allow everyone view/join, owner elevated perms + const ch = await guild.channels.create({ + name, + type: ChannelType.GuildVoice, + parent: catId, + permissionOverwrites: [ + { id: guild.roles.everyone.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.Connect] }, + { id: owner.id, allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.Connect, + PermissionFlagsBits.MoveMembers, + PermissionFlagsBits.ManageChannels, + PermissionFlagsBits.PrioritySpeaker, + PermissionFlagsBits.MuteMembers, + PermissionFlagsBits.DeafenMembers + ] + } + ] + }); + // move member + await owner.voice.setChannel(ch); + // persist session + const rec = await client.pb.createOne('tempvc_sessions', { + guildId: guild.id, + masterChannelId: newState.channelId, + channelId: ch.id, + ownerId: owner.id, + roleId: guild.roles.everyone.id, + mode: 'whitelist' + }); + client.tempvc.sessions.set(ch.id, { + pbId: rec.id, + guildId: guild.id, + masterChannelId: newState.channelId, + ownerId: owner.id, + roleId: guild.roles.everyone.id, + mode: 'whitelist' + }); + // send instructions to the voice channel itself + try { + const helpEmbed = new EmbedBuilder() + .setTitle('👋 Welcome to Your Temporary Voice Channel!') + .setColor('Blue') + .addFields( + { + name: 'Access Control', + value: '• /vc invite — Invite a user to this channel\n' + '• /vc kick — Kick a user from this channel\n' + '• /vc role — Set a role to allow/deny access\n' + '• /vc mode — Switch role mode\n' + - '• /vc limit — Set user limit (0–99)', - }, - { - name: 'Presets', - value: + '• /vc limit — Set user limit (0–99)' + }, + { + name: 'Presets', + value: '• /vc save — Save current settings as a preset\n' + '• /vc restore — Restore settings from a preset\n' + - '• /vc reset — Reset channel to default settings', - }, - { - name: 'Utilities', - value: + '• /vc reset — Reset channel to default settings' + }, + { + name: 'Utilities', + value: '• /vc rename — Rename this channel\n' + '• /vc info — Show channel info\n' + - '• /vc delete — Delete this channel', - } - ); - await ch.send({ embeds: [helpEmbed] }); - } catch (err) { - client.logger.error(`[module:tempvc] Error sending help message: ${err.message}`); + '• /vc delete — Delete this channel' + } + ); + await ch.send({ embeds: [helpEmbed] }); + } catch (err) { + client.logger.error(`[module:tempvc] Error sending help message: ${err.message}`); + } + } } - } - } - }); - // autocomplete for /vc save & restore presets - client.on('interactionCreate', async interaction => { - if (!interaction.isAutocomplete() || interaction.commandName !== 'vc') return; - const sub = interaction.options.getSubcommand(false); - if (!['save', 'restore'].includes(sub)) return; - const focused = interaction.options.getFocused(true); - if (focused.name !== 'name') return; - const guildId = interaction.guildId; - const userId = interaction.user.id; - try { - const recs = await client.pb.getAll('tempvc_presets', { - filter: `guildId = "${guildId}" && userId = "${userId}"` - }); - const choices = recs - .filter(r => r.name.toLowerCase().startsWith(focused.value.toLowerCase())) - .slice(0, 25) - .map(r => ({ name: r.name, value: r.name })); - await interaction.respond(choices); - } catch (err) { - client.logger.error(`[module:tempvc][autocomplete] ${err.message}`); - await interaction.respond([]); - } - }); - // On ready: load masters/sessions, then check required permissions - client.on('ready', async () => { - // Load persistent spawn masters and active sessions - for (const guild of client.guilds.cache.values()) { - const gid = guild.id; - try { - const masters = await client.pb.getAll('tempvc_masters', { filter: `guildId = "${gid}"` }); // guildId = "X" works, but escaped quotes are allowed - const gm = new Map(); - for (const rec of masters) gm.set(rec.masterChannelId, rec.categoryId); - client.tempvc.masters.set(gid, gm); - client.logger.info(`[module:tempvc] Loaded spawn masters for guild ${gid}: ${[...gm.keys()].join(', ')}`); - } catch (err) { - client.logger.error(`[module:tempvc] Error loading masters for guild ${gid}: ${err.message}`); - } - try { - const sessions = await client.pb.getAll('tempvc_sessions', { filter: `guildId = "${gid}"` }); - for (const rec of sessions) { - const ch = guild.channels.cache.get(rec.channelId); - if (ch && ch.isVoiceBased()) { - client.tempvc.sessions.set(rec.channelId, { - pbId: rec.id, - guildId: gid, - masterChannelId: rec.masterChannelId, - ownerId: rec.ownerId, - roleId: rec.roleId || '', - mode: rec.mode || 'whitelist' + }); + // autocomplete for /vc save & restore presets + client.on('interactionCreate', async interaction => { + if (!interaction.isAutocomplete() || interaction.commandName !== 'vc') return; + const sub = interaction.options.getSubcommand(false); + if (!['save', 'restore'].includes(sub)) return; + const focused = interaction.options.getFocused(true); + if (focused.name !== 'name') return; + const guildId = interaction.guildId; + const userId = interaction.user.id; + try { + const recs = await client.pb.getAll('tempvc_presets', { + filter: `guildId = "${guildId}" && userId = "${userId}"` }); - if (rec.roleId) await ch.permissionOverwrites.edit(rec.roleId, { Connect: true }).catch(()=>{}); - await ch.permissionOverwrites.edit(rec.ownerId, { Connect: true, ManageChannels: true, MoveMembers: true }); - } else { - await client.pb.deleteOne('tempvc_sessions', rec.id).catch(()=>{}); - } + const choices = recs + .filter(r => r.name.toLowerCase().startsWith(focused.value.toLowerCase())) + .slice(0, 25) + .map(r => ({ name: r.name, value: r.name })); + await interaction.respond(choices); + } catch (err) { + client.logger.error(`[module:tempvc][autocomplete] ${err.message}`); + await interaction.respond([]); } - } catch (err) { - client.logger.error(`[module:tempvc] Error loading sessions for guild ${gid}: ${err.message}`); - } - } - // Verify necessary permissions - for (const guild of client.guilds.cache.values()) { - // get bot's member in this guild - let me = guild.members.me; - if (!me) { - try { me = await guild.members.fetch(client.user.id); } catch { /* ignore */ } - } - if (!me) continue; - const missing = []; - if (!me.permissions.has(PermissionFlagsBits.ManageChannels)) missing.push('ManageChannels'); - if (!me.permissions.has(PermissionFlagsBits.MoveMembers)) missing.push('MoveMembers'); - if (missing.length) { - client.logger.warn( - `[module:tempvc] Missing permissions in guild ${guild.id} (${guild.name}): ${missing.join(', ')}` - ); - } - } - }); - client.logger.info('[module:tempvc] Module initialized'); -} \ No newline at end of file + }); + // On ready: load masters/sessions, then check required permissions + client.on('ready', async () => { + // Load persistent spawn masters and active sessions + for (const guild of client.guilds.cache.values()) { + const gid = guild.id; + try { + const masters = await client.pb.getAll('tempvc_masters', { filter: `guildId = "${gid}"` }); // guildId = "X" works, but escaped quotes are allowed + const gm = new Map(); + for (const rec of masters) gm.set(rec.masterChannelId, rec.categoryId); + client.tempvc.masters.set(gid, gm); + client.logger.info(`[module:tempvc] Loaded spawn masters for guild ${gid}: ${[...gm.keys()].join(', ')}`); + } catch (err) { + client.logger.error(`[module:tempvc] Error loading masters for guild ${gid}: ${err.message}`); + } + try { + const sessions = await client.pb.getAll('tempvc_sessions', { filter: `guildId = "${gid}"` }); + for (const rec of sessions) { + const ch = guild.channels.cache.get(rec.channelId); + if (ch && ch.isVoiceBased()) { + client.tempvc.sessions.set(rec.channelId, { + pbId: rec.id, + guildId: gid, + masterChannelId: rec.masterChannelId, + ownerId: rec.ownerId, + roleId: rec.roleId || '', + mode: rec.mode || 'whitelist' + }); + if (rec.roleId) await ch.permissionOverwrites.edit(rec.roleId, { Connect: true }).catch(()=>{}); + await ch.permissionOverwrites.edit(rec.ownerId, { Connect: true, ManageChannels: true, MoveMembers: true }); + } else { + await client.pb.deleteOne('tempvc_sessions', rec.id).catch(()=>{}); + } + } + } catch (err) { + client.logger.error(`[module:tempvc] Error loading sessions for guild ${gid}: ${err.message}`); + } + } + // Verify necessary permissions + for (const guild of client.guilds.cache.values()) { + // get bot's member in this guild + let me = guild.members.me; + if (!me) { + try { me = await guild.members.fetch(client.user.id); } catch { /* ignore */ } + } + if (!me) continue; + const missing = []; + if (!me.permissions.has(PermissionFlagsBits.ManageChannels)) missing.push('ManageChannels'); + if (!me.permissions.has(PermissionFlagsBits.MoveMembers)) missing.push('MoveMembers'); + if (missing.length) { + client.logger.warn( + `[module:tempvc] Missing permissions in guild ${guild.id} (${guild.name}): ${missing.join(', ')}` + ); + } + } + }); + client.logger.info('[module:tempvc] Module initialized'); +} diff --git a/_src/ansiColors.js b/_src/ansiColors.js index d0b6068..f66d551 100644 --- a/_src/ansiColors.js +++ b/_src/ansiColors.js @@ -2,70 +2,70 @@ // ANSI color/style codes const CODES = { - // text colors - gray: 30, red: 31, green: 32, yellow: 33, - blue: 34, pink: 35, cyan: 36, white: 37, - // background colors - bgGray: 40, bgOrange: 41, bgBlue: 42, - bgTurquoise: 43, bgFirefly: 44, bgIndigo: 45, - bgLightGray: 46, bgWhite: 47, - // styles - bold: 1, underline: 4, - // reset - reset: 0 + // text colors + gray: 30, red: 31, green: 32, yellow: 33, + blue: 34, pink: 35, cyan: 36, white: 37, + // background colors + bgGray: 40, bgOrange: 41, bgBlue: 42, + bgTurquoise: 43, bgFirefly: 44, bgIndigo: 45, + bgLightGray: 46, bgWhite: 47, + // styles + bold: 1, underline: 4, + // reset + reset: 0 }; /** * Escape literal brackets so users can write \[ and \] without triggering tags. */ export function escapeBrackets(str) { - return str - .replace(/\\\[/g, '__ESC_LB__') - .replace(/\\\]/g, '__ESC_RB__'); + return str + .replace(/\\\[/g, '__ESC_LB__') + .replace(/\\\]/g, '__ESC_RB__'); } /** Restore any escaped brackets after formatting. */ export function restoreBrackets(str) { - return str - .replace(/__ESC_LB__/g, '[') - .replace(/__ESC_RB__/g, ']'); + return str + .replace(/__ESC_LB__/g, '[') + .replace(/__ESC_RB__/g, ']'); } /** * Parse nested [tag1,tag2]…[/] patterns into ANSI codes (stack-based). */ export function formatAnsi(input) { - const stack = []; - let output = ''; - const pattern = /\[\/\]|\[([^\]]+)\]/g; - let lastIndex = 0; - let match; + const stack = []; + let output = ''; + const pattern = /\[\/\]|\[([^\]]+)\]/g; + let lastIndex = 0; + let match; - while ((match = pattern.exec(input)) !== null) { - output += input.slice(lastIndex, match.index); + while ((match = pattern.exec(input)) !== null) { + output += input.slice(lastIndex, match.index); - if (match[0] === '[/]') { - if (stack.length) stack.pop(); - output += `\u001b[${CODES.reset}m`; - for (const tag of stack) { - const code = CODES[tag] ?? CODES.gray; - output += `\u001b[${code}m`; - } - } else { - const tags = match[1].split(/[,;\s]+/).filter(Boolean); - for (const tag of tags) { - stack.push(tag); - const code = CODES[tag] ?? CODES.gray; - output += `\u001b[${code}m`; - } + if (match[0] === '[/]') { + if (stack.length) stack.pop(); + output += `\u001b[${CODES.reset}m`; + for (const tag of stack) { + const code = CODES[tag] ?? CODES.gray; + output += `\u001b[${code}m`; + } + } else { + const tags = match[1].split(/[,;\s]+/).filter(Boolean); + for (const tag of tags) { + stack.push(tag); + const code = CODES[tag] ?? CODES.gray; + output += `\u001b[${code}m`; + } + } + + lastIndex = pattern.lastIndex; } - lastIndex = pattern.lastIndex; - } - - output += input.slice(lastIndex); - if (stack.length) output += `\u001b[${CODES.reset}m`; - return output; + output += input.slice(lastIndex); + if (stack.length) output += `\u001b[${CODES.reset}m`; + return output; } /** @@ -73,18 +73,18 @@ export function formatAnsi(input) { * Escapes brackets, parses ANSI, and restores literals. */ export function ansi(strings, ...values) { - let built = ''; - for (let i = 0; i < strings.length; i++) { - built += strings[i]; - if (i < values.length) built += values[i]; - } - return restoreBrackets(formatAnsi(escapeBrackets(built))); + let built = ''; + for (let i = 0; i < strings.length; i++) { + built += strings[i]; + if (i < values.length) built += values[i]; + } + return restoreBrackets(formatAnsi(escapeBrackets(built))); } /** Wrap text in a ```ansi code block for Discord. */ export function wrapAnsi(text) { - return '```ansi\n' + text + '\n```'; + return '```ansi\n' + text + '\n```'; } // Export raw codes for advanced use (e.g., ansitheme module) -export { CODES }; \ No newline at end of file +export { CODES }; diff --git a/_src/loader.js b/_src/loader.js index 9273ade..e968c88 100644 --- a/_src/loader.js +++ b/_src/loader.js @@ -8,69 +8,69 @@ const rootDir = path.dirname(__dirname); // Load modules function - hot reload functionality removed export const loadModules = async (clientConfig, client) => { - const modules = clientConfig.modules || []; - const modulesDir = path.join(rootDir, '_opt'); + const modules = clientConfig.modules || []; + const modulesDir = path.join(rootDir, '_opt'); - // Create opt directory if it doesn't exist - if (!fs.existsSync(modulesDir)) { - fs.mkdirSync(modulesDir, { recursive: true }); - } + // Create opt directory if it doesn't exist + if (!fs.existsSync(modulesDir)) { + fs.mkdirSync(modulesDir, { recursive: true }); + } - client.logger.info(`[module:loader] Loading modules: ${modules.join(', ')}`); - // Load each module - for (const moduleName of modules) { - try { - // Try _opt first, then fallback to core _src modules - let modulePath = path.join(modulesDir, `${moduleName}.js`); - if (!fs.existsSync(modulePath)) { - // Fallback to core source directory - modulePath = path.join(rootDir, '_src', `${moduleName}.js`); - if (!fs.existsSync(modulePath)) { - client.logger.warn(`[module:loader] Module not found: ${moduleName}.js`); - continue; + client.logger.info(`[module:loader] Loading modules: ${modules.join(', ')}`); + // Load each module + for (const moduleName of modules) { + try { + // Try _opt first, then fallback to core _src modules + let modulePath = path.join(modulesDir, `${moduleName}.js`); + if (!fs.existsSync(modulePath)) { + // Fallback to core source directory + modulePath = path.join(rootDir, '_src', `${moduleName}.js`); + if (!fs.existsSync(modulePath)) { + client.logger.warn(`[module:loader] Module not found: ${moduleName}.js`); + continue; + } + } + + // Import module (using dynamic import for ES modules) + // Import module + const moduleUrl = `file://${modulePath}`; + const module = await import(moduleUrl); + + // Register commands if the module has them + if (module.commands) { + if (Array.isArray(module.commands)) { + // Handle array of commands + for (const command of module.commands) { + if (command.data && typeof command.execute === 'function') { + const commandName = command.data.name || command.name; + client.commands.set(commandName, command); + client.logger.info(`[module:loader] Registered command: ${commandName}`); + } + } + } else if (typeof module.commands === 'object') { + // Handle map/object of commands + for (const [commandName, command] of Object.entries(module.commands)) { + if (command.execute && typeof command.execute === 'function') { + client.commands.set(commandName, command); + client.logger.info(`Registered command: ${commandName}`); + } + } + } + } + + // Call init function if it exists + if (typeof module.init === 'function') { + await module.init(client, clientConfig); + client.logger.info(`[module:loader] Module initialized: ${moduleName}`); + } else { + client.logger.info(`[module:loader] Module loaded (no init): ${moduleName}`); + } + + // Store the module reference (this isn't used for hot reloading anymore) + client.modules = client.modules || new Map(); + client.modules.set(moduleName, module); + } catch (error) { + client.logger.error(`[module:loader] Failed to load module ${moduleName}: ${error.message}`); } - } - - // Import module (using dynamic import for ES modules) - // Import module - const moduleUrl = `file://${modulePath}`; - const module = await import(moduleUrl); - - // Register commands if the module has them - if (module.commands) { - if (Array.isArray(module.commands)) { - // Handle array of commands - for (const command of module.commands) { - if (command.data && typeof command.execute === 'function') { - const commandName = command.data.name || command.name; - client.commands.set(commandName, command); - client.logger.info(`[module:loader] Registered command: ${commandName}`); - } - } - } else if (typeof module.commands === 'object') { - // Handle map/object of commands - for (const [commandName, command] of Object.entries(module.commands)) { - if (command.execute && typeof command.execute === 'function') { - client.commands.set(commandName, command); - client.logger.info(`Registered command: ${commandName}`); - } - } - } - } - - // Call init function if it exists - if (typeof module.init === 'function') { - await module.init(client, clientConfig); - client.logger.info(`[module:loader] Module initialized: ${moduleName}`); - } else { - client.logger.info(`[module:loader] Module loaded (no init): ${moduleName}`); - } - - // Store the module reference (this isn't used for hot reloading anymore) - client.modules = client.modules || new Map(); - client.modules.set(moduleName, module); - } catch (error) { - client.logger.error(`[module:loader] Failed to load module ${moduleName}: ${error.message}`); - } - } + } }; diff --git a/_src/logger.js b/_src/logger.js index 7b8352f..10e103a 100644 --- a/_src/logger.js +++ b/_src/logger.js @@ -1,86 +1,87 @@ -import winston from 'winston'; -import 'winston-daily-rotate-file'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import winston from 'winston'; +import 'winston-daily-rotate-file'; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootDir = path.dirname(__dirname); // Create Winston logger export const createLogger = (clientConfig) => { - const { logging } = clientConfig; - const transports = []; + const { logging } = clientConfig; + const transports = []; - // Console transport - if (logging.console.enabled) { - transports.push(new winston.transports.Console({ - level: logging.console.level, - format: winston.format.combine( - winston.format.timestamp({ - format: logging.file.timestampFormat - }), - logging.console.colorize ? winston.format.colorize() : winston.format.uncolorize(), - winston.format.printf(info => `[${info.timestamp}] [${clientConfig.id}] [${info.level}] ${info.message}`) - ) - })); - } + // Console transport + if (logging.console.enabled) { + transports.push(new winston.transports.Console({ + level: logging.console.level, + format: winston.format.combine( + winston.format.timestamp({ + format: logging.file.timestampFormat + }), + logging.console.colorize ? winston.format.colorize() : winston.format.uncolorize(), + winston.format.printf(info => `[${info.timestamp}] [${clientConfig.id}] [${info.level}] ${info.message}`) + ) + })); + } - // Combined file transport with rotation - if (logging.file.combined.enabled) { - const logDir = path.join(rootDir, logging.file.combined.location); + // Combined file transport with rotation + if (logging.file.combined.enabled) { + const logDir = path.join(rootDir, logging.file.combined.location); - // Create log directory if it doesn't exist - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - } + // Create log directory if it doesn't exist + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } - const combinedTransport = new winston.transports.DailyRotateFile({ - filename: path.join(logDir, `${clientConfig.id}-combined-%DATE%.log`), - datePattern: logging.file.dateFormat, - level: logging.file.combined.level, - maxSize: logging.file.combined.maxSize, - maxFiles: logging.file.combined.maxFiles, - format: winston.format.combine( - winston.format.timestamp({ - format: logging.file.timestampFormat - }), - winston.format.printf(info => `[${info.timestamp}] [${info.level}] ${info.message}`) - ) - }); + const combinedTransport = new winston.transports.DailyRotateFile({ + filename: path.join(logDir, `${clientConfig.id}-combined-%DATE%.log`), + datePattern: logging.file.dateFormat, + level: logging.file.combined.level, + maxSize: logging.file.combined.maxSize, + maxFiles: logging.file.combined.maxFiles, + format: winston.format.combine( + winston.format.timestamp({ + format: logging.file.timestampFormat + }), + winston.format.printf(info => `[${info.timestamp}] [${info.level}] ${info.message}`) + ) + }); - transports.push(combinedTransport); - } + transports.push(combinedTransport); + } - // Error file transport with rotation - if (logging.file.error.enabled) { - const logDir = path.join(rootDir, logging.file.error.location); + // Error file transport with rotation + if (logging.file.error.enabled) { + const logDir = path.join(rootDir, logging.file.error.location); - // Create log directory if it doesn't exist - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - } + // Create log directory if it doesn't exist + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } - const errorTransport = new winston.transports.DailyRotateFile({ - filename: path.join(logDir, `${clientConfig.id}-error-%DATE%.log`), - datePattern: logging.file.dateFormat, - level: logging.file.error.level, - maxSize: logging.file.error.maxSize, - maxFiles: logging.file.error.maxFiles, - format: winston.format.combine( - winston.format.timestamp({ - format: logging.file.timestampFormat - }), - winston.format.printf(info => `[${info.timestamp}] [${info.level}] ${info.message}`) - ) - }); + const errorTransport = new winston.transports.DailyRotateFile({ + filename: path.join(logDir, `${clientConfig.id}-error-%DATE%.log`), + datePattern: logging.file.dateFormat, + level: logging.file.error.level, + maxSize: logging.file.error.maxSize, + maxFiles: logging.file.error.maxFiles, + format: winston.format.combine( + winston.format.timestamp({ + format: logging.file.timestampFormat + }), + winston.format.printf(info => `[${info.timestamp}] [${info.level}] ${info.message}`) + ) + }); - transports.push(errorTransport); - } + transports.push(errorTransport); + } - return winston.createLogger({ - levels: winston.config.npm.levels, - transports - }); + return winston.createLogger({ + levels: winston.config.npm.levels, + transports + }); }; diff --git a/_src/pocketbase.js b/_src/pocketbase.js index 493cf21..533ec80 100644 --- a/_src/pocketbase.js +++ b/_src/pocketbase.js @@ -2,19 +2,19 @@ import PocketBase from 'pocketbase'; // Initialize Pocketbase export const initializePocketbase = async (clientConfig, logger) => { - try { - const pb = new PocketBase(clientConfig.pocketbase.url); + try { + const pb = new PocketBase(clientConfig.pocketbase.url); - // Authenticate with admin credentials - await pb.collection('_users').authWithPassword( - clientConfig.pocketbase.username, - clientConfig.pocketbase.password - ); + // Authenticate with admin credentials + await pb.collection('_users').authWithPassword( + clientConfig.pocketbase.username, + clientConfig.pocketbase.password + ); - logger.info('PocketBase initialized and authenticated'); - return pb; - } catch (error) { - logger.error(`PocketBase initialization failed: ${error.message}`); - return new PocketBase(clientConfig.pocketbase.url); - } + logger.info('PocketBase initialized and authenticated'); + return pb; + } catch (error) { + logger.error(`PocketBase initialization failed: ${error.message}`); + return new PocketBase(clientConfig.pocketbase.url); + } }; diff --git a/_src/template.js b/_src/template.js index ed1d5b0..5ee6b83 100644 --- a/_src/template.js +++ b/_src/template.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; /** * expandTemplate: simple variable substitution in {{key}} placeholders. * @param {string} template - The template string with {{key}} tokens. @@ -6,10 +6,10 @@ * @returns {string} - The template with keys replaced by context values. */ export function expandTemplate(template, context) { - if (typeof template !== 'string') return ''; - return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => { - return Object.prototype.hasOwnProperty.call(context, key) - ? String(context[key]) - : ''; - }); -} \ No newline at end of file + if (typeof template !== 'string') return ''; + return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => { + return Object.prototype.hasOwnProperty.call(context, key) + ? String(context[key]) + : ''; + }); +} diff --git a/config.js b/config.js index db5446a..c3cd157 100644 --- a/config.js +++ b/config.js @@ -1,440 +1,332 @@ import dotenv from 'dotenv'; dotenv.config(); +const logging = { + console: { + enabled: true, + colorize: true, + level: 'silly' + }, + file: { + dateFormat: 'YYYY-MM-DD', + timestampFormat: 'YYYY-MM-DD HH:mm:ss', + combined: { + enabled: true, + level: 'silly', + location: 'logs', + maxSize: '12m', + maxFiles: '30d' + }, + error: { + enabled: true, + level: 'error', + location: 'logs', + maxSize: '12m', + maxFiles: '365d' + } + } +}; + +const pocketbase = { + url: process.env.SHARED_POCKETBASE_URL, + username: process.env.SHARED_POCKETBASE_USERNAME, + password: process.env.SHARED_POCKETBASE_PASSWORD +}; + export default { - clients: [ - { - id: 'SysAI', - enabled: true, - owner: process.env.OWNER_ID, + clients: [ + { + id: 'SysAI', + enabled: true, + owner: process.env.OWNER_ID, - discord: { - appId: process.env.SYSAI_DISCORD_APPID, - token: process.env.SYSAI_DISCORD_TOKEN - }, + discord: { + appId: process.env.SYSAI_DISCORD_APPID, + token: process.env.SYSAI_DISCORD_TOKEN + }, - logging: { - console: { - enabled: true, - colorize: true, - level: 'silly', - }, - file: { - dateFormat: 'YYYY-MM-DD', - timestampFormat: 'YYYY-MM-DD HH:mm:ss', - combined: { - enabled: true, - level: 'silly', - location: 'logs', - maxSize: '12m', - maxFiles: '30d', - }, - error: { - enabled: true, - level: 'error', - location: 'logs', - maxSize: '12m', - maxFiles: '365d', - } - } - }, + logging: { ...logging }, - pocketbase: { - url: process.env.SHARED_POCKETBASE_URL, - username: process.env.SHARED_POCKETBASE_USERNAME, - password: process.env.SHARED_POCKETBASE_PASSWORD - }, + pocketbase: { ...pocketbase }, - responses: { - apiKey: process.env.SHARED_OPENAI_API_KEY, - defaultModel: 'gpt-4.1', - defaultMaxTokens: 1000, - defaultTemperature: 0.7, - conversationExpiry: 30 * 60 * 1000, - minScore: 1.0, - enableMentions: true, - enableReplies: true, - tools: { - webSearch: true, - fileSearch: false, - imageGeneration: true, - }, - imageGeneration: { - defaultModel: 'gpt-image-1', - defaultQuality: 'standard', - imageSavePath: './images' - } - }, + responses: { + apiKey: process.env.SHARED_OPENAI_API_KEY, + defaultModel: 'gpt-4.1', + defaultMaxTokens: 1000, + defaultTemperature: 0.7, + conversationExpiry: 30 * 60 * 1000, + minScore: 1.0, + enableMentions: true, + enableReplies: true, + tools: { + webSearch: true, + fileSearch: false, + imageGeneration: true + }, + imageGeneration: { + defaultModel: 'gpt-image-1', + defaultQuality: 'standard', + imageSavePath: './images' + } + }, - modules: [ - 'ansi', - 'botUtils', - 'pbUtils', - 'gitUtils', - 'responses', - 'responsesPrompt', - 'responsesQuery', - 'tempvc' - ] + modules: [ + 'ansi', + 'botUtils', + 'pbUtils', + 'gitUtils', + 'responses', + 'responsesPrompt', + 'responsesQuery', + 'tempvc' + ] - }, + }, - { - id: 'ASOP', - enabled: true, - owner: process.env.OWNER_ID, + { + id: 'ASOP', + enabled: true, + owner: process.env.OWNER_ID, - discord: { - appId: process.env.ASOP_DISCORD_APPID, - token: process.env.ASOP_DISCORD_TOKEN - }, + discord: { + appId: process.env.ASOP_DISCORD_APPID, + token: process.env.ASOP_DISCORD_TOKEN + }, - logging: { - console: { - enabled: true, - colorize: true, - level: 'silly', - }, - file: { - dateFormat: 'YYYY-MM-DD', - timestampFormat: 'YYYY-MM-DD HH:mm:ss', - combined: { - enabled: true, - level: 'silly', - location: 'logs', - maxSize: '12m', - maxFiles: '30d', - }, - error: { - enabled: true, - level: 'error', - location: 'logs', - maxSize: '12m', - maxFiles: '365d', - } - } - }, + logging: { ...logging }, - condimentX: { - dryRun: false, - guildID: '983057544849272883', - debugChannel: '1247179154869325865', - blacklistUsers: [ - '1162531805006680064' // Crow - ], - blacklistRoles: [ - '1173012816228274256', // @Bots - '1209570635085520977', // @Kevin Arby - '1226903935344971786', // @Werebeef - '1250141348040933407' // @RIP - ], - graylistRoles: [ - '1246749335866310656' // @Most Active - ], - whitelistRoles: [ - '1256082910163767378' // @"Crow" - ], - indexRoleID: '1209570635085520977', // Kevin's Vessel - viralRoleID: '1226903935344971786', // Werebeef - antiIndexRoleID: '1241228932037214358', // Exorcised - antiViralRoleID: '1241230334079795330', // Immunized - firstCycleInterval: 30000, - cycleInterval: 3600000, - cycleIntervalRange: 900000, - incidenceDenominator: 40, - cessationDenominator: 20, - probabilityLimit: 20, - antiViralEffectiveness: 90, - proximityWindow: 120000, - messageHistoryLimit: 50, - ephemeralDelay: 60000, - openAI: true, - openAITriggerOnlyDuringIncident: true, - openAIResponseDenominator: 1, - openAIInstructionsFile: './assets/kevinarby.txt', - openAITriggers: [ - 'kevin', - 'arby', - 'werebeef' - ], - openAIWebhookID: '1251666161075097640', - openAIWebhookToken: process.env.SYSAI_CONDIMENTX_WEBHOOK_TOKEN, - openAIToken: process.env.SHARED_OPENAI_API_KEY - }, + condimentX: { + dryRun: false, + guildID: '983057544849272883', + debugChannel: '1247179154869325865', + blacklistUsers: [ + '1162531805006680064' // Crow + ], + blacklistRoles: [ + '1173012816228274256', // @Bots + '1209570635085520977', // @Kevin Arby + '1226903935344971786', // @Werebeef + '1250141348040933407' // @RIP + ], + graylistRoles: [ + '1246749335866310656' // @Most Active + ], + whitelistRoles: [ + '1256082910163767378' // @"Crow" + ], + indexRoleID: '1209570635085520977', // Kevin's Vessel + viralRoleID: '1226903935344971786', // Werebeef + antiIndexRoleID: '1241228932037214358', // Exorcised + antiViralRoleID: '1241230334079795330', // Immunized + firstCycleInterval: 30000, + cycleInterval: 3600000, + cycleIntervalRange: 900000, + incidenceDenominator: 40, + cessationDenominator: 20, + probabilityLimit: 20, + antiViralEffectiveness: 90, + proximityWindow: 120000, + messageHistoryLimit: 50, + ephemeralDelay: 60000, + openAI: true, + openAITriggerOnlyDuringIncident: true, + openAIResponseDenominator: 1, + openAIInstructionsFile: './assets/kevinarby.txt', + openAITriggers: [ + 'kevin', + 'arby', + 'werebeef' + ], + openAIWebhookID: '1251666161075097640', + openAIWebhookToken: process.env.SYSAI_CONDIMENTX_WEBHOOK_TOKEN, + openAIToken: process.env.SHARED_OPENAI_API_KEY + }, - pocketbase: { - url: process.env.SHARED_POCKETBASE_URL, - username: process.env.SHARED_POCKETBASE_USERNAME, - password: process.env.SHARED_POCKETBASE_PASSWORD - }, + pocketbase: { ...pocketbase }, - responses: { - apiKey: process.env.SHARED_OPENAI_API_KEY, - defaultModel: 'gpt-4.1-mini', - defaultMaxTokens: 1000, - defaultTemperature: 0.7, - conversationExpiry: 30 * 60 * 1000, - minScore: 0.5, - enableMentions: true, - enableReplies: true, - tools: { - webSearch: false, - fileSearch: false, - imageGeneration: true, - }, - imageGeneration: { - defaultModel: 'gpt-image-1', - defaultQuality: 'standard', - imageSavePath: './images' - } - }, + responses: { + apiKey: process.env.SHARED_OPENAI_API_KEY, + defaultModel: 'gpt-4.1-mini', + defaultMaxTokens: 1000, + defaultTemperature: 0.7, + conversationExpiry: 30 * 60 * 1000, + minScore: 0.5, + enableMentions: true, + enableReplies: true, + tools: { + webSearch: false, + fileSearch: false, + imageGeneration: true + }, + imageGeneration: { + defaultModel: 'gpt-image-1', + defaultQuality: 'standard', + imageSavePath: './images' + } + }, - scorekeeper: { - baseOutput: 1000, - commendationValue: 0.25, - citationValue: 0.35, - cooldown: 43200000, - decay: 80, - schedule: '0 0 * * 0' - }, + scorekeeper: { + baseOutput: 1000, + commendationValue: 0.25, + citationValue: 0.35, + cooldown: 43200000, + decay: 80, + schedule: '0 0 * * 0' + }, - modules: [ - 'ansi', - 'botUtils', - 'pbUtils', - 'gitUtils', - 'condimentX', - 'responses', - 'responsesPrompt', - 'responsesQuery', - 'scorekeeper', - 'scorekeeper-example', - 'scExecHangarStatus' - ] + modules: [ + 'ansi', + 'botUtils', + 'pbUtils', + 'gitUtils', + 'condimentX', + 'responses', + 'responsesPrompt', + 'responsesQuery', + 'scorekeeper', + 'scorekeeper-example', + 'scExecHangarStatus' + ] - }, + }, - { - id: 'Crowley', - enabled: true, - owner: process.env.OWNER_ID, + { + id: 'Crowley', + enabled: true, + owner: process.env.OWNER_ID, - discord: { - appId: process.env.CROWLEY_DISCORD_APPID, - token: process.env.CROWLEY_DISCORD_TOKEN - }, + discord: { + appId: process.env.CROWLEY_DISCORD_APPID, + token: process.env.CROWLEY_DISCORD_TOKEN + }, - logging: { - console: { - enabled: true, - colorize: true, - level: 'silly', - }, - file: { - dateFormat: 'YYYY-MM-DD', - timestampFormat: 'YYYY-MM-DD HH:mm:ss', - combined: { - enabled: true, - level: 'silly', - location: 'logs', - maxSize: '12m', - maxFiles: '30d', - }, - error: { - enabled: true, - level: 'error', - location: 'logs', - maxSize: '12m', - maxFiles: '365d', - } - } - }, + logging: { ...logging }, - pocketbase: { - url: process.env.SHARED_POCKETBASE_URL, - username: process.env.SHARED_POCKETBASE_USERNAME, - password: process.env.SHARED_POCKETBASE_PASSWORD - }, + pocketbase: { ...pocketbase }, - responses: { - apiKey: process.env.SHARED_OPENAI_API_KEY, - defaultModel: 'gpt-4.1', - defaultMaxTokens: 1000, - defaultTemperature: 0.7, - conversationExpiry: 30 * 60 * 1000, - minScore: 0, - enableMentions: true, - enableReplies: true, - tools: { - webSearch: false, - fileSearch: false, - imageGeneration: false, - }, - imageGeneration: { - defaultModel: 'gpt-image-1', - defaultQuality: 'standard', - imageSavePath: './images' - } - }, + responses: { + apiKey: process.env.SHARED_OPENAI_API_KEY, + defaultModel: 'gpt-4.1', + defaultMaxTokens: 1000, + defaultTemperature: 0.7, + conversationExpiry: 30 * 60 * 1000, + minScore: 0, + enableMentions: true, + enableReplies: true, + tools: { + webSearch: false, + fileSearch: false, + imageGeneration: false + }, + imageGeneration: { + defaultModel: 'gpt-image-1', + defaultQuality: 'standard', + imageSavePath: './images' + } + }, - modules: [ - 'botUtils', - 'pbUtils', - 'responses', - 'responsesPrompt', - 'responsesQuery' - ] + modules: [ + 'botUtils', + 'pbUtils', + 'responses', + 'responsesPrompt', + 'responsesQuery' + ] - }, + }, - { - id: 'GRANDPA', - enabled: true, - owner: process.env.OWNER_ID, + { + id: 'GRANDPA', + enabled: true, + owner: process.env.OWNER_ID, - discord: { - appId: process.env.GRANDPA_DISCORD_APPID, - token: process.env.GRANDPA_DISCORD_TOKEN - }, + discord: { + appId: process.env.GRANDPA_DISCORD_APPID, + token: process.env.GRANDPA_DISCORD_TOKEN + }, - logging: { - console: { - enabled: true, - colorize: true, - level: 'silly', - }, - file: { - dateFormat: 'YYYY-MM-DD', - timestampFormat: 'YYYY-MM-DD HH:mm:ss', - combined: { - enabled: true, - level: 'silly', - location: 'logs', - maxSize: '12m', - maxFiles: '30d', - }, - error: { - enabled: true, - level: 'error', - location: 'logs', - maxSize: '12m', - maxFiles: '365d', - } - } - }, + logging: { ...logging }, - pocketbase: { - url: process.env.SHARED_POCKETBASE_URL, - username: process.env.SHARED_POCKETBASE_USERNAME, - password: process.env.SHARED_POCKETBASE_PASSWORD - }, + pocketbase: { ...pocketbase }, - responses: { - apiKey: process.env.SHARED_OPENAI_API_KEY, - defaultModel: 'gpt-4.1', - defaultMaxTokens: 200, - defaultTemperature: 0.7, - conversationExpiry: 30 * 60 * 1000, - minScore: 0, - enableMentions: false, - enableReplies: true, - tools: { - webSearch: false, - fileSearch: false, - imageGeneration: false, - }, - imageGeneration: { - defaultModel: 'gpt-image-1', - defaultQuality: 'standard', - imageSavePath: './images' - } - }, + responses: { + apiKey: process.env.SHARED_OPENAI_API_KEY, + defaultModel: 'gpt-4.1', + defaultMaxTokens: 200, + defaultTemperature: 0.7, + conversationExpiry: 30 * 60 * 1000, + minScore: 0, + enableMentions: false, + enableReplies: true, + tools: { + webSearch: false, + fileSearch: false, + imageGeneration: false + }, + imageGeneration: { + defaultModel: 'gpt-image-1', + defaultQuality: 'standard', + imageSavePath: './images' + } + }, - responsesRandomizer: { - chance: 0.01, - }, - modules: [ - 'botUtils', - 'pbUtils', - 'responses', - 'responsesPrompt', - 'responsesRandomizer' - ] + responsesRandomizer: { + chance: 0.01 + }, + modules: [ + 'botUtils', + 'pbUtils', + 'responses', + 'responsesPrompt', + 'responsesRandomizer' + ] - }, + }, - { - id: 'Smuuush', - enabled: true, - owner: process.env.OWNER_ID, + { + id: 'Smuuush', + enabled: true, + owner: process.env.OWNER_ID, - discord: { - appId: process.env.SMUUUSH_DISCORD_APPID, - token: process.env.SMUUUSH_DISCORD_TOKEN - }, + discord: { + appId: process.env.SMUUUSH_DISCORD_APPID, + token: process.env.SMUUUSH_DISCORD_TOKEN + }, - logging: { - console: { - enabled: true, - colorize: true, - level: 'silly', - }, - file: { - dateFormat: 'YYYY-MM-DD', - timestampFormat: 'YYYY-MM-DD HH:mm:ss', - combined: { - enabled: true, - level: 'silly', - location: 'logs', - maxSize: '12m', - maxFiles: '30d', - }, - error: { - enabled: true, - level: 'error', - location: 'logs', - maxSize: '12m', - maxFiles: '365d', - } - } - }, + logging: { ...logging }, - pocketbase: { - url: process.env.SHARED_POCKETBASE_URL, - username: process.env.SHARED_POCKETBASE_USERNAME, - password: process.env.SHARED_POCKETBASE_PASSWORD - }, + pocketbase: { ...pocketbase }, - responses: { - apiKey: process.env.SHARED_OPENAI_API_KEY, - defaultModel: 'gpt-4.1-mini', - defaultMaxTokens: 1000, - defaultTemperature: 0.7, - conversationExpiry: 30 * 60 * 1000, - minScore: 0, - enableMentions: true, - enableReplies: true, - tools: { - webSearch: false, - fileSearch: false, - imageGeneration: true, - }, - imageGeneration: { - defaultModel: 'gpt-image-1', - defaultQuality: 'standard', - imageSavePath: './images' - } - }, + responses: { + apiKey: process.env.SHARED_OPENAI_API_KEY, + defaultModel: 'gpt-4.1-mini', + defaultMaxTokens: 1000, + defaultTemperature: 0.7, + conversationExpiry: 30 * 60 * 1000, + minScore: 0, + enableMentions: true, + enableReplies: true, + tools: { + webSearch: false, + fileSearch: false, + imageGeneration: true + }, + imageGeneration: { + defaultModel: 'gpt-image-1', + defaultQuality: 'standard', + imageSavePath: './images' + } + }, - modules: [ - 'botUtils', - 'pbUtils', - 'responses', - 'responsesPrompt', - 'responsesQuery' - ], + modules: [ + 'botUtils', + 'pbUtils', + 'responses', + 'responsesPrompt', + 'responsesQuery' + ] - } - ] -} + } + ] +}; diff --git a/index.js b/index.js index 4cdafb7..d42089e 100644 --- a/index.js +++ b/index.js @@ -1,130 +1,130 @@ import { Client, Collection, GatewayIntentBits } from 'discord.js'; + +import { ansi, wrapAnsi } from './_src/ansiColors.js'; +import { loadModules } from './_src/loader.js'; import { createLogger } from './_src/logger.js'; import { initializePocketbase } from './_src/pocketbase.js'; -import { loadModules } from './_src/loader.js'; import config from './config.js'; // For deprecated ephemeral option: convert to flags -import { ansi, wrapAnsi } from './_src/ansiColors.js'; // Initialize Discord client const initializeClient = async (clientConfig) => { - // Create Discord client with intents - const client = new Client({ - // Include GuildVoiceStates and GuildMembers intents to track voice channel events - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildVoiceStates - ] - }); + // Create Discord client with intents + const client = new Client({ + // Include GuildVoiceStates and GuildMembers intents to track voice channel events + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildVoiceStates + ] + }); - // Attach config to client - client.config = clientConfig; + // Attach config to client + client.config = clientConfig; - // Set up Winston logger - client.logger = createLogger(clientConfig); - client.logger.info(`Initializing client: ${clientConfig.id}`); + // Set up Winston logger + client.logger = createLogger(clientConfig); + client.logger.info(`Initializing client: ${clientConfig.id}`); - // Set up Pocketbase - client.pb = await initializePocketbase(clientConfig, client.logger); + // Set up Pocketbase + client.pb = await initializePocketbase(clientConfig, client.logger); - // Commands collection - client.commands = new Collection(); - // ANSI helper attached to client - client.ansi = ansi; - client.wrapAnsi = wrapAnsi; + // Commands collection + client.commands = new Collection(); + // ANSI helper attached to client + client.ansi = ansi; + client.wrapAnsi = wrapAnsi; - // Load optional modules - await loadModules(clientConfig, client); + // Load optional modules + await loadModules(clientConfig, client); - // TODO: If the logger level is debug, create event binds to raw and debug. + // TODO: If the logger level is debug, create event binds to raw and debug. - // Discord client events - client.on('interactionCreate', async (interaction) => { - if (!interaction.isChatInputCommand()) return; + // Discord client events + client.on('interactionCreate', async (interaction) => { + if (!interaction.isChatInputCommand()) return; + const commandName = interaction.commandName; - const commandName = interaction.commandName; + try { + // Find command in collection + const command = client.commands.get(commandName); - try { - // Find command in collection - const command = client.commands.get(commandName); + if (!command) { + client.logger.warn(`Command not found: ${commandName}`); + await interaction.reply({ + content: 'Sorry, this command is not properly registered.', + ephemeral: true + }); + return; + } - if (!command) { - client.logger.warn(`Command not found: ${commandName}`); - await interaction.reply({ - content: 'Sorry, this command is not properly registered.', - ephemeral: true - }); - return; - } + // Execute the command + client.logger.debug(`Executing command: ${commandName}`); + await command.execute(interaction, client); - // Execute the command - client.logger.debug(`Executing command: ${commandName}`); - await command.execute(interaction, client); + } catch (error) { + client.logger.error(`Error executing command ${commandName}: ${error.message}`); - } catch (error) { - client.logger.error(`Error executing command ${commandName}: ${error.message}`); + // Handle already replied interactions + const replyContent = { + content: 'There was an error while executing this command.', + ephemeral: true + }; - // Handle already replied interactions - const replyContent = { - content: 'There was an error while executing this command.', - ephemeral: true - }; + if (interaction.replied || interaction.deferred) { + await interaction.followUp(replyContent).catch(err => { + client.logger.error(`Failed to send followUp: ${err.message}`); + }); + } else { + await interaction.reply(replyContent).catch(err => { + client.logger.error(`Failed to reply: ${err.message}`); + }); + } + } + }); - if (interaction.replied || interaction.deferred) { - await interaction.followUp(replyContent).catch(err => { - client.logger.error(`Failed to send followUp: ${err.message}`); - }); - } else { - await interaction.reply(replyContent).catch(err => { - client.logger.error(`Failed to reply: ${err.message}`); - }); - } - } - }); + client.on('ready', () => { + client.logger.info(`Logged in as ${client.user.tag}`); + }); - client.on('ready', () => { - client.logger.info(`Logged in as ${client.user.tag}`); - }); + client.on('error', (error) => { + client.logger.error(`Client error: ${error.message}`); + }); - client.on('error', (error) => { - client.logger.error(`Client error: ${error.message}`); - }); - - // Login to Discord - try { - await client.login(clientConfig.discord.token); - return client; - } catch (error) { - client.logger.error(`Failed to login: ${error.message}`); - throw error; - } + // Login to Discord + try { + await client.login(clientConfig.discord.token); + return client; + } catch (error) { + client.logger.error(`Failed to login: ${error.message}`); + throw error; + } }; // Main function to start bot const startBot = async () => { - const clients = []; + const clients = []; - // Initialize each client from config - for (const clientConfig of config.clients) { - try { - const client = await initializeClient(clientConfig); - clients.push(client); - } catch (error) { - console.error(`Failed to initialize client ${clientConfig.id}:`, error); - } - } + // Initialize each client from config + for (const clientConfig of config.clients) { + try { + const client = await initializeClient(clientConfig); + clients.push(client); + } catch (error) { + console.error(`Failed to initialize client ${clientConfig.id}:`, error); + } + } - return clients; + return clients; }; // Launch the bot startBot().then(clients => { - console.log(`[main] Successfully initialized ${clients.length} Discord clients`); + console.log(`[main] Successfully initialized ${clients.length} Discord clients`); }).catch(error => { - console.error(`[main] Failed to start bot: ${error.message}`); - process.exit(1); + console.error(`[main] Failed to start bot: ${error.message}`); + process.exit(1); }); diff --git a/package-lock.json b/package-lock.json index a471e52..da7baab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "ISC", "dependencies": { + "@discordjs/rest": "^2.2.0", "axios": "^1.8.4", "discord-api-types": "^0.37.120", "discord.js": "^14.18.0", @@ -19,6 +20,10 @@ "pocketbase": "^0.25.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "eslint": "^8.57.0", + "eslint-plugin-import": "^2.29.1" } }, "node_modules/@colors/colors": { @@ -192,6 +197,152 @@ "integrity": "sha512-vijevLh06Gtmex6BQzc9jRrGce6La0qnsF4bKwKM2L1ou0/sbJIOAkg7wz6YLLaodnUwQLljIhtrGxnkMjc1Ew==", "license": "MIT" }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", @@ -225,6 +376,13 @@ "npm": ">=7.0.0" } }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.15.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.10.tgz", @@ -259,6 +417,13 @@ "@types/node": "*" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, "node_modules/@vladfrangu/async_event_emitter": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", @@ -281,6 +446,29 @@ "node": ">=6.5" } }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -293,18 +481,234 @@ "node": ">= 8.0.0" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", @@ -316,6 +720,43 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -329,6 +770,50 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -386,6 +871,143 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -434,6 +1056,19 @@ "integrity": "sha512-vijevLh06Gtmex6BQzc9jRrGce6La0qnsF4bKwKM2L1ou0/sbJIOAkg7wz6YLLaodnUwQLljIhtrGxnkMjc1Ew==", "license": "MIT" }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -466,6 +1101,72 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -511,6 +1212,308 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -547,12 +1550,49 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/file-stream-rotator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", @@ -562,6 +1602,45 @@ "moment": "^2.29.1" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -588,6 +1667,22 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -622,6 +1717,13 @@ "node": ">= 12.20" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -631,6 +1733,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -668,6 +1801,92 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -680,6 +1899,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -728,18 +2006,363 @@ "ms": "^2.0.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -752,18 +2375,223 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -823,6 +2651,29 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -838,6 +2689,13 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-cron": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", @@ -899,6 +2757,113 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -953,18 +2918,187 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/pocketbase": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.25.2.tgz", "integrity": "sha512-ONZl1+qHJMnhR2uacBlBJ90lm7njtL/zy0606+1ROfK9hSL4LRBRc8r89rMcNRzPzRqCNyoFTh2Qg/lYXdEC1w==", "license": "MIT" }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -979,6 +3113,153 @@ "node": ">= 6" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -999,6 +3280,41 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -1008,6 +3324,164 @@ "node": ">=10" } }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -1035,12 +3509,140 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -1062,12 +3664,148 @@ "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", "license": "MIT" }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici": { "version": "6.21.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", @@ -1083,6 +3821,16 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -1123,6 +3871,111 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/winston": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", @@ -1177,6 +4030,23 @@ "node": ">= 12.0.0" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", @@ -1197,6 +4067,19 @@ "optional": true } } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index c2047e4..b711356 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,12 @@ }, "scripts": { "start": "node index.js", - "registry": "node registry.js" + "registry": "node registry.js", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "dependencies": { + "@discordjs/rest": "^2.2.0", "axios": "^1.8.4", "discord-api-types": "^0.37.120", "discord.js": "^14.18.0", @@ -24,5 +27,9 @@ "pocketbase": "^0.25.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "eslint": "^8.57.0", + "eslint-plugin-import": "^2.29.1" } } diff --git a/registry.js b/registry.js index bf7f9b0..f7ca8c8 100644 --- a/registry.js +++ b/registry.js @@ -1,9 +1,11 @@ // registry.js -import { REST } from '@discordjs/rest'; -import { Routes } from 'discord-api-types/v10'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; + +import { REST } from '@discordjs/rest'; // eslint-disable-line import/no-unresolved +import { Routes } from 'discord-api-types/v10'; + import config from './config.js'; // Get directory name in ES module @@ -19,7 +21,7 @@ const dryRun = args.includes('--dryrun'); // Validate required parameters if (args.includes('--help') || args.includes('-h') || !actionArg || !guildArg || !clientArg) { - console.log(` + console.log(` [registry] Discord Command Registry Tool @@ -40,14 +42,14 @@ Examples: node registry.js --action=register --guild=all --client=ASOP node registry.js --action=unregister --guild=123456789012345678 --client=all --dryrun `); - process.exit(1); + process.exit(1); } // Validate action parameter const validActions = ['register', 'unregister', 'list']; if (!validActions.includes(actionArg.toLowerCase())) { -console.error(`[registry] Error: Invalid action "${actionArg}". Must be one of: ${validActions.join(', ')}`); - process.exit(1); + console.error(`[registry] Error: Invalid action "${actionArg}". Must be one of: ${validActions.join(', ')}`); + process.exit(1); } const action = actionArg.toLowerCase(); @@ -57,17 +59,17 @@ const targetGuildId = isGuildAll ? null : guildArg; // Validate client parameter - must be "all" or match a client in config const isClientAll = clientArg.toLowerCase() === 'all'; -const targetClients = isClientAll - ? config.clients.filter(client => client.enabled !== false) - : config.clients.filter(client => client.id === clientArg && client.enabled !== false); +const targetClients = isClientAll + ? config.clients.filter(client => client.enabled !== false) + : config.clients.filter(client => client.id === clientArg && client.enabled !== false); if (targetClients.length === 0) { - console.error(`[registry] Error: No matching clients found for "${clientArg}"`); - console.log('Available clients:'); - config.clients - .filter(client => client.enabled !== false) - .forEach(client => console.log(` - ${client.id}`)); - process.exit(1); + console.error(`[registry] Error: No matching clients found for "${clientArg}"`); + console.log('Available clients:'); + config.clients + .filter(client => client.enabled !== false) + .forEach(client => console.log(` - ${client.id}`)); + process.exit(1); } /** @@ -76,36 +78,36 @@ if (targetClients.length === 0) { * @returns {Promise} - Array of command data objects */ async function extractCommandsFromModule(modulePath) { - try { - // Import the module - const moduleUrl = `file://${modulePath}`; - const module = await import(moduleUrl); + try { + // Import the module + const moduleUrl = `file://${modulePath}`; + const module = await import(moduleUrl); - // Check for commands array - if (Array.isArray(module.commands)) { - // Extract command data - const extractedCommands = module.commands.map(cmd => { - if (cmd && cmd.data && typeof cmd.data.toJSON === 'function') { - try { - return cmd.data.toJSON(); - } catch (error) { - console.warn(`Error converting command to JSON in ${path.basename(modulePath)}: ${error.message}`); - return null; - } - } - return null; - }).filter(Boolean); // Remove null entries + // Check for commands array + if (Array.isArray(module.commands)) { + // Extract command data + const extractedCommands = module.commands.map(cmd => { + if (cmd && cmd.data && typeof cmd.data.toJSON === 'function') { + try { + return cmd.data.toJSON(); + } catch (error) { + console.warn(`Error converting command to JSON in ${path.basename(modulePath)}: ${error.message}`); + return null; + } + } + return null; + }).filter(Boolean); // Remove null entries - console.log(` - Extracted ${extractedCommands.length} commands from ${path.basename(modulePath)}`); - return extractedCommands; - } else { - console.log(` - No commands found in ${path.basename(modulePath)}`); - return []; - } - } catch (error) { - console.error(`Error loading module ${modulePath}: ${error.message}`); - return []; - } + console.log(` - Extracted ${extractedCommands.length} commands from ${path.basename(modulePath)}`); + return extractedCommands; + } else { + console.log(` - No commands found in ${path.basename(modulePath)}`); + return []; + } + } catch (error) { + console.error(`Error loading module ${modulePath}: ${error.message}`); + return []; + } } /** @@ -114,27 +116,27 @@ async function extractCommandsFromModule(modulePath) { * @returns {Promise} - Array of command data objects */ async function processClientModules(clientConfig) { - console.log(`\nExtracting commands from modules for client: ${clientConfig.id}`); + console.log(`\nExtracting commands from modules for client: ${clientConfig.id}`); - const commands = []; - const optDir = path.join(__dirname, '_opt'); + const commands = []; + const optDir = path.join(__dirname, '_opt'); - // Process each module - for (const moduleName of clientConfig.modules || []) { - console.log(`Processing module: ${moduleName}`); - const modulePath = path.join(optDir, `${moduleName}.js`); + // Process each module + for (const moduleName of clientConfig.modules || []) { + console.log(`Processing module: ${moduleName}`); + const modulePath = path.join(optDir, `${moduleName}.js`); - if (!fs.existsSync(modulePath)) { - console.warn(` - Module not found: ${moduleName}`); - continue; - } + if (!fs.existsSync(modulePath)) { + console.warn(` - Module not found: ${moduleName}`); + continue; + } - const moduleCommands = await extractCommandsFromModule(modulePath); - commands.push(...moduleCommands); - } + const moduleCommands = await extractCommandsFromModule(modulePath); + commands.push(...moduleCommands); + } - console.log(`Total commands extracted for ${clientConfig.id}: ${commands.length}`); - return commands; + console.log(`Total commands extracted for ${clientConfig.id}: ${commands.length}`); + return commands; } /** @@ -144,12 +146,12 @@ async function processClientModules(clientConfig) { * @returns {Promise} - Guild information */ async function getGuildInfo(rest, guildId) { - try { - return await rest.get(Routes.guild(guildId)); - } catch (error) { - console.error(`Error fetching guild info: ${error.message}`); - return { name: `Unknown Guild (${guildId})` }; - } + try { + return await rest.get(Routes.guild(guildId)); + } catch (error) { + console.error(`Error fetching guild info: ${error.message}`); + return { name: `Unknown Guild (${guildId})` }; + } } /** @@ -158,24 +160,24 @@ async function getGuildInfo(rest, guildId) { * @param {string|null} guildId - Guild ID or null for global */ async function listCommands(clientConfig, guildId) { - const { id, discord } = clientConfig; + const { id, discord } = clientConfig; - if (!discord || !discord.token || !discord.appId) { - console.error(`Invalid client configuration for ${id}`); - return; - } + if (!discord || !discord.token || !discord.appId) { + console.error(`Invalid client configuration for ${id}`); + return; + } - // Set up REST client - const rest = new REST({ version: '10' }).setToken(discord.token); + // Set up REST client + const rest = new REST({ version: '10' }).setToken(discord.token); - // Handle global or guild-specific commands - if (guildId === null) { - // Global commands - await listGlobalCommands(clientConfig, rest); - } else { - // Guild-specific commands - await listGuildCommands(clientConfig, rest, guildId); - } + // Handle global or guild-specific commands + if (guildId === null) { + // Global commands + await listGlobalCommands(clientConfig, rest); + } else { + // Guild-specific commands + await listGuildCommands(clientConfig, rest, guildId); + } } /** @@ -184,32 +186,32 @@ async function listCommands(clientConfig, guildId) { * @param {REST} rest - Discord REST client */ async function listGlobalCommands(clientConfig, rest) { - console.log(`\nListing global commands for client: ${clientConfig.id}`); + console.log(`\nListing global commands for client: ${clientConfig.id}`); - try { - const route = Routes.applicationCommands(clientConfig.discord.appId); - const commands = await rest.get(route); + try { + const route = Routes.applicationCommands(clientConfig.discord.appId); + const commands = await rest.get(route); - if (commands.length === 0) { - console.log(`No global commands registered for client ${clientConfig.id}`); - return; - } + if (commands.length === 0) { + console.log(`No global commands registered for client ${clientConfig.id}`); + return; + } - console.log(`Found ${commands.length} global commands:`); + console.log(`Found ${commands.length} global commands:`); - // Display commands in a formatted table - console.log(''); - console.log('ID'.padEnd(20) + 'NAME'.padEnd(20) + 'DESCRIPTION'.padEnd(60)); + // Display commands in a formatted table + console.log(''); + console.log('ID'.padEnd(20) + 'NAME'.padEnd(20) + 'DESCRIPTION'.padEnd(60)); - for (const cmd of commands) { - console.log( - `${cmd.id.toString().padEnd(20)}${cmd.name.padEnd(20)}${(cmd.description || '')}` - ); - } + for (const cmd of commands) { + console.log( + `${cmd.id.toString().padEnd(20)}${cmd.name.padEnd(20)}${(cmd.description || '')}` + ); + } - } catch (error) { - console.error(`Error listing global commands for client ${clientConfig.id}: ${error.message}`); - } + } catch (error) { + console.error(`Error listing global commands for client ${clientConfig.id}: ${error.message}`); + } } /** @@ -219,38 +221,38 @@ async function listGlobalCommands(clientConfig, rest) { * @param {string} guildId - Guild ID */ async function listGuildCommands(clientConfig, rest, guildId) { - // Get guild info - const guildInfo = await getGuildInfo(rest, guildId); - const guildName = guildInfo.name || `Unknown Guild (${guildId})`; + // Get guild info + const guildInfo = await getGuildInfo(rest, guildId); + const guildName = guildInfo.name || `Unknown Guild (${guildId})`; - console.log(`\nListing commands for client: ${clientConfig.id} in guild: ${guildName} (${guildId})`); + console.log(`\nListing commands for client: ${clientConfig.id} in guild: ${guildName} (${guildId})`); - try { - const route = Routes.applicationGuildCommands(clientConfig.discord.appId, guildId); - const commands = await rest.get(route); + try { + const route = Routes.applicationGuildCommands(clientConfig.discord.appId, guildId); + const commands = await rest.get(route); - if (commands.length === 0) { - console.log(`No commands registered for client ${clientConfig.id} in guild ${guildName}`); - return; - } + if (commands.length === 0) { + console.log(`No commands registered for client ${clientConfig.id} in guild ${guildName}`); + return; + } - console.log(`Found ${commands.length} commands:`); + console.log(`Found ${commands.length} commands:`); - // Display commands in a formatted table - console.log(''); - console.log('ID'.padEnd(20) + 'NAME'.padEnd(20) + 'DESCRIPTION'.padEnd(60)); + // Display commands in a formatted table + console.log(''); + console.log('ID'.padEnd(20) + 'NAME'.padEnd(20) + 'DESCRIPTION'.padEnd(60)); - for (const cmd of commands) { - console.log( - `${cmd.id.toString().padEnd(20)}${cmd.name.padEnd(20)}${(cmd.description || '')}` - ); - } + for (const cmd of commands) { + console.log( + `${cmd.id.toString().padEnd(20)}${cmd.name.padEnd(20)}${(cmd.description || '')}` + ); + } - console.log(''); + console.log(''); - } catch (error) { - console.error(`Error listing commands for client ${clientConfig.id} in guild ${guildName}: ${error.message}`); - } + } catch (error) { + console.error(`Error listing commands for client ${clientConfig.id} in guild ${guildName}: ${error.message}`); + } } /** @@ -259,57 +261,57 @@ async function listGuildCommands(clientConfig, rest, guildId) { * @param {string|null} guildId - Guild ID or null for global */ async function registerCommands(clientConfig, guildId) { - const { id, discord } = clientConfig; + const { id, discord } = clientConfig; - if (!discord || !discord.token || !discord.appId) { - console.error(`Invalid client configuration for ${id}`); - return; - } + if (!discord || !discord.token || !discord.appId) { + console.error(`Invalid client configuration for ${id}`); + return; + } - // Extract commands from modules - const commands = await processClientModules(clientConfig); + // Extract commands from modules + const commands = await processClientModules(clientConfig); - if (commands.length === 0) { - console.log(`No commands found for client ${id}`); - return; - } + if (commands.length === 0) { + console.log(`No commands found for client ${id}`); + return; + } - // Set up REST client - const rest = new REST({ version: '10' }).setToken(discord.token); + // Set up REST client + const rest = new REST({ version: '10' }).setToken(discord.token); - // Determine route and scope description - let route; - let scopeDesc; + // Determine route and scope description + let route; + let scopeDesc; - if (guildId === null) { - route = Routes.applicationCommands(discord.appId); - scopeDesc = 'global'; - } else { - route = Routes.applicationGuildCommands(discord.appId, guildId); - const guildInfo = await getGuildInfo(rest, guildId); - const guildName = guildInfo.name || `Unknown Guild (${guildId})`; - scopeDesc = `guild ${guildName} (${guildId})`; - } + if (guildId === null) { + route = Routes.applicationCommands(discord.appId); + scopeDesc = 'global'; + } else { + route = Routes.applicationGuildCommands(discord.appId, guildId); + const guildInfo = await getGuildInfo(rest, guildId); + const guildName = guildInfo.name || `Unknown Guild (${guildId})`; + scopeDesc = `guild ${guildName} (${guildId})`; + } - // Register commands - console.log(`\nRegistering ${commands.length} commands for client ${id} in ${scopeDesc}...`); + // Register commands + console.log(`\nRegistering ${commands.length} commands for client ${id} in ${scopeDesc}...`); - // List commands being registered - console.log('\nCommands to register:'); - for (const cmd of commands) { - console.log(` - ${cmd.name}: ${cmd.description}`); - } + // List commands being registered + console.log('\nCommands to register:'); + for (const cmd of commands) { + console.log(` - ${cmd.name}: ${cmd.description}`); + } - if (dryRun) { - console.log(`\n[DRY RUN] Would register ${commands.length} commands for client ${id} in ${scopeDesc}`); - } else { - try { - await rest.put(route, { body: commands }); - console.log(`\nSuccessfully registered ${commands.length} commands for client ${id} in ${scopeDesc}`); - } catch (error) { - console.error(`Error registering commands for client ${id} in ${scopeDesc}: ${error.message}`); - } - } + if (dryRun) { + console.log(`\n[DRY RUN] Would register ${commands.length} commands for client ${id} in ${scopeDesc}`); + } else { + try { + await rest.put(route, { body: commands }); + console.log(`\nSuccessfully registered ${commands.length} commands for client ${id} in ${scopeDesc}`); + } catch (error) { + console.error(`Error registering commands for client ${id} in ${scopeDesc}: ${error.message}`); + } + } } /** @@ -318,107 +320,107 @@ async function registerCommands(clientConfig, guildId) { * @param {string|null} guildId - Guild ID or null for global */ async function unregisterCommands(clientConfig, guildId) { - const { id, discord } = clientConfig; + const { id, discord } = clientConfig; - if (!discord || !discord.token || !discord.appId) { - console.error(`Invalid client configuration for ${id}`); - return; - } + if (!discord || !discord.token || !discord.appId) { + console.error(`Invalid client configuration for ${id}`); + return; + } - // Set up REST client - const rest = new REST({ version: '10' }).setToken(discord.token); + // Set up REST client + const rest = new REST({ version: '10' }).setToken(discord.token); - // Determine route and scope description - let route; - let scopeDesc; + // Determine route and scope description + let route; + let scopeDesc; - if (guildId === null) { - route = Routes.applicationCommands(discord.appId); - scopeDesc = 'global'; - } else { - route = Routes.applicationGuildCommands(discord.appId, guildId); - const guildInfo = await getGuildInfo(rest, guildId); - const guildName = guildInfo.name || `Unknown Guild (${guildId})`; - scopeDesc = `guild ${guildName} (${guildId})`; - } + if (guildId === null) { + route = Routes.applicationCommands(discord.appId); + scopeDesc = 'global'; + } else { + route = Routes.applicationGuildCommands(discord.appId, guildId); + const guildInfo = await getGuildInfo(rest, guildId); + const guildName = guildInfo.name || `Unknown Guild (${guildId})`; + scopeDesc = `guild ${guildName} (${guildId})`; + } - // Get current commands to show what will be unregistered - try { - const currentCommands = await rest.get(route); - console.log(`\nFound ${currentCommands.length} commands for client ${id} in ${scopeDesc}`); + // Get current commands to show what will be unregistered + try { + const currentCommands = await rest.get(route); + console.log(`\nFound ${currentCommands.length} commands for client ${id} in ${scopeDesc}`); - if (currentCommands.length > 0) { - console.log('\nCommands to unregister:'); - for (const cmd of currentCommands) { - console.log(` - ${cmd.name}: ${cmd.description}`); - } - } else { - console.log(`No commands to unregister for client ${id} in ${scopeDesc}`); - return; - } + if (currentCommands.length > 0) { + console.log('\nCommands to unregister:'); + for (const cmd of currentCommands) { + console.log(` - ${cmd.name}: ${cmd.description}`); + } + } else { + console.log(`No commands to unregister for client ${id} in ${scopeDesc}`); + return; + } - if (dryRun) { - console.log(`\n[DRY RUN] Would unregister ${currentCommands.length} commands for client ${id} in ${scopeDesc}`); - } else { - await rest.put(route, { body: [] }); - console.log(`\nSuccessfully unregistered all commands for client ${id} in ${scopeDesc}`); - } - } catch (error) { - console.error(`Error unregistering commands for client ${id} in ${scopeDesc}: ${error.message}`); - } + if (dryRun) { + console.log(`\n[DRY RUN] Would unregister ${currentCommands.length} commands for client ${id} in ${scopeDesc}`); + } else { + await rest.put(route, { body: [] }); + console.log(`\nSuccessfully unregistered all commands for client ${id} in ${scopeDesc}`); + } + } catch (error) { + console.error(`Error unregistering commands for client ${id} in ${scopeDesc}: ${error.message}`); + } } // Main execution async function main() { - console.log(''); - console.log('Discord Command Registry Tool'); + console.log(''); + console.log('Discord Command Registry Tool'); - console.log(`\nOperation: ${action.toUpperCase()}`); - console.log(`Target Guild: ${isGuildAll ? 'ALL (Global)' : targetGuildId}`); - console.log(`Target Client: ${isClientAll ? 'ALL' : targetClients[0].id}`); + console.log(`\nOperation: ${action.toUpperCase()}`); + console.log(`Target Guild: ${isGuildAll ? 'ALL (Global)' : targetGuildId}`); + console.log(`Target Client: ${isClientAll ? 'ALL' : targetClients[0].id}`); - if (dryRun) { - console.log('\n*** DRY RUN MODE - NO CHANGES WILL BE MADE ***'); - } + if (dryRun) { + console.log('\n*** DRY RUN MODE - NO CHANGES WILL BE MADE ***'); + } - // Process each client - for (const clientConfig of targetClients) { - // Skip disabled clients - if (clientConfig.enabled === false) { - console.log(`\nSkipping disabled client: ${clientConfig.id}`); - continue; - } + // Process each client + for (const clientConfig of targetClients) { + // Skip disabled clients + if (clientConfig.enabled === false) { + console.log(`\nSkipping disabled client: ${clientConfig.id}`); + continue; + } - console.log(''); - console.log(`Processing client: ${clientConfig.id}`); + console.log(''); + console.log(`Processing client: ${clientConfig.id}`); - if (isGuildAll) { - // Global operation - if (action === 'list') { - await listCommands(clientConfig, null); - } else if (action === 'register') { - await registerCommands(clientConfig, null); - } else if (action === 'unregister') { - await unregisterCommands(clientConfig, null); - } - } else { - // Guild-specific operation - if (action === 'list') { - await listCommands(clientConfig, targetGuildId); - } else if (action === 'register') { - await registerCommands(clientConfig, targetGuildId); - } else if (action === 'unregister') { - await unregisterCommands(clientConfig, targetGuildId); - } - } - } + if (isGuildAll) { + // Global operation + if (action === 'list') { + await listCommands(clientConfig, null); + } else if (action === 'register') { + await registerCommands(clientConfig, null); + } else if (action === 'unregister') { + await unregisterCommands(clientConfig, null); + } + } else { + // Guild-specific operation + if (action === 'list') { + await listCommands(clientConfig, targetGuildId); + } else if (action === 'register') { + await registerCommands(clientConfig, targetGuildId); + } else if (action === 'unregister') { + await unregisterCommands(clientConfig, targetGuildId); + } + } + } - console.log(''); - console.log('Command registry operation complete'); + console.log(''); + console.log('Command registry operation complete'); } main().catch(error => { - console.error('Fatal error:', error); - process.exit(1); + console.error('Fatal error:', error); + process.exit(1); });