diff --git a/_opt/gitUtils.js b/_opt/gitUtils.js new file mode 100644 index 0000000..f743c06 --- /dev/null +++ b/_opt/gitUtils.js @@ -0,0 +1,102 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +const execAsync = promisify(exec); + +// Wrap Git errors +class GitError extends Error { + constructor(message) { + super(message); + this.name = 'GitError'; + } +} + +// Run `git ` and return trimmed output or throw +async function runGit(args) { + try { + const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`); + const out = stdout.trim() || stderr.trim(); + return out || '(no output)'; + } catch (err) { + const msg = err.stderr?.trim() || err.message; + throw new GitError(msg); + } +} + +// Wrap content in Markdown code block +function formatCodeBlock(content, lang = '') { + const fence = '```'; + return lang + ? `${fence}${lang}\n${content}\n${fence}` + : `${fence}\n${content}\n${fence}`; +} + +// Split string into chunks of at most chunkSize +function chunkString(str, chunkSize) { + const chunks = []; + for (let i = 0; i < str.length; i += chunkSize) { + chunks.push(str.slice(i, i + chunkSize)); + } + return chunks; +} + +// Single /git command: run arbitrary git +export const commands = [ + { + data: new SlashCommandBuilder() + .setName('git') + .setDescription('Run an arbitrary git command (owner only)') + .addStringOption(opt => + opt.setName('args') + .setDescription('Arguments to pass to git') + .setRequired(true)) + .addBooleanOption(opt => + opt.setName('ephemeral') + .setDescription('Make the reply ephemeral') + .setRequired(false)), + async execute(interaction, client) { + const ownerId = client.config.owner; + if (interaction.user.id !== ownerId) { + return interaction.reply({ content: 'Only the bot owner can run git commands.', ephemeral: true }); + } + const raw = interaction.options.getString('args'); + // Disallow semicolons to prevent command chaining + if (raw.includes(';')) { + return interaction.reply({ content: 'Semicolons are not allowed in git arguments.', ephemeral: true }); + } + const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; + const args = raw.match(/(?:[^\s"]+|"[^"]*")+/g) + .map(s => s.replace(/^"(.+)"$/, '$1')); + + try { + // Log the exact git command being executed + const cmdStr = args.join(' '); + client.logger.warn(`Executing git command: git ${cmdStr}`); + const output = await runGit(args); + // Prepend the git command as a header; keep it intact when chunking + const header = `git ${cmdStr}\n`; + // Discord message limit ~2000; reserve for code fences + const maxContent = 1990; + // Calculate how much output can fit after the header in the first chunk + const firstChunkSize = Math.max(0, maxContent - header.length); + // Split the raw output into chunks + const outputChunks = chunkString(output, firstChunkSize); + // Send first block with header + first output chunk + const firstBlock = header + (outputChunks[0] || ''); + await interaction.reply({ content: formatCodeBlock(firstBlock), ephemeral }); + // Send any remaining blocks without the header + for (let i = 1; i < outputChunks.length; i++) { + await interaction.followUp({ content: formatCodeBlock(outputChunks[i]), ephemeral }); + } + } catch (err) { + const msg = err instanceof GitError ? err.message : String(err); + await interaction.reply({ content: `Error: ${msg}`, ephemeral: true }); + } + } + } +]; + +// No special init logic +export async function init(client) { + client.logger.warn('Git utilities module loaded - dangerous module, use with caution'); +} \ No newline at end of file diff --git a/_opt/messageQueue-example.js b/_opt/messageQueue-example.js new file mode 100644 index 0000000..bbf1674 --- /dev/null +++ b/_opt/messageQueue-example.js @@ -0,0 +1,28 @@ +// _opt/messageQueue-example.js +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('Initializing Message Queue Example module'); + onMessageQueueEvent(client, async (action, record) => { + // Only process newly created records + if (action !== 'create') return; + // Only process messages meant for this client + if (record.destination !== client.config.id) return; + // Only handle test dataType + if (record.dataType !== 'test') return; + + // At this point we have a test message for us + client.logger.info('test received'); + + // Delete the processed message from the queue + try { + await client.pb.deleteMessageQueue(record.id); + client.logger.debug(`Deleted message_queue record ${record.id}`); + } catch (err) { + client.logger.error(`Failed to delete message_queue record ${record.id}: ${err.message}`); + } + }); +}; \ No newline at end of file diff --git a/_src/loader.js b/_src/loader.js index 12693ba..6bec33d 100644 --- a/_src/loader.js +++ b/_src/loader.js @@ -19,13 +19,16 @@ export const loadModules = async (clientConfig, client) => { // 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; - } + // Try _opt first, then fallback to core _src modules + let modulePath = path.join(modulesDir, `${moduleName}.js`); + if (!fs.existsSync(modulePath)) { + // Fallback to core source directory + modulePath = path.join(rootDir, '_src', `${moduleName}.js`); + if (!fs.existsSync(modulePath)) { + client.logger.warn(`Module not found in _opt or _src: ${moduleName}.js`); + continue; + } + } // Import module (using dynamic import for ES modules) // Import module diff --git a/config.js b/config.js index cf61dd2..6a1ebba 100644 --- a/config.js +++ b/config.js @@ -7,7 +7,7 @@ export default { { id: 'IO3', enabled: true, - owner: 378741522822070272, + owner: process.env.OWNER_ID, discord: { appId: process.env.IO3_DISCORD_APPID, @@ -70,6 +70,7 @@ export default { 'pbUtils', 'responses', 'responsesQuery', + 'gitUtils' ] }, diff --git a/package-lock.json b/package-lock.json index b36a723..33460e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "discord-api-types": "^0.37.120", "discord.js": "^14.18.0", "dotenv": "^16.5.0", + "eventsource": "^3.0.6", "node-cron": "^3.0.3", "openai": "^4.95.1", "pocketbase": "^0.25.2", @@ -519,6 +520,27 @@ "node": ">=6" } }, + "node_modules/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 54298e8..268165b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "discord-api-types": "^0.37.120", "discord.js": "^14.18.0", "dotenv": "^16.5.0", + "eventsource": "^3.0.6", "node-cron": "^3.0.3", "openai": "^4.95.1", "pocketbase": "^0.25.2",