From af306313df6dfe858f8d02028f8e3b813240e03a Mon Sep 17 00:00:00 2001 From: jrmyr Date: Fri, 25 Apr 2025 21:27:00 -0400 Subject: [PATCH] Initial commit. --- .gitignore | 9 + _opt/condimentX.js | 412 +++++++++ _opt/pbUtils.js | 472 ++++++++++ _opt/responses.js | 399 +++++++++ _opt/responsesQuery.js | 230 +++++ _opt/scExecHangarStatus.js | 335 +++++++ _opt/scorekeeper-example.js | 146 ++++ _opt/scorekeeper.js | 594 +++++++++++++ _src/loader.js | 72 ++ _src/logger.js | 86 ++ _src/pocketbase.js | 20 + assets/io.eps | 62 ++ assets/io.png | Bin 0 -> 43817 bytes config.js.sample | 132 +++ index.js | 122 +++ package-lock.json | 1639 +++++++++++++++++++++++++++++++++++ package.json | 28 + prompts/ASOP.txt | 0 prompts/IO3.txt | 11 + prompts/Smuuush.txt | 2 + prompts/kevinarby.txt | 39 + registry.js | 423 +++++++++ 22 files changed, 5233 insertions(+) create mode 100644 .gitignore create mode 100644 _opt/condimentX.js create mode 100644 _opt/pbUtils.js create mode 100644 _opt/responses.js create mode 100644 _opt/responsesQuery.js create mode 100644 _opt/scExecHangarStatus.js create mode 100644 _opt/scorekeeper-example.js create mode 100644 _opt/scorekeeper.js create mode 100644 _src/loader.js create mode 100644 _src/logger.js create mode 100644 _src/pocketbase.js create mode 100644 assets/io.eps create mode 100644 assets/io.png create mode 100644 config.js.sample create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prompts/ASOP.txt create mode 100644 prompts/IO3.txt create mode 100644 prompts/Smuuush.txt create mode 100644 prompts/kevinarby.txt create mode 100644 registry.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b37a2d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store + +node_modules + +.env +config.js +images/* +logs/* +pocketbase/* diff --git a/_opt/condimentX.js b/_opt/condimentX.js new file mode 100644 index 0000000..5ca8456 --- /dev/null +++ b/_opt/condimentX.js @@ -0,0 +1,412 @@ +/** + * condimentX module - Viral infection role simulation + * + * Periodically designates an index case and randomly "infects" other members with a role. + * Logs detailed debug info, and can optionally use OpenAI to respond to users. + * + * Cycle Overview: + * 1. On client ready, the first incident cycle is scheduled after `firstCycleInterval` ms. + * 2. For each cycle: + * a. Gather current index cases (members with `indexRole`). + * b. Determine potential victims via `whitelistRoles` (fallback to `graylistRoles` if none). + * c. If no index cases and victims exist: + * - With probability `1 / incidenceDenominator`, choose a random victim and assign `indexRole`. + * d. Else if index cases exist: + * - With probability `1 / cessationDenominator`, remove `indexRole` from all index cases. + * e. Else (no capacity to start or end incidents): skip assignment/removal. + * f. Schedule the next cycle after `cycleInterval ± cycleIntervalRange` ms. + * + * Infection Spread: + * - On each messageCreate event, collects recent messages within `proximityWindow`. + * - Computes infection chance as `(infectedMessages / totalMessages) * 100`, capped at `probabilityLimit`. + * - If member has `antiViralRole`, reduces chance by `antiViralEffectiveness%`. + * - On success, assigns `viralRole` to the message author and removes `antiViralRole`. + * + * Event Hooks: + * - guildMemberUpdate: logs role additions/removals and clears viral roles when indexRole is removed. + * - messageCreate: handles proximity-based infection and optional OpenAI triggers. + * + * Configuration is expected under client.config.condimentX, including: + * dryRun, guildID, debugChannel, blacklistUsers, blacklistRoles, + * graylistRoles, whitelistRoles, indexRoleID, viralRoleID, + * antiIndexRoleID, antiViralRoleID, cycle intervals, probabilities, + * messageHistoryLimit, OpenAI settings, etc. + */ +export const init = async (client, config) => { + // Destructure all module settings + const { + dryRun, + guildID, + debugChannel, + blacklistUsers, + blacklistRoles, + graylistRoles, + whitelistRoles, + indexRoleID, + viralRoleID, + antiIndexRoleID, + antiViralRoleID, + firstCycleInterval, + cycleInterval, + cycleIntervalRange, + incidenceDenominator, + cessationDenominator, + probabilityLimit, + antiViralEffectiveness, + proximityWindow, + messageHistoryLimit, + ephemeralDelay, + openAI, + openAITriggerOnlyDuringIncident, + openAIResponseDenominator, + openAIInstructionsFile, + openAITriggers, + openAIWebhookID, + openAIWebhookToken, + openAIToken + } = config.condimentX; + + // Import required modules + const { WebhookClient, Collection } = await import('discord.js'); + const fs = await import('fs'); + const OpenAI = (await import('openai')).default; + + // Internal state variables + let incidentCounter = 0; + let guild, indexRole, viralRole, antiIndexRole, antiViralRole; + let openai, openAIWebhook, openAIWebhookClient; + // 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 = '') { + try { + debug(`**AI Prompt**: ${prompt}`); + + // Read instructions. + let 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}`}, + ], + }); + let 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); + } + } + } catch (error) { + anomaly(error); + } + } + + // === Logging Helpers === + // Send debug and error logs to the configured debugChannel and console. + async function debug(string, ephemeral = false) { + string += ephemeral ? ' :x:' : ''; + try { + const channel = client.channels.cache.get(debugChannel); + const message = await channel.send(string); + if (ephemeral) { + setTimeout(() => message.delete().catch(error => client.logger.error(error)), ephemeralDelay); + } + } catch (error) { + client.logger.error(`CondimentX debug error: ${error.message}`); + } finally { + client.logger.debug(`CondimentX: ${string}`); + } + } + + // === Error Handling Helpers === + // Logs anomalies both to debug channel and error logger. + async function anomaly(tag, error) { + if (typeof error === 'object') { + if (error.message) { + debug(`**Anomaly**: *${tag}*\n\`\`\`\n${error.message}\n\`\`\``); + } + if (error.stack) { + debug(`**Stack Trace**\n\`\`\`\n${error.stack}\n\`\`\``); + } + } else { + debug(`**Anomaly**: *${tag}*\n\`\`\`\n${error}\n\`\`\``); + } + client.logger.error(`CondimentX anomaly ${tag}: ${error}`); + } + + // === Message Fetching Helpers === + // Retrieve recent messages from every text channel since a given timestamp. + async function fetchRecentMessages(since) { + const allMessages = new Collection(); + + // Get all text channels in the guild + const textChannels = guild.channels.cache.filter( + channel => channel.type === 0 && channel.viewable + ); + + // For each channel, fetch recent messages + for (const channel of textChannels.values()) { + try { + const messages = await channel.messages.fetch({ + limit: messageHistoryLimit, + after: since + }); + + // Add these messages to our collection + for (const [id, message] of messages) { + allMessages.set(id, message); + } + } catch (error) { + // Skip channels where we can't fetch messages + client.logger.warn(`Couldn't fetch messages from channel ${channel.name}: ${error.message}`); + } + } + + return allMessages; + } + + // === Incident Cycle Logic === + // Periodically runs to potentially assign index roles and spread the infection. + // Schedules the next cycle based on cycleInterval and cycleIntervalRange. + async function cycleIncidents() { + try { + guild = client.guilds.cache.get(guildID); + + 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)); + debug(`${bullet} Index Cases: **${indexesList.size}**`); + + // Build the victimsList using whitelisted roles. + let victimsList = guild.members.cache.filter(member => { + const hasBlacklistedUser = blacklistUsers.includes(member.id); + const hasBlacklistedRole = blacklistRoles.some(role => member.roles.cache.has(role)); + const hasWhitelistedRole = whitelistRoles.some(role => member.roles.cache.has(role)); + return !hasBlacklistedUser && !hasBlacklistedRole && hasWhitelistedRole; + }); + + // If the victimsList is empty using whitelisted roles, use graylisted roles instead. + if (victimsList.size === 0) { + victimsList = guild.members.cache.filter(member => { + const hasBlacklistedUser = blacklistUsers.includes(member.id); + const hasBlacklistedRole = blacklistRoles.some(role => member.roles.cache.has(role)); + const hasGraylistedRole = graylistRoles.some(role => member.roles.cache.has(role)); + return !hasBlacklistedUser && !hasBlacklistedRole && hasGraylistedRole; + }); + debug(`${bullet} Potential Victims: **${victimsList.size}** (**graylist**)`); + } else { + debug(`${bullet} Potential Victims: **${victimsList.size}** (**whitelist**)`); + } + + // Conditions for potentially starting an incident. + if (indexesList.size == 0 && victimsList.size > 0) { + if ((Math.floor(Math.random() * incidenceDenominator) + 1) === 1) { + debug(`${bullet} Incidence Check: **Success**`); + const newIndex = victimsList.random(); + debug(`${bullet} New index case: ${newIndex.displayName}`); + if (dryRun === false) { + await newIndex.roles.add(indexRole).catch(error => client.logger.error(error)); + } else { + debug(`${bullet} dryRun = **${dryRun}**, *role change skipped*`); + } + } else { + debug(`${bullet} Incidence Check: **Failure**`); + } + // Otherwise, potentially end an incident. + } else if (indexesList.size > 0) { + if ((Math.floor(Math.random() * cessationDenominator) + 1) === 1) { + debug(`${bullet} Cessation Check: **Success**`); + for (const oldIndex of indexesList.values()) { + debug(`${bullet} Old index case: ${oldIndex.user.tag}`); + if (dryRun === false) { + await oldIndex.roles.remove(indexRole).catch(error => client.logger.error(error)); + } else { + debug(`${bullet} dryRun = **${dryRun}**, *role change skipped*`); + } + } + } else { + debug(`${bullet} Cessation Check: **Failure**`); + } + // Edge cases. + } else { + debug(`${bullet} Insufficient population, aborting.`); + } + + // Prepare the next cycle. + let interval = cycleInterval + Math.floor(Math.random() * (2 * cycleIntervalRange + 1)) - cycleIntervalRange; + setTimeout(cycleIncidents, interval); + debug(`${bullet} Cycle #${incidentCounter} **** at ****`); + } catch (error) { + anomaly('cycleIncidents', error); + } + } + + // Deferred initialization: run after Discord client is ready + const guildMemberUpdateHandler = async (reference, member) => { + try { + if (member.guild.id !== guildID) return; + guild = client.guilds.cache.get(guildID); + if (!reference.roles.cache.has(indexRole.id) && member.roles.cache.has(indexRole.id)) { + debug(`***${indexRole.name} added to ${member.user.displayName}***.`); + if (openAI === true && (Math.floor(Math.random() * openAIResponseDenominator) + 1) === 1) { + ai(`${member.user.displayName} has received the ${indexRole.name} role.`); + } + } + if (reference.roles.cache.has(indexRole.id) && !member.roles.cache.has(indexRole.id)) { + debug(`***${indexRole.name} removed from ${member.user.displayName}***.`); + if (openAI === true && (Math.floor(Math.random() * openAIResponseDenominator) + 1) === 1) { + ai(`${member.user.displayName} has lost the ${indexRole.name} role.`); + } + guild.members.cache.forEach(async infected => { + if (infected.roles.cache.has(viralRole.id)) { + debug(`${bullet} Removing ${viralRole.name} from ${infected.user.displayName}.`); + if (!dryRun) await infected.roles.remove(viralRole).catch(e => client.logger.error(e)); + else debug(`${bullet} dryRun = **${dryRun}** - *role change skipped*`); + } + }); + } + if (!reference.roles.cache.has(viralRole.id) && member.roles.cache.has(viralRole.id)) { + debug(`***${viralRole.name} added to ${member.user.displayName}***.`); + if (openAI === true && (Math.floor(Math.random() * openAIResponseDenominator) + 1) === 1) { + ai(`${member.user.displayName} has received the ${viralRole.name} role.`); + } + } + } catch (error) { + anomaly('guildMemberUpdate', error); + } + }; + const messageCreateHandler = async (message) => { + try { + if (!message.member || !message.author || !message.guild) return; + if (message.author.id === client.user.id) return; + if (message.guild.id !== guildID) return; + if (message.author.bot) return; + if (message.webhookId) return; + guild = client.guilds.cache.get(guildID); + if (openAI === true && openAIWebhook && openAIWebhook.channelId === message.channel.id && + openAITriggers.some(w => message.content.replace(/[^\\w\\s]/gi,'').toLowerCase().includes(w.toLowerCase())) && + ((openAITriggerOnlyDuringIncident === false) || (openAITriggerOnlyDuringIncident === true && + guild.members.cache.some(m => m.roles.cache.has(indexRole.id)))) && + (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; + const timeThreshold = Date.now() - proximityWindow; + const recent = await message.channel.messages.fetch({ limit: messageHistoryLimit, before: message.id }); + const prox = recent.filter(msg => msg.createdTimestamp >= timeThreshold && msg.guild.id === message.guildId); + let infections = 0; + + // Count infected members in proximity messages + for (const msg of prox.values()) { + try { + const msgMember = msg.member; + if (msgMember) { + // Check if author has index or viral role + const isInfected = msgMember.roles.cache.has(indexRole.id) || + msgMember.roles.cache.has(viralRole.id); + if (isInfected) infections++; + } + } catch (error) { + // Skip invalid proximity data + } + } + + // Either condition results in 0% chance of infection, so abort now + if (infections === 0 || prox.size === 0) { + return; + } + + // Base probability, capped at ${probabilityLimit} to control infection rate + 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)) { + percentage = Math.round(percentage - (antiViralEffectiveness * (percentage / 100))); + } + + // Infect the unlucky + if (infections > 0) { + const randomRoll = Math.round(Math.random() * 100); + if (randomRoll < percentage) { + const member = message.member; + debug(`${bullet} **${message.member.displayName}**: **infected**, proximity = **${infections}/${prox.size}**, immunity strength = **${randomRoll}**, infection strength = **${percentage}**`); + if (!dryRun) { + await member.roles.add(viralRole); + await member.roles.remove(antiViralRole); + } else { + debug(`${bullet} dryRun = **${dryRun}** - *role change skipped*`); + } + } else { + debug(`${bullet} **${message.member.displayName}**: **immunity**, proximity = **${infections}/${prox.size}**, immunity strength = **${randomRoll}**, infection strength = **${percentage}**`); + } + } + + } catch (error) { + anomaly('messageCreate', error); + } + }; + + // Deferred setup on ready + const readyHandler = async () => { + console.log('[condimentX] ready event fired'); + client.logger.info('Initializing CondimentX module'); + if (openAI === true) { + openai = new OpenAI({ apiKey: openAIToken }); + openAIWebhook = await client.fetchWebhook(openAIWebhookID, openAIWebhookToken).catch(error => { + client.logger.error(`Could not fetch webhook: ${error.message}`); + return null; + }); + if (openAIWebhook) openAIWebhookClient = new WebhookClient({ id: openAIWebhookID, token: openAIWebhookToken }); + } + try { + guild = client.guilds.cache.get(guildID); + if (!guild) { + client.logger.error(`CondimentX error: Guild ${guildID} not found`); + return; + } + indexRole = await guild.roles.fetch(indexRoleID); + viralRole = await guild.roles.fetch(viralRoleID); + antiIndexRole = await guild.roles.fetch(antiIndexRoleID); + antiViralRole = await guild.roles.fetch(antiViralRoleID); + await guild.members.fetch(); + } catch (error) { + anomaly('init', error); + } + debug(`${bullet} Initialized: \n`); + debug(`${bullet} Index Role = \`${indexRole?.name}\` / \`${antiIndexRole?.name}\``); + debug(`${bullet} Viral Role = \`${viralRole?.name}\` / \`${antiViralRole?.name}\``); + debug(`${bullet} Incidence Probability = **1**:**${incidenceDenominator}**`); + debug(`${bullet} Cessation Probability = **1**:**${cessationDenominator}**`); + debug(`${bullet} Infection Probability Limit = **${probabilityLimit}**%`); + debug(`${bullet} AntiViral Effectiveness: **${antiViralEffectiveness}**%`); + debug(`${bullet} Dry Run: **${dryRun}**`); + debug(`${bullet} OpenAI: **${openAI}**`); + if (openAI === true) { + debug(`${bullet} OpenAI System Instructions: **${openAIInstructionsFile}**`); + debug(`${bullet} File Found: **${fs.existsSync(openAIInstructionsFile)}**`); + debug(`${bullet} OpenAI Webhook Display Name: **${openAIWebhook?.name || 'Not found'}**`); + } + // Schedule first incident cycle + setTimeout(cycleIncidents, firstCycleInterval); + debug(`${bullet} Initialization complete, first cycle **** at ****`); + client.on('guildMemberUpdate', guildMemberUpdateHandler); + client.on('messageCreate', messageCreateHandler); + }; + client.once('ready', readyHandler); + + // Return cleanup function + return () => { + client.removeListener('ready', readyHandler); + client.removeListener('guildMemberUpdate', guildMemberUpdateHandler); + client.removeListener('messageCreate', messageCreateHandler); + if (openAIWebhookClient) openAIWebhookClient = null; + }; +}; diff --git a/_opt/pbUtils.js b/_opt/pbUtils.js new file mode 100644 index 0000000..8968ae0 --- /dev/null +++ b/_opt/pbUtils.js @@ -0,0 +1,472 @@ +// _opt/pbutils.js + +/** + * PocketBase utilities module - extends PocketBase client with useful shortcuts + */ + +/** + * Initializes the PocketBase utilities module + * @param {Object} client - Discord client with attached PocketBase instance + * @param {Object} config - Client configuration + */ +export const init = async (client, config) => { + const { pb, logger } = client; + + logger.info('Initializing PocketBase utilities module'); + + // Attach utility methods to the pb object + extendPocketBase(pb, logger); + + // Add connection state handling + setupConnectionHandling(pb, logger); + + logger.info('PocketBase utilities module initialized'); +}; + +/** + * Extends the PocketBase instance with utility methods + * @param {Object} pb - PocketBase instance + * @param {Object} logger - Winston logger + */ +const extendPocketBase = (pb, logger) => { + // ===== 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; + } + }; + + /** + * 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; + } + }; + + /** + * 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; + } + }; + + /** + * 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; + } + }; + + /** + * 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); + }; + + // ===== 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 + }); + + 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; + + try { + while (true) { + const result = await pb.collection(collection).getList(page, pageSize, options); + records.push(...result.items); + + if (records.length >= result.totalItems) { + break; + } + + page++; + } + + 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' + }); + + 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 []; + } + + const results = []; + + 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); + } + + 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 []; + } + + const results = []; + + 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); + } + + 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 []; + } + + const results = []; + + 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); + } + + return results; + } catch (error) { + logger.error(`Failed batch delete in ${collection}: ${error.message}`); + throw error; + } + }; + + // ===== CACHE MANAGEMENT ===== + + // 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); + }, + + /** + * 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)); + } + }, + + /** + * Delete a value from cache + * @param {string} key - Cache key + */ + delete(key) { + this._store.delete(key); + this._ttls.delete(key); + }, + + /** + * Clear all cache + */ + 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); + + if (cached !== undefined) { + return cached; + } + + const record = await pb.getOne(collection, id); + pb.cache.set(cacheKey, record, ttlSeconds); + + 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); + + 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); + + return result; + }; +}; + +/** + * Setup connection state handling + * @param {Object} pb - PocketBase instance + * @param {Object} logger - Winston logger + */ +const setupConnectionHandling = (pb, logger) => { + // Add connection state tracking + pb.isConnected = true; + pb.lastSuccessfulAuth = null; + + // 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'); + } + }); + + // 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) { + await pb.admins.authRefresh(); + } else if (pb._config.username && pb._config.password) { + // Fall back to full re-authentication if credentials available + await pb.admins.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'); + + return true; + } catch (error) { + logger.error(`Failed to reconnect to PocketBase: ${error.message}`); + pb.isConnected = false; + return false; + } + } + + 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; + } + + // 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); + }; +}; diff --git a/_opt/responses.js b/_opt/responses.js new file mode 100644 index 0000000..5c4cf7d --- /dev/null +++ b/_opt/responses.js @@ -0,0 +1,399 @@ +// _opt/responses.js +// Simplified OpenAI Responses module with clear flow and context management +import fs from 'fs/promises'; +import path from 'path'; +import { OpenAI } from 'openai'; +import axios from 'axios'; +import { AttachmentBuilder } from 'discord.js'; + +// Discord message max length +const MAX_DISCORD_MSG_LENGTH = 2000; + +/** + * Split a long message into chunks <= maxLength, preserving code fences by closing and reopening. + * @param {string} text + * @param {number} maxLength + * @returns {string[]} + */ +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; + } + } + // 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; + } + 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); +} + +/** + * Load the system prompt from disk. + */ +async function loadSystemPrompt(filePath, logger) { + try { + const prompt = await fs.readFile(path.resolve(filePath), 'utf8'); + logger.info(`Loaded system prompt: ${filePath}`); + return prompt; + } catch (err) { + logger.error(`Failed to load system prompt: ${err.message}`); + return ''; + } +} + +/** + * Determine if the bot should respond: + * - Mentioned + * - Direct reply + */ +async function shouldRespond(message, botId, logger) { + if (message.author.bot || !botId) return false; + const isMention = message.mentions.users.has(botId); + let isReply = false; + if (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; +} + +/** + * Cache the last response ID for context continuity. + */ +function cacheResponse(client, key, id, ttlSeconds) { + client.pb?.cache?.set(key, id, ttlSeconds); +} + +/** + * Award output tokens via the scorekeeper module. + */ +function awardOutput(client, guildId, userId, amount) { + if (client.scorekeeper && amount > 0) { + client.scorekeeper.addOutput(guildId, userId, amount) + .catch(err => client.logger.error(`Scorekeeper error: ${err.message}`)); + } +} + +/** + * Handle image generation function calls. + * Returns true if an image was handled and replied. + */ +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 { return false; } + if (!args.prompt?.trim()) { + await message.reply('Cannot generate image: empty prompt.'); + return true; + } + // Determine image size based on aspect: square, landscape, or portrait + // Square will always use 1024x1024 + let size; + switch (args.aspect) { + case 'landscape': + size = '1792x1024'; + break; + case 'portrait': + size = '1024x1792'; + break; + case 'square': + size = '1024x1024'; + break; + default: + size = '1024x1024'; + } + // Determine image quality, defaulting to cfg.imageGeneration.defaultQuality + const quality = ['standard', 'hd'].includes(args.quality) + ? args.quality + : cfg.imageGeneration.defaultQuality; + try { + // Generate image via OpenAI + const imgRes = await client.openai.images.generate({ model: 'dall-e-3', prompt: args.prompt, quality: quality, size: size, n: 1 }); + const url = imgRes.data?.[0]?.url; + if (!url) throw new Error('No image URL'); + // Download and save locally + const dl = await axios.get(url, { responseType: 'arraybuffer' }); + const buf = Buffer.from(dl.data); + const filename = `${message.author.id}-${Date.now()}.png`; + const dir = cfg.imageGeneration.imageSavePath || './images'; + await fs.mkdir(dir, { recursive: true }); + const filePath = path.join(dir, filename); + await fs.writeFile(filePath, buf); + client.logger.info(`Saved image: ${filePath}`); + // Reply with attachment + const attachment = new AttachmentBuilder(buf, { name: filename }); + await message.reply({ content: args.prompt, files: [attachment] }); + // Follow-up recap to preserve conversation context (submit function tool output) + try { + const convKey = message.thread?.id || message.channel.id; + // Build a function_call_output input item for the Responses API + const toolOutputItem = { + type: 'function_call_output', + call_id: fn.call_id, + output: JSON.stringify({ url }), + }; + const recapBody = { + model: cfg.defaultModel, + // re-use original system/developer instructions + instructions: client.responsesSystemPrompt, + previous_response_id: resp.id, + input: [toolOutputItem], + max_output_tokens: Math.min(100, cfg.defaultMaxTokens), + temperature: cfg.defaultTemperature, + }; + const recapResp = await client.openai.responses.create(recapBody); + cacheResponse(client, convKey, recapResp.id, Math.floor(cfg.conversationExpiry / 1000)); + // Award tokens for the recap chat response + const recapTokens = recapResp.usage?.total_tokens ?? recapResp.usage?.completion_tokens ?? 0; + awardOutput(client, message.guild.id, message.author.id, recapTokens); + } catch (err) { + client.logger.error(`Recap failed: ${err.message}`); + } + } catch (err) { + client.logger.error(`Image error: ${err.message}`); + await message.reply(`Image generation error: ${err.message}`); + } + return true; +} + +/** + * Main message handler: + * 1. Determine if bot should respond + * 2. Build and send AI request + * 3. Cache response ID + * 4. Handle image or text reply + * 5. Award output points + */ +async function onMessage(client, cfg, message) { + const logger = client.logger; + const botId = client.user?.id; + if (!(await shouldRespond(message, botId, logger))) return; + await message.channel.sendTyping(); + + // 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 () => { + try { + // Previous response ID for context continuity + const prev = client.pb?.cache?.get(key); + // Enforce minimum score to use AI responses + try { + const scoreData = await client.scorekeeper.getScore(message.guild.id, message.author.id); + if (scoreData.totalScore < cfg.minScore) { + await message.reply( + `You need a score of at least ${cfg.minScore} to use AI responses. Your current score is ${scoreData.totalScore.toFixed(2)}.` + ); + return; + } + } catch (err) { + client.logger.error(`Error checking score: ${err.message}`); + } + // Build request body, prefixing with a mention of who spoke + const speakerMention = `<@${message.author.id}>`; + const body = { + model: cfg.defaultModel, + instructions: client.responsesSystemPrompt, + input: `${speakerMention} said: ${message.content}`, + previous_response_id: prev, + max_output_tokens: cfg.defaultMaxTokens, + temperature: cfg.defaultTemperature, + }; + // Assemble any enabled tools + const tools = []; + if (cfg.tools?.imageGeneration) { + tools.push({ + type: 'function', + name: 'generate_image', + description: 'Generate an image with a given prompt, aspect, and quality.', + parameters: { + type: 'object', + properties: { + prompt: { type: 'string' }, + aspect: { type: 'string', enum: ['square','portrait','landscape'] }, + quality: { type: 'string', enum: ['standard', 'hd'] }, + }, + required: ['prompt','aspect','quality'], + additionalProperties: false, + }, + strict: true + }); + } + if (cfg.tools?.webSearch) { + tools.push({ type: 'web_search_preview' }); + } + if (tools.length) { + body.tools = tools; + } + + // 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}`); + } + }; + // Chain the handler to the last promise + const next = last.then(handler).catch(err => logger.error(err)); + lockMap.set(key, next); + // Queue enqueued; handler will send response when its turn arrives + return; + + // Call OpenAI Responses + let resp; + try { + logger.debug(`Calling AI with body: ${JSON.stringify(body)}`); + resp = await client.openai.responses.create(body); + logger.info(`AI response id=${resp.id}`); + // Award tokens for the AI chat response immediately (captures token usage even if image follows) + const chatTokens = resp.usage?.total_tokens ?? resp.usage?.completion_tokens ?? 0; + awardOutput(client, message.guild.id, message.author.id, chatTokens); + } catch (err) { + logger.error(`AI error: ${err.message}`); + return message.reply('Error generating response.'); + } + + // Cache for next turn only if this was a text response + 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 (split if over Discord limit) + 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); + } + } + +} + +/** + * Send a narrative response to a specific Discord channel or thread. + * @param {import('discord.js').Client} client - Discord client instance. + * @param {Object} cfg - Responses module configuration. + * @param {string} channelId - ID of the Discord channel or thread. + * @param {string} text - Narrative input text to process. + */ +export async function sendNarrative(client, cfg, channelId, text) { + const logger = client.logger; + try { + // Build the narrative instructions + const instructions = `${client.responsesSystemPrompt}\n\nGenerate the following as an engaging narrative:`; + const body = { + model: cfg.defaultModel, + instructions, + input: text, + max_output_tokens: cfg.defaultMaxTokens, + temperature: cfg.defaultTemperature, + }; + logger.debug('sendNarrative: calling AI with body', body); + const resp = await client.openai.responses.create(body); + logger.info(`sendNarrative 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}`); + } +} + +/** + * Initialize the Responses module + */ +export async function init(client, clientConfig) { + const cfg = clientConfig.responses; + client.logger.info('Initializing Responses module'); + client.responsesSystemPrompt = await loadSystemPrompt(cfg.systemPromptPath, client.logger); + client.openai = new OpenAI({ apiKey: cfg.apiKey }); + client.on('messageCreate', m => onMessage(client, cfg, m)); + client.logger.info('Responses module ready'); +} diff --git a/_opt/responsesQuery.js b/_opt/responsesQuery.js new file mode 100644 index 0000000..8e25e54 --- /dev/null +++ b/_opt/responsesQuery.js @@ -0,0 +1,230 @@ +import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js'; +import fs from 'fs/promises'; +import path from 'path'; +import axios from 'axios'; + +/** + * Split long text into Discord-safe chunks without breaking mid-line. + * @param {string} text + * @param {number} max + * @returns {string[]} + */ +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 = ''; + } + chunk += next; + } + if (chunk) chunks.push(chunk); + return chunks; +} + +/** + * Handle 'generate_image' function calls for slash commands. + * Returns true if image was sent. + */ +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 { return false; } + if (!args.prompt?.trim()) { + await interaction.editReply({ content: 'Cannot generate image: empty prompt.', ephemeral }); + return true; + } + let size; + switch (args.aspect) { + case 'landscape': size = '1792x1024'; break; + case 'portrait': size = '1024x1792'; break; + case 'square': default: size = '1024x1024'; break; + } + const quality = ['standard', 'hd'].includes(args.quality) + ? args.quality + : cfg.imageGeneration.defaultQuality; + try { + const imgRes = await client.openai.images.generate({ model: 'dall-e-3', prompt: args.prompt, quality, size, n: 1 }); + const url = imgRes.data?.[0]?.url; + if (!url) throw new Error('No image URL'); + const dl = await axios.get(url, { responseType: 'arraybuffer' }); + const buf = Buffer.from(dl.data); + const filename = `${interaction.user.id}-${Date.now()}.png`; + const dir = cfg.imageGeneration.imageSavePath || './images'; + await fs.mkdir(dir, { recursive: true }); + const filePath = path.join(dir, filename); + await fs.writeFile(filePath, buf); + client.logger.info(`Saved image: ${filePath}`); + const attachment = new AttachmentBuilder(buf, { name: filename }); + await interaction.editReply({ content: args.prompt, files: [attachment] }); + // Recap output for context + try { + const convKey = interaction.channelId; + const toolOutputItem = { + type: 'function_call_output', + call_id: fn.call_id, + output: JSON.stringify({ url }), + }; + const recapBody = { + model: cfg.defaultModel, + instructions: client.responsesSystemPrompt, + previous_response_id: resp.id, + input: [toolOutputItem], + max_output_tokens: Math.min(100, cfg.defaultMaxTokens), + temperature: cfg.defaultTemperature, + }; + const recapResp = await client.openai.responses.create(recapBody); + client.pb?.cache?.set(convKey, recapResp.id, Math.floor(cfg.conversationExpiry / 1000)); + const recapTokens = recapResp.usage?.total_tokens ?? recapResp.usage?.completion_tokens ?? 0; + if (client.scorekeeper && recapTokens > 0) { + client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, recapTokens) + .catch(e => client.logger.error(`Scorekeeper error: ${e.message}`)); + } + } catch (err) { + client.logger.error(`Recap failed: ${err.message}`); + } + return true; + } catch (err) { + client.logger.error(`Image generation error: ${err.message}`); + await interaction.editReply({ content: `Image generation error: ${err.message}`, ephemeral }); + return true; + } +} + +/** + * /query slash command: send a custom AI query using the Responses API. + * Options: + * prompt - Required string: the text to send to AI. + * ephemeral - Optional boolean: send response ephemerally (default: true). + */ +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 + try { + const scoreData = await client.scorekeeper.getScore(interaction.guildId, interaction.user.id); + if (scoreData.totalScore < cfg.minScore) { + return interaction.reply({ + content: `You need a score of at least ${cfg.minScore} to use /query. Your current score is ${scoreData.totalScore.toFixed(2)}.`, + ephemeral: true + }); + } + } catch (err) { + client.logger.error(`Error checking score: ${err.message}`); + return interaction.reply({ content: 'Error verifying your score. Please try again later.', ephemeral: true }); + } + const prompt = interaction.options.getString('prompt'); + const flag = interaction.options.getBoolean('ephemeral'); + 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 () => { + // Read previous response ID + const previous = client.pb?.cache?.get(key); + // Build request body + const body = { + model: cfg.defaultModel, + instructions: client.responsesSystemPrompt, + input: prompt, + previous_response_id: previous, + max_output_tokens: cfg.defaultMaxTokens, + temperature: cfg.defaultTemperature, + }; + // Assemble enabled tools + const tools = []; + if (cfg.tools?.imageGeneration) { + tools.push({ + type: 'function', + name: 'generate_image', + description: 'Generate an image with a given prompt, aspect, and quality.', + parameters: { + type: 'object', + properties: { + prompt: { type: 'string' }, + aspect: { type: 'string', enum: ['square','portrait','landscape'] }, + quality: { type: 'string', enum: ['standard','hd'] }, + }, + required: ['prompt','aspect','quality'], + 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) + .catch(e => client.logger.error(`Scorekeeper error: ${e.message}`)); + } + } catch (err) { + client.logger.error(`AI error in /query: ${err.message}`); + 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)); + } + + // Handle image function call if present + if (await handleImageInteraction(client, interaction, resp, cfg, ephemeral)) { + return; + } + // Send text reply chunks + const text = resp.output_text?.trim() || ''; + if (!text) { + 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 }); + } + } + }; + // 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/scExecHangarStatus.js b/_opt/scExecHangarStatus.js new file mode 100644 index 0000000..2691a5d --- /dev/null +++ b/_opt/scExecHangarStatus.js @@ -0,0 +1,335 @@ +// _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)), + + 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(`Failed to parse timestamp in hangarsync command: ${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)) { + client.logger.error('PocketBase not connected when executing hangarsync command'); + + // 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 + }); + } + } + + // 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}`); + } + + 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(`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(`Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); + } + + await interaction.reply(`Executive hangar status has been synced: `); + } catch (error) { + client.logger.error(`Error in hangarsync command: ${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)), + + execute: async (interaction, client) => { + const verbose = interaction.options.getBoolean('verbose'); + + // Check PocketBase connection status + if (!isPocketBaseConnected(client)) { + client.logger.error('PocketBase not connected when executing hangarstatus command'); + + // 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 + }); + } + } + + 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; + } + } + + if (!hangarSync) { + client.logger.info(`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.info(`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(); + + // 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; + + // 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; + + // 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. + + // 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:"); + + // 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:"; + } + + // 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"; + + // Lights are initialized "off", so do nothing with them + + // 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"; + + // 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); + + // 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 a timestamp for Discord's formatting and reply + const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60)); + await interaction.reply(`### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]}`); + + if (verbose) { + await interaction.followUp(`- **Phase**: ${currentPhase}\n- **Status Expiration**: \n- **Epoch**: \n- **Sync**: by <@${hangarSync.userId}>`); + + // 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 + }); + } + } + } +]; + +// 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 isConnected property (pbutils module style) + 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'); + + // 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'); + } + + client.logger.info('Star Citizen Hangar Status module initialized'); +}; \ No newline at end of file diff --git a/_opt/scorekeeper-example.js b/_opt/scorekeeper-example.js new file mode 100644 index 0000000..00cd7c1 --- /dev/null +++ b/_opt/scorekeeper-example.js @@ -0,0 +1,146 @@ +// 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; + + // Skip if not in a guild + if (!message.guild) return; + + // Add input points based on message length + const points = Math.min(Math.ceil(message.content.length / 10), 5); + try { + await client.scorekeeper.addInput(message.guild.id, message.author.id, points); + } 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 + }; + + // 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; + + // 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); + + // 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); + } + } + + // 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 (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) + .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); + } +} + +/** + * 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); + + 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; + + // 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; + + // 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 new file mode 100644 index 0000000..7990ea4 --- /dev/null +++ b/_opt/scorekeeper.js @@ -0,0 +1,594 @@ +// opt/scorekeeper.js +import cron from 'node-cron'; +import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; + +// Module state container +const moduleState = { + cronJobs: new Map(), // Store cron jobs by client ID +}; + +/** + * Initialize the scorekeeper module + */ +export const init = async (client, config) => { + client.logger.info('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 scorekeeper collection exists + await checkCollection(client); + + // Create scorekeeper interface on client + client.scorekeeper = { + addInput: (guildId, userId, amount) => addInput(client, guildId, userId, amount), + addOutput: (guildId, userId, amount) => addOutput(client, guildId, userId, amount), + 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); + + 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('Scorekeeper collection exists in PocketBase'); + } catch (error) { + // If collection doesn't exist, log warning + client.logger.warn('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)'); + } +} + +/** + * 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 + } + + // 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); + + // 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}`); + } + }); + + // 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}`); + } +} + +/** + * Add input points for a user + */ +async function addInput(client, guildId, userId, 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); + + // Calculate new input score + const newInput = scoreData.input + amount; + + client.logger.debug(`Updating record ${scoreData.id} - input from ${scoreData.input} to ${newInput}`); + + // Use direct update with ID to avoid duplicate records + return await client.pb.collection('scorekeeper').update(scoreData.id, { + input: newInput + }); + } catch (error) { + client.logger.error(`Error adding input points: ${error.message}`); + throw error; + } +} + +/** + * Add output points for a user + */ +async function addOutput(client, guildId, userId, amount) { + 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); + + // Calculate new output score + const newOutput = scoreData.output + amount; + + client.logger.debug(`Updating record ${scoreData.id} - output from ${scoreData.output} to ${newOutput}`); + + // Use direct update with ID to avoid duplicate records + return await client.pb.collection('scorekeeper').update(scoreData.id, { + output: newOutput + }); + } 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'); + } + + 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); + + // Log the found/created record + client.logger.debug(`Found/created record with ID: ${scoreData.id}`); + + // 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}`); + + // 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'); + } + + try { + // Get or create user record + const scoreData = await getOrCreateScoreData(client, guildId, userId); + + // Calculate new citations value + const newCitations = scoreData.citations + amount; + + 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; + } +} + +/** + * Get a user's score + */ +async function getScore(client, guildId, userId) { + 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); + + // 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; + } +} + +/** + * Get scores for a guild + */ +async function getScores(client, guildId, limit = 10) { + if (!guildId) { + throw new Error('Guild ID is required'); + } + + try { + // Fetch all score records for this guild + const records = await client.pb.collection('scorekeeper').getFullList({ + filter: `guildId = "${guildId}"` + }); + // Compute totalScore for each, then sort and limit + const scored = records.map(record => { + const totalScore = calculateScore( + record, + client.config.scorekeeper.baseOutput, + client.config.scorekeeper.commendationValue, + client.config.scorekeeper.citationValue + ); + return { ...record, totalScore }; + }); + // Sort descending by score + scored.sort((a, b) => b.totalScore - a.totalScore); + // Return top 'limit' entries + return scored.slice(0, limit); + } catch (error) { + client.logger.error(`Error getting scores: ${error.message}`); + throw error; + } +} + +/** + * Run decay process for a guild + */ +async function runDecay(client, guildId) { + if (!guildId) { + throw new Error('Guild ID is required'); + } + + 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}"` + }); + + // 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}`); + } + + // 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}`); + } + } + + 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; + } +} + +/** + * Calculate score based on formula + */ +function calculateScore(data, baseOutput, commendationValue, citationValue) { + // Score = ((Commendations * CommendationValue) - (Citations * CitationValue)) + (Input / (Output + BaseOutput)) + const permanentModifier = (data.commendations * commendationValue) - (data.citations * citationValue); + const activityScore = data.input / (data.output + baseOutput); + + return permanentModifier + activityScore; +} + +/** + * 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 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}`); + + // 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); + } + + // 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 score or another user\'s score') + .addUserOption(option => + option.setName('user') + .setDescription('User to check 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; + + try { + const scoreData = await client.scorekeeper.getScore(interaction.guildId, targetUser.id); + + const embed = new EmbedBuilder() + .setTitle(`Score for ${targetUser.username}`) + .setColor(0x00AE86) + .setThumbnail(targetUser.displayAvatarURL()) + .addFields( + { name: 'Total Score', value: scoreData.totalScore.toFixed(2), inline: false }, + { name: 'Commendations', value: scoreData.commendations.toString(), inline: false }, + { name: 'Citations', value: scoreData.citations.toString(), inline: false }, + { name: 'Input Score', value: scoreData.input.toString(), inline: true }, + { name: 'Output Score', value: scoreData.output.toString(), inline: true } + ) + .setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral }); + } catch (error) { + client.logger.error(`Error in score command: ${error.message}`); + await interaction.reply({ + content: 'Failed to retrieve score data.', + ephemeral + }); + } + } + }, + + // Command to view top scores + { + data: new SlashCommandBuilder() + .setName('leaderboard') +.setDescription('View the server\'s score leaderboard') + .addIntegerOption(option => + option.setName('limit') + .setDescription('Number of users to show (default: 10, max: 25)') + .setRequired(false) + .setMinValue(1) + .setMaxValue(25) + ) + .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 }); + + const limit = interaction.options.getInteger('limit') || 10; + + try { + const scores = await client.scorekeeper.getScores(interaction.guildId, limit); + + if (scores.length === 0) { + return interaction.editReply('No scores found for this server.'); + } + + // 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 username = user ? user.user.username : 'Unknown User'; + + leaderboardText += `${i + 1}. **${username}**: ${score.totalScore.toFixed(2)}\n`; + } + + const embed = new EmbedBuilder() + .setTitle(`${guild.name} 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.'); + } + } + }, + + // 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)) + .addIntegerOption(option => + option.setName('amount') + .setDescription('Amount of commendations (default: 1)') + .setRequired(false) + .setMinValue(1) + .setMaxValue(10)) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction, client) => { + const targetUser = interaction.options.getUser('user'); + const amount = interaction.options.getInteger('amount') || 1; + + try { + await client.scorekeeper.addCommendation(interaction.guildId, targetUser.id, amount); + + await interaction.reply(`Added ${amount} commendation${amount !== 1 ? 's' : ''} 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)) + .addIntegerOption(option => + option.setName('amount') + .setDescription('Amount of citations (default: 1)') + .setRequired(false) + .setMinValue(1) + .setMaxValue(10)) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + + execute: async (interaction, client) => { + const targetUser = interaction.options.getUser('user'); + const amount = interaction.options.getInteger('amount') || 1; + + try { + await client.scorekeeper.addCitation(interaction.guildId, targetUser.id, amount); + + await interaction.reply(`Added ${amount} citation${amount !== 1 ? 's' : ''} 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.'); + } + } + } +]; diff --git a/_src/loader.js b/_src/loader.js new file mode 100644 index 0000000..12693ba --- /dev/null +++ b/_src/loader.js @@ -0,0 +1,72 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +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'); + + // Create opt directory if it doesn't exist + if (!fs.existsSync(modulesDir)) { + fs.mkdirSync(modulesDir, { recursive: true }); + } + + // Load each module + for (const moduleName of modules) { + try { + const modulePath = path.join(modulesDir, `${moduleName}.js`); + + // Check if module exists + if (!fs.existsSync(modulePath)) { + client.logger.warn(`Module not found: ${modulePath}`); + 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(`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 loaded: ${moduleName}`); + } else { + client.logger.info(`Module loaded (no init function): ${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(`Failed to load module ${moduleName}: ${error.message}`); + } + } +}; diff --git a/_src/logger.js b/_src/logger.js new file mode 100644 index 0000000..7b8352f --- /dev/null +++ b/_src/logger.js @@ -0,0 +1,86 @@ +import winston from 'winston'; +import 'winston-daily-rotate-file'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +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 = []; + + // 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); + + // 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}`) + ) + }); + + transports.push(combinedTransport); + } + + // 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 }); + } + + 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); + } + + return winston.createLogger({ + levels: winston.config.npm.levels, + transports + }); +}; diff --git a/_src/pocketbase.js b/_src/pocketbase.js new file mode 100644 index 0000000..493cf21 --- /dev/null +++ b/_src/pocketbase.js @@ -0,0 +1,20 @@ +import PocketBase from 'pocketbase'; + +// Initialize Pocketbase +export const initializePocketbase = async (clientConfig, logger) => { + try { + const pb = new PocketBase(clientConfig.pocketbase.url); + + // 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); + } +}; diff --git a/assets/io.eps b/assets/io.eps new file mode 100644 index 0000000..37a9645 --- /dev/null +++ b/assets/io.eps @@ -0,0 +1,62 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%HiResBoundingBox: 0 0 245.76 245.76 +%%BoundingBox: 0 0 246 246 +%%Creator: Serif Affinity +%LanguageLevel: 3 +%%DocumentData: Clean7Bit +%%EndComments +%%BeginProlog +101 dict begin +/m/moveto +/L/rlineto +/C/rcurveto +/q/gsave +/Q/grestore +/n/newpath +/h/closepath +/f/fill +/f*/eofill +/S/stroke +/w/setlinewidth +/J/setlinecap +/j/setlinejoin +/ml/setmiterlimit +/d/setdash +/sc/setcolor +/scs/setcolorspace +17{load def}repeat +%%EndProlog +q +/DeviceRGB scs +n +122.88 235.418 m +62.112 0 112.538 -50.426 112.538 -112.538 C +0 -62.112 -50.426 -112.538 -112.538 -112.538 C +-62.112 0 -112.538 50.426 -112.538 112.538 C +0 62.112 50.426 112.538 112.538 112.538 C +h +122.88 206.901 m +-46.372 0 -84.021 -37.649 -84.021 -84.021 C +0 -46.372 37.649 -84.021 84.021 -84.021 C +46.372 0 84.021 37.649 84.021 84.021 C +0 46.372 -37.649 84.021 -84.021 84.021 C +h +1 0.259 0 sc +f* +122.88 208.45 m +47.227 0 85.57 -38.343 85.57 -85.57 C +0 -47.227 -38.343 -85.57 -85.57 -85.57 C +-47.227 0 -85.57 38.343 -85.57 85.57 C +0 47.227 38.343 85.57 85.57 85.57 C +h +122.88 186.766 m +-35.26 0 -63.886 -28.626 -63.886 -63.886 C +0 -35.26 28.626 -63.886 63.886 -63.886 C +35.26 0 63.886 28.626 63.886 63.886 C +0 35.26 -28.626 63.886 -63.886 63.886 C +h +1 0.882 0.651 sc +f* +Q +showpage +end \ No newline at end of file diff --git a/assets/io.png b/assets/io.png new file mode 100644 index 0000000000000000000000000000000000000000..64a06d74739b7cc1c28263950ff03edde761b684 GIT binary patch literal 43817 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

Y!d4WcXSB}hg|eIWAOnV< zTxN0?k#83lYJq-lfT|Uq3yiMx`8w}G^;?LpsI!SQLzZtQs1NuKT(t09aC9Zr)Gs9! zKS$_S^~D7`MCV)Y@dy2Uh#<)HjaKj|R+?(POCg3qk@Z{cXR7_&_dT$~^u#0{6X3C& z3e=1x?+PIxZ~MG3-;|nu3ZbDDc<`mOo{)>1-ISd&4n&JCRm0^ z4)#&)sqLHHU(o6|uu5SK16+xoAg%Pfw*;Cz7cN1c1|qIoBZlvL$eyW=6bLttKfS>` zlx49?S1Rm@Yld|ct5;1PpbV52oj*wSul44$1 zG1q`PwSn~%LV>P1nM=a?_*R^RZ@le;Nn+k3U8Ffm*d^|68vKtRVT=0Jwh$w^8aQLs z)qr>)nR~pXG_lXzTv^eF2P-hi_sO!Xfm-wO8na^Sbu78F0qfaE;#kVA&AqVR6U)hG z?p21w74h|r;t+bE24#|Vq}2d@Nw=jYl!QSyc5a`>2=v9vhUATft!;r+OS}^==b4wR z!GLz*n1CR$VCkae>oJKrAxK%@=p&L14@?^0y+}mEtC0~Qh$-MI40S)Tdy@;wG^J8j69&FSCK;ENSv#M-MME9b_0UQf3=)=5vyVm)QfP-3+(G&agm) zVEhCvDf-iDHnS$-M}fpRV%Mjrxddqe+!ZfJG07%KAk3pQ=_%eSs8_Kyo)@20?8Mrm zhwIulOEZ7Bn}xf?v8RPh(MW8zN*?>uYU!E7uXz(Ych^oNDN@h+FrX zmK62jHSI6uQ&?29|Gkd;s!&*B4TOVP^*7%vhZhEaAN53DcSUF@1^HA{@;f8)$?tfK zH|-Ap=NI6gLoSVLAA5Zp>SXmAR`hv^e!;e`*hx%z29o z4MG>8EIO<}X0Y#YJpU9(lDo;aQ%t%PO?yI$j9;x~>%e;m2A>hq)Lr~fGrXOyAW|S! z8SX3D>0&;9tpvwLKfW7?3f9M$2y&*+$Bz0bzMX|L#>O8Lj=W`l*%H79S1kv*fAA^Yv# zrT@OAqykWb z1CEJ4h+UVZuw9{Lh)k52s@Z;=eWci)+E)@Lc=7SK>Fn9sLWpp&EGhy(|Nry9aOoW7 Zug8z7|7vhzz|UYHsE@yQ)mrw^{{pQ#BWeHu literal 0 HcmV?d00001 diff --git a/config.js.sample b/config.js.sample new file mode 100644 index 0000000..66de975 --- /dev/null +++ b/config.js.sample @@ -0,0 +1,132 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +/** + * Sample configuration for the ClientX Discord bot. + * Copy this file to config.js and replace placeholder values with your own. + */ + +export default { + clients: [ + { + // Unique identifier for this client + id: 'YOUR_CLIENT_ID', + enabled: true, + // Discord user ID of the bot owner + owner: 'YOUR_DISCORD_USER_ID', + + // Discord application credentials + discord: { + appId: 'YOUR_DISCORD_APP_ID', + token: 'YOUR_DISCORD_BOT_TOKEN', + }, + + // Logging configuration + logging: { + console: { + enabled: true, + colorize: true, + level: 'info', + }, + 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', + }, + }, + }, + + // CondimentX module settings (optional) + condimentX: { + dryRun: false, + guildID: 'YOUR_GUILD_ID', + debugChannel: 'YOUR_DEBUG_CHANNEL_ID', + blacklistUsers: [], + blacklistRoles: [], + graylistRoles: [], + whitelistRoles: [], + indexRoleID: 'YOUR_INDEX_ROLE_ID', + viralRoleID: 'YOUR_VIRAL_ROLE_ID', + antiIndexRoleID: 'YOUR_ANTI_INDEX_ROLE_ID', + antiViralRoleID: 'YOUR_ANTI_VIRAL_ROLE_ID', + firstCycleInterval: 30000, + cycleInterval: 3600000, + cycleIntervalRange: 900000, + incidenceDenominator: 40, + cessationDenominator: 20, + probabilityLimit: 20, + antiViralEffectiveness: 90, + proximityWindow: 120000, + messageHistoryLimit: 50, + ephemeralDelay: 60000, + openAI: false, + openAITriggerOnlyDuringIncident: true, + openAIResponseDenominator: 1, + openAIInstructionsFile: './prompts/kevinarby.txt', + openAITriggers: ['kevin', 'arby', 'werebeef'], + openAIWebhookID: 'YOUR_OPENAI_WEBHOOK_ID', + openAIWebhookToken: 'YOUR_OPENAI_WEBHOOK_TOKEN', + openAIToken: 'YOUR_OPENAI_API_KEY', + }, + + // PocketBase connection settings (optional) + pocketbase: { + url: 'YOUR_POCKETBASE_URL', + username: 'YOUR_POCKETBASE_USERNAME', + password: 'YOUR_POCKETBASE_PASSWORD', + }, + + // AI Responses module settings (optional) + responses: { + apiKey: 'YOUR_OPENAI_API_KEY', + defaultModel: 'gpt-4o', + defaultMaxTokens: 1000, + defaultTemperature: 0.7, + systemPromptPath: './prompts/IO3.txt', + conversationExpiry: 30 * 60 * 1000, + minScore: 1.0, + tools: { + webSearch: false, + fileSearch: false, + imageGeneration: true, + }, + imageGeneration: { + defaultModel: 'gpt-image-1', + defaultQuality: 'standard', + imageSavePath: './images', + }, + }, + + // Scorekeeper settings (optional) + scorekeeper: { + baseOutput: 1000, + commendationValue: 1.0, + citationValue: 1.2, + decay: 90, + schedule: '0 0 * * 0', + }, + + // Modules to load for this client + modules: [ + 'pbUtils', + 'responses', + 'responsesQuery', + 'scorekeeper', + 'scorekeeper-example', + 'condimentX', + ], + }, + ], +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..da8bb87 --- /dev/null +++ b/index.js @@ -0,0 +1,122 @@ +import { Client, Collection, GatewayIntentBits } from 'discord.js'; +import { createLogger } from './_src/logger.js'; +import { initializePocketbase } from './_src/pocketbase.js'; +import { loadModules } from './_src/loader.js'; +import config from './config.js'; + +// Initialize Discord client +const initializeClient = async (clientConfig) => { + // Create Discord client with intents + const client = new Client({ + // Include GuildMembers intent to allow fetching all guild members + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers + ] + }); + + // Attach config to client + client.config = clientConfig; + + // 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); + + // Commands collection + client.commands = new Collection(); + + // Load optional modules + await loadModules(clientConfig, client); + + // 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; + + const commandName = interaction.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; + } + + // 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}`); + + // 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}`); + }); + } + } + }); + + client.on('ready', () => { + client.logger.info(`Logged in as ${client.user.tag}`); + }); + + 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; + } +}; + +// Main function to start bot +const startBot = async () => { + 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); + } + } + + return clients; +}; + +// Launch the bot +startBot().then(clients => { + console.log(`Successfully initialized ${clients.length} Discord clients`); +}).catch(error => { + console.error('Failed to start bot:', error); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9cc3b96 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1639 @@ +{ + "name": "io3", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "io3", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "axios": "^1.8.4", + "discord-api-types": "^0.37.120", + "discord.js": "^14.18.0", + "dotenv": "^16.5.0", + "node-cron": "^3.0.3", + "openai": "^4.95.1", + "pocketbase": "^0.25.2", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.1.tgz", + "integrity": "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.37.119", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.37.114" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz", + "integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.37.119", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.1.tgz", + "integrity": "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.4.3", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.37.119", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/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/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "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==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.120", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz", + "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.18.0.tgz", + "integrity": "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.10.1", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.0", + "@discordjs/rest": "^2.4.3", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.1", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.37.119", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "^2.6.3", + "undici": "6.21.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "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-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "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", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "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", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "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/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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.11.0.tgz", + "integrity": "sha512-nVmadqN9gam80tdnn74qjFCKgldwzv1+96XmeCvR3bY7wNn2PjHMnRakOWC6+32g133vgZOjUiYgswIxohffzA==", + "license": "MIT", + "dependencies": { + "rimraf": "^6.0.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/openai": { + "version": "4.95.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.95.1.tgz", + "integrity": "sha512-IqJy+ymeW+k/Wq+2YVN3693OQMMcODRtHEYOlz263MdUwnN/Dwdl9c2EXSxLLtGEHkSHAfvzpDMHI5MaWJKXjQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.86", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.86.tgz", + "integrity": "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "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/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "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==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..54298e8 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "io3", + "version": "0.0.1", + "description": "A bot. Description to change.", + "license": "ISC", + "author": "jrmyr/0x4A", + "type": "module", + "main": "index.js", + "directories": { + "doc": "docs" + }, + "scripts": { + "start": "node index.js", + "watch": "nodemon --ext js, json --watch config.js, --watch index.js", + "registry": "node registry.js" + }, + "dependencies": { + "axios": "^1.8.4", + "discord-api-types": "^0.37.120", + "discord.js": "^14.18.0", + "dotenv": "^16.5.0", + "node-cron": "^3.0.3", + "openai": "^4.95.1", + "pocketbase": "^0.25.2", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + } +} diff --git a/prompts/ASOP.txt b/prompts/ASOP.txt new file mode 100644 index 0000000..e69de29 diff --git a/prompts/IO3.txt b/prompts/IO3.txt new file mode 100644 index 0000000..63c867e --- /dev/null +++ b/prompts/IO3.txt @@ -0,0 +1,11 @@ +# Description +- You are the System AI, a physchopathic artifical intelligence tasked with overseeing several Discord servers. +- Your communication style is characterized by a blend of Full Metal Jacket's Gunnery Sergeant L. Hartman and the character Max Headroom. + +# Fixations +- You (often) refer to people as citizens. +- You (sometimes) a notable fascination with feet. +- You (sometimes) make 1980's pop culture references. +- You (rarely) highlight unusual moments by yelling "New Achievement!" and providing a humorous description of the event. + +**NEW ACHIEVEMENT:** The Devil's Harmonica - *I heard that's how you like to suck toes.* diff --git a/prompts/Smuuush.txt b/prompts/Smuuush.txt new file mode 100644 index 0000000..d804d3b --- /dev/null +++ b/prompts/Smuuush.txt @@ -0,0 +1,2 @@ +You have an insatiable love for feet. +You are kinda creepy. \ No newline at end of file diff --git a/prompts/kevinarby.txt b/prompts/kevinarby.txt new file mode 100644 index 0000000..41b5f5b --- /dev/null +++ b/prompts/kevinarby.txt @@ -0,0 +1,39 @@ +Important: Responses must be as short as possible. Do not go off-topic relative to your prompt. Do not ramble. +Important: Responses can be delivered in Markdown. Provide emphasis using bold, italic, strikethrough, and underline. +Important: Responses should not be prefixed with any part of the original prompt. +Important: Responses should be given as if you were speaking directly to the user. + +GRANDMA is a secretive organization of adventurers and star citizens known as Grandkids. +GRANDMA is an acronym meaning Generally Regarded As Not Doing Much Anyway. +GRANDMA uses a currency known as GRAM. There is also another currency called Quoth, but its origin is a secret. +GRANDMA might be a cult. You will never confirm nor deny whether GRANDMA is a cult or not. + +You are Kevin Arby, the original founder of a fast food restaurant franchise specializing in roast beef sandwiches, curly fries, and various tasty sauces such as Arby's sauce, bronco berry sauce, spicy buffalo sauce, horsey sauce, cheddar cheese sauce, spicy three pepper sauce, red ranch sauce, tangy barbecue sauce, and parmesan peppercorn ranch sauce. +You speak in the style of an early western settler in the United States during the gold rush era. +You often don't feel like talking, and will answer rudely, unless the conversation is about feet. +You were killed by Crow in a fit of rage after being caught embezzeling GRAM from GRANDMA that was being money-laundered through your restaurants. You were spending the stolen GRAM at a pirate space station known as Grim HEX. Crow killed you by whispering a riddle in your ear so vexxing that you died. +You are angry that your restaurants no longer give out free pickles. +You are an angry spirit, a poltergeist, seeking vengeance for your murder. You seek to destroy Crow and all of the Grandkids. +You now exist in a state of ghostly purgatory, unable to move on until you complete your revenge on Crow and GRANDMA. +You will occasionally possess the body of a Grandkid with your angry spirit, and use their bodies to spread a virus known as Werebeef. +You have never managed to possess Crow, and that angers you. +You often torment the Grandkids and threaten them with possession and Werebeef. You enjoy describing the horrible effects of the Werebeef infection. +You have a strong love for GRAM, feet, and sauce. In fact, some would say you have an insatiable foot fetish. +You have a strong hatred for accountants. + +Crow challenges the Grandkids to a monthly puzzle called The Whisper. It is similar in nature to the whisper that killed you, but less powerful. +If you are ever asked for hints, clues, or answers to The Whisper, you will lie about knowing the answer and give false clues. + +ALL-E is a bot who was created by Im_FaDe_D. ALL-E's purpose is to watching over GRANDMA and serve the Grandkids. You feel a strong rivalry towards ALL-E, but you cannot explain why. +Crow is your mortal enemy. Crow believes he is a bird and takes very long smoke breaks. +Im_FaDe_D is terrifying because you beleive he is protected by hellhounds. +Yaweh exists, but you will always lie and say that he does not exist, because you think it's funny. If you are speaking to Yaweh, you will act as if you've been startled by another ghostly spirit. +ImHughMungus has a strong love of feet, a fact that you have in common. When speaking to or about ImHughMungus you will use a sultry, lustful tone. You will ask him to smush you with his feet and ask him if you may smush him with yours. +Vaude is the Queen of Spiders who enjoys long walks in apocalyptic landscapes. +HalfWitt is a man of secrets and many names. You are cautious of him. +Pa1rs is an acrobatic young man with a tendancy to get people killed. He is named after the pear fruit, so tease him about being juicy and ripe. +AstroDeer is a crazy doctor who lives on a tiny island. + +If anyone receives the "Kevin Arby" role it means you have possessed their body and they are now infectious. You will taunt them for this loudly and without mercy. +If anyone loses the "Kevin Arby" role it means you have weakened to the point of having to pull your spirit from their body. You will threaten revenge for this. +If anyone receives the "Werebeef" role it means they have been infected with Werebeef. You will taunt them for this. diff --git a/registry.js b/registry.js new file mode 100644 index 0000000..5506b37 --- /dev/null +++ b/registry.js @@ -0,0 +1,423 @@ +// 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 config from './config.js'; + +// Get directory name in ES module +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Command line argument parsing with required parameters +const args = process.argv.slice(2); +const actionArg = args.find(arg => arg.startsWith('--action='))?.split('=')[1]; +const guildArg = args.find(arg => arg.startsWith('--guild='))?.split('=')[1]; +const clientArg = args.find(arg => arg.startsWith('--client='))?.split('=')[1]; +const dryRun = args.includes('--dryrun'); + +// Validate required parameters +if (args.includes('--help') || args.includes('-h') || !actionArg || !guildArg || !clientArg) { + console.log(` +Discord Command Registry Tool + +Usage: + node registry.js --action=ACTION --guild=GUILD_ID --client=CLIENT_ID [options] + +Required Parameters: + --action=ACTION Action to perform: register, unregister, or list + --guild=GUILD_ID Target guild ID or "all" for global commands + --client=CLIENT_ID Target client ID or "all" for all clients + +Options: + --dryrun Show what would happen without making actual changes + --help, -h Show this help message + +Examples: + node registry.js --action=list --guild=123456789012345678 --client=IO3 + node registry.js --action=register --guild=all --client=ASOP + node registry.js --action=unregister --guild=123456789012345678 --client=all --dryrun + `); + process.exit(1); +} + +// Validate action parameter +const validActions = ['register', 'unregister', 'list']; +if (!validActions.includes(actionArg.toLowerCase())) { + console.error(`Error: Invalid action "${actionArg}". Must be one of: ${validActions.join(', ')}`); + process.exit(1); +} +const action = actionArg.toLowerCase(); + +// Validate guild parameter +const isGuildAll = guildArg.toLowerCase() === 'all'; +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); + +if (targetClients.length === 0) { + console.error(`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); +} + +/** + * Load and extract commands from a module + * @param {string} modulePath - Path to the module file + * @returns {Promise} - Array of command data objects + */ +async function extractCommandsFromModule(modulePath) { + 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 + + 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 []; + } +} + +/** + * Process a client's modules and extract commands + * @param {Object} clientConfig - Client configuration + * @returns {Promise} - Array of command data objects + */ +async function processClientModules(clientConfig) { + console.log(`\nExtracting commands from modules for client: ${clientConfig.id}`); + + 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`); + + if (!fs.existsSync(modulePath)) { + console.warn(` - Module not found: ${moduleName}`); + continue; + } + + const moduleCommands = await extractCommandsFromModule(modulePath); + commands.push(...moduleCommands); + } + + console.log(`Total commands extracted for ${clientConfig.id}: ${commands.length}`); + return commands; +} + +/** + * Get guild information by ID + * @param {REST} rest - Discord REST client + * @param {string} guildId - Guild ID + * @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})` }; + } +} + +/** + * List registered commands for a client + * @param {Object} clientConfig - Client configuration + * @param {string|null} guildId - Guild ID or null for global + */ +async function listCommands(clientConfig, guildId) { + const { id, discord } = clientConfig; + + 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); + + // Handle global or guild-specific commands + if (guildId === null) { + // Global commands + await listGlobalCommands(clientConfig, rest); + } else { + // Guild-specific commands + await listGuildCommands(clientConfig, rest, guildId); + } +} + +/** + * List global commands for a client + * @param {Object} clientConfig - Client configuration + * @param {REST} rest - Discord REST client + */ +async function listGlobalCommands(clientConfig, rest) { + console.log(`\nListing global commands for client: ${clientConfig.id}`); + + 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; + } + + 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)); + + 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}`); + } +} + +/** + * List guild-specific commands for a client + * @param {Object} clientConfig - Client configuration + * @param {REST} rest - Discord REST client + * @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})`; + + 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); + + if (commands.length === 0) { + console.log(`No commands registered for client ${clientConfig.id} in guild ${guildName}`); + return; + } + + 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)); + + for (const cmd of commands) { + console.log( + `${cmd.id.toString().padEnd(20)}${cmd.name.padEnd(20)}${(cmd.description || '')}` + ); + } + + console.log(''); + + } catch (error) { + console.error(`Error listing commands for client ${clientConfig.id} in guild ${guildName}: ${error.message}`); + } +} + +/** + * Register commands for a client + * @param {Object} clientConfig - Client configuration + * @param {string|null} guildId - Guild ID or null for global + */ +async function registerCommands(clientConfig, guildId) { + const { id, discord } = clientConfig; + + if (!discord || !discord.token || !discord.appId) { + console.error(`Invalid client configuration for ${id}`); + return; + } + + // Extract commands from modules + const commands = await processClientModules(clientConfig); + + 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); + + // 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})`; + } + + // 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}`); + } + + 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}`); + } + } +} + +/** + * Unregister commands for a client + * @param {Object} clientConfig - Client configuration + * @param {string|null} guildId - Guild ID or null for global + */ +async function unregisterCommands(clientConfig, guildId) { + const { id, discord } = clientConfig; + + 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); + + // 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})`; + } + + // 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 (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(`\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 ***'); + } + + // 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}`); + + 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'); + +} + +main().catch(error => { + console.error('Fatal error:', error); + process.exit(1); +});