This commit is contained in:
jrmyr 2025-05-08 01:52:12 +00:00
parent 8231b5a105
commit 601e5a703f
26 changed files with 7182 additions and 4314 deletions

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
node_modules/
logs/
images/
dist/
coverage/
*.min.js

67
.eslintrc.json Normal file
View File

@ -0,0 +1,67 @@
{
"env": {
"node": true,
"es2022": true
},
"extends": [
"eslint:recommended",
"plugin:import/recommended"
],
"plugins": [
"import"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"settings": {
"import/resolver": {
"node": {
"extensions": [
".js",
".mjs"
]
}
}
},
"rules": {
// Error prevention
"no-const-assign": "error",
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-unreachable": "error",
"valid-typeof": "error",
// Best practices
"eqeqeq": "error",
"no-eval": "error",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"no-var": "error",
"prefer-const": "error",
"no-empty": ["error", { "allowEmptyCatch": true }],
// Style
"indent": ["error", 4, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-multiple-empty-lines": ["error", { "max": 1 }],
"no-trailing-spaces": "error",
"eol-last": "error",
"no-mixed-spaces-and-tabs": "error",
// Object and array formatting
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"comma-dangle": ["error", "never"],
// Import/Export
"import/no-duplicates": "error",
"import/order": ["error", {
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
"newlines-between": "always",
"alphabetize": { "order": "asc" }
}]
}
}

View File

@ -1,5 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
import { MessageFlags } from 'discord-api-types/v10'; import { MessageFlags } from 'discord-api-types/v10';
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
import { CODES } from '../_src/ansiColors.js'; import { CODES } from '../_src/ansiColors.js';
/** /**

View File

@ -1,5 +1,5 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
import { MessageFlags } from 'discord-api-types/v10'; import { MessageFlags } from 'discord-api-types/v10';
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
/** /**
* botUtils module - provides administrative bot control commands * botUtils module - provides administrative bot control commands
@ -53,7 +53,7 @@ export const commands = [
}, },
/** /**
* Slash command `/status` (Administrator only): * Slash command `/status` (Administrator only):
* Shows this bot clients status including CPU, memory, environment, * Shows this bot client's status including CPU, memory, environment,
* uptime, module list, and entity counts. Optionally displays Git info * uptime, module list, and entity counts. Optionally displays Git info
* (Git Reference and Git Status) when the gitUtils module is loaded. * (Git Reference and Git Status) when the gitUtils module is loaded.
* @param {import('discord.js').CommandInteraction} interaction * @param {import('discord.js').CommandInteraction} interaction
@ -158,6 +158,10 @@ export const commands = [
]; ];
// Module loaded logging // Module loaded logging
export async function init(client, clientConfig) { export async function init(_client, _clientConfig) {
client.logger.info('[module:botUtils] Module loaded'); _client.logger.info('[module:botUtils] Module loaded');
}
export async function handleInteractionCreate(_client, _clientConfig, _interaction) {
// ... existing code ...
} }

View File

@ -78,7 +78,6 @@ export const init = async (client, config) => {
// Used as a prefix before any line that runs within a loop. // Used as a prefix before any line that runs within a loop.
const bullet = '>'; const bullet = '>';
// === OpenAI Interaction === // === OpenAI Interaction ===
// Chat completion via OpenAI with provided instructions. // Chat completion via OpenAI with provided instructions.
async function ai(prompt = '') { async function ai(prompt = '') {
@ -86,17 +85,17 @@ export const init = async (client, config) => {
debug(`**AI Prompt**: ${prompt}`); debug(`**AI Prompt**: ${prompt}`);
// Read instructions. // Read instructions.
let openAIInstructions = fs.readFileSync(openAIInstructionsFile, 'utf8'); const openAIInstructions = fs.readFileSync(openAIInstructionsFile, 'utf8');
const unmention = /<@(\w+)>/g; const unmention = /<@(\w+)>/g;
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini', model: 'gpt-4o-mini',
messages: [ messages: [
{ role: 'user', content: `${prompt.replace(unmention, '$1')}` }, { role: 'user', content: `${prompt.replace(unmention, '$1')}` },
{role: 'system', content: `${openAIInstructions}`}, { role: 'system', content: `${openAIInstructions}` }
], ]
}); });
let chunk = completion.choices[0]?.message?.content; const chunk = completion.choices[0]?.message?.content;
if (chunk != '') { if (chunk !== '') {
for (const line of chunk.split(/\n\s*\n/).filter(Boolean)) { for (const line of chunk.split(/\n\s*\n/).filter(Boolean)) {
debug(`${bullet} ${line}`); debug(`${bullet} ${line}`);
openAIWebhookClient.send(line); openAIWebhookClient.send(line);
@ -142,7 +141,7 @@ export const init = async (client, config) => {
// === Message Fetching Helpers === // === Message Fetching Helpers ===
// Retrieve recent messages from every text channel since a given timestamp. // Retrieve recent messages from every text channel since a given timestamp.
async function fetchRecentMessages(since) { async function _fetchRecentMessages(since) {
const allMessages = new Collection(); const allMessages = new Collection();
// Get all text channels in the guild // Get all text channels in the guild
@ -181,7 +180,7 @@ export const init = async (client, config) => {
debug(`**Incident Cycle #${incidentCounter++}**`); debug(`**Incident Cycle #${incidentCounter++}**`);
// Rebuild the list of current index cases, if any. // Rebuild the list of current index cases, if any.
let indexesList = guild.members.cache.filter(member => member.roles.cache.has(indexRole.id)); const indexesList = guild.members.cache.filter(member => member.roles.cache.has(indexRole.id));
debug(`${bullet} Index Cases: **${indexesList.size}**`); debug(`${bullet} Index Cases: **${indexesList.size}**`);
// Build the victimsList using whitelisted roles. // Build the victimsList using whitelisted roles.
@ -206,7 +205,7 @@ export const init = async (client, config) => {
} }
// Conditions for potentially starting an incident. // Conditions for potentially starting an incident.
if (indexesList.size == 0 && victimsList.size > 0) { if (indexesList.size === 0 && victimsList.size > 0) {
if ((Math.floor(Math.random() * incidenceDenominator) + 1) === 1) { if ((Math.floor(Math.random() * incidenceDenominator) + 1) === 1) {
debug(`${bullet} Incidence Check: **Success**`); debug(`${bullet} Incidence Check: **Success**`);
const newIndex = victimsList.random(); const newIndex = victimsList.random();
@ -240,7 +239,7 @@ export const init = async (client, config) => {
} }
// Prepare the next cycle. // Prepare the next cycle.
let interval = cycleInterval + Math.floor(Math.random() * (2 * cycleIntervalRange + 1)) - cycleIntervalRange; const interval = cycleInterval + Math.floor(Math.random() * (2 * cycleIntervalRange + 1)) - cycleIntervalRange;
setTimeout(cycleIncidents, interval); setTimeout(cycleIncidents, interval);
debug(`${bullet} Cycle #${incidentCounter} **<t:${Math.floor((Date.now() + interval) / 1000)}:R>** at **<t:${Math.floor((Date.now() + interval) / 1000)}:t>**`); debug(`${bullet} Cycle #${incidentCounter} **<t:${Math.floor((Date.now() + interval) / 1000)}:R>** at **<t:${Math.floor((Date.now() + interval) / 1000)}:t>**`);
} catch (error) { } catch (error) {
@ -291,7 +290,6 @@ export const init = async (client, config) => {
if (message.webhookId) return; if (message.webhookId) return;
guild = client.guilds.cache.get(guildID); guild = client.guilds.cache.get(guildID);
// Someone mentioned us - respond if openAI is enabled, the message was in the webhook channel, and a trigger was used. // Someone mentioned us - respond if openAI is enabled, the message was in the webhook channel, and a trigger was used.
if (openAI === true && openAIWebhook.channel.id === message.channel.id && openAITriggers.some(word => message.content.replace(/[^\w\s]/gi, '').toLowerCase().includes(word.toLowerCase()))) { if (openAI === true && openAIWebhook.channel.id === message.channel.id && openAITriggers.some(word => message.content.replace(/[^\w\s]/gi, '').toLowerCase().includes(word.toLowerCase()))) {
// Also check if an active incident is required to respond. // Also check if an active incident is required to respond.
@ -334,7 +332,7 @@ export const init = async (client, config) => {
let percentage = Math.min(infections / prox.size * 100, probabilityLimit); let percentage = Math.min(infections / prox.size * 100, probabilityLimit);
// Reduce base probability by ${antiViralEffectiveness}% for those with ${antiViralRole} // Reduce base probability by ${antiViralEffectiveness}% for those with ${antiViralRole}
if (message.member.roles.cache.has(antiViralRole.id)) { if (message.member.roles.cache.has(antiViralRole.id) && Math.random() * 100 === antiViralEffectiveness) {
percentage = Math.round(percentage - (antiViralEffectiveness * (percentage / 100))); percentage = Math.round(percentage - (antiViralEffectiveness * (percentage / 100)));
} }

View File

@ -1,7 +1,8 @@
import { SlashCommandBuilder } from 'discord.js';
import { MessageFlags } from 'discord-api-types/v10';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { MessageFlags } from 'discord-api-types/v10';
import { SlashCommandBuilder } from 'discord.js';
// Use execFile to avoid shell interpretation of arguments // Use execFile to avoid shell interpretation of arguments
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);

View File

@ -4,8 +4,8 @@ import { onMessageQueueEvent } from './pbUtils.js';
/** /**
* Example module that listens for 'test' messages in the message_queue collection. * Example module that listens for 'test' messages in the message_queue collection.
*/ */
export const init = async (client, config) => { export async function init(client, _config) {
client.logger.info('[module:messageQueueExample] Initializing Message Queue Example module'); client.logger.info('[module:messageQueueExample] Message Queue Example module initialized');
onMessageQueueEvent(client, async (action, record) => { onMessageQueueEvent(client, async (action, record) => {
// Only process newly created records // Only process newly created records
if (action !== 'create') return; if (action !== 'create') return;
@ -25,4 +25,4 @@ export const init = async (client, config) => {
client.logger.error(`[module:messageQueueExample] Failed to delete message_queue record ${record.id}: ${err.message}`); client.logger.error(`[module:messageQueueExample] Failed to delete message_queue record ${record.id}: ${err.message}`);
} }
}); });
}; }

View File

@ -1,6 +1,7 @@
// _opt/pbutils.js // _opt/pbutils.js
// Polyfill global EventSource for PocketBase realtime in Node.js (using CommonJS require) // Polyfill global EventSource for PocketBase realtime in Node.js (using CommonJS require)
import { createRequire } from 'module'; import { createRequire } from 'module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { EventSource } = require('eventsource'); const { EventSource } = require('eventsource');
if (typeof global.EventSource === 'undefined') { if (typeof global.EventSource === 'undefined') {
@ -16,7 +17,7 @@ if (typeof global.EventSource === 'undefined') {
* @param {Object} client - Discord client with attached PocketBase instance * @param {Object} client - Discord client with attached PocketBase instance
* @param {Object} config - Client configuration * @param {Object} config - Client configuration
*/ */
export const init = async (client, config) => { export async function init(client, _config) {
const { pb, logger } = client; const { pb, logger } = client;
logger.info('[module:pbUtils] Initializing PocketBase utilities module'); logger.info('[module:pbUtils] Initializing PocketBase utilities module');
@ -41,7 +42,7 @@ export const init = async (client, config) => {
// end of init() // end of init()
logger.info('PocketBase utilities module initialized'); logger.info('PocketBase utilities module initialized');
}; }
/** /**
* Register a handler for incoming message_queue pub/sub events. * Register a handler for incoming message_queue pub/sub events.
@ -215,9 +216,9 @@ const extendPocketBase = (client, pb, logger) => {
const records = []; const records = [];
const pageSize = options.pageSize || 200; const pageSize = options.pageSize || 200;
let page = 1; let page = 1;
const isRunning = true;
while (isRunning) {
try { try {
while (true) {
const result = await pb.collection(collection).getList(page, pageSize, options); const result = await pb.collection(collection).getList(page, pageSize, options);
records.push(...result.items); records.push(...result.items);
@ -226,13 +227,13 @@ const extendPocketBase = (client, pb, logger) => {
} }
page++; page++;
}
return records;
} catch (error) { } catch (error) {
logger.error(`Failed to get all records from ${collection}: ${error.message}`); logger.error(`Failed to get all records from ${collection}: ${error.message}`);
throw error; throw error;
} }
}
return records;
}; };
/** /**
@ -361,7 +362,6 @@ const extendPocketBase = (client, pb, logger) => {
return await pb.deleteOne('message_queue', id); return await pb.deleteOne('message_queue', id);
}; };
// ===== CACHE MANAGEMENT ===== // ===== CACHE MANAGEMENT =====
// Simple in-memory cache // Simple in-memory cache

View File

@ -4,13 +4,15 @@
* and handles text or image (function_call) outputs. * and handles text or image (function_call) outputs.
*/ */
// Removed local file fallback; prompt now comes exclusively from PocketBase via responsesPrompt module // Removed local file fallback; prompt now comes exclusively from PocketBase via responsesPrompt module
import { OpenAI } from 'openai';
import axios from 'axios';
import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
import { expandTemplate } from '../_src/template.js';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import axios from 'axios';
import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
import { OpenAI } from 'openai';
import { expandTemplate } from '../_src/template.js';
// Discord message max length // Discord message max length
const MAX_DISCORD_MSG_LENGTH = 2000; const MAX_DISCORD_MSG_LENGTH = 2000;
@ -26,7 +28,7 @@ function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) {
let chunk = ''; let chunk = '';
let codeBlockOpen = false; let codeBlockOpen = false;
let codeBlockFence = '```'; let codeBlockFence = '```';
for (let line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
const isFenceLine = trimmed.startsWith('```'); const isFenceLine = trimmed.startsWith('```');
if (isFenceLine) { if (isFenceLine) {
@ -72,7 +74,6 @@ function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) {
return chunks.map(c => c.endsWith('\n') ? c.slice(0, -1) : c); return chunks.map(c => c.endsWith('\n') ? c.slice(0, -1) : c);
} }
/** /**
* Determine whether the bot should respond to a message. * Determine whether the bot should respond to a message.
* Controlled by enableMentions and enableReplies in config. * Controlled by enableMentions and enableReplies in config.
@ -143,7 +144,7 @@ async function handleImage(client, message, resp, cfg) {
const promptText = args.prompt; const promptText = args.prompt;
// Determine number of images (1-10); DALL·E-3 only supports 1 // Determine number of images (1-10); DALL·E-3 only supports 1
let count = 1; let count = 1;
if (args.n != null) { if (args.n !== null) {
const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10); const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10);
if (!Number.isNaN(nVal)) count = nVal; if (!Number.isNaN(nVal)) count = nVal;
} }

View File

@ -1,6 +1,8 @@
import { _fs } from 'fs';
import { _path } from 'path';
import { _MessageFlags } from 'discord-api-types/v10';
import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js'; import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js';
import fs from 'fs';
import path from 'path';
// Placeholder info for template variables // Placeholder info for template variables
const TEMPLATE_KEYS_INFO = 'Available keys: userName, userId, locationName, locationId, date, time, datetime, clientId'; const TEMPLATE_KEYS_INFO = 'Available keys: userName, userId, locationName, locationId, date, time, datetime, clientId';
@ -27,7 +29,7 @@ export const commands = [
.setAutocomplete(true) .setAutocomplete(true)
), ),
async execute(interaction, client) { async execute(interaction, client) {
const clientId = client.config.id; const _clientId = client.config.id;
const versionId = interaction.options.getString('version'); const versionId = interaction.options.getString('version');
// Fetch prompt: live latest or selected historic // Fetch prompt: live latest or selected historic
let promptText = client.responsesPrompt || ''; let promptText = client.responsesPrompt || '';
@ -87,12 +89,12 @@ export const commands = [
const _clients = []; const _clients = [];
export async function init(client, clientConfig) { export async function init(client, clientConfig) {
const clientId = clientConfig.id; const _clientId = client.config.id;
client.logger.info('[module:responsesPrompt] initialized'); client.logger.info('[module:responsesPrompt] initialized');
// Load live prompt (latest version) // Load live prompt (latest version)
try { try {
const { items } = await client.pb.collection('responses_prompts') const { items } = await client.pb.collection('responses_prompts')
.getList(1, 1, { filter: `clientId="${clientId}"`, sort: '-created' }); .getList(1, 1, { filter: `clientId="${_clientId}"`, sort: '-created' });
client.responsesPrompt = items[0]?.prompt || ''; client.responsesPrompt = items[0]?.prompt || '';
} catch (err) { } catch (err) {
client.logger.error(`Error loading current prompt: ${err.message}`); client.logger.error(`Error loading current prompt: ${err.message}`);
@ -106,7 +108,7 @@ export async function init(client, clientConfig) {
if (focused.name === 'version') { if (focused.name === 'version') {
try { try {
const { items } = await client.pb.collection('responses_prompts') const { items } = await client.pb.collection('responses_prompts')
.getList(1, 25, { filter: `clientId="${clientId}"`, sort: '-created' }); .getList(1, 25, { filter: `clientId="${_clientId}"`, sort: '-created' });
const choices = items.map(r => ({ name: new Date(r.created).toLocaleString(), value: r.id })); const choices = items.map(r => ({ name: new Date(r.created).toLocaleString(), value: r.id }));
await interaction.respond(choices); await interaction.respond(choices);
} catch (err) { } catch (err) {
@ -129,9 +131,9 @@ export async function init(client, clientConfig) {
} }
const newPrompt = parts.join('\n'); const newPrompt = parts.join('\n');
// Persist new version // Persist new version
let newRec; let _newRec;
try { try {
newRec = await client.pb.createOne('responses_prompts', { clientId, prompt: newPrompt, updatedBy: interaction.user.id }); _newRec = await client.pb.createOne('responses_prompts', { clientId: _clientId, prompt: newPrompt, updatedBy: interaction.user.id });
client.responsesPrompt = newPrompt; client.responsesPrompt = newPrompt;
} catch (err) { } catch (err) {
client.logger.error(`Failed to save prompt: ${err.message}`); client.logger.error(`Failed to save prompt: ${err.message}`);
@ -140,7 +142,7 @@ export async function init(client, clientConfig) {
// Prune older versions beyond the 10 most recent // Prune older versions beyond the 10 most recent
try { try {
const { items } = await client.pb.collection('responses_prompts') const { items } = await client.pb.collection('responses_prompts')
.getList(1, 100, { filter: `clientId="${clientId}"`, sort: '-created' }); .getList(1, 100, { filter: `clientId="${_clientId}"`, sort: '-created' });
const toDelete = items.map(r => r.id).slice(10); const toDelete = items.map(r => r.id).slice(10);
for (const id of toDelete) { for (const id of toDelete) {
await client.pb.deleteOne('responses_prompts', id); await client.pb.deleteOne('responses_prompts', id);

View File

@ -1,3 +1,7 @@
import fs from 'fs/promises';
import path from 'path';
import axios from 'axios';
import { MessageFlags } from 'discord-api-types/v10'; import { MessageFlags } from 'discord-api-types/v10';
/** /**
* Slash command module for '/query'. * Slash command module for '/query'.
@ -5,10 +9,8 @@ import { MessageFlags } from 'discord-api-types/v10';
* including optional image generation function calls. * including optional image generation function calls.
*/ */
import { SlashCommandBuilder, AttachmentBuilder, PermissionFlagsBits } from 'discord.js'; import { SlashCommandBuilder, AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
import { expandTemplate } from '../_src/template.js'; import { expandTemplate } from '../_src/template.js';
import fs from 'fs/promises';
import path from 'path';
import axios from 'axios';
/** /**
* Split long text into chunks safe for Discord messaging. * Split long text into chunks safe for Discord messaging.
@ -58,7 +60,7 @@ async function handleImageInteraction(client, interaction, resp, cfg, ephemeral)
const promptText = args.prompt; const promptText = args.prompt;
// Determine number of images (1-10); DALL·E-3 only supports 1 // Determine number of images (1-10); DALL·E-3 only supports 1
let count = 1; let count = 1;
if (args.n != null) { if (args.n !== null) {
const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10); const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10);
if (!Number.isNaN(nVal)) count = nVal; if (!Number.isNaN(nVal)) count = nVal;
} }
@ -298,7 +300,7 @@ export const commands = [
required, required,
additionalProperties: false additionalProperties: false
}, },
strict: true, strict: true
}); });
} }
if (cfg.tools?.webSearch) { if (cfg.tools?.webSearch) {

View File

@ -1,4 +1,4 @@
import { MessageFlags } from 'discord-api-types/v10'; import { _MessageFlags } from 'discord-api-types/v10';
// _opt/schangar.js // _opt/schangar.js
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
@ -100,12 +100,12 @@ export const commands = [
if (typeof client.pb.updateOne === 'function') { if (typeof client.pb.updateOne === 'function') {
await client.pb.updateOne('command_hangarsync', record.id, { await client.pb.updateOne('command_hangarsync', record.id, {
userId: `${interaction.user.id}`, userId: `${interaction.user.id}`,
epoch: `${syncEpoch}`, epoch: `${syncEpoch}`
}); });
} else { } else {
await client.pb.collection('command_hangarsync').update(record.id, { await client.pb.collection('command_hangarsync').update(record.id, {
userId: `${interaction.user.id}`, userId: `${interaction.user.id}`,
epoch: `${syncEpoch}`, epoch: `${syncEpoch}`
}); });
} }
client.logger.info(`[cmd:hangarsync] Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); client.logger.info(`[cmd:hangarsync] Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`);
@ -115,13 +115,13 @@ export const commands = [
await client.pb.createOne('command_hangarsync', { await client.pb.createOne('command_hangarsync', {
guildId: `${interaction.guildId}`, guildId: `${interaction.guildId}`,
userId: `${interaction.user.id}`, userId: `${interaction.user.id}`,
epoch: `${syncEpoch}`, epoch: `${syncEpoch}`
}); });
} else { } else {
await client.pb.collection('command_hangarsync').create({ await client.pb.collection('command_hangarsync').create({
guildId: `${interaction.guildId}`, guildId: `${interaction.guildId}`,
userId: `${interaction.user.id}`, userId: `${interaction.user.id}`,
epoch: `${syncEpoch}`, epoch: `${syncEpoch}`
}); });
} }
client.logger.info(`[cmd:hangarsync] Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); client.logger.info(`[cmd:hangarsync] Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`);
@ -131,7 +131,7 @@ export const commands = [
} catch (error) { } catch (error) {
client.logger.error(`[cmd:hangarsync] Error: ${error.message}`); client.logger.error(`[cmd:hangarsync] Error: ${error.message}`);
await interaction.reply({ await interaction.reply({
content: `Error syncing hangar status. Please try again later.`, content: 'Error syncing hangar status. Please try again later.',
ephemeral: true ephemeral: true
}); });
} }
@ -212,8 +212,8 @@ export const commands = [
// Key positions in the cycle // Key positions in the cycle
const allOffDuration = 5; const allOffDuration = 5;
const turningGreenDuration = 5 * 24; const _turningGreenDuration = 5 * 24 * 1000;
const turningOffDuration = 5 * 12; const turningOffDuration = 5 * 12 * 1000;
// Calculate how much time has passed since the epoch // Calculate how much time has passed since the epoch
const timeSinceEpoch = (currentTime - hangarSync.epoch) / (60 * 1000); const timeSinceEpoch = (currentTime - hangarSync.epoch) / (60 * 1000);
@ -222,26 +222,26 @@ export const commands = [
const cyclePosition = ((timeSinceEpoch % cycleDuration) + cycleDuration) % cycleDuration; const cyclePosition = ((timeSinceEpoch % cycleDuration) + cycleDuration) % cycleDuration;
// Initialize stuff and things // Initialize stuff and things
const lights = [":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:"]; const lights = [':black_circle:', ':black_circle:', ':black_circle:', ':black_circle:', ':black_circle:'];
let minutesUntilNextPhase = 0; let minutesUntilNextPhase = 0;
let currentPhase = ""; let currentPhase = '';
// If the epoch is now, we should be at the all-green position. // 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. // From there, we need to determine where we are in the cycle.
// Case 1: We're in the unlocked phase, right after epoch // Case 1: We're in the unlocked phase, right after epoch
if (cyclePosition < turningOffDuration) { if (cyclePosition < turningOffDuration) {
currentPhase = "Unlocked"; currentPhase = 'Unlocked';
// All lights start as green // All lights start as green
lights.fill(":green_circle:"); lights.fill(':green_circle:');
// Calculate how many lights have turned off // Calculate how many lights have turned off
const offLights = Math.floor(cyclePosition / 12); const offLights = Math.floor(cyclePosition / 12);
// Set the appropriate number of lights to off // Set the appropriate number of lights to off
for (let i = 0; i < offLights; i++) { for (let i = 0; i < offLights; i++) {
lights[i] = ":black_circle:"; lights[i] = ':black_circle:';
} }
// Calculate time until next light turns off // Calculate time until next light turns off
@ -251,7 +251,7 @@ export const commands = [
// Case 2: We're in the reset phase // Case 2: We're in the reset phase
else if (cyclePosition < turningOffDuration + allOffDuration) { else if (cyclePosition < turningOffDuration + allOffDuration) {
currentPhase = "Resetting"; currentPhase = 'Resetting';
// Lights are initialized "off", so do nothing with them // Lights are initialized "off", so do nothing with them
@ -262,10 +262,10 @@ export const commands = [
// Case 3: We're in the locked phase // Case 3: We're in the locked phase
else { else {
currentPhase = "Locked"; currentPhase = 'Locked';
// All lights start as red // All lights start as red
lights.fill(":red_circle:"); lights.fill(':red_circle:');
// Calculate how many lights have turned green // Calculate how many lights have turned green
const timeIntoPhase = cyclePosition - (turningOffDuration + allOffDuration); const timeIntoPhase = cyclePosition - (turningOffDuration + allOffDuration);
@ -273,7 +273,7 @@ export const commands = [
// Set the appropriate number of lights to green // Set the appropriate number of lights to green
for (let i = 0; i < greenLights; i++) { for (let i = 0; i < greenLights; i++) {
lights[i] = ":green_circle:"; lights[i] = ':green_circle:';
} }
// Calculate time until next light turns green // Calculate time until next light turns green
@ -323,7 +323,7 @@ export const commands = [
} catch (error) { } catch (error) {
client.logger.error(`Error in hangarstatus command: ${error.message}`); client.logger.error(`Error in hangarstatus command: ${error.message}`);
await interaction.reply({ await interaction.reply({
content: `Error retrieving hangar status. Please try again later.`, content: 'Error retrieving hangar status. Please try again later.',
ephemeral: true ephemeral: true
}); });
} }
@ -345,7 +345,7 @@ function isPocketBaseConnected(client) {
} }
// Initialize module // Initialize module
export const init = async (client, config) => { export async function init(client, _config) {
client.logger.info('Initializing Star Citizen Hangar Status module'); client.logger.info('Initializing Star Citizen Hangar Status module');
// Check PocketBase connection // Check PocketBase connection
@ -361,4 +361,4 @@ export const init = async (client, config) => {
} }
client.logger.info('Star Citizen Hangar Status module initialized'); client.logger.info('Star Citizen Hangar Status module initialized');
}; }

View File

@ -1,5 +1,5 @@
// Example of another module using scorekeeper // Example of another module using scorekeeper
export const init = async (client, config) => { export async function init(client, _config) {
// Set up message listener that adds input points when users chat // Set up message listener that adds input points when users chat
client.on('messageCreate', async (message) => { client.on('messageCreate', async (message) => {
if (message.author.bot) return; if (message.author.bot) return;
@ -76,7 +76,7 @@ export const init = async (client, config) => {
// If someone joined or left a channel, update tracking for everyone in that channel // If someone joined or left a channel, update tracking for everyone in that channel
updateChannelUserTracking(client, oldState, newState); updateChannelUserTracking(client, oldState, newState);
}); });
}; }
/** /**
* Process when a user leaves a voice channel * Process when a user leaves a voice channel

View File

@ -1,11 +1,11 @@
import { MessageFlags } from 'discord-api-types/v10'; import { MessageFlags } from 'discord-api-types/v10';
// opt/scorekeeper.js // opt/scorekeeper.js
import cron from 'node-cron';
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
import cron from 'node-cron';
// Module state container // Module state container
const moduleState = { const moduleState = {
cronJobs: new Map(), // Store cron jobs by client ID cronJobs: new Map() // Store cron jobs by client ID
}; };
/** /**
@ -435,7 +435,8 @@ async function runDecay(client, guildId) {
} }
} }
client.logger.info(`Decay completed for guild ${guildId}: ${updatedCount} records updated`); const _reason = 'Automated decay';
client.logger.info(`[module:scorekeeper] Decayed ${updatedCount} records by ${client.config.scorekeeper.decay}% (${_reason})`);
return updatedCount; return updatedCount;
} catch (error) { } catch (error) {
client.logger.error(`Error running decay: ${error.message}`); client.logger.error(`Error running decay: ${error.message}`);
@ -609,7 +610,7 @@ export const commands = [
{ name: 'Commendation Value', value: `-# ${commendationValue}`, inline: true }, { name: 'Commendation Value', value: `-# ${commendationValue}`, inline: true },
{ name: 'Citation Value', value: `-# ${citationValue}`, inline: true }, { name: 'Citation Value', value: `-# ${citationValue}`, inline: true },
{ name: 'Multiplier Formula', value: `-# 1 + (${scoreData.commendations} * ${commendationValue}) - (${scoreData.citations} * ${citationValue}) = ${multiplierValue.toFixed(2)}`, inline: false }, { name: 'Multiplier Formula', value: `-# 1 + (${scoreData.commendations} * ${commendationValue}) - (${scoreData.citations} * ${citationValue}) = ${multiplierValue.toFixed(2)}`, inline: false },
{ name: 'Priority Score Formula', value: `-# ${multiplierValue.toFixed(2)} × ${scoreData.input} / (${scoreData.output} + ${baseOutput}) = ${scoreData.totalScore.toFixed(2)}`, inline: false }, { name: 'Priority Score Formula', value: `-# ${multiplierValue.toFixed(2)} × ${scoreData.input} / (${scoreData.output} + ${baseOutput}) = ${scoreData.totalScore.toFixed(2)}`, inline: false }
) )
.setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() }) .setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() })
.setTimestamp(); .setTimestamp();
@ -721,7 +722,7 @@ export const commands = [
const cooldown = client.config.scorekeeper.cooldown || 0; const cooldown = client.config.scorekeeper.cooldown || 0;
if (cooldown > 0) { if (cooldown > 0) {
const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, { const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, {
filter: `guildId = \"${guildId}\" && userId = \"${targetUser.id}\" && type = \"commendation\" && categoryId = \"${categoryId}\"`, filter: `guildId = "${guildId}" && userId = "${targetUser.id}" && type = "commendation" && categoryId = "${categoryId}"`,
sort: '-created' sort: '-created'
}); });
const lastItem = recent.items?.[0]; const lastItem = recent.items?.[0];
@ -806,12 +807,13 @@ export const commands = [
} }
const targetUser = interaction.options.getUser('user'); const targetUser = interaction.options.getUser('user');
const categoryId = interaction.options.getString('category'); const categoryId = interaction.options.getString('category');
const reason = interaction.options.getString('reason');
const amount = 1; const amount = 1;
// Enforce per-category cooldown // Enforce per-category cooldown
const cooldown = client.config.scorekeeper.cooldown || 0; const cooldown = client.config.scorekeeper.cooldown || 0;
if (cooldown > 0) { if (cooldown > 0) {
const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, { const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, {
filter: `guildId = \"${guildId}\" && userId = \"${targetUser.id}\" && type = \"citation\" && categoryId = \"${categoryId}\"`, filter: `guildId = "${guildId}" && userId = "${targetUser.id}" && type = "citation" && categoryId = "${categoryId}"`,
sort: '-created' sort: '-created'
}); });
const lastItem = recent.items?.[0]; const lastItem = recent.items?.[0];
@ -834,7 +836,6 @@ export const commands = [
try { try {
await client.scorekeeper.addCitation(interaction.guildId, targetUser.id, amount); await client.scorekeeper.addCitation(interaction.guildId, targetUser.id, amount);
// Log event // Log event
// Log event (timestamp managed by PocketBase "created" field)
await client.pb.collection('scorekeeper_events').create({ await client.pb.collection('scorekeeper_events').create({
guildId: interaction.guildId, guildId: interaction.guildId,
userId: targetUser.id, userId: targetUser.id,

View File

@ -1,5 +1,5 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js';
import { MessageFlags } from 'discord-api-types/v10'; import { MessageFlags } from 'discord-api-types/v10';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js';
// Init function to handle autocomplete for /vc invite // Init function to handle autocomplete for /vc invite
/** /**
* tempvc module: temporary voice channel manager * tempvc module: temporary voice channel manager
@ -266,7 +266,7 @@ export const commands = [
} catch {} } catch {}
await interaction.reply({ content: `Kicked <@${u.id}>.`, flags: MessageFlags.Ephemeral }); await interaction.reply({ content: `Kicked <@${u.id}>.`, flags: MessageFlags.Ephemeral });
} else if (sub === 'limit') { } else if (sub === 'limit') {
let num = interaction.options.getInteger('number', true); const num = interaction.options.getInteger('number', true);
// enforce range 0-99 // enforce range 0-99
if (num < 0 || num > 99) { if (num < 0 || num > 99) {
return interaction.reply({ content: 'User limit must be between 0 (no limit) and 99.', flags: MessageFlags.Ephemeral }); return interaction.reply({ content: 'User limit must be between 0 (no limit) and 99.', flags: MessageFlags.Ephemeral });
@ -571,21 +571,21 @@ export async function init(client) {
'• /vc kick <user> — Kick a user from this channel\n' + '• /vc kick <user> — Kick a user from this channel\n' +
'• /vc role <role> — Set a role to allow/deny access\n' + '• /vc role <role> — Set a role to allow/deny access\n' +
'• /vc mode <whitelist|blacklist> — Switch role mode\n' + '• /vc mode <whitelist|blacklist> — Switch role mode\n' +
'• /vc limit <number> — Set user limit (099)', '• /vc limit <number> — Set user limit (099)'
}, },
{ {
name: 'Presets', name: 'Presets',
value: value:
'• /vc save <name> — Save current settings as a preset\n' + '• /vc save <name> — Save current settings as a preset\n' +
'• /vc restore <name> — Restore settings from a preset\n' + '• /vc restore <name> — Restore settings from a preset\n' +
'• /vc reset — Reset channel to default settings', '• /vc reset — Reset channel to default settings'
}, },
{ {
name: 'Utilities', name: 'Utilities',
value: value:
'• /vc rename <new_name> — Rename this channel\n' + '• /vc rename <new_name> — Rename this channel\n' +
'• /vc info — Show channel info\n' + '• /vc info — Show channel info\n' +
'• /vc delete — Delete this channel', '• /vc delete — Delete this channel'
} }
); );
await ch.send({ embeds: [helpEmbed] }); await ch.send({ embeds: [helpEmbed] });

View File

@ -1,9 +1,10 @@
import winston from 'winston';
import 'winston-daily-rotate-file';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import winston from 'winston';
import 'winston-daily-rotate-file';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const rootDir = path.dirname(__dirname); const rootDir = path.dirname(__dirname);

View File

@ -1,4 +1,4 @@
"use strict"; 'use strict';
/** /**
* expandTemplate: simple variable substitution in {{key}} placeholders. * expandTemplate: simple variable substitution in {{key}} placeholders.
* @param {string} template - The template string with {{key}} tokens. * @param {string} template - The template string with {{key}} tokens.

208
config.js
View File

@ -1,6 +1,38 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
const logging = {
console: {
enabled: true,
colorize: true,
level: 'silly'
},
file: {
dateFormat: 'YYYY-MM-DD',
timestampFormat: 'YYYY-MM-DD HH:mm:ss',
combined: {
enabled: true,
level: 'silly',
location: 'logs',
maxSize: '12m',
maxFiles: '30d'
},
error: {
enabled: true,
level: 'error',
location: 'logs',
maxSize: '12m',
maxFiles: '365d'
}
}
};
const pocketbase = {
url: process.env.SHARED_POCKETBASE_URL,
username: process.env.SHARED_POCKETBASE_USERNAME,
password: process.env.SHARED_POCKETBASE_PASSWORD
};
export default { export default {
clients: [ clients: [
@ -14,37 +46,9 @@ export default {
token: process.env.SYSAI_DISCORD_TOKEN token: process.env.SYSAI_DISCORD_TOKEN
}, },
logging: { logging: { ...logging },
console: {
enabled: true,
colorize: true,
level: 'silly',
},
file: {
dateFormat: 'YYYY-MM-DD',
timestampFormat: 'YYYY-MM-DD HH:mm:ss',
combined: {
enabled: true,
level: 'silly',
location: 'logs',
maxSize: '12m',
maxFiles: '30d',
},
error: {
enabled: true,
level: 'error',
location: 'logs',
maxSize: '12m',
maxFiles: '365d',
}
}
},
pocketbase: { pocketbase: { ...pocketbase },
url: process.env.SHARED_POCKETBASE_URL,
username: process.env.SHARED_POCKETBASE_USERNAME,
password: process.env.SHARED_POCKETBASE_PASSWORD
},
responses: { responses: {
apiKey: process.env.SHARED_OPENAI_API_KEY, apiKey: process.env.SHARED_OPENAI_API_KEY,
@ -58,7 +62,7 @@ export default {
tools: { tools: {
webSearch: true, webSearch: true,
fileSearch: false, fileSearch: false,
imageGeneration: true, imageGeneration: true
}, },
imageGeneration: { imageGeneration: {
defaultModel: 'gpt-image-1', defaultModel: 'gpt-image-1',
@ -90,31 +94,7 @@ export default {
token: process.env.ASOP_DISCORD_TOKEN token: process.env.ASOP_DISCORD_TOKEN
}, },
logging: { logging: { ...logging },
console: {
enabled: true,
colorize: true,
level: 'silly',
},
file: {
dateFormat: 'YYYY-MM-DD',
timestampFormat: 'YYYY-MM-DD HH:mm:ss',
combined: {
enabled: true,
level: 'silly',
location: 'logs',
maxSize: '12m',
maxFiles: '30d',
},
error: {
enabled: true,
level: 'error',
location: 'logs',
maxSize: '12m',
maxFiles: '365d',
}
}
},
condimentX: { condimentX: {
dryRun: false, dryRun: false,
@ -163,11 +143,7 @@ export default {
openAIToken: process.env.SHARED_OPENAI_API_KEY openAIToken: process.env.SHARED_OPENAI_API_KEY
}, },
pocketbase: { pocketbase: { ...pocketbase },
url: process.env.SHARED_POCKETBASE_URL,
username: process.env.SHARED_POCKETBASE_USERNAME,
password: process.env.SHARED_POCKETBASE_PASSWORD
},
responses: { responses: {
apiKey: process.env.SHARED_OPENAI_API_KEY, apiKey: process.env.SHARED_OPENAI_API_KEY,
@ -181,7 +157,7 @@ export default {
tools: { tools: {
webSearch: false, webSearch: false,
fileSearch: false, fileSearch: false,
imageGeneration: true, imageGeneration: true
}, },
imageGeneration: { imageGeneration: {
defaultModel: 'gpt-image-1', defaultModel: 'gpt-image-1',
@ -225,37 +201,9 @@ export default {
token: process.env.CROWLEY_DISCORD_TOKEN token: process.env.CROWLEY_DISCORD_TOKEN
}, },
logging: { logging: { ...logging },
console: {
enabled: true,
colorize: true,
level: 'silly',
},
file: {
dateFormat: 'YYYY-MM-DD',
timestampFormat: 'YYYY-MM-DD HH:mm:ss',
combined: {
enabled: true,
level: 'silly',
location: 'logs',
maxSize: '12m',
maxFiles: '30d',
},
error: {
enabled: true,
level: 'error',
location: 'logs',
maxSize: '12m',
maxFiles: '365d',
}
}
},
pocketbase: { pocketbase: { ...pocketbase },
url: process.env.SHARED_POCKETBASE_URL,
username: process.env.SHARED_POCKETBASE_USERNAME,
password: process.env.SHARED_POCKETBASE_PASSWORD
},
responses: { responses: {
apiKey: process.env.SHARED_OPENAI_API_KEY, apiKey: process.env.SHARED_OPENAI_API_KEY,
@ -269,7 +217,7 @@ export default {
tools: { tools: {
webSearch: false, webSearch: false,
fileSearch: false, fileSearch: false,
imageGeneration: false, imageGeneration: false
}, },
imageGeneration: { imageGeneration: {
defaultModel: 'gpt-image-1', defaultModel: 'gpt-image-1',
@ -298,37 +246,9 @@ export default {
token: process.env.GRANDPA_DISCORD_TOKEN token: process.env.GRANDPA_DISCORD_TOKEN
}, },
logging: { logging: { ...logging },
console: {
enabled: true,
colorize: true,
level: 'silly',
},
file: {
dateFormat: 'YYYY-MM-DD',
timestampFormat: 'YYYY-MM-DD HH:mm:ss',
combined: {
enabled: true,
level: 'silly',
location: 'logs',
maxSize: '12m',
maxFiles: '30d',
},
error: {
enabled: true,
level: 'error',
location: 'logs',
maxSize: '12m',
maxFiles: '365d',
}
}
},
pocketbase: { pocketbase: { ...pocketbase },
url: process.env.SHARED_POCKETBASE_URL,
username: process.env.SHARED_POCKETBASE_USERNAME,
password: process.env.SHARED_POCKETBASE_PASSWORD
},
responses: { responses: {
apiKey: process.env.SHARED_OPENAI_API_KEY, apiKey: process.env.SHARED_OPENAI_API_KEY,
@ -342,7 +262,7 @@ export default {
tools: { tools: {
webSearch: false, webSearch: false,
fileSearch: false, fileSearch: false,
imageGeneration: false, imageGeneration: false
}, },
imageGeneration: { imageGeneration: {
defaultModel: 'gpt-image-1', defaultModel: 'gpt-image-1',
@ -352,7 +272,7 @@ export default {
}, },
responsesRandomizer: { responsesRandomizer: {
chance: 0.01, chance: 0.01
}, },
modules: [ modules: [
'botUtils', 'botUtils',
@ -374,37 +294,9 @@ export default {
token: process.env.SMUUUSH_DISCORD_TOKEN token: process.env.SMUUUSH_DISCORD_TOKEN
}, },
logging: { logging: { ...logging },
console: {
enabled: true,
colorize: true,
level: 'silly',
},
file: {
dateFormat: 'YYYY-MM-DD',
timestampFormat: 'YYYY-MM-DD HH:mm:ss',
combined: {
enabled: true,
level: 'silly',
location: 'logs',
maxSize: '12m',
maxFiles: '30d',
},
error: {
enabled: true,
level: 'error',
location: 'logs',
maxSize: '12m',
maxFiles: '365d',
}
}
},
pocketbase: { pocketbase: { ...pocketbase },
url: process.env.SHARED_POCKETBASE_URL,
username: process.env.SHARED_POCKETBASE_USERNAME,
password: process.env.SHARED_POCKETBASE_PASSWORD
},
responses: { responses: {
apiKey: process.env.SHARED_OPENAI_API_KEY, apiKey: process.env.SHARED_OPENAI_API_KEY,
@ -418,7 +310,7 @@ export default {
tools: { tools: {
webSearch: false, webSearch: false,
fileSearch: false, fileSearch: false,
imageGeneration: true, imageGeneration: true
}, },
imageGeneration: { imageGeneration: {
defaultModel: 'gpt-image-1', defaultModel: 'gpt-image-1',
@ -433,8 +325,8 @@ export default {
'responses', 'responses',
'responsesPrompt', 'responsesPrompt',
'responsesQuery' 'responsesQuery'
], ]
} }
] ]
} };

View File

@ -1,10 +1,11 @@
import { Client, Collection, GatewayIntentBits } from 'discord.js'; import { Client, Collection, GatewayIntentBits } from 'discord.js';
import { ansi, wrapAnsi } from './_src/ansiColors.js';
import { loadModules } from './_src/loader.js';
import { createLogger } from './_src/logger.js'; import { createLogger } from './_src/logger.js';
import { initializePocketbase } from './_src/pocketbase.js'; import { initializePocketbase } from './_src/pocketbase.js';
import { loadModules } from './_src/loader.js';
import config from './config.js'; import config from './config.js';
// For deprecated ephemeral option: convert to flags // For deprecated ephemeral option: convert to flags
import { ansi, wrapAnsi } from './_src/ansiColors.js';
// Initialize Discord client // Initialize Discord client
const initializeClient = async (clientConfig) => { const initializeClient = async (clientConfig) => {
@ -45,7 +46,6 @@ const initializeClient = async (clientConfig) => {
client.on('interactionCreate', async (interaction) => { client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return; if (!interaction.isChatInputCommand()) return;
const commandName = interaction.commandName; const commandName = interaction.commandName;
try { try {

2883
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,12 @@
}, },
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"registry": "node registry.js" "registry": "node registry.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@discordjs/rest": "^2.2.0",
"axios": "^1.8.4", "axios": "^1.8.4",
"discord-api-types": "^0.37.120", "discord-api-types": "^0.37.120",
"discord.js": "^14.18.0", "discord.js": "^14.18.0",
@ -24,5 +27,9 @@
"pocketbase": "^0.25.2", "pocketbase": "^0.25.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1"
} }
} }

View File

@ -1,9 +1,11 @@
// registry.js // registry.js
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v10';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { REST } from '@discordjs/rest'; // eslint-disable-line import/no-unresolved
import { Routes } from 'discord-api-types/v10';
import config from './config.js'; import config from './config.js';
// Get directory name in ES module // Get directory name in ES module