revert v2

This commit is contained in:
Xargana 2025-04-17 17:11:04 +03:00
parent 5a474c5592
commit f2cc49124f
18 changed files with 5666 additions and 0 deletions

224
discord/classes/Bot.js Normal file
View file

@ -0,0 +1,224 @@
const { Client, GatewayIntentBits, ChannelType } = require("discord.js");
const CommandManager = require('./CommandManager');
const NotificationService = require('./NotificationService');
const fs = require('fs');
const path = require('path');
class Bot {
constructor() {
// Initialize client with minimal required intents
this.client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessageReactions,
GatewayIntentBits.DirectMessageTyping
],
partials: ['CHANNEL', 'MESSAGE']
});
// Initialize command manager
this.commandManager = new CommandManager(this.client);
// Authorized user ID - CHANGE THIS to your Discord user ID
this.authorizedUserId = process.env.AUTHORIZED_USER_ID;
// Setup temp directory
this.setupTempDirectory();
// Setup event handlers
this.setupEventHandlers();
// Initialize notification service
this.notificationService = null;
}
setupTempDirectory() {
const tempDir = path.join(__dirname, '../../temp');
if (fs.existsSync(tempDir)) {
console.log("Cleaning up temp directory...");
const files = fs.readdirSync(tempDir);
for (const file of files) {
fs.unlinkSync(path.join(tempDir, file));
}
} else {
fs.mkdirSync(tempDir, { recursive: true });
}
}
setupEventHandlers() {
// Ready event
this.client.once("ready", async () => {
console.log(`Logged in as ${this.client.user.tag}`);
// 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) : 5000,
statusEndpoint: process.env.STATUS_ENDPOINT || 'https://blahaj.tr:2589/status'
});
await this.notificationService.initialize();
this.notificationService.start();
// Send startup notification
await this.sendStartupNotification();
});
// Interaction event
this.client.on("interactionCreate", async (interaction) => {
// Only process commands if the user is authorized
if (interaction.user.id !== this.authorizedUserId) {
console.log(`Unauthorized access attempt by ${interaction.user.tag} (${interaction.user.id})`);
await interaction.reply({
content: "You are not authorized to use this bot.",
ephemeral: true
});
return;
}
console.log(`Authorized command: ${interaction.commandName} from ${interaction.user.tag}`);
// Handle the interaction
await this.commandManager.handleInteraction(interaction);
});
// Error handling
process.on('unhandledRejection', error => {
console.error('Unhandled promise rejection:', error);
});
}
async sendStartupNotification() {
// Create startup embed
const startupEmbed = {
title: "blahaj.tr bot status update",
description: `Bot started successfully at <t:${Math.floor(Date.now() / 1000)}:F>`,
color: 0x00ff00,
fields: [
{
name: "Bot Name",
value: this.client.user.tag,
inline: true
},
{
name: "Relative Time",
value: `<t:${Math.floor(Date.now() / 1000)}:R>`,
inline: true
},
{
name: "Status Monitoring",
value: this.notificationService?.isRunning ? "✅ Active" : "❌ Inactive",
inline: true
}
],
footer: {
text: "blahaj.tr"
}
};
// 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");
}
// 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 <t:${Math.floor(Date.now() / 1000)}:F>`,
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: `<t:${Math.floor(Date.now() / 1000)}:R>`,
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);
}
}
}
async start() {
// Login to Discord
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;

View file

@ -0,0 +1,82 @@
const { SlashCommandBuilder } = require('discord.js');
class CommandBase {
constructor(client) {
this.client = client;
this.name = '';
this.description = '';
this.options = [];
}
/**
* Execute the command
* @param {Interaction} interaction - The interaction object
*/
async execute(interaction) {
throw new Error('Method not implemented');
}
/**
* Defer the reply to the interaction
* @param {Interaction} interaction - The interaction object
* @param {boolean} ephemeral - Whether the reply should be ephemeral
*/
async deferReply(interaction, ephemeral = false) {
if (!interaction.deferred && !interaction.replied) {
await interaction.deferReply({ ephemeral });
}
}
/**
* Send a response to the interaction
* @param {Interaction} interaction - The interaction object
* @param {Object} options - The response options
* @param {boolean} ephemeral - Whether the response should be ephemeral
*/
async sendResponse(interaction, options, ephemeral = false) {
if (interaction.deferred || interaction.replied) {
await interaction.editReply(options);
} else {
options.ephemeral = ephemeral;
await interaction.reply(options);
}
}
/**
* Send an error response to the interaction
* @param {Interaction} interaction - The interaction object
* @param {string} message - The error message
*/
async sendErrorResponse(interaction, message) {
const errorEmbed = {
title: "Error",
description: message,
color: 0xFF0000,
timestamp: new Date()
};
if (interaction.deferred || interaction.replied) {
await interaction.editReply({ embeds: [errorEmbed] });
} else {
await interaction.reply({ embeds: [errorEmbed], ephemeral: true });
}
}
/**
* Convert the command to JSON for registration
*/
toJSON() {
const builder = new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description);
// Add options if defined in the child class
if (typeof this.addOptions === 'function') {
this.addOptions(builder);
}
return builder.toJSON();
}
}
module.exports = CommandBase;

