diff --git a/discord/classes/Bot.js b/discord/classes/Bot.js index 3ef5d61..01b60fc 100644 --- a/discord/classes/Bot.js +++ b/discord/classes/Bot.js @@ -18,11 +18,26 @@ class Bot { partials: ['CHANNEL', 'MESSAGE'] }); - // Initialize command manager - this.commandManager = new CommandManager(this.client); + // Add reference to this bot instance on the client for access from commands + this.client.bot = this; + + // Authorized users for commands - Parse comma-separated list from env variable + this.authorizedUserIds = process.env.AUTHORIZED_USER_IDS + ? process.env.AUTHORIZED_USER_IDS.split(',').map(id => id.trim()) + : []; - // Authorized user ID - CHANGE THIS to your Discord user ID - this.authorizedUserId = process.env.AUTHORIZED_USER_ID; + // For backward compatibility, add the old env var if it exists + if (process.env.AUTHORIZED_USER_ID && !this.authorizedUserIds.includes(process.env.AUTHORIZED_USER_ID)) { + this.authorizedUserIds.push(process.env.AUTHORIZED_USER_ID); + } + + // Parse notification recipient IDs (separate from command authorization) + this.notificationRecipientIds = process.env.NOTIFICATION_USER_IDS ? + process.env.NOTIFICATION_USER_IDS.split(',').map(id => id.trim()) : + this.authorizedUserIds; // Default to authorized users if not specified + + console.log(`Authorized users configured: ${this.authorizedUserIds.length}`); + console.log(`Notification recipients configured: ${this.notificationRecipientIds.length}`); // Setup temp directory this.setupTempDirectory(); @@ -120,14 +135,15 @@ class Bot { } }; - // Only notify the authorized user - try { - const owner = await this.client.users.fetch(this.authorizedUserId); - await owner.send({ embeds: [startupEmbed] }); - console.log(`Sent startup notification to authorized user: ${owner.tag}`); - } catch (error) { - console.error("Failed to send startup notification to authorized user:", error.message); - console.log("This is not critical - the bot will still function normally"); + // Notify all recipients + for (const userId of this.notificationRecipientIds) { + try { + const user = await this.client.users.fetch(userId); + await user.send({ embeds: [startupEmbed] }); + console.log(`Sent startup notification to recipient: ${user.tag}`); + } catch (error) { + console.error(`Failed to send startup notification to user ${userId}:`, error.message); + } } // Also notify in status channel if configured @@ -182,13 +198,15 @@ class Bot { this.notificationService.stop(); } - // Notify authorized user - try { - const owner = await this.client.users.fetch(this.authorizedUserId); - await owner.send({ embeds: [shutdownEmbed] }); - console.log(`Sent shutdown notification to authorized user: ${owner.tag}`); - } catch (error) { - console.error("Failed to send shutdown notification to authorized user:", error.message); + // Notify all recipients + for (const userId of this.notificationRecipientIds) { + try { + const user = await this.client.users.fetch(userId); + await user.send({ embeds: [shutdownEmbed] }); + console.log(`Sent shutdown notification to recipient: ${user.tag}`); + } catch (error) { + console.error(`Failed to send shutdown notification to user ${userId}:`, error.message); + } } // Also notify in status channel if available diff --git a/discord/classes/CommandManager.js b/discord/classes/CommandManager.js index 8d10b5b..5f2f7b1 100644 --- a/discord/classes/CommandManager.js +++ b/discord/classes/CommandManager.js @@ -48,26 +48,35 @@ class CommandManager { async registerGlobalCommands() { try { - await this.loadCommands(); + console.log("Registering global commands..."); - if (this.commands.size === 0) { - console.log("No commands to register."); - return; - } + const commandsData = this.commands.map(command => { + const data = { + name: command.name, + description: command.description, + options: command.options || [], + // Add these lines for global availability in all contexts + integration_types: [1], // Add integration type for global availability + contexts: [0, 1, 2], // Available in all contexts (DM, GROUP_DM, GUILD) + }; + + // If the command has an addOptions method, call it + if (typeof command.addOptions === 'function') { + data.options = command.addOptions(new SlashCommandBuilder()).options; + } + + return data; + }); - const commandsData = this.commands.map(command => command.toJSON()); - - console.log(`Started refreshing ${commandsData.length} application (/) commands.`); - - // Register as global commands for DMs - const data = await this.rest.put( + const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); + await rest.put( Routes.applicationCommands(this.client.user.id), { body: commandsData }, ); - console.log(`Successfully reloaded ${data.length} application (/) commands.`); + console.log(`Successfully registered ${commandsData.length} global commands`); } catch (error) { - console.error(error); + console.error('Error registering global commands:', error); } } @@ -102,6 +111,22 @@ class CommandManager { } } } + + async handleAutocomplete(interaction) { + const command = this.commands.get(interaction.commandName); + + if (!command || typeof command.handleAutocomplete !== 'function') { + return; + } + + try { + await command.handleAutocomplete(interaction); + } catch (error) { + console.error(`Error handling autocomplete for ${interaction.commandName}:`, error); + // Respond with empty array as fallback + await interaction.respond([]); + } + } } module.exports = CommandManager; diff --git a/discord/classes/NotificationService.js b/discord/classes/NotificationService.js index 070a206..5894db8 100644 --- a/discord/classes/NotificationService.js +++ b/discord/classes/NotificationService.js @@ -3,7 +3,28 @@ const axios = require('axios'); class NotificationService { constructor(client, options = {}) { this.client = client; - this.authorizedUserId = process.env.AUTHORIZED_USER_ID; + + // Parse notification recipient IDs (separate from command authorization) + this.notificationRecipientIds = process.env.NOTIFICATION_USER_IDS ? + process.env.NOTIFICATION_USER_IDS.split(',').map(id => id.trim()) : + []; + + // For backward compatibility - if no notification IDs specified, use authorized IDs + if (this.notificationRecipientIds.length === 0) { + // Use the bot's authorizedUserIds as fallback + this.notificationRecipientIds = client.bot.authorizedUserIds || + (process.env.AUTHORIZED_USER_IDS ? + process.env.AUTHORIZED_USER_IDS.split(',').map(id => id.trim()) : + []); + + // Add legacy single user ID for backward compatibility + if (process.env.AUTHORIZED_USER_ID && !this.notificationRecipientIds.includes(process.env.AUTHORIZED_USER_ID)) { + this.notificationRecipientIds.push(process.env.AUTHORIZED_USER_ID); + } + } + + console.log(`Notification recipients configured: ${this.notificationRecipientIds.length}`); + this.statusChannel = null; this.checkInterval = options.checkInterval || 10000; // Changed to 10 seconds default this.statusEndpoint = options.statusEndpoint || 'https://blahaj.tr:2589/status'; @@ -263,14 +284,14 @@ class NotificationService { } } - // Send to owner - if (this.authorizedUserId) { + // Send to all notification recipients + for (const userId of this.notificationRecipientIds) { try { - const owner = await this.client.users.fetch(this.authorizedUserId); - await owner.send({ embeds: [embed] }); - console.log('Status change notification sent to owner'); + const user = await this.client.users.fetch(userId); + await user.send({ embeds: [embed] }); + console.log(`Status change notification sent to recipient: ${user.tag}`); } catch (error) { - console.error(`Failed to send status notification to owner: ${error.message}`); + console.error(`Failed to send status notification to user ${userId}: ${error.message}`); } } } diff --git a/discord/commands/system/pm2.js b/discord/commands/system/pm2.js new file mode 100644 index 0000000..99d32b5 --- /dev/null +++ b/discord/commands/system/pm2.js @@ -0,0 +1,539 @@ +const SystemCommandBase = require('../../classes/SystemCommandBase'); +const { SlashCommandBuilder } = require('discord.js'); + +class PM2Control extends SystemCommandBase { + constructor(client) { + super(client); + this.name = 'pm2'; + this.description = 'Control PM2 processes'; + } + + addOptions(builder) { + return builder + .addSubcommand(subcommand => + subcommand + .setName('list') + .setDescription('List all PM2 processes') + ) + .addSubcommand(subcommand => + subcommand + .setName('info') + .setDescription('Get detailed information about a PM2 process') + .addStringOption(option => + option + .setName('process') + .setDescription('Process name or ID') + .setRequired(true) + .setAutocomplete(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('start') + .setDescription('Start a PM2 process') + .addStringOption(option => + option + .setName('process') + .setDescription('Process name or ID') + .setRequired(true) + .setAutocomplete(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('stop') + .setDescription('Stop a PM2 process') + .addStringOption(option => + option + .setName('process') + .setDescription('Process name or ID') + .setRequired(true) + .setAutocomplete(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('restart') + .setDescription('Restart a PM2 process') + .addStringOption(option => + option + .setName('process') + .setDescription('Process name or ID') + .setRequired(true) + .setAutocomplete(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('logs') + .setDescription('Show recent logs for a PM2 process') + .addStringOption(option => + option + .setName('process') + .setDescription('Process name or ID') + .setRequired(true) + .setAutocomplete(true) + ) + .addIntegerOption(option => + option + .setName('lines') + .setDescription('Number of log lines to show') + .setRequired(false) + ) + ); + } + + async execute(interaction) { + await interaction.deferReply(); + + const subcommand = interaction.options.getSubcommand(); + + try { + switch (subcommand) { + case 'list': + await this.handleListCommand(interaction); + break; + + case 'info': + await this.handleInfoCommand(interaction); + break; + + case 'start': + await this.handleStartCommand(interaction); + break; + + case 'stop': + await this.handleStopCommand(interaction); + break; + + case 'restart': + await this.handleRestartCommand(interaction); + break; + + case 'logs': + await this.handleLogsCommand(interaction); + break; + + default: + await interaction.editReply(`Unknown subcommand: ${subcommand}`); + } + } catch (error) { + console.error(`Error executing PM2 command:`, error); + await interaction.editReply({ + content: `Error executing command: ${error.message}` + }); + } + } + + async handleListCommand(interaction) { + const { stdout } = await this.execCommand('pm2 jlist'); + + try { + const processes = JSON.parse(stdout); + + if (processes.length === 0) { + await interaction.editReply('No PM2 processes found.'); + return; + } + + const embed = { + title: 'PM2 Process List', + color: 0x3498db, + fields: [], + timestamp: new Date(), + footer: { text: 'PM2 Process Manager' } + }; + + processes.forEach(proc => { + // Format memory to MB + const memory = Math.round(proc.monit.memory / (1024 * 1024) * 10) / 10; + + // Get appropriate status emoji + let statusEmoji = '⚪'; + switch (proc.pm2_env.status) { + case 'online': statusEmoji = '🟢'; break; + case 'stopping': statusEmoji = '🟠'; break; + case 'stopped': statusEmoji = '🔴'; break; + case 'errored': statusEmoji = '❌'; break; + case 'launching': statusEmoji = '🟡'; break; + } + + // Calculate uptime + const uptime = proc.pm2_env.status === 'online' ? + this.formatUptime(Date.now() - proc.pm2_env.pm_uptime) : + 'Not running'; + + embed.fields.push({ + name: `${statusEmoji} ${proc.name} (ID: ${proc.pm_id})`, + value: [ + `**Status:** ${proc.pm2_env.status}`, + `**CPU:** ${proc.monit.cpu}%`, + `**Memory:** ${memory} MB`, + `**Uptime:** ${uptime}`, + `**Restarts:** ${proc.pm2_env.restart_time}` + ].join('\n'), + inline: true + }); + }); + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Error parsing PM2 process list:', error); + await interaction.editReply({ + content: `Failed to parse PM2 process list: ${error.message}`, + files: stdout.length > 0 ? [{ + attachment: Buffer.from(stdout), + name: 'pm2-list.json' + }] : [] + }); + } + } + + async handleInfoCommand(interaction) { + const processName = interaction.options.getString('process'); + + // Get detailed info about the process + const { success, stdout, stderr } = await this.execCommand(`pm2 show ${processName} --format json`); + + if (!success) { + await interaction.editReply(`Failed to get info for PM2 process "${processName}":\n\`\`\`${stderr}\`\`\``); + return; + } + + try { + // Parse the JSON output + const procInfo = JSON.parse(stdout); + + // Get status emoji + let statusEmoji = '⚪'; + switch (procInfo.status) { + case 'online': statusEmoji = '🟢'; break; + case 'stopping': statusEmoji = '🟠'; break; + case 'stopped': statusEmoji = '🔴'; break; + case 'errored': statusEmoji = '❌'; break; + case 'launching': statusEmoji = '🟡'; break; + } + + // Format memory + const memory = procInfo.memory ? + Math.round(procInfo.memory / (1024 * 1024) * 10) / 10 : + 0; + + // Create embed + const embed = { + title: `${statusEmoji} PM2 Process: ${procInfo.name}`, + color: procInfo.status === 'online' ? 0x00FF00 : 0xFF0000, + fields: [ + { + name: 'General', + value: [ + `**ID:** ${procInfo.pm_id}`, + `**Status:** ${procInfo.status}`, + `**Version:** ${procInfo.version || 'N/A'}`, + `**Instances:** ${procInfo.exec_instances || 1}`, + `**Exec Mode:** ${procInfo.exec_mode || 'N/A'}` + ].join('\n'), + inline: true + }, + { + name: 'Resources', + value: [ + `**CPU:** ${procInfo.cpu || 0}%`, + `**Memory:** ${memory} MB`, + `**Uptime:** ${this.formatUptime(procInfo.pm_uptime) || 'Not running'}`, + `**Restarts:** ${procInfo.restart_time || 0}`, + `**Unstable Restarts:** ${procInfo.unstable_restarts || 0}` + ].join('\n'), + inline: true + }, + { + name: 'Paths', + value: [ + `**Path:** ${procInfo.path || 'N/A'}`, + `**Current Path:** ${procInfo.cwd || 'N/A'}`, + `**Script:** ${procInfo.script || 'N/A'}` + ].join('\n'), + inline: false + } + ], + timestamp: new Date(), + footer: { text: 'PM2 Process Manager' } + }; + + // Add logs section if available + if (procInfo.out_log_path || procInfo.error_log_path) { + embed.fields.push({ + name: 'Logs', + value: [ + `**Output:** ${procInfo.out_log_path || 'N/A'}`, + `**Error:** ${procInfo.error_log_path || 'N/A'}` + ].join('\n'), + inline: false + }); + } + + await interaction.editReply({ embeds: [embed] }); + + } catch (error) { + console.error('Error parsing PM2 process info:', error); + await interaction.editReply(`Failed to parse info for PM2 process "${processName}":\n\`\`\`${error.message}\`\`\``); + } + } + + async handleStartCommand(interaction) { + const processName = interaction.options.getString('process'); + + // First get current status + const { success: infoSuccess, stdout: infoStdout } = await this.execCommand(`pm2 jlist`); + let beforeStatus = 'unknown'; + + if (infoSuccess) { + try { + const processes = JSON.parse(infoStdout); + const proc = processes.find(p => p.name === processName || p.pm_id.toString() === processName); + if (proc) { + beforeStatus = proc.pm2_env.status; + } + } catch (error) { + console.error('Error parsing PM2 process list before start:', error); + } + } + + // Start the process + await interaction.editReply(`Starting PM2 process \`${processName}\`...`); + const { success, stdout, stderr } = await this.execCommand(`pm2 start ${processName}`); + + if (!success) { + await interaction.editReply(`Failed to start PM2 process "${processName}":\n\`\`\`${stderr}\`\`\``); + return; + } + + // Get new status + const { success: newInfoSuccess, stdout: newInfoStdout } = await this.execCommand(`pm2 jlist`); + let afterStatus = 'unknown'; + + if (newInfoSuccess) { + try { + const processes = JSON.parse(newInfoStdout); + const proc = processes.find(p => p.name === processName || p.pm_id.toString() === processName); + if (proc) { + afterStatus = proc.pm2_env.status; + } + } catch (error) { + console.error('Error parsing PM2 process list after start:', error); + } + } + + // Create status emoji + let statusEmoji = '⚪'; + switch (afterStatus) { + case 'online': statusEmoji = '🟢'; break; + case 'stopping': statusEmoji = '🟠'; break; + case 'stopped': statusEmoji = '🔴'; break; + case 'errored': statusEmoji = '❌'; break; + case 'launching': statusEmoji = '🟡'; break; + } + + await interaction.editReply(`PM2 process \`${processName}\` started.\n\nStatus: ${statusEmoji} ${afterStatus}\nPrevious status: ${beforeStatus}`); + } + + async handleStopCommand(interaction) { + const processName = interaction.options.getString('process'); + + // First get current status + const { success: infoSuccess, stdout: infoStdout } = await this.execCommand(`pm2 jlist`); + let beforeStatus = 'unknown'; + + if (infoSuccess) { + try { + const processes = JSON.parse(infoStdout); + const proc = processes.find(p => p.name === processName || p.pm_id.toString() === processName); + if (proc) { + beforeStatus = proc.pm2_env.status; + } + } catch (error) { + console.error('Error parsing PM2 process list before stop:', error); + } + } + + // Stop the process + await interaction.editReply(`Stopping PM2 process \`${processName}\`...`); + const { success, stdout, stderr } = await this.execCommand(`pm2 stop ${processName}`); + + if (!success) { + await interaction.editReply(`Failed to stop PM2 process "${processName}":\n\`\`\`${stderr}\`\`\``); + return; + } + + // Get new status + const { success: newInfoSuccess, stdout: newInfoStdout } = await this.execCommand(`pm2 jlist`); + let afterStatus = 'unknown'; + + if (newInfoSuccess) { + try { + const processes = JSON.parse(newInfoStdout); + const proc = processes.find(p => p.name === processName || p.pm_id.toString() === processName); + if (proc) { + afterStatus = proc.pm2_env.status; + } + } catch (error) { + console.error('Error parsing PM2 process list after stop:', error); + } + } + + // Create status emoji + let statusEmoji = '⚪'; + switch (afterStatus) { + case 'online': statusEmoji = '🟢'; break; + case 'stopping': statusEmoji = '🟠'; break; + case 'stopped': statusEmoji = '🔴'; break; + case 'errored': statusEmoji = '❌'; break; + case 'launching': statusEmoji = '🟡'; break; + } + + await interaction.editReply(`PM2 process \`${processName}\` stopped.\n\nStatus: ${statusEmoji} ${afterStatus}\nPrevious status: ${beforeStatus}`); + } + + async handleRestartCommand(interaction) { + const processName = interaction.options.getString('process'); + + // First get current status + const { success: infoSuccess, stdout: infoStdout } = await this.execCommand(`pm2 jlist`); + let beforeStatus = 'unknown'; + + if (infoSuccess) { + try { + const processes = JSON.parse(infoStdout); + const proc = processes.find(p => p.name === processName || p.pm_id.toString() === processName); + if (proc) { + beforeStatus = proc.pm2_env.status; + } + } catch (error) { + console.error('Error parsing PM2 process list before restart:', error); + } + } + + // Restart the process + await interaction.editReply(`Restarting PM2 process \`${processName}\`...`); + const { success, stdout, stderr } = await this.execCommand(`pm2 restart ${processName}`); + + if (!success) { + await interaction.editReply(`Failed to restart PM2 process "${processName}":\n\`\`\`${stderr}\`\`\``); + return; + } + + // Get new status + const { success: newInfoSuccess, stdout: newInfoStdout } = await this.execCommand(`pm2 jlist`); + let afterStatus = 'unknown'; + + if (newInfoSuccess) { + try { + const processes = JSON.parse(newInfoStdout); + const proc = processes.find(p => p.name === processName || p.pm_id.toString() === processName); + if (proc) { + afterStatus = proc.pm2_env.status; + } + } catch (error) { + console.error('Error parsing PM2 process list after restart:', error); + } + } + + // Create status emoji + let statusEmoji = '⚪'; + switch (afterStatus) { + case 'online': statusEmoji = '🟢'; break; + case 'stopping': statusEmoji = '🟠'; break; + case 'stopped': statusEmoji = '🔴'; break; + case 'errored': statusEmoji = '❌'; break; + case 'launching': statusEmoji = '🟡'; break; + } + + await interaction.editReply(`PM2 process \`${processName}\` restarted.\n\nStatus: ${statusEmoji} ${afterStatus}\nPrevious status: ${beforeStatus}`); + } + + async handleLogsCommand(interaction) { + const processName = interaction.options.getString('process'); + const lines = interaction.options.getInteger('lines') || 20; + + // Get logs for the process + await interaction.editReply(`Fetching logs for PM2 process \`${processName}\`...`); + const { success, stdout, stderr } = await this.execCommand(`pm2 logs ${processName} --lines ${lines} --nostream --raw`); + + if (!success) { + await interaction.editReply(`Failed to get logs for PM2 process "${processName}":\n\`\`\`${stderr}\`\`\``); + return; + } + + // Format the logs + const logs = stdout.trim(); + + if (!logs) { + await interaction.editReply(`No logs found for PM2 process \`${processName}\`.`); + return; + } + + // If logs are too long, split into files + if (logs.length > 1950) { + await interaction.editReply({ + content: `Logs for PM2 process \`${processName}\` (last ${lines} lines):`, + files: [{ + attachment: Buffer.from(logs), + name: `${processName}-logs.txt` + }] + }); + } else { + await interaction.editReply(`Logs for PM2 process \`${processName}\` (last ${lines} lines):\n\`\`\`\n${logs}\n\`\`\``); + } + } + + // Helper method to autocomplete process names + async handleAutocomplete(interaction) { + try { + const focusedValue = interaction.options.getFocused(); + const { success, stdout } = await this.execCommand('pm2 jlist'); + + if (!success) { + return interaction.respond([]); + } + + const processes = JSON.parse(stdout); + const choices = processes.map(proc => ({ + name: `${proc.name} (${proc.pm2_env.status})`, + value: proc.name + })); + + // Filter choices based on user input + const filtered = choices.filter(choice => + choice.name.toLowerCase().includes(focusedValue.toLowerCase()) + ); + + await interaction.respond(filtered.slice(0, 25)); + } catch (error) { + console.error('Error in PM2 autocomplete:', error); + await interaction.respond([]); + } + } + + // Helper to format uptime + formatUptime(ms) { + if (!ms || ms <= 0) return 'Not running'; + + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days}d ${hours % 24}h ${minutes % 60}m`; + } else if (hours > 0) { + return `${hours}h ${minutes % 60}m ${seconds % 60}s`; + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } else { + return `${seconds}s`; + } + } +} + +module.exports = PM2Control;