From 88071c43206017e524536256474f199c641dc424 Mon Sep 17 00:00:00 2001 From: Xargana Date: Thu, 17 Apr 2025 15:11:43 +0300 Subject: [PATCH] added status notifications --- discord/classes/Bot.js | 104 +++++++++- discord/classes/NotificationService.js | 267 +++++++++++++++++++++++++ index.js | 8 +- 3 files changed, 375 insertions(+), 4 deletions(-) create mode 100644 discord/classes/NotificationService.js diff --git a/discord/classes/Bot.js b/discord/classes/Bot.js index b2991bc..0d28cbb 100644 --- a/discord/classes/Bot.js +++ b/discord/classes/Bot.js @@ -1,5 +1,6 @@ const { Client, GatewayIntentBits, ChannelType } = require("discord.js"); const CommandManager = require('./CommandManager'); +const NotificationService = require('./NotificationService'); const fs = require('fs'); const path = require('path'); @@ -28,6 +29,9 @@ class Bot { // Setup event handlers this.setupEventHandlers(); + + // Initialize notification service + this.notificationService = null; } setupTempDirectory() { @@ -51,6 +55,15 @@ class Bot { // Only register global commands for direct messages await this.commandManager.registerGlobalCommands(); + // Initialize and start the notification service + this.notificationService = new NotificationService(this.client, { + checkInterval: process.env.STATUS_CHECK_INTERVAL ? parseInt(process.env.STATUS_CHECK_INTERVAL) : 60000, + statusEndpoint: process.env.STATUS_ENDPOINT || 'https://blahaj.tr:2589/status' + }); + + await this.notificationService.initialize(); + this.notificationService.start(); + // Send startup notification await this.sendStartupNotification(); }); @@ -95,6 +108,11 @@ class Bot { name: "Relative Time", value: ``, inline: true + }, + { + name: "Status Monitoring", + value: this.notificationService?.isRunning ? "✅ Active" : "❌ Inactive", + inline: true } ], footer: { @@ -108,7 +126,79 @@ class Bot { 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); + console.error("Failed to send startup notification to authorized user:", error.message); + console.log("This is not critical - the bot will still function normally"); + } + + // Also notify in status channel if configured + if (this.notificationService?.statusChannel) { + try { + await this.notificationService.statusChannel.send({ embeds: [startupEmbed] }); + console.log(`Sent startup notification to status channel: ${this.notificationService.statusChannel.name}`); + } catch (error) { + console.error("Failed to send startup notification to status channel:", error.message); + } + } + } + + async sendShutdownNotification(reason = "Manual shutdown", error = null) { + // Create shutdown embed + const shutdownEmbed = { + title: "blahaj.tr bot status update", + description: `Bot is shutting down at `, + color: 0xFF0000, + fields: [ + { + name: "Bot Name", + value: this.client.user.tag, + inline: true + }, + { + name: "Shutdown Reason", + value: reason || "Unknown", + inline: true + }, + { + name: "Relative Time", + value: ``, + inline: true + } + ], + footer: { + text: "blahaj.tr" + } + }; + + if (error) { + shutdownEmbed.fields.push({ + name: "Error Details", + value: `\`\`\`\n${error.message || String(error).substring(0, 1000)}\n\`\`\``, + inline: false + }); + } + + // Stop notification service if running + if (this.notificationService?.isRunning) { + 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); + } + + // Also notify in status channel if available + if (this.notificationService?.statusChannel) { + try { + await this.notificationService.statusChannel.send({ embeds: [shutdownEmbed] }); + console.log(`Sent shutdown notification to status channel: ${this.notificationService.statusChannel.name}`); + } catch (error) { + console.error("Failed to send shutdown notification to status channel:", error.message); + } } } @@ -117,6 +207,18 @@ class Bot { await this.client.login(process.env.DISCORD_TOKEN); return this; } + + async stop() { + // Stop notification service + if (this.notificationService) { + this.notificationService.stop(); + } + + // Destroy the client + if (this.client) { + this.client.destroy(); + } + } } module.exports = Bot; diff --git a/discord/classes/NotificationService.js b/discord/classes/NotificationService.js new file mode 100644 index 0000000..ca843fa --- /dev/null +++ b/discord/classes/NotificationService.js @@ -0,0 +1,267 @@ +const axios = require('axios'); + +class NotificationService { + constructor(client, options = {}) { + this.client = client; + this.authorizedUserId = process.env.AUTHORIZED_USER_ID; + this.statusChannel = null; + this.checkInterval = options.checkInterval || 60000; // Default: check every minute + this.statusEndpoint = options.statusEndpoint || 'https://blahaj.tr:2589/status'; + this.notificationChannelId = process.env.STATUS_NOTIFICATION_CHANNEL; + + // Store the previous status to compare for changes + this.previousStatus = { + servers: {}, + services: {} + }; + + // Track if this is the first check (to avoid notifications on startup) + this.isFirstCheck = true; + + // Indicate if the service is running + this.isRunning = false; + } + + async initialize() { + // Fetch the channel if a channel ID is provided + if (this.notificationChannelId) { + try { + this.statusChannel = await this.client.channels.fetch(this.notificationChannelId); + console.log(`Status notification channel set to: ${this.statusChannel.name}`); + } catch (error) { + console.error(`Failed to fetch status notification channel: ${error.message}`); + } + } + + // Do an initial check to populate the previous status + try { + const initialStatus = await this.fetchStatus(); + this.previousStatus = initialStatus; + console.log('Initial status check complete'); + } catch (error) { + console.error(`Initial status check failed: ${error.message}`); + } + } + + start() { + if (this.isRunning) return; + + console.log(`Starting status notification service (checking every ${this.checkInterval/1000} seconds)`); + this.isRunning = true; + this.checkTimer = setInterval(() => this.checkStatus(), this.checkInterval); + + // Run the first check + this.checkStatus(); + } + + stop() { + if (!this.isRunning) return; + + console.log('Stopping status notification service'); + clearInterval(this.checkTimer); + this.isRunning = false; + } + + async fetchStatus() { + try { + const response = await axios.get(this.statusEndpoint); + return response.data; + } catch (error) { + console.error(`Error fetching status: ${error.message}`); + throw error; + } + } + + async checkStatus() { + try { + const currentStatus = await this.fetchStatus(); + const changes = this.detectChanges(this.previousStatus, currentStatus); + + // If changes detected and not the first check, send notifications + if (changes.length > 0 && !this.isFirstCheck) { + await this.sendNotifications(changes, currentStatus); + } + + // Update previous status and set first check to false + this.previousStatus = currentStatus; + this.isFirstCheck = false; + } catch (error) { + console.error(`Status check failed: ${error.message}`); + } + } + + detectChanges(previousStatus, currentStatus) { + const changes = []; + + // Check for server status changes + if (previousStatus.servers && currentStatus.servers) { + for (const server in currentStatus.servers) { + // New server or status changed + if (!previousStatus.servers[server] || + previousStatus.servers[server].online !== currentStatus.servers[server].online) { + changes.push({ + type: 'server', + name: server, + status: currentStatus.servers[server].online ? 'online' : 'offline', + previous: previousStatus.servers[server]?.online ? 'online' : 'offline', + isNew: !previousStatus.servers[server] + }); + } + } + + // Check for removed servers + for (const server in previousStatus.servers) { + if (!currentStatus.servers[server]) { + changes.push({ + type: 'server', + name: server, + status: 'removed', + previous: previousStatus.servers[server].online ? 'online' : 'offline' + }); + } + } + } + + // Check for PM2 service status changes + if (previousStatus.services && currentStatus.services) { + for (const service in currentStatus.services) { + if (!previousStatus.services[service] || + previousStatus.services[service].status !== currentStatus.services[service].status) { + changes.push({ + type: 'service', + name: service, + status: currentStatus.services[service].status, + previous: previousStatus.services[service]?.status || 'unknown', + isNew: !previousStatus.services[service] + }); + } + } + + // Check for removed services + for (const service in previousStatus.services) { + if (!currentStatus.services[service]) { + changes.push({ + type: 'service', + name: service, + status: 'removed', + previous: previousStatus.services[service].status + }); + } + } + } + + return changes; + } + + async sendNotifications(changes, currentStatus) { + if (changes.length === 0) return; + + // Create an embed for the notification + const embed = { + title: 'Status Change Detected', + color: 0xFFAA00, // Amber color for notifications + timestamp: new Date(), + fields: [], + footer: { + text: 'blahaj.tr Status Monitor' + } + }; + + // Add fields for each change + changes.forEach(change => { + let fieldContent = ''; + + if (change.type === 'server') { + const statusEmoji = change.status === 'online' ? '🟢' : (change.status === 'offline' ? '🔴' : '⚪'); + const previousEmoji = change.previous === 'online' ? '🟢' : (change.previous === 'offline' ? '🔴' : '⚪'); + + if (change.isNew) { + fieldContent = `${statusEmoji} New server detected: **${change.status}**`; + } else if (change.status === 'removed') { + fieldContent = `⚪ Server removed (was ${previousEmoji} **${change.previous}**)`; + } else { + fieldContent = `${previousEmoji} **${change.previous}** → ${statusEmoji} **${change.status}**`; + } + } else if (change.type === 'service') { + let statusEmoji = '⚪'; + switch (change.status) { + case 'online': statusEmoji = '🟢'; break; + case 'stopping': statusEmoji = '🟠'; break; + case 'stopped': statusEmoji = '🔴'; break; + case 'errored': statusEmoji = '❌'; break; + case 'launching': statusEmoji = '🟡'; break; + } + + let previousEmoji = '⚪'; + switch (change.previous) { + case 'online': previousEmoji = '🟢'; break; + case 'stopping': previousEmoji = '🟠'; break; + case 'stopped': previousEmoji = '🔴'; break; + case 'errored': previousEmoji = '❌'; break; + case 'launching': previousEmoji = '🟡'; break; + } + + if (change.isNew) { + fieldContent = `${statusEmoji} New service detected: **${change.status}**`; + } else if (change.status === 'removed') { + fieldContent = `⚪ Service removed (was ${previousEmoji} **${change.previous}**)`; + } else { + fieldContent = `${previousEmoji} **${change.previous}** → ${statusEmoji} **${change.status}**`; + } + } + + embed.fields.push({ + name: `${change.type === 'server' ? 'Server' : 'Service'}: ${change.name}`, + value: fieldContent, + inline: false + }); + }); + + // Add a detailed status field if there are many services + if (Object.keys(currentStatus.services || {}).length > 0) { + let servicesStatus = ''; + for (const [name, info] of Object.entries(currentStatus.services)) { + let statusEmoji = '⚪'; + switch (info.status) { + case 'online': statusEmoji = '🟢'; break; + case 'stopping': statusEmoji = '🟠'; break; + case 'stopped': statusEmoji = '🔴'; break; + case 'errored': statusEmoji = '❌'; break; + case 'launching': statusEmoji = '🟡'; break; + } + servicesStatus += `${statusEmoji} **${name}**: ${info.status}\n`; + } + + if (servicesStatus) { + embed.fields.push({ + name: 'Current Services Status', + value: servicesStatus, + inline: false + }); + } + } + + // Send to channel if available + if (this.statusChannel) { + try { + await this.statusChannel.send({ embeds: [embed] }); + console.log('Status change notification sent to channel'); + } catch (error) { + console.error(`Failed to send status notification to channel: ${error.message}`); + } + } + + // Send to owner + if (this.authorizedUserId) { + try { + const owner = await this.client.users.fetch(this.authorizedUserId); + await owner.send({ embeds: [embed] }); + console.log('Status change notification sent to owner'); + } catch (error) { + console.error(`Failed to send status notification to owner: ${error.message}`); + } + } + } +} + +module.exports = NotificationService; diff --git a/index.js b/index.js index 3694afa..510db2c 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ require('dotenv').config(); // Import the Bot class const Bot = require('./discord/classes/Bot'); + // Global variables to hold our services let apiServer; let discordBot; @@ -32,10 +33,11 @@ async function startServices() { async function shutdown(signal) { console.log(`Received ${signal}. Shutting down gracefully...`); - // Shutdown Discord bot if it exists and has a shutdown method - if (discordBot && typeof discordBot.sendShutdownNotification === 'function') { + // Shutdown Discord bot if it exists + if (discordBot) { try { await discordBot.sendShutdownNotification(`Manual shutdown triggered by ${signal}`); + await discordBot.stop(); console.log('Discord bot shutdown complete'); } catch (error) { console.error('Error shutting down Discord bot:', error); @@ -55,7 +57,7 @@ process.on('SIGTERM', () => shutdown('SIGTERM')); // Catch uncaught exceptions process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); - if (discordBot && typeof discordBot.sendShutdownNotification === 'function') { + if (discordBot) { discordBot.sendShutdownNotification('Uncaught exception', error) .finally(() => process.exit(1)); } else {