View file

@ -0,0 +1,107 @@
const { Collection, REST, Routes } = require('discord.js');
const fs = require('fs');
const path = require('path');
class CommandManager {
constructor(client) {
this.client = client;
this.commands = new Collection();
this.commandFolders = ['info', 'system']; // Only include info and system commands
this.rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
this.authorizedUserId = process.env.AUTHORIZED_USER_ID;
}
async loadCommands() {
const commandsPath = path.join(__dirname, '../commands');
// Only load commands from allowed folders
for (const folder of this.commandFolders) {
const folderPath = path.join(commandsPath, folder);
// Skip if folder doesn't exist
if (!fs.existsSync(folderPath)) continue;
const commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(folderPath, file);
const CommandClass = require(filePath);
const command = new CommandClass(this.client);
// Add authorization check to command
const originalExecute = command.execute;
command.execute = async function(interaction) {
if (interaction.user.id !== process.env.AUTHORIZED_USER_ID) {
return interaction.reply({
content: "You are not authorized to use this command.",
ephemeral: true
});
}
return originalExecute.call(this, interaction);
};
this.commands.set(command.name, command);
console.log(`Loaded command: ${command.name}`);
}
}
}
async registerGlobalCommands() {
try {
await this.loadCommands();
if (this.commands.size === 0) {
console.log("No commands to register.");
return;
}
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(
Routes.applicationCommands(this.client.user.id),
{ body: commandsData },
);
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
console.error(error);
}
}
async handleInteraction(interaction) {
if (!interaction.isChatInputCommand()) return;
// Double-check authorization
if (interaction.user.id !== this.authorizedUserId) {
return interaction.reply({
content: "You are not authorized to use this command.",
ephemeral: true
});
}
const command = this.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction);
} catch (error) {
console.error(`Error executing command ${interaction.commandName}:`, error);
const errorMessage = {
content: "There was an error while executing this command!",
ephemeral: true
};
if (interaction.replied || interaction.deferred) {
await interaction.followUp(errorMessage);
} else {
await interaction.reply(errorMessage);
}
}
}
}
module.exports = CommandManager;

View file

@ -0,0 +1,279 @@
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 || 10000; // Changed to 10 seconds default
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: {},
pm2Services: {} // Changed from services to pm2Services to match API response
};
// 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],
responseTime: currentStatus.servers[server].responseTime
});
}
}
// 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 - updated to use pm2Services
if (previousStatus.pm2Services && currentStatus.pm2Services) {
for (const service in currentStatus.pm2Services) {
if (!previousStatus.pm2Services[service] ||
previousStatus.pm2Services[service].status !== currentStatus.pm2Services[service].status) {
changes.push({
type: 'service',
name: service,
status: currentStatus.pm2Services[service].status,
previous: previousStatus.pm2Services[service]?.status || 'unknown',
isNew: !previousStatus.pm2Services[service],
details: currentStatus.pm2Services[service]
});
}
}
// Check for removed services
for (const service in previousStatus.pm2Services) {
if (!currentStatus.pm2Services[service]) {
changes.push({
type: 'service',
name: service,
status: 'removed',
previous: previousStatus.pm2Services[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}**`;
if (change.responseTime !== 'unknown') {
fieldContent += `\nResponse time: ${change.responseTime}ms`;
}
}
} 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}**`;
// Add resource usage if available
if (change.details) {
const memory = change.details.memory ? Math.round(change.details.memory / (1024 * 1024) * 10) / 10 : 0;
fieldContent += `\nCPU: ${change.details.cpu}% | Memory: ${memory}MB`;
fieldContent += `\nUptime: ${Math.floor(change.details.uptime / 1000)}s | Restarts: ${change.details.restarts}`;
}
}
}
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.pm2Services || {}).length > 0) {
let servicesStatus = '';
for (const [name, info] of Object.entries(currentStatus.pm2Services)) {
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;

View file

@ -0,0 +1,40 @@
const CommandBase = require('./CommandBase');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
class SystemCommandBase extends CommandBase {
constructor(client) {
super(client);
// Add security check for all system commands
const originalExecute = this.execute;
this.execute = async function(interaction) {
if (interaction.user.id !== process.env.AUTHORIZED_USER_ID) {
return interaction.reply({
content: "You are not authorized to use system commands.",
ephemeral: true
});
}
return originalExecute.call(this, interaction);
};
}
async execCommand(command, options = {}) {
try {
const { stdout, stderr } = await execPromise(command, options);
return { success: true, stdout, stderr };
} catch (error) {
return {
success: false,
error: error.message,
stdout: error.stdout,
stderr: error.stderr
};
}
}
}
module.exports = SystemCommandBase;