Compare commits
3 Commits
bb3c97c5bb
...
e02ffcebed
| Author | SHA1 | Date | |
|---|---|---|---|
| e02ffcebed | |||
| c35aeec42f | |||
| 1f99e26b50 |
102
_opt/gitUtils.js
Normal file
102
_opt/gitUtils.js
Normal file
@ -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 <args>` 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 <args>
|
||||||
|
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');
|
||||||
|
}
|
||||||
28
_opt/messageQueue-example.js
Normal file
28
_opt/messageQueue-example.js
Normal file
@ -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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -19,12 +19,15 @@ export const loadModules = async (clientConfig, client) => {
|
|||||||
// Load each module
|
// Load each module
|
||||||
for (const moduleName of modules) {
|
for (const moduleName of modules) {
|
||||||
try {
|
try {
|
||||||
const modulePath = path.join(modulesDir, `${moduleName}.js`);
|
// Try _opt first, then fallback to core _src modules
|
||||||
|
let modulePath = path.join(modulesDir, `${moduleName}.js`);
|
||||||
// Check if module exists
|
|
||||||
if (!fs.existsSync(modulePath)) {
|
if (!fs.existsSync(modulePath)) {
|
||||||
client.logger.warn(`Module not found: ${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;
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import module (using dynamic import for ES modules)
|
// Import module (using dynamic import for ES modules)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default {
|
|||||||
{
|
{
|
||||||
id: 'IO3',
|
id: 'IO3',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
owner: 378741522822070272,
|
owner: process.env.OWNER_ID,
|
||||||
|
|
||||||
discord: {
|
discord: {
|
||||||
appId: process.env.IO3_DISCORD_APPID,
|
appId: process.env.IO3_DISCORD_APPID,
|
||||||
@ -70,6 +70,7 @@ export default {
|
|||||||
'pbUtils',
|
'pbUtils',
|
||||||
'responses',
|
'responses',
|
||||||
'responsesQuery',
|
'responsesQuery',
|
||||||
|
'gitUtils'
|
||||||
]
|
]
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
22
package-lock.json
generated
22
package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
"discord-api-types": "^0.37.120",
|
"discord-api-types": "^0.37.120",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"eventsource": "^3.0.6",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"openai": "^4.95.1",
|
"openai": "^4.95.1",
|
||||||
"pocketbase": "^0.25.2",
|
"pocketbase": "^0.25.2",
|
||||||
@ -519,6 +520,27 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"discord-api-types": "^0.37.120",
|
"discord-api-types": "^0.37.120",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"eventsource": "^3.0.6",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"openai": "^4.95.1",
|
"openai": "^4.95.1",
|
||||||
"pocketbase": "^0.25.2",
|
"pocketbase": "^0.25.2",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user