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

View File

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

View File

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

View File

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

View File

@ -4,13 +4,15 @@
* and handles text or image (function_call) outputs.
*/
// Removed local file fallback; prompt now comes exclusively from PocketBase via responsesPrompt module
import { OpenAI } from 'openai';
import axios from 'axios';
import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
import { expandTemplate } from '../_src/template.js';
import fs from 'fs/promises';
import path from 'path';
import axios from 'axios';
import { AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
import { OpenAI } from 'openai';
import { expandTemplate } from '../_src/template.js';
// Discord message max length
const MAX_DISCORD_MSG_LENGTH = 2000;
@ -26,7 +28,7 @@ function splitMessage(text, maxLength = MAX_DISCORD_MSG_LENGTH) {
let chunk = '';
let codeBlockOpen = false;
let codeBlockFence = '```';
for (let line of lines) {
for (const line of lines) {
const trimmed = line.trim();
const isFenceLine = trimmed.startsWith('```');
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);
}
/**
* Determine whether the bot should respond to a message.
* Controlled by enableMentions and enableReplies in config.
@ -143,7 +144,7 @@ async function handleImage(client, message, resp, cfg) {
const promptText = args.prompt;
// Determine number of images (1-10); DALL·E-3 only supports 1
let count = 1;
if (args.n != null) {
if (args.n !== null) {
const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10);
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 fs from 'fs';
import path from 'path';
// Placeholder info for template variables
const TEMPLATE_KEYS_INFO = 'Available keys: userName, userId, locationName, locationId, date, time, datetime, clientId';
@ -27,7 +29,7 @@ export const commands = [
.setAutocomplete(true)
),
async execute(interaction, client) {
const clientId = client.config.id;
const _clientId = client.config.id;
const versionId = interaction.options.getString('version');
// Fetch prompt: live latest or selected historic
let promptText = client.responsesPrompt || '';
@ -87,12 +89,12 @@ export const commands = [
const _clients = [];
export async function init(client, clientConfig) {
const clientId = clientConfig.id;
const _clientId = client.config.id;
client.logger.info('[module:responsesPrompt] initialized');
// Load live prompt (latest version)
try {
const { items } = await client.pb.collection('responses_prompts')
.getList(1, 1, { filter: `clientId="${clientId}"`, sort: '-created' });
.getList(1, 1, { filter: `clientId="${_clientId}"`, sort: '-created' });
client.responsesPrompt = items[0]?.prompt || '';
} catch (err) {
client.logger.error(`Error loading current prompt: ${err.message}`);
@ -106,7 +108,7 @@ export async function init(client, clientConfig) {
if (focused.name === 'version') {
try {
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 }));
await interaction.respond(choices);
} catch (err) {
@ -129,9 +131,9 @@ export async function init(client, clientConfig) {
}
const newPrompt = parts.join('\n');
// Persist new version
let newRec;
let _newRec;
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;
} catch (err) {
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
try {
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);
for (const id of toDelete) {
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';
/**
* Slash command module for '/query'.
@ -5,10 +9,8 @@ import { MessageFlags } from 'discord-api-types/v10';
* including optional image generation function calls.
*/
import { SlashCommandBuilder, AttachmentBuilder, PermissionFlagsBits } from 'discord.js';
import { expandTemplate } from '../_src/template.js';
import fs from 'fs/promises';
import path from 'path';
import axios from 'axios';
/**
* Split long text into chunks safe for Discord messaging.
@ -58,7 +60,7 @@ async function handleImageInteraction(client, interaction, resp, cfg, ephemeral)
const promptText = args.prompt;
// Determine number of images (1-10); DALL·E-3 only supports 1
let count = 1;
if (args.n != null) {
if (args.n !== null) {
const nVal = typeof args.n === 'number' ? args.n : parseInt(args.n, 10);
if (!Number.isNaN(nVal)) count = nVal;
}
@ -190,7 +192,7 @@ export const commands = [
}
} catch (err) {
client.logger.error(`[cmd:query] Error checking score: ${err.message}`);
return interaction.reply({ content: 'Error verifying your score. Please try again later.', flags: MessageFlags.Ephemeral});
return interaction.reply({ content: 'Error verifying your score. Please try again later.', flags: MessageFlags.Ephemeral });
}
}
const prompt = interaction.options.getString('prompt');
@ -298,7 +300,7 @@ export const commands = [
required,
additionalProperties: false
},
strict: true,
strict: true
});
}
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
import { SlashCommandBuilder } from 'discord.js';
@ -100,12 +100,12 @@ export const commands = [
if (typeof client.pb.updateOne === 'function') {
await client.pb.updateOne('command_hangarsync', record.id, {
userId: `${interaction.user.id}`,
epoch: `${syncEpoch}`,
epoch: `${syncEpoch}`
});
} else {
await client.pb.collection('command_hangarsync').update(record.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}`);
@ -115,13 +115,13 @@ export const commands = [
await client.pb.createOne('command_hangarsync', {
guildId: `${interaction.guildId}`,
userId: `${interaction.user.id}`,
epoch: `${syncEpoch}`,
epoch: `${syncEpoch}`
});
} else {
await client.pb.collection('command_hangarsync').create({
guildId: `${interaction.guildId}`,
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}`);
@ -131,7 +131,7 @@ export const commands = [
} catch (error) {
client.logger.error(`[cmd:hangarsync] Error: ${error.message}`);
await interaction.reply({
content: `Error syncing hangar status. Please try again later.`,
content: 'Error syncing hangar status. Please try again later.',
ephemeral: true
});
}
@ -212,8 +212,8 @@ export const commands = [
// Key positions in the cycle
const allOffDuration = 5;
const turningGreenDuration = 5 * 24;
const turningOffDuration = 5 * 12;
const _turningGreenDuration = 5 * 24 * 1000;
const turningOffDuration = 5 * 12 * 1000;
// Calculate how much time has passed since the epoch
const timeSinceEpoch = (currentTime - hangarSync.epoch) / (60 * 1000);
@ -222,26 +222,26 @@ export const commands = [
const cyclePosition = ((timeSinceEpoch % cycleDuration) + cycleDuration) % cycleDuration;
// 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 currentPhase = "";
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";
currentPhase = 'Unlocked';
// All lights start as green
lights.fill(":green_circle:");
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:";
lights[i] = ':black_circle:';
}
// Calculate time until next light turns off
@ -251,7 +251,7 @@ export const commands = [
// Case 2: We're in the reset phase
else if (cyclePosition < turningOffDuration + allOffDuration) {
currentPhase = "Resetting";
currentPhase = 'Resetting';
// Lights are initialized "off", so do nothing with them
@ -262,10 +262,10 @@ export const commands = [
// Case 3: We're in the locked phase
else {
currentPhase = "Locked";
currentPhase = 'Locked';
// All lights start as red
lights.fill(":red_circle:");
lights.fill(':red_circle:');
// Calculate how many lights have turned green
const timeIntoPhase = cyclePosition - (turningOffDuration + allOffDuration);
@ -273,7 +273,7 @@ export const commands = [
// Set the appropriate number of lights to green
for (let i = 0; i < greenLights; i++) {
lights[i] = ":green_circle:";
lights[i] = ':green_circle:';
}
// Calculate time until next light turns green
@ -323,7 +323,7 @@ export const commands = [
} catch (error) {
client.logger.error(`Error in hangarstatus command: ${error.message}`);
await interaction.reply({
content: `Error retrieving hangar status. Please try again later.`,
content: 'Error retrieving hangar status. Please try again later.',
ephemeral: true
});
}
@ -345,7 +345,7 @@ function isPocketBaseConnected(client) {
}
// Initialize module
export const init = async (client, config) => {
export async function init(client, _config) {
client.logger.info('Initializing Star Citizen Hangar Status module');
// Check PocketBase connection
@ -361,4 +361,4 @@ export const init = async (client, config) => {
}
client.logger.info('Star Citizen Hangar Status module initialized');
};
}

View File

@ -1,5 +1,5 @@
// 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
client.on('messageCreate', async (message) => {
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
updateChannelUserTracking(client, oldState, newState);
});
};
}
/**
* Process when a user leaves a voice channel

View File

@ -1,11 +1,11 @@
import { MessageFlags } from 'discord-api-types/v10';
// opt/scorekeeper.js
import cron from 'node-cron';
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
import cron from 'node-cron';
// Module state container
const moduleState = {
cronJobs: new Map(), // Store cron jobs by client ID
cronJobs: new Map() // Store cron jobs by client ID
};
/**
@ -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;
} catch (error) {
client.logger.error(`Error running decay: ${error.message}`);
@ -609,7 +610,7 @@ export const commands = [
{ name: 'Commendation Value', value: `-# ${commendationValue}`, inline: true },
{ name: 'Citation Value', value: `-# ${citationValue}`, inline: true },
{ name: 'Multiplier Formula', value: `-# 1 + (${scoreData.commendations} * ${commendationValue}) - (${scoreData.citations} * ${citationValue}) = ${multiplierValue.toFixed(2)}`, inline: false },
{ name: 'Priority Score Formula', value: `-# ${multiplierValue.toFixed(2)} × ${scoreData.input} / (${scoreData.output} + ${baseOutput}) = ${scoreData.totalScore.toFixed(2)}`, inline: false },
{ name: 'Priority Score Formula', value: `-# ${multiplierValue.toFixed(2)} × ${scoreData.input} / (${scoreData.output} + ${baseOutput}) = ${scoreData.totalScore.toFixed(2)}`, inline: false }
)
.setFooter({ text: 'Last decay: ' + new Date(scoreData.lastDecay).toLocaleDateString() })
.setTimestamp();
@ -721,7 +722,7 @@ export const commands = [
const cooldown = client.config.scorekeeper.cooldown || 0;
if (cooldown > 0) {
const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, {
filter: `guildId = \"${guildId}\" && userId = \"${targetUser.id}\" && type = \"commendation\" && categoryId = \"${categoryId}\"`,
filter: `guildId = "${guildId}" && userId = "${targetUser.id}" && type = "commendation" && categoryId = "${categoryId}"`,
sort: '-created'
});
const lastItem = recent.items?.[0];
@ -806,12 +807,13 @@ export const commands = [
}
const targetUser = interaction.options.getUser('user');
const categoryId = interaction.options.getString('category');
const reason = interaction.options.getString('reason');
const amount = 1;
// Enforce per-category cooldown
const cooldown = client.config.scorekeeper.cooldown || 0;
if (cooldown > 0) {
const recent = await client.pb.collection('scorekeeper_events').getList(1, 1, {
filter: `guildId = \"${guildId}\" && userId = \"${targetUser.id}\" && type = \"citation\" && categoryId = \"${categoryId}\"`,
filter: `guildId = "${guildId}" && userId = "${targetUser.id}" && type = "citation" && categoryId = "${categoryId}"`,
sort: '-created'
});
const lastItem = recent.items?.[0];
@ -834,7 +836,6 @@ export const commands = [
try {
await client.scorekeeper.addCitation(interaction.guildId, targetUser.id, amount);
// Log event
// Log event (timestamp managed by PocketBase "created" field)
await client.pb.collection('scorekeeper_events').create({
guildId: interaction.guildId,
userId: targetUser.id,
@ -909,7 +910,7 @@ export const commands = [
await interaction.reply({ content: `Category '${name}' created.`, ephemeral: true });
} catch (err) {
client.logger.error(`Error in addcategory: ${err.message}`);
await interaction.reply({ content: 'Failed to create category.', flags: MessageFlags.Ephemeral});
await interaction.reply({ content: 'Failed to create category.', flags: MessageFlags.Ephemeral });
}
}
}
@ -938,7 +939,7 @@ export const commands = [
await interaction.reply({ content: `Category '${name}' removed.`, ephemeral: true });
} catch (err) {
client.logger.error(`Error in removecategory: ${err.message}`);
await interaction.reply({ content: 'Failed to remove category.', flags: MessageFlags.Ephemeral});
await interaction.reply({ content: 'Failed to remove category.', flags: MessageFlags.Ephemeral });
}
}
}

View File

@ -1,5 +1,5 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js';
import { MessageFlags } from 'discord-api-types/v10';
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js';
// Init function to handle autocomplete for /vc invite
/**
* tempvc module: temporary voice channel manager
@ -266,7 +266,7 @@ export const commands = [
} catch {}
await interaction.reply({ content: `Kicked <@${u.id}>.`, flags: MessageFlags.Ephemeral });
} else if (sub === 'limit') {
let num = interaction.options.getInteger('number', true);
const num = interaction.options.getInteger('number', true);
// enforce range 0-99
if (num < 0 || num > 99) {
return interaction.reply({ content: 'User limit must be between 0 (no limit) and 99.', flags: MessageFlags.Ephemeral });
@ -571,21 +571,21 @@ export async function init(client) {
'• /vc kick <user> — Kick a user from this channel\n' +
'• /vc role <role> — Set a role to allow/deny access\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',
value:
'• /vc save <name> — Save current settings as 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',
value:
'• /vc rename <new_name> — Rename this channel\n' +
'• /vc info — Show channel info\n' +
'• /vc delete — Delete this channel',
'• /vc delete — Delete this channel'
}
);
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 path from 'path';
import { fileURLToPath } from 'url';
import winston from 'winston';
import 'winston-daily-rotate-file';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.dirname(__dirname);

View File

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

208
config.js
View File

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

View File

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

2883
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,9 +1,11 @@
// registry.js
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v10';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { REST } from '@discordjs/rest'; // eslint-disable-line import/no-unresolved
import { Routes } from 'discord-api-types/v10';
import config from './config.js';
// Get directory name in ES module
@ -46,7 +48,7 @@ Examples:
// Validate action parameter
const validActions = ['register', 'unregister', 'list'];
if (!validActions.includes(actionArg.toLowerCase())) {
console.error(`[registry] Error: Invalid action "${actionArg}". Must be one of: ${validActions.join(', ')}`);
console.error(`[registry] Error: Invalid action "${actionArg}". Must be one of: ${validActions.join(', ')}`);
process.exit(1);
}
const action = actionArg.toLowerCase();