From 68f88f053e5c3665e826961f3f3b8ef649108437 Mon Sep 17 00:00:00 2001 From: ihasTaco <77412945+ihasTaco@users.noreply.github.com> Date: Tue, 1 Aug 2023 19:16:41 -0600 Subject: [PATCH 1/4] Fixed some issues, and added logging I believe I have fixed a couple of issues: * Possible Fix for [issue #9](https://github.com/ihasTaco/ServerQuery/issues/9) * Possible Fix for [issue #1](https://github.com/ihasTaco/ServerQuery/issues/1) Added a new feature * Added a logging system --- backend/api/get/localRoutes.js | 2 +- backend/api/post/postRoutes.js | 8 +-- discord/api/index.js | 8 ++- discord/bot.js | 86 +++++++++++------------------ discord/embeds/index.js | 5 +- discord/graphs/graph_area.js | 12 ++--- discord/graphs/index.js | 59 +++++++++++++------- discord/index.js | 16 ++++-- discord/query/index.js | 32 +++++------ discord/query/packageData.js | 32 +++++------ discord/utils/logger.js | 99 ++++++++++++++++++++++++++++++++++ 11 files changed, 226 insertions(+), 133 deletions(-) create mode 100644 discord/utils/logger.js diff --git a/backend/api/get/localRoutes.js b/backend/api/get/localRoutes.js index e36038f..5ee4046 100644 --- a/backend/api/get/localRoutes.js +++ b/backend/api/get/localRoutes.js @@ -7,7 +7,7 @@ require('dotenv').config({ path: './.env' }); const router = express.Router(); -const defaultSettings = require('./public/default_server.json'); +const defaultSettings = require('../../public/default_server.json'); router.get('/authenticatedToken', async (req, res) => { const jwtToken = req.cookies.Authenticated; diff --git a/backend/api/post/postRoutes.js b/backend/api/post/postRoutes.js index cb80136..7291920 100644 --- a/backend/api/post/postRoutes.js +++ b/backend/api/post/postRoutes.js @@ -226,22 +226,22 @@ router.delete('/delete-server', async (req, res) => { try { // Read and parse server_info.json - let serverInfoData = JSON.parse(await fs.readFile('./public/server_info.json', 'utf8')); + let serverInfoData = JSON.parse(await fs.promises.readFile('./public/server_info.json', 'utf8')); // Delete server_uuid if (serverInfoData[guild_id]) { delete serverInfoData[guild_id][server_uuid]; } // Write the modified data back to server_info.json - await fs.writeFile('./public/server_info.json', JSON.stringify(serverInfoData, null, 2)); + await fs.promises.writeFile('./public/server_info.json', JSON.stringify(serverInfoData, null, 2)); // Read and parse servers.json - let serversData = JSON.parse(await fs.readFile('./public/servers.json', 'utf8')); + let serversData = JSON.parse(await fs.promises.readFile('./public/servers.json', 'utf8')); // Delete server_uuid if (serversData[guild_id]) { delete serversData[guild_id][server_uuid]; } // Write the modified data back to servers.json - await fs.writeFile('./public/servers.json', JSON.stringify(serversData, null, 2)); + await fs.promises.writeFile('./public/servers.json', JSON.stringify(serversData, null, 2)); res.json({ message: 'Server UUID deleted successfully.' }); } catch (err) { diff --git a/discord/api/index.js b/discord/api/index.js index 71f1acf..9ef13ee 100644 --- a/discord/api/index.js +++ b/discord/api/index.js @@ -1,7 +1,9 @@ const axios = require('axios'); +const logger = require('../utils/logger'); require('dotenv').config({ path: './.env' }); async function getGuilds(page = 0, pageSize = 10) { + logger.debug(`Fetching guilds with page: ${page} and pageSize: ${pageSize}`); const guilds = await axios.get(`${process.env.BACKEND_URL}api/get/bot/guilds`, { params: { page: page, @@ -12,29 +14,33 @@ async function getGuilds(page = 0, pageSize = 10) { } async function getServerUUIDsForGuild(guild_id) { + logger.debug(`Fetching server UUIDs for guild: ${guild_id}`); const servers = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guild_id}/servers`, {}); return servers; } async function getServerDetails(guild_id, server_uuid) { + logger.debug(`Fetching server details for guild: ${guild_id} and server UUID: ${server_uuid}`); const details = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guild_id}/server/${server_uuid}`, {}); return details; } async function getServerInfo(guild_id, server_uuid) { + logger.debug(`Fetching server info for guild: ${guild_id} and server UUID: ${server_uuid}`); const serverInfo = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guild_id}/serverInfo/${server_uuid}`, {}); return serverInfo; } async function writeServerInfo(guild_id, server_uuid, server_info) { try { + logger.debug(`Writing server info for guild: ${guild_id} and server UUID: ${server_uuid}`); const response = await axios.post(`${process.env.BACKEND_URL}api/post/write-server-info`, { guild_id, server_uuid, server_info, }); } catch (error) { - console.error('Error: ', error); + logger.error(`Error writing server info for guild: ${guild_id} and server UUID: ${server_uuid}, error: ${error}`); } } diff --git a/discord/bot.js b/discord/bot.js index 5fa6628..27517ee 100644 --- a/discord/bot.js +++ b/discord/bot.js @@ -5,6 +5,7 @@ const queryServer = require('./query'); const createGraph = require('./graphs'); const { createEmbed } = require('./embeds'); const async = require('async'); +const logger = require('./utils/logger'); const client = new Client({ intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.Guilds] }); @@ -13,18 +14,18 @@ const serverQueue = async.queue((task, callback) => { }, 1); // '1' is the number of simultaneous tasks allowed. client.on('ready', async () => { - console.log(`Shard ${client.shard.ids[0]} is ready`); + logger.info(`Shard ${client.shard.ids[0]} is ready`); manageServers(); setInterval(manageServers, 30 * 1000); }); async function manageServers() { - console.log('Getting New Servers') + logger.debug('Getting New Servers') let response = await getGuilds(); let guild_info = response.data; const managedGuilds = client.guilds.cache.filter(guild => guild_info[guild.id]); - console.log(`Shard ${client.shard.ids[0]} is managing ${managedGuilds.size} guild(s)`); + logger.info(`Shard ${client.shard.ids[0]} is managing ${managedGuilds.size} guild(s)`); for (const guild of managedGuilds.values()) { const server_uuids = await getServerUUIDsForGuild(guild.id); @@ -35,13 +36,13 @@ async function manageServers() { // Check if the server has been deleted if (!server_settings) { - console.log(`Server with UUID ${server} in guild ${guild.id} has been deleted.`); + logger.warn(`Server with UUID ${server} in guild ${guild.id} has been deleted.`); continue; // Skip to the next server } // Convert refresh interval to milliseconds - //let interval = server_settings.bot_settings.refresh_interval * 1000; - let interval = 30 * 1000; + let interval = server_settings.bot_settings.refresh_interval * 1000; + //let interval = 30 * 1000; // Start an independent interval for each server startInterval(guild.id, server, server_settings, interval); @@ -60,64 +61,48 @@ async function startInterval(guild_id, server, server_settings, interval) { } async function handleServer(guild_id, server_uuid, server_settings) { - console.log('handleServer') + logger.debug('handleServer'); try { // Get the guild object const guild = client.guilds.cache.get(guild_id); - console.log('getting Guild with Guild ID: ', guild_id) - - console.log('Checking If Guild Exists...') + logger.debug(`Getting Guild with Guild ID: ${guild_id}`); // Check if the guild exists if (!guild) { - console.log(`Guild ${guild_id} not found`); + logger.warn(`Guild ${guild_id} not found`); return; } - console.log('Guild Exist\'s, moving on!') - // Get the channel object const channel = guild.channels.cache.get(server_settings.bot_settings.channel_id); - console.log('getting Channel with Channel ID: ', server_settings.bot_settings.channel_id) + logger.debug(`Getting Channel with Channel ID: ${server_settings.bot_settings.channel_id}`); - console.log('Checking If Channel Exists...') // Check if the channel exists if (!channel) { - console.log(`Channel with ID ${server_settings.bot_settings.channel_id} not found in guild ${guild_id}`); + logger.warn(`Channel with ID ${server_settings.bot_settings.channel_id} not found in guild ${guild_id}`); return; } - console.log('Channel Exist\'s, moving on!') - - console.log('Querying Game Server...') // Query the server const { server_info: server_query } = await queryServer(server_settings.server_settings.ip, server_settings.server_settings.query_port,server_settings.server_settings.query_protocol, guild_id, server_uuid); - - console.log('Finished Querying Game Server!') - + + let graph_url; // Create the Player Graph if (!server_settings.graph_settings.disable) { - console.log('Creating Player Graph!') graph_url = await createGraph(guild_id, server_uuid); - console.log('Done! Graph can be found here: ', graph_url) + logger.debug(`Done! Graph can be found here`); + logger.debug(`${graph_url}`); } - console.log('Attempting to build and send the discord embed') - let message; if (server_query.message_id === null) { - console.log('message_id is null, attempting to send a new one') - const [embed, attachment] = await createEmbed(server_settings, graph_url, server_query); - message = await channel.send({ embeds: [embed], files: [attachment] }).catch(console.error); + message = await channel.send({ embeds: [embed], files: [attachment] }).catch(logger.error); - console.log('New Message Sent! New message_id is: ', message) server_query.message_id = message.id; - console.log('Attempting to write new message id to Server Info JSON file!') await writeServerInfo(guild_id, server_uuid, server_query); - console.log('New message ID has been written to Server Info JSON file!') } else { let message = null; let attempts = 0; @@ -127,39 +112,37 @@ async function handleServer(guild_id, server_uuid, server_settings) { try { message = await channel.messages.fetch(server_query.message_id); const [embed, attachment] = await createEmbed(server_settings, graph_url, server_query); - await message.edit({ embeds: [embed], files: [attachment] }).catch(console.error); + await message.edit({ embeds: [embed], files: [attachment] }).catch(logger.error); } catch (err) { - console.error(`Error fetching message: ${err}`); + logger.error(`Error fetching message: ${err}`); if (err instanceof DiscordAPIError) { - console.log(err.code) if (err.code === 10008) { // 'Unknown Message' error - console.error('Message not found, it might have been deleted.'); + logger.error('Message not found, it might have been deleted.'); shouldSendNewMessage = true; break; } else if (err.code === 50001) { // 'Missing Access' error - console.error('Bot does not have access to the channel.'); + logger.error('Bot does not have access to the channel.'); break; } else if (err.code === 50013) { // 'Missing Permissions' error - console.error('Bot does not have permission to read the message.'); + logger.error('Bot does not have permission to read the message.'); break; } - // Add more error codes as needed based on your requirements } // If it's a RateLimitError, you can wait for the duration specified by the 'retry_after' property before trying again if (err instanceof RateLimitError) { - await new Promise(resolve => setTimeout(resolve, err.retry_after * 1000)); // 'retry_after' is in seconds - continue; // Try again + await new Promise(resolve => setTimeout(resolve, err.retry_after * 1000)); + continue; } // If it's a HTTPError (like a network error), you can wait for a bit before trying again if (err instanceof HTTPError) { - await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for 1 second - continue; // Try again + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; } // If it's some other type of error, you might not know how to handle it, so just log it and exit the loop - console.error('Unknown error type, not retrying.'); + logger.error('Unknown error type, not retrying.'); break; } @@ -167,26 +150,19 @@ async function handleServer(guild_id, server_uuid, server_settings) { } if (!message && shouldSendNewMessage) { - console.log('the message is null, sending a new message') - console.log('Creating embed...'); const [embed, attachment] = await createEmbed(server_settings, graph_url, server_query); - console.log('Embed created.'); - console.log('Sending message...'); - message = await channel.send({ embeds: [embed], files: [attachment] }).catch(console.error); - console.log('Message sent.'); + message = await channel.send({ embeds: [embed], files: [attachment] }).catch(logger.error); - console.log('Attempting to write new message id to Server Info JSON file!') server_query.message_id = message.id; await writeServerInfo(guild_id, server_uuid, server_query); - console.log('New message ID has been written to Server Info JSON file!') } } } catch (err) { - console.error(`Error handling server: ${err}`); + logger.error(`Error handling server: ${err}`); } } -client.on('error', console.error); +client.on('error', logger.error); -client.login(process.env.BOT_TOKEN); +client.login(process.env.BOT_TOKEN); \ No newline at end of file diff --git a/discord/embeds/index.js b/discord/embeds/index.js index b59abc6..7fd2766 100644 --- a/discord/embeds/index.js +++ b/discord/embeds/index.js @@ -2,6 +2,7 @@ require('dotenv').config(); const axios = require('axios'); const { EmbedBuilder } = require('discord.js'); const { AttachmentBuilder } = require('discord.js'); +const logger = require('../utils/logger'); function replaceVariables(str, server_settings, server_query) { if (str) { @@ -26,6 +27,7 @@ function replaceVariables(str, server_settings, server_query) { } async function createEmbed(server_settings, graph_url, server_query) { + logger.debug('Creating embed...'); let embed = new EmbedBuilder() .setTitle(replaceVariables(server_settings.embed_settings.embed_title, server_settings, server_query)) .setDescription(replaceVariables(server_settings.embed_settings.embed_description, server_settings, server_query)) @@ -80,10 +82,11 @@ async function createEmbed(server_settings, graph_url, server_query) { } } - // Image + logger.debug('Adding image to embed...'); const attachment = new AttachmentBuilder(graph_url, { name: 'chart.png' }); embed.setImage('attachment://chart.png'); + logger.debug('Embed created.'); return [embed, attachment]; } diff --git a/discord/graphs/graph_area.js b/discord/graphs/graph_area.js index a88c6d1..e1ada18 100644 --- a/discord/graphs/graph_area.js +++ b/discord/graphs/graph_area.js @@ -1,3 +1,5 @@ +const logger = require('../utils/logger'); + function hexToRgba(hex, opacity) { let r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), @@ -7,14 +9,9 @@ function hexToRgba(hex, opacity) { } function graph_area(graphSettings, players, trend_line, labels) { - //console.log(`\n-- Configuring Area Graph --`) - //console.log(`Graph Settings: `, graphSettings) - //console.log(`Players: `, players) - //console.log(`Trend Line: `, trend_line) - //console.log(`Labels: `, labels) - - let playersDataForBar = []; + logger.debug('Configuring graph area'); + let playersDataForBar = []; let maxPlayerValue; if (Math.max(...players) == 0) { @@ -133,6 +130,7 @@ function graph_area(graphSettings, players, trend_line, labels) { } } }; + logger.debug('Graph area configured'); return configuration; } diff --git a/discord/graphs/index.js b/discord/graphs/index.js index 3e2037e..08bd4a4 100644 --- a/discord/graphs/index.js +++ b/discord/graphs/index.js @@ -2,6 +2,7 @@ const { ChartJSNodeCanvas } = require('chartjs-node-canvas'); const { sma } = require('moving-averages'); const path = require('path'); const fs = require('fs'); +const logger = require('../utils/logger'); const { getServerDetails, getServerInfo } = require('../api'); @@ -9,25 +10,31 @@ const { getServerDetails, getServerInfo } = require('../api'); const graph_area = require('./graph_area'); async function getServerPlayers(guild_id, server_uuid) { + logger.debug(`Getting server players for guild: ${guild_id}, server: ${server_uuid}`); return getServerInfo(guild_id, server_uuid) .then(response => { + logger.debug(`Server players retrieved`); return response.data.players; }) - .catch(console.error); + .catch(error => { + logger.error(`Error getting server players: ${error}`); + }); } + async function getServerData(guild_id, server_uuid) { + logger.debug(`Getting server data for guild: ${guild_id}, server: ${server_uuid}`); return getServerDetails(guild_id, server_uuid) .then(response => { + logger.debug(`Server data retrieved`); return response.data; }) - .catch(console.error); + .catch(error => { + logger.error(`Error getting server data: ${error}`); + }); } async function createGraph(guild_id, server_uuid) { - //console.log(`\n-- Create Graph --`) - //console.log(`Guild ID: ${guild_id}`) - //console.log(`Server UUID: ${server_uuid}`) - + logger.debug(`Creating graph for guild: ${guild_id}, server: ${server_uuid}`); Array.prototype.sma = require('moving-averages').sma; const width = 700; const height = 500; @@ -35,35 +42,35 @@ async function createGraph(guild_id, server_uuid) { // Set up Player Data, Server Data, and Player Trends Data let player_data = await getServerPlayers(guild_id, server_uuid); - //console.log(`Player Data: `, player_data) - const server_data = await getServerData(guild_id, server_uuid); - //console.log(`Server Data: `, server_data) - // Gets the entries per week based on the refresh_interval - // with a refresh_interval of 15sec that would be about 40320 points of data const secondsInDay = 24 * 60 * 60; let entriesPerDay = Math.round(secondsInDay / server_data.bot_settings.refresh_interval); + let players; let trend_data; if (Array.isArray(player_data)) { players = player_data.slice(-entriesPerDay); trend_data = player_data.slice(0, -entriesPerDay); - // rest of your code } else { - console.log('player_data is not an array'); + logger.warn('player_data is not an array'); } let players_trend = sma(trend_data, 60 / server_data.bot_settings.refresh_interval); if (players != entriesPerDay) { + logger.debug(`entriesPerDay: ${entriesPerDay}, players.length: ${players.length}`); players = Array(entriesPerDay - players.length).fill(0).concat(players); } - + if (trend_data != entriesPerDay) { + logger.debug(`entriesPerDay: ${entriesPerDay}, trend_data.length: ${trend_data.length}`); + trend_data = Array(entriesPerDay - trend_data.length).fill(0).concat(trend_data); + } + // LABELS // // This makes labels for every hour of the day the latest (right) value being the current time // Will add a customization in the dashboard so the user can select their timezone, for now though, we will use UTC - let userTimezone = "America/Denver"; // Default Option: UTC + let userTimezone = "UTC"; // Default Option: UTC let use12Format = true; // Get the current time in the user's timezone @@ -72,7 +79,7 @@ async function createGraph(guild_id, server_uuid) { hour: '2-digit', minute: '2-digit', second: '2-digit', - hour12: false, // Keep this false, as if set to true 12 hour format wont work... + hour12: false, // Keep this false, if set to true 12 hour format wont work... timeZone: userTimezone }); let timeParts = formatter.format(date).split(':'); @@ -118,12 +125,24 @@ async function createGraph(guild_id, server_uuid) { let outputPath = path.join(process.cwd(), `./images/${guild_id}.${server_uuid}.png`); return new Promise((resolve, reject) => { fs.writeFile(outputPath, buffer, (err) => { - if (err) reject(err); - else resolve(outputPath); + if (err) { + logger.error(`Error writing graph to file: ${err}`); + reject(err); + } else { + logger.debug(`Graph written to file`); + resolve(outputPath); + } }); }); }) - .catch(console.error); + .catch(error => { + logger.error(`Error creating graph: ${error}`); + }); } -module.exports = createGraph; \ No newline at end of file +module.exports = createGraph; + + + + + diff --git a/discord/index.js b/discord/index.js index acd87bb..58bef25 100644 --- a/discord/index.js +++ b/discord/index.js @@ -2,25 +2,31 @@ const { ShardingManager } = require('discord.js'); const path = require('path'); const { getGuilds } = require('./api'); require('dotenv').config({ path: '../.env' }); +const logger = require('./utils/logger'); process.on('unhandledRejection', (error) => { - console.error('Unhandled promise rejection:', error); + logger.error('Unhandled promise rejection:', error); }); process.on('error', (error) => { - console.error('Unhandled error:', error); + logger.error('Unhandled error:', error); }); async function setup() { + logger.info('Starting Server Query'); + logger.info('Version: v1.0.0-alpha.2'); // Guild threshold before sharding const guildThreshold = 2500; + logger.debug(`Guild Threshold: ${guildThreshold}`); let guilds = await getGuilds(); let numGuilds = Object.keys(guilds.data).length; + logger.debug(`Guilds : ${numGuilds}`); // Calculate the number of shards we need let numShards = Math.ceil(numGuilds / guildThreshold); + logger.debug(`Number of Shards: ${numShards}`); // Start the sharding manager const manager = new ShardingManager(path.join(__dirname, 'bot.js'), { @@ -28,11 +34,13 @@ async function setup() { totalShards: numShards }); + logger.info('Starting the Sharding Manager'); + // Spawn shards manager.spawn(); // Log when a shard is created - manager.on('shardCreate', shard => console.log(`Shard ${shard.id} created`)); + manager.on('shardCreate', shard => logger.info(`Shard ${shard.id} created`)); } -setup(); \ No newline at end of file +setup(); diff --git a/discord/query/index.js b/discord/query/index.js index dfac373..cd1c097 100644 --- a/discord/query/index.js +++ b/discord/query/index.js @@ -1,52 +1,44 @@ const Gamedig = require('gamedig'); const packageData = require('./packageData'); +const logger = require('../utils/logger'); async function queryServer(ip, query_port, query_protocol, guild_id, server_uuid) { - //console.log(`\n-- Query --`) - //console.log(`Guild ID: ${guild_id}`) - //console.log(`Server UUID: ${server_uuid}`) - //console.log(`IP: ${ip}`) - //console.log(`Query Port: ${query_port}`) - //console.log(`Query Protocol: ${query_protocol}`) + logger.debug('Starting server query...'); - console.log('queryServer') - - console.log('Attempting to query server') const maxRetries = 5; let retries = 0; let state; while (retries < maxRetries) { - console.log('trying to query server. Retries: ', retries) + logger.debug(`Attempting to query server. Attempt: ${retries + 1}`); try { - console.log(`Trying to Query Game Server: ${server_uuid}`) + logger.debug(`Querying game server with UUID: ${server_uuid}`); state = await Gamedig.query({ type: query_protocol, host: ip, port: query_port }); - console.log(`Finished Querying Game Server: ${server_uuid}`) + logger.debug(`Successfully queried game server with UUID: ${server_uuid}`); break; } catch (error) { - console.log(`Failed to Query Game Server: ${server_uuid}`) - //console.error('Error querying server:', error); + logger.warn(`Failed to query game server with UUID: ${server_uuid}. Error: ${error}`); retries++; if (retries < maxRetries) { - console.log(`Retrying query (${retries}/${maxRetries})...`); + logger.warn(`Retrying query (${retries}/${maxRetries})...`); } } } - console.log('Finished querying game server!') + logger.debug('Finished querying game server.'); if (!state) { - console.log('Server query failed after maximum retries. Treating server as offline.'); + logger.warn('Server query failed after maximum retries. Treating server as offline.'); state = undefined; } - console.log('Sending State to packageData') + logger.debug('Packaging data...'); const packagedData = await packageData(state, guild_id, server_uuid); - console.log('Sending packagedData to handleServer') + logger.debug('Data packaged successfully. Sending data to handleServer.'); return packagedData; } -module.exports = queryServer; \ No newline at end of file +module.exports = queryServer; diff --git a/discord/query/packageData.js b/discord/query/packageData.js index 1f6b1c8..2053ddc 100644 --- a/discord/query/packageData.js +++ b/discord/query/packageData.js @@ -1,27 +1,23 @@ const { writeServerInfo, getServerInfo } = require('../api/index'); +const logger = require('../utils/logger'); async function packageData(state, guild_id, server_uuid) { - //console.log(`\n-- Packaging Data --`) - //console.log(`Guild ID: ${guild_id}`) - //console.log(`Server UUID: ${server_uuid}`) - //console.log(`State: `, state) - - console.log('packageData') + logger.debug('Packaging data...'); let server_info = {}; let info = {}; let did_restart = ''; let map = ''; - console.log('Getting server info') + logger.debug('Getting server info...'); await getServerInfo(guild_id, server_uuid) .then(response => { info = response.data; - console.log('Got server info') + logger.debug('Got server info.'); }) .catch(error => { if (error.response && error.response.status === 404) { - console.log('Server info not found, initializing new server info'); + logger.warn('Server info not found, initializing new server info.'); info = { map: null, players: [], @@ -31,26 +27,24 @@ async function packageData(state, guild_id, server_uuid) { message_id: null, }; } else { - console.error('Error getting server info:', error); + logger.error(`Error getting server info: ${error}`); throw error; } }); - //console.log(`API Server Info: `, info) - console.log('Setting Server Info details') - + logger.debug('Setting server info details...'); if (!info.status && state) { did_restart = new Date().toISOString(); } else { did_restart = info.last_restart; } - if (!state) { // server is offline + if (!state) { if (info.map) { map = info.map; } else { map = 'Unavailable'; } - } else { // server is online + } else { map = state.map; } @@ -80,12 +74,10 @@ async function packageData(state, guild_id, server_uuid) { }; } - //console.log(`Generated Server Info: `, server_info) - console.log('Writing Server Info to Server Info JSON...') + logger.debug('Writing server info to server info JSON...'); await writeServerInfo(guild_id, server_uuid, server_info); - console.log('Finished writing Server Info to Server Info JSON!') - - console.log('Sending back server_info to queryServer') + logger.debug('Finished writing server info to server info JSON.'); + logger.debug('Packaged data successfully. Sending data back to queryServer.'); return { server_info }; } diff --git a/discord/utils/logger.js b/discord/utils/logger.js new file mode 100644 index 0000000..c9cfd85 --- /dev/null +++ b/discord/utils/logger.js @@ -0,0 +1,99 @@ +require('dotenv').config({ path: '../.env' }); +const fs = require('fs'); +const path = require('path'); +const schedule = require('node-schedule'); +const chalk = require('chalk'); + +const logDir = path.join(`${__dirname}/../`, 'logs'); +const logFile = path.join(logDir, '_latest.log'); + +// Ensure logs directory exists +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir); +} + +// Ensure _latest.txt exists +if (!fs.existsSync(logFile)) { + fs.writeFileSync(logFile, ''); +} + +// Debug log function +function debug(...messages) { + writeLog('DEBUG', messages, chalk.green); +} + +// Info log function +function info(...messages) { + writeLog('INFO ', messages, chalk.white); +} + +// Warning log function +function warn(...messages) { + writeLog('WARN ', messages, chalk.yellow); +} + +// Error log function +function error(...messages) { + writeLog('ERROR', messages, chalk.red); +} + +// Fatal log function +function fatal(...messages) { + writeLog('FATAL', messages, chalk.magenta); +} + +// Function to write a log message with a given type +function writeLog(type, messages, color) { + let date = new Date(); + let timestamp = chalk.cyan(`[ ${date.toISOString()} ]`); + let coloredType = color(`[ ${type} ]`); + let logMessage = messages.map(message => { + if (typeof message === 'object') { + const cache = new Set(); + return JSON.stringify(message, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (cache.has(value)) { + // Duplicate reference found, discard key + return; + } + // Store value in our collection + cache.add(value); + } + return value; + }, 2); + } else { + return message; + } + }).join(' '); + + if (type != 'DEBUG') { + let consoleLogMessage = `${timestamp} | ${coloredType} | ${chalk.white(logMessage)}`; + console.log(consoleLogMessage); + } else if (process.env.USE_DEBUG === 'true' && type === 'DEBUG') { + let consoleLogMessage = `${timestamp} | ${coloredType} | ${chalk.white(logMessage)}`; + console.log(consoleLogMessage); + } + + fs.appendFileSync(logFile, `[ ${date.toISOString()} ] | [ ${type} ] | ${logMessage}\n`); +} + +// Schedule job to rotate logs +schedule.scheduleJob('0 0 * * *', function(){ + let date = new Date(); + let filename = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.txt`; + let newFilePath = path.join(logDir, filename); + + // Rename _latest.txt to the new file + fs.renameSync(logFile, newFilePath); + + // Create a new _latest.txt for the next day's logs + fs.writeFileSync(logFile, ''); +}); + +module.exports = { + debug, + info, + warn, + error, + fatal +}; \ No newline at end of file From 0ded89fe3c7f1a43bb4a4a2db7ac9d55d69bd533 Mon Sep 17 00:00:00 2001 From: ihasTaco <77412945+ihasTaco@users.noreply.github.com> Date: Tue, 1 Aug 2023 20:05:58 -0600 Subject: [PATCH 2/4] Graph Changes I've added a fix for the graph height issue for issue #12 Changed how Trend data is calculated, the trend data will now include the first days worth of data --- discord/graphs/graph_area.js | 24 +++++++++++++++++++----- discord/graphs/index.js | 19 ++++++++----------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/discord/graphs/graph_area.js b/discord/graphs/graph_area.js index e1ada18..cbf43cb 100644 --- a/discord/graphs/graph_area.js +++ b/discord/graphs/graph_area.js @@ -12,17 +12,31 @@ function graph_area(graphSettings, players, trend_line, labels) { logger.debug('Configuring graph area'); let playersDataForBar = []; - let maxPlayerValue; - if (Math.max(...players) == 0) { + // Calculate max value for players and trend_line + let maxPlayerValue = Math.max(...players); + let maxTrendValue = Math.max(...trend_line); + + // If maxPlayerValue is zero, set it to 2 + if (maxPlayerValue === 0) { maxPlayerValue = 2; } else { - maxPlayerValue = 2 * Math.max(...players); + maxPlayerValue = maxPlayerValue * 2; } + // If maxTrendValue is zero, set it to 2 + if (maxTrendValue === 0) { + maxTrendValue = 2; + } else { + maxTrendValue = maxTrendValue * 2; + } + + // Choose the maximum value between maxPlayerValue and maxTrendValue + let graphHeight = Math.max(maxPlayerValue, maxTrendValue); + for (let i = 0; i < players.length; i++) { if (players[i] === -1) { - playersDataForBar[i] = maxPlayerValue; + playersDataForBar[i] = graphHeight; players[i] = 0; } else { playersDataForBar[i] = null; @@ -113,7 +127,7 @@ function graph_area(graphSettings, players, trend_line, labels) { }, y: { min: 0, - max: maxPlayerValue, + max: graphHeight, display: true, title: { display: true, diff --git a/discord/graphs/index.js b/discord/graphs/index.js index 08bd4a4..115a6aa 100644 --- a/discord/graphs/index.js +++ b/discord/graphs/index.js @@ -50,22 +50,24 @@ async function createGraph(guild_id, server_uuid) { let players; let trend_data; if (Array.isArray(player_data)) { + trend_data = player_data; players = player_data.slice(-entriesPerDay); - trend_data = player_data.slice(0, -entriesPerDay); } else { logger.warn('player_data is not an array'); } - - let players_trend = sma(trend_data, 60 / server_data.bot_settings.refresh_interval); - if (players != entriesPerDay) { + if (players.length != entriesPerDay) { logger.debug(`entriesPerDay: ${entriesPerDay}, players.length: ${players.length}`); players = Array(entriesPerDay - players.length).fill(0).concat(players); + logger.debug(`players.length after padding: ${players.length}`); } - if (trend_data != entriesPerDay) { + if (trend_data.length != entriesPerDay) { logger.debug(`entriesPerDay: ${entriesPerDay}, trend_data.length: ${trend_data.length}`); trend_data = Array(entriesPerDay - trend_data.length).fill(0).concat(trend_data); + logger.debug(`trend_data.length after padding: ${trend_data.length}`); } + + let players_trend = sma(trend_data, 60 / server_data.bot_settings.refresh_interval); // LABELS // // This makes labels for every hour of the day the latest (right) value being the current time @@ -140,9 +142,4 @@ async function createGraph(guild_id, server_uuid) { }); } -module.exports = createGraph; - - - - - +module.exports = createGraph; \ No newline at end of file From 476cfb59b3943e292bb28742c4a1999c177796c0 Mon Sep 17 00:00:00 2001 From: ihasTaco <77412945+ihasTaco@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:06:27 -0600 Subject: [PATCH 3/4] Optimization Update I am working on optimizing the bot so that it will run smoother and faster I've also changed a bunch of variable names, and set up ESLint to help with consistency and find errors quicker Please note, this will only process the servers.json file once and will just halt, as I havent set up the loop yet! --- backend/api/get/localRoutes.js | 28 ++- backend/api/post/postRoutes.js | 44 +++- discord/.eslintrc.json | 15 ++ discord/api/index.js | 127 ++++++++---- discord/bot.js | 364 +++++++++++++++++++-------------- discord/embeds/index.js | 184 +++++++++-------- discord/graphs/graph_area.js | 282 ++++++++++++------------- discord/graphs/index.js | 251 +++++++++++------------ discord/index.js | 56 ++--- discord/query/index.js | 79 +++---- discord/query/packageData.js | 149 +++++++------- discord/utils/logger.js | 135 ++++++------ 12 files changed, 953 insertions(+), 761 deletions(-) create mode 100644 discord/.eslintrc.json diff --git a/backend/api/get/localRoutes.js b/backend/api/get/localRoutes.js index 5ee4046..b6d4d93 100644 --- a/backend/api/get/localRoutes.js +++ b/backend/api/get/localRoutes.js @@ -85,9 +85,35 @@ router.get('/generate-uuid/:guild_id', (req, res) => { fs.writeFile('./public/servers.json', JSON.stringify(servers, null, 2), 'utf8', (err) => { if (err) throw err; - res.json({ server_uuid }) }); }); + + fs.readFile('./public/server_info.json', 'utf8', (err, data) => { + if (err) throw err; + + const serverInfo = JSON.parse(data || '{}'); + + // Check if guild ID and server UUID are present, and initialize with default settings if not + if (!serverInfo[guild_id]) { + serverInfo[guild_id] = {}; + } + if (!serverInfo[guild_id][server_uuid]) { + serverInfo[guild_id][server_uuid] = { + 'players': [], + 'ping': [], + 'map': null, + 'status': null, + 'last_restart': null, + 'message_id': null + }; + } + + fs.writeFile('./public/server_info.json', JSON.stringify(serverInfo, null, 2), 'utf8', (err) => { + if (err) throw err; + }); + }); + + res.json({ server_uuid }) }); router.get('/servers/:guildId', (req, res) => { diff --git a/backend/api/post/postRoutes.js b/backend/api/post/postRoutes.js index 7291920..1cb7cb4 100644 --- a/backend/api/post/postRoutes.js +++ b/backend/api/post/postRoutes.js @@ -165,7 +165,7 @@ function queueWrite(data, res) { } router.post('/write-server-info', async (req, res) => { - const { guild_id, server_uuid, server_info } = req.body; + const { guildID, serverUUID, queryState } = req.body; try { let data = null; @@ -180,6 +180,8 @@ router.post('/write-server-info', async (req, res) => { await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for 1 second } } + + console.log(queryState.map); // If data is still null after 5 attempts, handle the error if (!data) { @@ -188,12 +190,12 @@ router.post('/write-server-info', async (req, res) => { const jsonData = JSON.parse(data || '{}'); - if (!(guild_id in jsonData)) { - jsonData[guild_id] = {}; + if (!(guildID in jsonData)) { + jsonData[guildID] = {}; } - if (!(server_uuid in jsonData[guild_id])) { - jsonData[guild_id][server_uuid] = { + if (!(serverUUID in jsonData[guildID])) { + jsonData[guildID][serverUUID] = { map: null, players: [], ping: [], @@ -203,12 +205,32 @@ router.post('/write-server-info', async (req, res) => { }; } - jsonData[guild_id][server_uuid].map = server_info.map; - jsonData[guild_id][server_uuid].players.push(server_info.active_players); - jsonData[guild_id][server_uuid].ping.push(server_info.ping); - jsonData[guild_id][server_uuid].status = server_info.status; - jsonData[guild_id][server_uuid].last_restart = server_info.last_restart; - jsonData[guild_id][server_uuid].message_id = server_info.message_id; + jsonData[guildID][serverUUID].map = queryState.map; + jsonData[guildID][serverUUID].players.push(queryState.active_players); + jsonData[guildID][serverUUID].ping.push(queryState.ping); + jsonData[guildID][serverUUID].status = queryState.status; + jsonData[guildID][serverUUID].last_restart = queryState.last_restart; + + queueWrite(jsonData, res); + } catch (err) { + console.log(`Error reading or processing file: ${err}`); + res.status(500).send(err.message); + } +}); + +router.post('/write-message-id', async (req, res) => { + const { guildID, serverUUID, messageID } = req.body; + + try { + let data = await fs.promises.readFile('./public/server_info.json', 'utf8'); + + const jsonData = JSON.parse(data || '{}'); + + if (!(guildID in jsonData) || !(serverUUID in jsonData[guildID])) { + return res.status(404).send('Guild or server not found'); + } + + jsonData[guildID][serverUUID].message_id = messageID; queueWrite(jsonData, res); } catch (err) { diff --git a/discord/.eslintrc.json b/discord/.eslintrc.json new file mode 100644 index 0000000..cdd5136 --- /dev/null +++ b/discord/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es2021": true + }, + "extends": "google", + "parserOptions": { + "ecmaVersion": "latest" + }, + "rules": { + "max-len": "off", + "linebreak-style": "off" + } +} diff --git a/discord/api/index.js b/discord/api/index.js index 9ef13ee..d39f02e 100644 --- a/discord/api/index.js +++ b/discord/api/index.js @@ -1,53 +1,102 @@ const axios = require('axios'); const logger = require('../utils/logger'); -require('dotenv').config({ path: './.env' }); +require('dotenv').config({path: './.env'}); -async function getGuilds(page = 0, pageSize = 10) { - logger.debug(`Fetching guilds with page: ${page} and pageSize: ${pageSize}`); - const guilds = await axios.get(`${process.env.BACKEND_URL}api/get/bot/guilds`, { - params: { - page: page, - pageSize: pageSize - } - }); - return guilds; +/** + * + * @return {object} + */ +async function getGuilds() { + logger.debug(`Fetching guilds`); + const guilds = await axios.get(`${process.env.BACKEND_URL}api/get/bot/guilds`); + return guilds.data; } -async function getServerUUIDsForGuild(guild_id) { - logger.debug(`Fetching server UUIDs for guild: ${guild_id}`); - const servers = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guild_id}/servers`, {}); - return servers; +/** + * + * @param {string} guildID + * @return {object} + */ +async function getServerUUIDsForGuild(guildID) { + logger.debug(`Fetching server UUIDs for guild: ${guildID}`); + const servers = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guildID}/servers`, {}); + return servers; } -async function getServerDetails(guild_id, server_uuid) { - logger.debug(`Fetching server details for guild: ${guild_id} and server UUID: ${server_uuid}`); - const details = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guild_id}/server/${server_uuid}`, {}); - return details; +/** + * + * @param {string} guildID + * @param {string} serverUUID + * @return {object} + */ +async function getServerSettings(guildID, serverUUID) { + logger.debug(`Fetching server details for guild: ${guildID} and server UUID: ${serverUUID}`); + const settings = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guildID}/server/${serverUUID}`, {}); + return settings.data; } -async function getServerInfo(guild_id, server_uuid) { - logger.debug(`Fetching server info for guild: ${guild_id} and server UUID: ${server_uuid}`); - const serverInfo = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guild_id}/serverInfo/${server_uuid}`, {}); - return serverInfo; +/** + * + * @param {string} guildID + * @param {string} serverUUID + * @return {object} + */ +async function getServerInfo(guildID, serverUUID) { + logger.debug(`Fetching server info for guild: ${guildID} and server UUID: ${serverUUID}`); + let serverInfo; + try { + serverInfo = await axios.get(`${process.env.BACKEND_URL}api/get/bot/${guildID}/serverInfo/${serverUUID}`, {}); + } catch { + logger.warn(`Couldn't find info for guild: ${guildID} and server UUID: ${serverUUID}`); + logger.info(`Creating new info for guild: ${guildID} and server UUID: ${serverUUID}`); + serverInfo = {'players': 0, 'ping': 0, 'map': null, 'status': null, 'last_restart': null, 'message_id': null}; + writeServerInfo(guildID, serverUUID, serverInfo); + } + return serverInfo; } -async function writeServerInfo(guild_id, server_uuid, server_info) { - try { - logger.debug(`Writing server info for guild: ${guild_id} and server UUID: ${server_uuid}`); - const response = await axios.post(`${process.env.BACKEND_URL}api/post/write-server-info`, { - guild_id, - server_uuid, - server_info, - }); - } catch (error) { - logger.error(`Error writing server info for guild: ${guild_id} and server UUID: ${server_uuid}, error: ${error}`); - } +/** + * + * @param {string} guildID + * @param {string} serverUUID + * @param {object} queryState + */ +async function writeServerInfo(guildID, serverUUID, queryState) { + try { + logger.debug(`Writing server info for guild: ${guildID} and server UUID: ${serverUUID}`); + await axios.post(`${process.env.BACKEND_URL}api/post/write-server-info`, { + guildID, + serverUUID, + queryState, + }); + } catch (error) { + logger.error(`Error writing server info for guild: ${guildID} and server UUID: ${serverUUID}, error: ${error}`); + } +} +/** + * + * @param {string} guildID + * @param {string} serverUUID + * @param {object} messageID + */ +async function writeMessageID(guildID, serverUUID, messageID) { + try { + logger.debug(`Writing server info for guild: ${guildID} and server UUID: ${serverUUID}`); + await axios.post(`${process.env.BACKEND_URL}api/post/write-message-id`, { + guildID, + serverUUID, + messageID, + }); + } catch (error) { + logger.error(`Error writing server info for guild: ${guildID} and server UUID: ${serverUUID}, error: ${error}`); + } } module.exports = { - getGuilds, - getServerUUIDsForGuild, - getServerDetails, - getServerInfo, - writeServerInfo -}; \ No newline at end of file + getGuilds, + getServerUUIDsForGuild, + getServerSettings, + getServerInfo, + writeServerInfo, + writeMessageID, +}; diff --git a/discord/bot.js b/discord/bot.js index 27517ee..ac8deec 100644 --- a/discord/bot.js +++ b/discord/bot.js @@ -1,168 +1,232 @@ -require('dotenv').config({ path: '../.env' }); -const { Client, GatewayIntentBits, DiscordAPIError, RateLimitError, HTTPError } = require('discord.js'); -const { getGuilds, getServerUUIDsForGuild, getServerDetails, writeServerInfo, getServerInfo } = require('./api'); -const queryServer = require('./query'); +require('dotenv').config({path: './.env'}); +const {Client, GatewayIntentBits} = require('discord.js'); +const {getGuilds, getServerUUIDsForGuild, getServerSettings, writeMessageID, getServerInfo} = require('./api'); +const serverQuery = require('./query'); const createGraph = require('./graphs'); -const { createEmbed } = require('./embeds'); -const async = require('async'); +const createEmbed = require('./embeds'); const logger = require('./utils/logger'); -const client = new Client({ intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.Guilds] }); - -const serverQueue = async.queue((task, callback) => { - handleServer(task.guild_id, task.server, task.server_settings).then(callback); -}, 1); // '1' is the number of simultaneous tasks allowed. +const client = new Client({intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.Guilds]}); + +/** + * Retrieves the guild object from the Discord API using a specified guild ID. + * If the guild is not found, a warning is logged, and the function returns null. + * + * @async + * @param {string} guildID - The ID of the guild to retrieve. + * @return {Object | null} - The object if found, or null if the guild with the given ID does not exist. + */ +async function getGuild(guildID) { + logger.debug(`Checking guild ${guildID}`); + const guild = client.guilds.cache.get(guildID); + if (!guild) { + logger.warn(`Guild with ID ${guildID} not found`); + + return null; + } + logger.debug(`Finished checking guild ${guildID}`); + + return guild; +} -client.on('ready', async () => { - logger.info(`Shard ${client.shard.ids[0]} is ready`); - manageServers(); - setInterval(manageServers, 30 * 1000); -}); +/** + * Retrieves a specific channel from a given guild using the channel ID found in the server settings. + * The function logs the process of checking the channel and returns the channel object if found. + * If the channel is not found within the guild, a warning is logged, and the function returns null. + * + * @param {Object} guild - The guild object where the channel is expected to be found. + * @param {String} guildID - The ID of the guild associated with the channel. + * @param {Object} serverCustomizationSettings - The server settings containing the channel ID for retrieval. + * @return {Object | null} - The channel object if found, or null if the channel with the given ID does not exist in the guild. + */ +async function getChannel(guild, guildID, serverCustomizationSettings) { + logger.debug(`Checking channel ${serverCustomizationSettings.bot_settings.channel_id} in guild ${guildID}`); + const channel = await guild.channels.cache.get(serverCustomizationSettings.bot_settings.channel_id); + if (!channel) { + logger.warn(`Channel with ID ${serverCustomizationSettings.bot_settings.channel_id} not found in guild ${guildID}`); + + return null; + } + logger.debug(`Finished checking guild ${guildID}`); + + return channel; +} -async function manageServers() { - logger.debug('Getting New Servers') - let response = await getGuilds(); - let guild_info = response.data; - const managedGuilds = client.guilds.cache.filter(guild => guild_info[guild.id]); - - logger.info(`Shard ${client.shard.ids[0]} is managing ${managedGuilds.size} guild(s)`); - - for (const guild of managedGuilds.values()) { - const server_uuids = await getServerUUIDsForGuild(guild.id); - - for (const server of server_uuids.data) { - let response = await getServerDetails(guild.id, server); - let server_settings = response.data; - - // Check if the server has been deleted - if (!server_settings) { - logger.warn(`Server with UUID ${server} in guild ${guild.id} has been deleted.`); - continue; // Skip to the next server - } - - // Convert refresh interval to milliseconds - let interval = server_settings.bot_settings.refresh_interval * 1000; - //let interval = 30 * 1000; - - // Start an independent interval for each server - startInterval(guild.id, server, server_settings, interval); - } - } +/** + * Retrieves a specific message from a given channel using the message ID found in the server state. + * The function logs the process of checking the channel and returns the message object if found. + * If the message is not found within the channel, an error is logged, and the function returns null. + * + * @param {Object} channel - The channel object where the message is expected to be found. + * @param {String} guildID - The ID of the guild associated with the channel. + * @param {Object} serverCustomizationSettings - The server customization settings containing the channel ID for verification. + * @param {Object} serverInfo - The current state of the server, including the message ID to be fetched. + * @return {Object | null} - The message object if found, or null if the message with the given ID does not exist in the channel. + */ +async function getMessage(channel, guildID, serverCustomizationSettings, serverInfo) { + logger.debug(`Checking channel ${serverCustomizationSettings.bot_settings.channel_id} in guild ${guildID}`); + const message = await channel.messages.fetch(serverInfo.data.message_id); + if (!message) { + logger.error(`Message with ID ${queryState.message_id} not found in channel ${channel.id} in guild ${guildID}`); + + return null; + } + logger.debug(`Finished getting message: ${message.id}`); + + return message; } -async function startInterval(guild_id, server, server_settings, interval) { - serverQueue.push({ - guild_id, - server, - server_settings - }); +/** + * Queries a specific game server using the provided customization settings, guild ID, and server UUID. + * The server's IP, query port, and query protocol are extracted from the customization settings and used + * to perform the query. The result of the query is an object containing the current state of the server. + * If an error occurs during the query process, it is logged, and the function may return undefined. + * + * @param {String} guildID - The ID of the guild associated with the server being queried. + * @param {String} serverUUID - The unique identifier for the server being queried. + * @param {Object} serverCustomizationSettings - The server customization settings containing the IP, query port, and query protocol. + * @return {Object} - An object representing the current state of the queried server, or undefined if an error occurred. + */ +async function queryServer(guildID, serverUUID, serverCustomizationSettings ) { + logger.debug(`Querying server for guild ${guildID} and server ${serverUUID}`); + let queryState; + try { + queryState = await serverQuery( + serverCustomizationSettings.server_settings.ip, + serverCustomizationSettings.server_settings.query_port, + serverCustomizationSettings.server_settings.query_protocol, + guildID, + serverUUID, + ); + } catch (err) { + logger.error(`There was an error while querying server for guild ${guildID} and server ${serverUUID}`); + logger.error(`Error: ${err}`); + } + logger.debug(`Finished querying server for guild ${guildID} and server ${serverUUID}`); + return queryState; +} - setTimeout(() => startInterval(guild_id, server, server_settings, interval), interval); +/** + * Generates a graph for a specific server within a guild based on the server customization settings. + * The graph is created by calling an external function `createGraph`, and the URL of the generated graph is returned. + * Any errors encountered during the graph generation process are logged. + * + * @param {String} guildID - The ID of the guild for which the graph is being generated. + * @param {String} serverUUID - The unique identifier for the server associated with the graph. + * @param {Object} serverCustomizationSettings - Customization settings for the graph, such as title, labels, colors, etc. + * @return {string} - The URL of the generated graph, or undefined if an error occurred. + */ +async function generateGraph(guildID, serverUUID, serverCustomizationSettings) { + logger.debug(`Generating graph for guild ${guildID} and server ${serverUUID}`); + let graphURL; + try { + graphURL = createGraph(guildID, serverUUID, serverCustomizationSettings); + } catch (err) { + logger.error(`There was an error while generating graph for guild ${guildID} and server ${serverUUID}`); + logger.error(`Error: ${err}`); + } + logger.debug(`Finished generating graph for guild ${guildID} and server ${serverUUID}`); + + return graphURL; } -async function handleServer(guild_id, server_uuid, server_settings) { - logger.debug('handleServer'); - try { - // Get the guild object - const guild = client.guilds.cache.get(guild_id); - logger.debug(`Getting Guild with Guild ID: ${guild_id}`); - - // Check if the guild exists - if (!guild) { - logger.warn(`Guild ${guild_id} not found`); - return; - } +/** + * Creates a new message in a specified channel within a guild. The message includes an embed + * and an attachment, generated based on the server customization settings, server profile, and graph URL. + * The newly created message's ID is then updated in the server state and written to the server info. + * + * @param {String} guildID - The ID of the guild where the message will be posted. + * @param {String} serverUUID - The unique identifier for the server associated with the message. + * @param {Object} channel - The channel object where the message will be posted. + * @param {Object} serverCustomizationSettings - Customization settings for the bot's behavior and appearance. + * @param {String} graphURL - The URL of the graph to be embedded in the message. + * @param {Object} queryState - The state of the server, which will be updated with the new message's ID. + * @return {Object | null} - The created message object if successful, or null if an error occurred (e.g., missing permissions, rate-limited). + */ +async function createMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState) { + logger.debug(`Creating a new message for guild ${guildID} and server ${serverUUID} in channel: ${channel.id}`); + let message; + try { + const [embed, attachment] = await createEmbed(serverCustomizationSettings, graphURL, queryState); + + message = await channel.send({embeds: [embed], files: [attachment]}); + await writeMessageID(guildID, serverUUID, message.id); + logger.debug(`Finished creating a new message for guild ${guildID} and server ${serverUUID} in channel: ${channel.id}`); + + return message; + } catch (err) { + if (err.message.includes('Missing Permissions')) { + logger.error(`Missing permissions to send message in channel ${channel.id} in guild ${guildID}`); + } else if (err.code === 429) { // Rate limited + logger.error(`Rate limited while sending message in channel ${channel.id} in guild ${guildID}`); + } else { + logger.error(`An error occurred while sending the message in channel ${channel.id} in guild ${guildID}: ${err}`); + } - // Get the channel object - const channel = guild.channels.cache.get(server_settings.bot_settings.channel_id); - logger.debug(`Getting Channel with Channel ID: ${server_settings.bot_settings.channel_id}`); + return null; + } +} - // Check if the channel exists - if (!channel) { - logger.warn(`Channel with ID ${server_settings.bot_settings.channel_id} not found in guild ${guild_id}`); - return; - } +/** + * Edits an existing message in a specified channel within a guild. The message is modified + * to include a newly created embed and attachment, both of which are generated based on the + * provided bot settings, server information, and graph URL. + * + * @param {String} guildID - The ID of the guild where the message is located. + * @param {String} serverUUID - The unique identifier for the server associated with the message. + * @param {Object} channel - The channel object where the message is posted. + * @param {Object} serverCustomizationSettings - Customization settings for the bot's behavior and appearance. + * @param {String} graphURL - The URL of the graph to be embedded in the message. + * @param {Object} queryState - Information related to the server, such as map, players, etc. + * @param {Object} serverInfo - Information related to the server, such as map, players, etc. + * @return {Object | null} - The edited message object if successful, or null if an error occurred (e.g., missing permissions, rate-limited). + */ +async function editMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState, serverInfo) { + logger.debug(`Editing message for guild ${guildID} and server ${serverUUID} in channel: ${channel.id}`); + try { + const message = await getMessage(channel, guildID, serverCustomizationSettings, serverInfo); + const [embed, attachment] = await createEmbed(serverCustomizationSettings, graphURL, queryState); + await message.edit({embeds: [embed], files: [attachment]}); + logger.debug(`Finished editing message for guild ${guildID} and server ${serverUUID} in channel: ${channel.id}`); + + return message; + } catch (err) { + if (err.message.includes('Missing Permissions')) { + logger.error(`Missing permissions to edit message in channel ${channel.id} in guild ${guildID}`); + } else if (err.code === 429) { // Rate limited + logger.error(`Rate limited while editing message in channel ${channel.id} in guild ${guildID}`); + } else { + logger.error(`An error occurred while editing the message in channel ${channel.id} in guild ${guildID}: ${err.message}`); + } - // Query the server - const { server_info: server_query } = await queryServer(server_settings.server_settings.ip, server_settings.server_settings.query_port,server_settings.server_settings.query_protocol, guild_id, server_uuid); - - let graph_url; - // Create the Player Graph - if (!server_settings.graph_settings.disable) { - graph_url = await createGraph(guild_id, server_uuid); - logger.debug(`Done! Graph can be found here`); - logger.debug(`${graph_url}`); - } + return null; + } +} - let message; - if (server_query.message_id === null) { - const [embed, attachment] = await createEmbed(server_settings, graph_url, server_query); - - message = await channel.send({ embeds: [embed], files: [attachment] }).catch(logger.error); - - server_query.message_id = message.id; - - await writeServerInfo(guild_id, server_uuid, server_query); +client.on('ready', async () => { + const guilds = Object.keys(await getGuilds()); + for (const guild of guilds) { + const guildID = guild; + const servers = await getServerUUIDsForGuild(guildID); + if (Object.keys(servers.data).length > 0) { + for (const server of servers.data) { + const serverUUID = server; + const serverCustomizationSettings = await getServerSettings(guildID, serverUUID); + const serverInfo = await getServerInfo(guildID, serverUUID); + const guild = await getGuild(guildID); + const channel = await getChannel(guild, guildID, serverCustomizationSettings); + const queryState = await queryServer(guildID, serverUUID, serverCustomizationSettings); + const graphURL = await generateGraph(guildID, serverUUID, serverCustomizationSettings); + if (serverInfo.data.message_id) { + await editMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState, serverInfo); } else { - let message = null; - let attempts = 0; - let shouldSendNewMessage = false; - - while (!message && attempts < 5) { - try { - message = await channel.messages.fetch(server_query.message_id); - const [embed, attachment] = await createEmbed(server_settings, graph_url, server_query); - await message.edit({ embeds: [embed], files: [attachment] }).catch(logger.error); - } catch (err) { - logger.error(`Error fetching message: ${err}`); - if (err instanceof DiscordAPIError) { - if (err.code === 10008) { // 'Unknown Message' error - logger.error('Message not found, it might have been deleted.'); - shouldSendNewMessage = true; - break; - } else if (err.code === 50001) { // 'Missing Access' error - logger.error('Bot does not have access to the channel.'); - break; - } else if (err.code === 50013) { // 'Missing Permissions' error - logger.error('Bot does not have permission to read the message.'); - break; - } - } - - // If it's a RateLimitError, you can wait for the duration specified by the 'retry_after' property before trying again - if (err instanceof RateLimitError) { - await new Promise(resolve => setTimeout(resolve, err.retry_after * 1000)); - continue; - } - - // If it's a HTTPError (like a network error), you can wait for a bit before trying again - if (err instanceof HTTPError) { - await new Promise(resolve => setTimeout(resolve, 1000)); - continue; - } - - // If it's some other type of error, you might not know how to handle it, so just log it and exit the loop - logger.error('Unknown error type, not retrying.'); - break; - } - - attempts += 1; - } - - if (!message && shouldSendNewMessage) { - const [embed, attachment] = await createEmbed(server_settings, graph_url, server_query); - - message = await channel.send({ embeds: [embed], files: [attachment] }).catch(logger.error); - - server_query.message_id = message.id; - await writeServerInfo(guild_id, server_uuid, server_query); - } + await createMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState); } - } catch (err) { - logger.error(`Error handling server: ${err}`); + } + } else { + continue; } -} - -client.on('error', logger.error); + } +}); -client.login(process.env.BOT_TOKEN); \ No newline at end of file +client.login(process.env.BOT_TOKEN); diff --git a/discord/embeds/index.js b/discord/embeds/index.js index 7fd2766..8962f1f 100644 --- a/discord/embeds/index.js +++ b/discord/embeds/index.js @@ -1,95 +1,115 @@ -require('dotenv').config(); -const axios = require('axios'); -const { EmbedBuilder } = require('discord.js'); -const { AttachmentBuilder } = require('discord.js'); +require('dotenv').config({path: './.env'}); +const {EmbedBuilder} = require('discord.js'); +const {AttachmentBuilder} = require('discord.js'); const logger = require('../utils/logger'); -function replaceVariables(str, server_settings, server_query) { - if (str) { - let server_variables = { - game: server_settings.server_settings.game.toString(), - map: server_query.map.toString(), - ip: server_settings.server_settings.ip.toString(), - connection_port: server_settings.server_settings.connection_port.toString(), - query_port: server_settings.server_settings.query_port.toString(), - players_active: server_query.status ? server_query.active_players.toString() : "--", - players_max: server_query.status ? server_query.max_players.toString() : "--", - server_name: server_settings.bot_settings.server_name.toString(), - server_status: server_query.status - }; +/** + * Replaces text variables within a string with specific server information. + * + * @param {string} str - The input string containing placeholders enclosed in curly braces. + * @param {object} serverCustomizationSettings - The server settings object containing game, IP, port, and other details. + * @param {object} queryState - The server query object containing information about the map, active players, max players, and server status. + * @return {string} The input string with placeholders replaced with actual server information. + */ +function replaceVariables(str, serverCustomizationSettings, queryState) { + if (str) { + const serverVariables = { + game: serverCustomizationSettings?.server_settings?.game?.toString() || '', + map: queryState.map?.toString() || '', + ip: serverCustomizationSettings?.server_settings?.ip?.toString() || '', + connection_port: serverCustomizationSettings?.server_settings?.connection_port?.toString() || '', + query_port: serverCustomizationSettings?.server_settings?.query_port?.toString() || '', + players_active: queryState.status ? queryState.active_players?.toString() : '--', + players_max: queryState.status ? queryState.max_players?.toString() : '--', + server_name: serverCustomizationSettings?.bot_settings?.server_name?.toString() || '', + server_status: queryState.status || '', + }; - return str.replace(/\{([a-z_]+)\}/gi, function(match, variable) { - return server_variables[variable.toLowerCase()] || match; - }); - } else { - return; - } + return str.replace(/\{([a-z_]+)\}/gi, function(match, variable) { + return serverVariables[variable.toLowerCase()] || match; + }); + } } -async function createEmbed(server_settings, graph_url, server_query) { - logger.debug('Creating embed...'); - let embed = new EmbedBuilder() - .setTitle(replaceVariables(server_settings.embed_settings.embed_title, server_settings, server_query)) - .setDescription(replaceVariables(server_settings.embed_settings.embed_description, server_settings, server_query)) - .setColor(server_settings.embed_settings.embed_color); - - if (!server_settings.embed_settings.disable_timestamp) { - embed.setTimestamp(); - } - if (!server_settings.embed_settings.thumbnail_settings.disable_thumbnail) { - embed.setThumbnail(server_settings.embed_settings.thumbnail_settings.thumbnail_url); +/** + * Creates an embed object with server details, including title, description, color, fields, and attachments. + * + * @async + * @param {object} serverCustomizationSettings - The server settings object containing embed settings, field settings, thumbnail, footer, etc. + * @param {string} graphURL - The URL for the graph image to be attached to the embed. + * @param {object} queryState - The server query object containing information about the server status, players, etc. + * @return {Array} An array containing the embed object and attachment (graph image). + */ +async function createEmbed(serverCustomizationSettings, graphURL, queryState) { + logger.debug('Creating embed...'); + const embed = new EmbedBuilder(); + + logger.debug(`Setting Embed Title: ${replaceVariables(serverCustomizationSettings.embed_settings.embed_title, serverCustomizationSettings, queryState)}`); + embed.setTitle(replaceVariables(serverCustomizationSettings.embed_settings.embed_title, serverCustomizationSettings, queryState)); + logger.debug(`Setting Embed Description: ${serverCustomizationSettings.embed_settings.embed_description}`); + embed.setDescription(replaceVariables(serverCustomizationSettings.embed_settings.embed_description, serverCustomizationSettings, queryState)); + logger.debug(`Setting Embed Color: ${serverCustomizationSettings.embed_settings.embed_color}`); + embed.setColor(serverCustomizationSettings.embed_settings.embed_color); + + if (!serverCustomizationSettings.embed_settings.disable_timestamp) { + logger.debug('Timestamp is enabled!'); + embed.setTimestamp(); + } + if (!serverCustomizationSettings.embed_settings.thumbnail_settings.disable_thumbnail) { + logger.debug('Thumbnail is enabled!'); + embed.setThumbnail(serverCustomizationSettings.embed_settings.thumbnail_settings.thumbnail_url); + } + if (!serverCustomizationSettings.embed_settings.footer_settings.disable_footer) { + logger.debug('Footer is enabled!'); + embed.setFooter({text: serverCustomizationSettings.embed_settings.footer_settings.footer_text, iconURL: serverCustomizationSettings.embed_settings.footer_settings.footer_url}); + } + + // Field Assignment + const fieldsArray = Object.entries(serverCustomizationSettings.embed_field_settings.embed_fields) + .map(([key, value]) => ({name: key, ...value})) + .filter((field) => !field.disable) + .sort((a, b) => a.index - b.index); + + fieldsArray.forEach((field) => { + let fieldName = field.name; + let fieldValue; + + if (field.name === 'status') { + // For the status field, use online/offline text as the value + fieldValue = queryState.status ? `${field.online_emoji} ${replaceVariables(field.online_text, serverCustomizationSettings, queryState)}` : `${field.offline_emoji} ${replaceVariables(field.offline_text, serverCustomizationSettings, queryState)}`; + } else { + fieldValue = ''; + if (field.emoji) { + fieldValue = fieldValue + field.emoji; + } + if (field.text) { + fieldValue = `${fieldValue} ${replaceVariables(field.text, serverCustomizationSettings, queryState)}`; + } } - if (!server_settings.embed_settings.footer_settings.disable_footer) { - embed.setFooter({ text: server_settings.embed_settings.footer_settings.footer_text, iconURL: server_settings.embed_settings.footer_settings.footer_url }) + fieldName = fieldName.replace(/_/g, ' ').split(' ').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + // Check that both name and value are defined before adding the field + if (fieldName && fieldValue) { + embed.addFields({name: fieldName, value: fieldValue, inline: field.inline}); } + }); - // Field Assignment - let fieldsArray = Object.entries(server_settings.embed_field_settings.embed_fields) - .map(([key, value]) => ({ name: key, ...value })) - .filter(field => !field.disable) - .sort((a, b) => a.index - b.index); + if (!serverCustomizationSettings.embed_settings.disable_player_names) { + if (queryState.players && queryState.players.length > 0) { + const playerNames = queryState.players.map((player) => player.name); + const playerList = '\n' + playerNames.join('\n'); - fieldsArray.forEach(field => { - let fieldName = field.name; - let fieldValue; - - if (field.name === 'status') { - // For the status field, use online/offline text as the value - fieldValue = server_query.status ? `${field.online_emoji} ${replaceVariables(field.online_text, server_settings, server_query)}` : `${field.offline_emoji} ${replaceVariables(field.offline_text, server_settings, server_query)}` ; - } else { - fieldValue = ''; - if (field.emoji) { - fieldValue = fieldValue + field.emoji; - } - if (field.text) { - fieldValue = `${fieldValue} ${replaceVariables(field.text, server_settings, server_query)}`; - } - } - fieldName = fieldName.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); - - // Check that both name and value are defined before adding the field - if (fieldName && fieldValue) { - embed.addFields({name: fieldName, value: fieldValue, inline: field.inline}); - } - }); - - if (!server_settings.embed_settings.disable_player_names) { - if (server_query.players && server_query.players.length > 0) { - const playerNames = server_query.players.map(player => player.name); - const playerList = '\n' + playerNames.join('\n'); - - embed.addFields({name: 'Online Players', value: `\`\`\`${playerList}\`\`\``, inline: false}); - } + embed.addFields({name: 'Online Players', value: `\`\`\`${playerList}\`\`\``, inline: false}); } + } - logger.debug('Adding image to embed...'); - const attachment = new AttachmentBuilder(graph_url, { name: 'chart.png' }); - embed.setImage('attachment://chart.png'); + logger.debug('Adding image to embed...'); + const attachment = new AttachmentBuilder() + .setFile(graphURL) + .setName('chart.png'); - logger.debug('Embed created.'); - return [embed, attachment]; -} + embed.setImage('attachment://chart.png'); -module.exports = { - createEmbed -}; \ No newline at end of file + logger.debug('Embed created.'); + return [embed, attachment]; +} +module.exports = createEmbed; diff --git a/discord/graphs/graph_area.js b/discord/graphs/graph_area.js index cbf43cb..fbbae81 100644 --- a/discord/graphs/graph_area.js +++ b/discord/graphs/graph_area.js @@ -1,151 +1,157 @@ const logger = require('../utils/logger'); +/** + * + * @param {*} hex + * @param {*} opacity + * @return {string} + */ function hexToRgba(hex, opacity) { - let r = parseInt(hex.slice(1, 3), 16), - g = parseInt(hex.slice(3, 5), 16), - b = parseInt(hex.slice(5, 7), 16); - - return `rgba(${r}, ${g}, ${b}, ${opacity})`; + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r}, ${g}, ${b}, ${opacity})`; } -function graph_area(graphSettings, players, trend_line, labels) { - logger.debug('Configuring graph area'); - - let playersDataForBar = []; - - // Calculate max value for players and trend_line - let maxPlayerValue = Math.max(...players); - let maxTrendValue = Math.max(...trend_line); - - // If maxPlayerValue is zero, set it to 2 - if (maxPlayerValue === 0) { - maxPlayerValue = 2; - } else { - maxPlayerValue = maxPlayerValue * 2; - } +/** + * + * @param {*} graphSettings + * @param {*} players + * @param {*} trendLine + * @param {*} labels + * @return {object} + */ +function graphArea(graphSettings, players, trendLine, labels) { + logger.debug('Configuring graph area'); - // If maxTrendValue is zero, set it to 2 - if (maxTrendValue === 0) { - maxTrendValue = 2; + const playersDataForBar = []; + // Calculate max value for players and trend_line + let maxPlayerValue = Math.max(...players); + let maxTrendValue = Math.max(...trendLine); + // If maxPlayerValue is zero, set it to 2 + if (maxPlayerValue === 0) { + maxPlayerValue = 2; + } else { + maxPlayerValue = maxPlayerValue * 2; + } + // If maxTrendValue is zero, set it to 2 + if (maxTrendValue === 0) { + maxTrendValue = 2; + } else { + maxTrendValue = maxTrendValue * 2; + } + // Choose the maximum value between maxPlayerValue and maxTrendValue + const graphHeight = Math.max(maxPlayerValue, maxTrendValue); + for (let i = 0; i < players.length; i++) { + if (players[i] === -1) { + playersDataForBar[i] = graphHeight; + players[i] = 0; } else { - maxTrendValue = maxTrendValue * 2; - } - - // Choose the maximum value between maxPlayerValue and maxTrendValue - let graphHeight = Math.max(maxPlayerValue, maxTrendValue); - - for (let i = 0; i < players.length; i++) { - if (players[i] === -1) { - playersDataForBar[i] = graphHeight; - players[i] = 0; - } else { - playersDataForBar[i] = null; - } + playersDataForBar[i] = null; } - - for(let i = 0; i < trend_line.length; i++) { - if(trend_line[i] < 0) { - trend_line[i] = 0; - } + } + for (let i = 0; i < trendLine.length; i++) { + if (trendLine[i] < 0) { + trendLine[i] = 0; } - - const configuration = { - type: 'line', - data: { - labels: labels, - datasets: [ - { - label: 'Server Offline', - type: 'bar', - data: playersDataForBar, - backgroundColor: 'red' - }, - { - label: 'Players', - data: players, - borderColor: hexToRgba(graphSettings.graph_line_settings.player_settings.line_color, 1), - fill: !graphSettings.graph_line_settings.player_settings.disable, - backgroundColor: hexToRgba(graphSettings.graph_line_settings.player_settings.fill_color, graphSettings.graph_line_settings.player_settings.fill_opacity), - pointRadius: 0, - borderWidth: 1 - }, - { - label: 'Trends', - data: trend_line, - borderColor: hexToRgba(graphSettings.graph_line_settings.trend_settings.color, 1), - borderDash: [5, 5], - fill: !graphSettings.graph_line_settings.trend_settings.disable, - pointRadius: 0, - borderWidth: 1 - } - ] + } + const configuration = { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Server Offline', + type: 'bar', + data: playersDataForBar, + backgroundColor: 'red', + }, + { + label: 'Players', + data: players, + borderColor: hexToRgba(graphSettings.graph_line_settings.player_settings.line_color, 1), + fill: !graphSettings.graph_line_settings.player_settings.disable, + backgroundColor: hexToRgba(graphSettings.graph_line_settings.player_settings.fill_color, graphSettings.graph_line_settings.player_settings.fill_opacity), + pointRadius: 0, + borderWidth: 1, + }, + { + label: 'Trends', + data: trendLine, + borderColor: hexToRgba(graphSettings.graph_line_settings.trend_settings.color, 1), + borderDash: [5, 5], + fill: !graphSettings.graph_line_settings.trend_settings.disable, + pointRadius: 0, + borderWidth: 1, }, - options: { - plugins: { - legend: { - display: !graphSettings.legend_settings.disable, - labels: { - color: hexToRgba(graphSettings.legend_settings.background_color, 1), - borderColor: hexToRgba(graphSettings.legend_settings.border_color, 1), - } - }, - title: { - display: !graphSettings.title_settings.disable, - text: graphSettings.title_settings.text, - color: hexToRgba(graphSettings.title_settings.color, 1), - } + ], + }, + options: { + plugins: { + legend: { + display: !graphSettings.legend_settings.disable, + labels: { + color: hexToRgba(graphSettings.legend_settings.background_color, 1), + borderColor: hexToRgba(graphSettings.legend_settings.border_color, 1), + }, + }, + title: { + display: !graphSettings.title_settings.disable, + text: graphSettings.title_settings.text, + color: hexToRgba(graphSettings.title_settings.color, 1), + }, + }, + scales: { + x: { + display: true, + time: { + unit: 'hour', + }, + title: { + display: true, + text: graphSettings.y_label_settings.text, + color: hexToRgba(graphSettings.x_label_settings.color, 1), + }, + ticks: { + major: true, + autoSkip: false, + min: 10, + max: 24, + grace: '5%', + color: hexToRgba(graphSettings.tick_settings.x_color, 1), + afterBuildTicks: function(scale, ticks) { + const majorTicks = ticks.filter((tick) => { + return tick.major; + }); + return majorTicks; }, - scales: { - x: { - display: true, - time: { - unit: 'hour' - }, - title: { - display: true, - text: graphSettings.y_label_settings.text, - color: hexToRgba(graphSettings.x_label_settings.color, 1), - }, - ticks: { - major: true, - autoSkip: false, - min: 10, - max: 24, - grace: '5%', - color: hexToRgba(graphSettings.tick_settings.x_color, 1), - afterBuildTicks: function(scale, ticks) { - var majorTicks = ticks.filter((tick) => { - return tick.major; - }); - return majorTicks; - } - }, - grid: { - display: false, - color: hexToRgba(graphSettings.graph_line_settings.grid_settings.color, graphSettings.graph_line_settings.grid_settings.opacity), - } - }, - y: { - min: 0, - max: graphHeight, - display: true, - title: { - display: true, - text: graphSettings.x_label_settings.text, - color: hexToRgba(graphSettings.y_label_settings.color, 1), - }, - ticks: { - color: hexToRgba(graphSettings.tick_settings.y_color, 1), - }, - grid: { - color: hexToRgba(graphSettings.graph_line_settings.grid_settings.color, graphSettings.graph_line_settings.grid_settings.opacity), - } - } - } - } - }; - logger.debug('Graph area configured'); - return configuration; + }, + grid: { + display: false, + color: hexToRgba(graphSettings.graph_line_settings.grid_settings.color, graphSettings.graph_line_settings.grid_settings.opacity), + }, + }, + y: { + min: 0, + max: graphHeight, + display: true, + title: { + display: true, + text: graphSettings.x_label_settings.text, + color: hexToRgba(graphSettings.y_label_settings.color, 1), + }, + ticks: { + color: hexToRgba(graphSettings.tick_settings.y_color, 1), + }, + grid: { + color: hexToRgba(graphSettings.graph_line_settings.grid_settings.color, graphSettings.graph_line_settings.grid_settings.opacity), + }, + }, + }, + }, + }; + logger.debug('Graph area configured'); + return configuration; } -module.exports = graph_area; \ No newline at end of file +module.exports = graphArea; diff --git a/discord/graphs/index.js b/discord/graphs/index.js index 115a6aa..8c4c5ac 100644 --- a/discord/graphs/index.js +++ b/discord/graphs/index.js @@ -1,145 +1,136 @@ -const { ChartJSNodeCanvas } = require('chartjs-node-canvas'); -const { sma } = require('moving-averages'); -const path = require('path'); -const fs = require('fs'); +/* eslint-disable no-extend-native */ +const {ChartJSNodeCanvas} = require('chartjs-node-canvas'); +const {sma} = require('moving-averages'); +const {join} = require('path'); +const {writeFile} = require('fs'); const logger = require('../utils/logger'); - -const { getServerDetails, getServerInfo } = require('../api'); +const {getServerInfo} = require('../api'); // Graph Configurations -const graph_area = require('./graph_area'); - -async function getServerPlayers(guild_id, server_uuid) { - logger.debug(`Getting server players for guild: ${guild_id}, server: ${server_uuid}`); - return getServerInfo(guild_id, server_uuid) - .then(response => { - logger.debug(`Server players retrieved`); - return response.data.players; - }) - .catch(error => { - logger.error(`Error getting server players: ${error}`); - }); -} +const graphArea = require('./graph_area'); -async function getServerData(guild_id, server_uuid) { - logger.debug(`Getting server data for guild: ${guild_id}, server: ${server_uuid}`); - return getServerDetails(guild_id, server_uuid) - .then(response => { - logger.debug(`Server data retrieved`); - return response.data; - }) - .catch(error => { - logger.error(`Error getting server data: ${error}`); - }); +/** + * + * @param {string} guildID + * @param {string} serverUUID + * @return {object} + */ +async function getServerPlayers(guildID, serverUUID) { + logger.debug(`Getting server players for guild: ${guildID}, server: ${serverUUID}`); + return getServerInfo(guildID, serverUUID) + .then((response) => { + logger.debug(`Server players retrieved`); + return response.data.players; + }) + .catch((error) => { + logger.error(`Error getting server players: ${error}`); + }); } -async function createGraph(guild_id, server_uuid) { - logger.debug(`Creating graph for guild: ${guild_id}, server: ${server_uuid}`); - Array.prototype.sma = require('moving-averages').sma; - const width = 700; - const height = 500; - const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height }); +/** + * + * @param {*} guildID + * @param {*} serverUUID + * @param {*} serverCustomizationSettings + * @return {string} + */ +async function createGraph(guildID, serverUUID, serverCustomizationSettings) { + logger.debug(`Creating graph for guild: ${guildID}, server: ${serverUUID}`); + Array.prototype.sma = require('moving-averages').sma; + const width = 700; + const height = 500; + const chartJSNodeCanvas = new ChartJSNodeCanvas({width, height}); + // Set up Player Data, Server Data, and Player Trends Data + const playerData = await getServerPlayers(guildID, serverUUID); - // Set up Player Data, Server Data, and Player Trends Data - let player_data = await getServerPlayers(guild_id, server_uuid); - const server_data = await getServerData(guild_id, server_uuid); - - const secondsInDay = 24 * 60 * 60; - let entriesPerDay = Math.round(secondsInDay / server_data.bot_settings.refresh_interval); + const secondsInDay = 24 * 60 * 60; + const entriesPerDay = Math.round(secondsInDay / serverCustomizationSettings.bot_settings.refresh_interval); + let players; + let trendData; + if (Array.isArray(playerData)) { + trendData = playerData; + players = playerData.slice(-entriesPerDay); + } else { + logger.warn('playerData is not an array'); + } - let players; - let trend_data; - if (Array.isArray(player_data)) { - trend_data = player_data; - players = player_data.slice(-entriesPerDay); - } else { - logger.warn('player_data is not an array'); - } - - if (players.length != entriesPerDay) { - logger.debug(`entriesPerDay: ${entriesPerDay}, players.length: ${players.length}`); - players = Array(entriesPerDay - players.length).fill(0).concat(players); - logger.debug(`players.length after padding: ${players.length}`); - } - if (trend_data.length != entriesPerDay) { - logger.debug(`entriesPerDay: ${entriesPerDay}, trend_data.length: ${trend_data.length}`); - trend_data = Array(entriesPerDay - trend_data.length).fill(0).concat(trend_data); - logger.debug(`trend_data.length after padding: ${trend_data.length}`); - } + if (players.length != entriesPerDay) { + logger.debug(`entriesPerDay: ${entriesPerDay}, players.length: ${players.length}`); + players = Array(entriesPerDay - players.length).fill(0).concat(players); + logger.debug(`players.length after padding: ${players.length}`); + } + if (trendData.length != entriesPerDay) { + logger.debug(`entriesPerDay: ${entriesPerDay}, trendData.length: ${trendData.length}`); + trendData = Array(entriesPerDay - trendData.length).fill(0).concat(trendData); + logger.debug(`trend_data.length after padding: ${trendData.length}`); + } + const playersTrend = sma(trendData, 60 / serverCustomizationSettings.bot_settings.refresh_interval); - let players_trend = sma(trend_data, 60 / server_data.bot_settings.refresh_interval); - - // LABELS // - // This makes labels for every hour of the day the latest (right) value being the current time - // Will add a customization in the dashboard so the user can select their timezone, for now though, we will use UTC - let userTimezone = "UTC"; // Default Option: UTC - let use12Format = true; + // LABELS // + // This makes labels for every hour of the day the latest (right) value being the current time + // Will add a customization in the dashboard so the user can select their timezone, for now though, we will use UTC + const userTimezone = 'UTC'; // Default Option: UTC + const use12Format = true; + // Get the current time in the user's timezone + const date = new Date(); + const formatter = new Intl.DateTimeFormat([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, // Keep this false, if set to true 12 hour format wont work... + timeZone: userTimezone, + }); + const timeParts = formatter.format(date).split(':'); + const currentHour = Number(timeParts[0]); + const currentMinute = Number(timeParts[1]); + const currentSecond = Number(timeParts[2]); + const currentTimeInSeconds = (currentHour * 3600) + (currentMinute * 60) + currentSecond; - // Get the current time in the user's timezone - let date = new Date(); - let formatter = new Intl.DateTimeFormat([], { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, // Keep this false, if set to true 12 hour format wont work... - timeZone: userTimezone + // Generate Labels 1 label for every hour + let labels = []; + // I will add a customization that will switch between 12-24 hour formats + if (use12Format) { + labels = Array.from({length: 24 * 60 * 60 / serverCustomizationSettings.bot_settings.refresh_interval}, (v, i) => { + const totalSeconds = ((i * serverCustomizationSettings.bot_settings.refresh_interval) + currentTimeInSeconds) % (24 * 60 * 60); + const hour24 = Math.floor(totalSeconds / 3600) % 24; + const hour = hour24 % 12 || 12; + const ampm = hour24 < 12 ? 'AM' : 'PM'; + const minute = Math.floor((totalSeconds / 60) % 60); + return (minute % 60 === 0) ? `${hour}:00 ${ampm}` : ''; }); - let timeParts = formatter.format(date).split(':'); - let currentHour = Number(timeParts[0]); - let currentMinute = Number(timeParts[1]); - let currentSecond = Number(timeParts[2]); - - let currentTimeInSeconds = (currentHour * 3600) + (currentMinute * 60) + currentSecond; - - // Generate Labels 1 label for every hour - let labels = []; - // I will add a customization that will switch between 12-24 hour formats - if (use12Format) { - labels = Array.from({ length: 24 * 60 * 60 / server_data.bot_settings.refresh_interval }, (v, i) => { - let totalSeconds = ((i * server_data.bot_settings.refresh_interval) + currentTimeInSeconds) % (24 * 60 * 60); - let hour24 = Math.floor(totalSeconds / 3600) % 24; - let hour = hour24 % 12 || 12; - let ampm = hour24 < 12 ? 'AM' : 'PM'; - let minute = Math.floor((totalSeconds / 60) % 60); - return (minute % 60 === 0) ? `${hour}:00 ${ampm}` : ''; - }); - } else { - labels = Array.from({ length: 24 * 60 * 60 / server_data.bot_settings.refresh_interval }, (v, i) => { - let totalSeconds = ((i * server_data.bot_settings.refresh_interval) + currentTimeInSeconds) % (24 * 60 * 60); - let hour = Math.floor(totalSeconds / 3600) % 24; - let minute = Math.floor((totalSeconds / 60) % 60); - return (minute % 60 === 0) ? `${hour}:00` : ''; - }); - } - - const graphSettings = server_data.graph_settings; - let configuration = {}; - - // Once the graph types are implemented, i will change this so it will check which graph to use based on user input - // paddedPlayers is the player data after it has been padded with zero's - // I need to add the functionality to pad the player data. - if (true) { - configuration = graph_area(graphSettings, players, players_trend, labels, server_data.bot_settings.refresh_interval) - } + } else { + labels = Array.from({length: 24 * 60 * 60 / serverCustomizationSettings.bot_settings.refresh_interval}, (v, i) => { + const totalSeconds = ((i * serverCustomizationSettings.bot_settings.refresh_interval) + currentTimeInSeconds) % (24 * 60 * 60); + const hour = Math.floor(totalSeconds / 3600) % 24; + const minute = Math.floor((totalSeconds / 60) % 60); + return (minute % 60 === 0) ? `${hour}:00` : ''; + }); + } - return chartJSNodeCanvas.renderToBuffer(configuration) - .then((buffer) => { - let outputPath = path.join(process.cwd(), `./images/${guild_id}.${server_uuid}.png`); - return new Promise((resolve, reject) => { - fs.writeFile(outputPath, buffer, (err) => { - if (err) { - logger.error(`Error writing graph to file: ${err}`); - reject(err); - } else { - logger.debug(`Graph written to file`); - resolve(outputPath); - } - }); - }); - }) - .catch(error => { - logger.error(`Error creating graph: ${error}`); + let configuration = {}; + // Once the graph types are implemented, i will change this so it will check which graph to use based on user input + if (true) { + configuration = graphArea(serverCustomizationSettings.graph_settings, players, playersTrend, labels, serverCustomizationSettings.bot_settings.refresh_interval); + } + return chartJSNodeCanvas.renderToBuffer(configuration) + .then((buffer) => { + const outputPath = join(process.cwd(), `./images/${guildID}.${serverUUID}.png`); + return new Promise((resolve, reject) => { + writeFile(outputPath, buffer, (err) => { + if (err) { + logger.error(`Error writing graph to file: ${err}`); + reject(err); + } else { + logger.debug(`Graph written to file`); + resolve(outputPath); + } + }); }); + }) + .catch((error) => { + logger.error(`Error creating graph: ${error}`); + }); } -module.exports = createGraph; \ No newline at end of file + +module.exports = createGraph; diff --git a/discord/index.js b/discord/index.js index 58bef25..ed33609 100644 --- a/discord/index.js +++ b/discord/index.js @@ -1,46 +1,26 @@ const { ShardingManager } = require('discord.js'); -const path = require('path'); -const { getGuilds } = require('./api'); -require('dotenv').config({ path: '../.env' }); -const logger = require('./utils/logger'); +const { getGuildCount } = require('./bot'); -process.on('unhandledRejection', (error) => { - logger.error('Unhandled promise rejection:', error); -}); +const SHARDING_THRESHOLD = 2500; // Number of guilds to enable sharding -process.on('error', (error) => { - logger.error('Unhandled error:', error); -}); +async function startBot() { + const guildCount = await getGuildCount(); -async function setup() { - logger.info('Starting Server Query'); - logger.info('Version: v1.0.0-alpha.2'); + if (guildCount < SHARDING_THRESHOLD) { + // Run without sharding + require('./bot'); + } else { + // Run with sharding + const manager = new ShardingManager('./bot.js', { + totalShards: 'auto', + }); - // Guild threshold before sharding - const guildThreshold = 2500; - logger.debug(`Guild Threshold: ${guildThreshold}`); + manager.on('shardCreate', shard => { + console.log(`Launched shard ${shard.id}`); + }); - let guilds = await getGuilds(); - let numGuilds = Object.keys(guilds.data).length; - logger.debug(`Guilds : ${numGuilds}`); - - // Calculate the number of shards we need - let numShards = Math.ceil(numGuilds / guildThreshold); - logger.debug(`Number of Shards: ${numShards}`); - - // Start the sharding manager - const manager = new ShardingManager(path.join(__dirname, 'bot.js'), { - token: process.env.BOT_TOKEN, - totalShards: numShards - }); - - logger.info('Starting the Sharding Manager'); - - // Spawn shards - manager.spawn(); - - // Log when a shard is created - manager.on('shardCreate', shard => logger.info(`Shard ${shard.id} created`)); + manager.spawn(); + } } -setup(); +startBot(); \ No newline at end of file diff --git a/discord/query/index.js b/discord/query/index.js index cd1c097..f32c24e 100644 --- a/discord/query/index.js +++ b/discord/query/index.js @@ -2,43 +2,48 @@ const Gamedig = require('gamedig'); const packageData = require('./packageData'); const logger = require('../utils/logger'); -async function queryServer(ip, query_port, query_protocol, guild_id, server_uuid) { - logger.debug('Starting server query...'); - - const maxRetries = 5; - let retries = 0; - let state; - while (retries < maxRetries) { - logger.debug(`Attempting to query server. Attempt: ${retries + 1}`); - try { - logger.debug(`Querying game server with UUID: ${server_uuid}`); - state = await Gamedig.query({ - type: query_protocol, - host: ip, - port: query_port - }); - logger.debug(`Successfully queried game server with UUID: ${server_uuid}`); - break; - } catch (error) { - logger.warn(`Failed to query game server with UUID: ${server_uuid}. Error: ${error}`); - retries++; - if (retries < maxRetries) { - logger.warn(`Retrying query (${retries}/${maxRetries})...`); - } - } +/** + * + * @param {*} ip + * @param {*} queryPort + * @param {*} queryProtocol + * @param {*} guildID + * @param {*} serverUUID + * @return {object} + */ +async function serverQuery(ip, queryPort, queryProtocol, guildID, serverUUID) { + logger.debug('Starting server query...'); + const maxRetries = 5; + let retries = 0; + let state; + while (retries < maxRetries) { + logger.debug(`Attempting to query server. Attempt: ${retries + 1}`); + try { + logger.debug(`Querying game server with UUID: ${serverUUID}`); + state = await Gamedig.query({ + type: queryProtocol, + host: ip, + port: queryPort, + }); + logger.debug(`Successfully queried game server with UUID: ${serverUUID}`); + break; + } catch (error) { + logger.warn(`Failed to query game server with UUID: ${serverUUID}. Error: ${error}`); + retries++; + if (retries < maxRetries) { + logger.warn(`Retrying query (${retries}/${maxRetries})...`); + } } - - logger.debug('Finished querying game server.'); - - if (!state) { - logger.warn('Server query failed after maximum retries. Treating server as offline.'); - state = undefined; - } - - logger.debug('Packaging data...'); - const packagedData = await packageData(state, guild_id, server_uuid); - logger.debug('Data packaged successfully. Sending data to handleServer.'); - return packagedData; + } + logger.debug('Finished querying game server.'); + if (!state) { + logger.warn('Server query failed after maximum retries. Treating server as offline.'); + state = undefined; + } + logger.debug('Sending info to packageData!'); + const packagedData = await packageData(state, guildID, serverUUID); + logger.debug('Data packaged successfully. Sending data to handleServer.'); + return packagedData; } -module.exports = queryServer; +module.exports = serverQuery; diff --git a/discord/query/packageData.js b/discord/query/packageData.js index 2053ddc..8858ce1 100644 --- a/discord/query/packageData.js +++ b/discord/query/packageData.js @@ -1,84 +1,79 @@ -const { writeServerInfo, getServerInfo } = require('../api/index'); +const {writeServerInfo, getServerInfo} = require('../api/index'); const logger = require('../utils/logger'); -async function packageData(state, guild_id, server_uuid) { - logger.debug('Packaging data...'); - - let server_info = {}; - let info = {}; - let did_restart = ''; - let map = ''; - - logger.debug('Getting server info...'); - await getServerInfo(guild_id, server_uuid) - .then(response => { - info = response.data; - logger.debug('Got server info.'); - }) - .catch(error => { - if (error.response && error.response.status === 404) { - logger.warn('Server info not found, initializing new server info.'); - info = { - map: null, - players: [], - ping: [], - status: null, - last_restart: null, - message_id: null, - }; - } else { - logger.error(`Error getting server info: ${error}`); - throw error; - } - }); - - logger.debug('Setting server info details...'); - if (!info.status && state) { - did_restart = new Date().toISOString(); - } else { - did_restart = info.last_restart; - } - if (!state) { - if (info.map) { - map = info.map; - } else { - map = 'Unavailable'; - } - } else { - map = state.map; +/** + * + * @param {*} guildID + * @param {*} serverUUID + * @return {object} + */ +async function fetchServerInfo(guildID, serverUUID) { + try { + const response = await getServerInfo(guildID, serverUUID); + return response.data; + } catch (error) { + if (error.response && error.response.status === 404) { + logger.warn('Server info not found, initializing new server info.'); + return { + map: null, + players: [], + ping: [], + status: null, + last_restart: null, + }; } + logger.error(`Error getting server info: ${error}`); + throw error; + } +} - if (!state) { - server_info = { - name: null, - map: map, - active_players: -1, - max_players: -1, - players: null, - ping: -1, - status: false, - last_restart: 'Offline', - message_id: info.message_id ? info.message_id : null, - }; - } else { - server_info = { - name: state.name, - map: map, - active_players: state.players.length, - max_players: state.maxplayers, - players: state.players.map(player => player), - ping: state.ping, - status: true, - last_restart: did_restart, - message_id: info.message_id ? info.message_id : null, - }; - } +/** + * + * @param {*} serverInfo + * @param {*} state + * @return {object} + */ +function buildQueryState(serverInfo, state) { + const didRestart = !serverInfo.status && state ? new Date().toISOString() : serverInfo.last_restart; + const map = (!state ? serverInfo.map || 'Unavailable' : state.map); + + return state ? { + name: state.name, + map: map, + active_players: state.players.length, + max_players: state.maxplayers, + players: state.players.map((player) => player), + ping: state.ping, + status: true, + last_restart: didRestart, + } : { + name: null, + map: map, + active_players: -1, + max_players: -1, + players: null, + ping: -1, + status: false, + last_restart: 'Offline', + }; +} - logger.debug('Writing server info to server info JSON...'); - await writeServerInfo(guild_id, server_uuid, server_info); - logger.debug('Finished writing server info to server info JSON.'); - logger.debug('Packaged data successfully. Sending data back to queryServer.'); - return { server_info }; +/** + * + * @param {*} state + * @param {*} guildID + * @param {*} serverUUID + * @return {object} + */ +async function packageData(state, guildID, serverUUID) { + logger.debug('Packaging data...'); + const serverInfo = await fetchServerInfo(guildID, serverUUID); + const queryState = buildQueryState(serverInfo, state); + logger.debug('Writing server info to server info JSON...'); + await writeServerInfo(guildID, serverUUID, queryState); + logger.debug('Finished writing server info to server info JSON.'); + logger.debug('Packaged data successfully. Sending data back to queryServer.'); + return queryState; } -module.exports = packageData; \ No newline at end of file +module.exports = packageData; diff --git a/discord/utils/logger.js b/discord/utils/logger.js index c9cfd85..059cc6e 100644 --- a/discord/utils/logger.js +++ b/discord/utils/logger.js @@ -1,4 +1,4 @@ -require('dotenv').config({ path: '../.env' }); +require('dotenv').config({path: '../.env'}); const fs = require('fs'); const path = require('path'); const schedule = require('node-schedule'); @@ -9,91 +9,110 @@ const logFile = path.join(logDir, '_latest.log'); // Ensure logs directory exists if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir); + fs.mkdirSync(logDir); } // Ensure _latest.txt exists if (!fs.existsSync(logFile)) { - fs.writeFileSync(logFile, ''); + fs.writeFileSync(logFile, ''); } // Debug log function +/** + * Debug log function + * @param {...any} messages + */ function debug(...messages) { - writeLog('DEBUG', messages, chalk.green); + writeLog('DEBUG', messages, chalk.green); } -// Info log function +/** + * Info log function + * @param {...any} messages + */ function info(...messages) { - writeLog('INFO ', messages, chalk.white); + writeLog('INFO ', messages, chalk.white); } -// Warning log function +/** + * Warning log function + * @param {...any} messages + */ function warn(...messages) { - writeLog('WARN ', messages, chalk.yellow); + writeLog('WARN ', messages, chalk.yellow); } -// Error log function +/** + * Error log function + * @param {...any} messages + */ function error(...messages) { - writeLog('ERROR', messages, chalk.red); + writeLog('ERROR', messages, chalk.red); } -// Fatal log function +/** + * Fatal log function + * @param {...any} messages + */ function fatal(...messages) { - writeLog('FATAL', messages, chalk.magenta); + writeLog('FATAL', messages, chalk.magenta); } // Function to write a log message with a given type +/** + * Function to write a log message with a given type + * @param {*} type + * @param {*} messages + * @param {*} color + */ function writeLog(type, messages, color) { - let date = new Date(); - let timestamp = chalk.cyan(`[ ${date.toISOString()} ]`); - let coloredType = color(`[ ${type} ]`); - let logMessage = messages.map(message => { - if (typeof message === 'object') { - const cache = new Set(); - return JSON.stringify(message, (key, value) => { - if (typeof value === 'object' && value !== null) { - if (cache.has(value)) { - // Duplicate reference found, discard key - return; - } - // Store value in our collection - cache.add(value); - } - return value; - }, 2); - } else { - return message; + const date = new Date(); + const timestamp = chalk.cyan(`[ ${date.toISOString()} ]`); + const coloredType = color(`[ ${type} ]`); + const logMessage = messages.map((message) => { + if (typeof message === 'object') { + const cache = new Set(); + return JSON.stringify(message, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (cache.has(value)) { + return; + } + // Store value in our collection + cache.add(value); } - }).join(' '); - - if (type != 'DEBUG') { - let consoleLogMessage = `${timestamp} | ${coloredType} | ${chalk.white(logMessage)}`; - console.log(consoleLogMessage); - } else if (process.env.USE_DEBUG === 'true' && type === 'DEBUG') { - let consoleLogMessage = `${timestamp} | ${coloredType} | ${chalk.white(logMessage)}`; - console.log(consoleLogMessage); + return value; + }, 2); + } else { + return message; } - - fs.appendFileSync(logFile, `[ ${date.toISOString()} ] | [ ${type} ] | ${logMessage}\n`); + }).join(' '); + if (type != 'DEBUG') { + const consoleLogMessage = `${timestamp} | ${coloredType} | ${chalk.white(logMessage)}`; + console.log(consoleLogMessage); + } else if (process.env.USE_DEBUG === 'true' && type === 'DEBUG') { + const consoleLogMessage = `${timestamp} | ${coloredType} | ${chalk.white(logMessage)}`; + console.log(consoleLogMessage); + } + fs.appendFileSync(logFile, `[ ${date.toISOString()} ] | [ ${type} ] | ${logMessage}\n`); } -// Schedule job to rotate logs -schedule.scheduleJob('0 0 * * *', function(){ - let date = new Date(); - let filename = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.txt`; - let newFilePath = path.join(logDir, filename); - - // Rename _latest.txt to the new file - fs.renameSync(logFile, newFilePath); - - // Create a new _latest.txt for the next day's logs - fs.writeFileSync(logFile, ''); +/** + * Schedule job to rotate logs + */ +schedule.scheduleJob('0 0 * * *', function() { + const date = new Date(); + const filename = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.txt`; + const newFilePath = path.join(logDir, filename); + // Rename _latest.txt to the new file + fs.renameSync(logFile, newFilePath); + // Create a new _latest.txt for the next day's logs + fs.writeFileSync(logFile, ''); }); module.exports = { - debug, - info, - warn, - error, - fatal -}; \ No newline at end of file + debug, + info, + warn, + error, + fatal, +}; From eb81b2e3ae60e938c7123c9430d6812d1005b800 Mon Sep 17 00:00:00 2001 From: ihasTaco <77412945+ihasTaco@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:30:46 -0600 Subject: [PATCH 4/4] Added Looping Functionality Ive added looping functionality to the bot, also I've fixed an issue where the guild wasnt being initialized if the bot was added when the bot was running --- discord/bot.js | 110 +++++++++++++++++++++++++++++++++++++++-------- discord/index.js | 42 +++++++++--------- 2 files changed, 113 insertions(+), 39 deletions(-) diff --git a/discord/bot.js b/discord/bot.js index ac8deec..4a3ac97 100644 --- a/discord/bot.js +++ b/discord/bot.js @@ -8,6 +8,8 @@ const logger = require('./utils/logger'); const client = new Client({intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.Guilds]}); +// This keeps track of all the servers intializations, and only allows servers to be initialized once +const initializedServers = new Set(); /** * Retrieves the guild object from the Discord API using a specified guild ID. * If the guild is not found, a warning is logged, and the function returns null. @@ -144,6 +146,7 @@ async function generateGraph(guildID, serverUUID, serverCustomizationSettings) { * @return {Object | null} - The created message object if successful, or null if an error occurred (e.g., missing permissions, rate-limited). */ async function createMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState) { + console.log(channel); logger.debug(`Creating a new message for guild ${guildID} and server ${serverUUID} in channel: ${channel.id}`); let message; try { @@ -203,30 +206,101 @@ async function editMessage(guildID, serverUUID, channel, serverCustomizationSett } } -client.on('ready', async () => { +/** + * Queries a server, generates a player graph, and creates/edits an embed. + * @param {*} guildID + * @param {*} serverUUID + * @param {*} serverCustomizationSettings + */ +async function queryAndUpdateServer(guildID, serverUUID, serverCustomizationSettings) { + const serverInfo = await getServerInfo(guildID, serverUUID); + const guild = await getGuild(guildID); + const channel = await getChannel(guild, guildID, serverCustomizationSettings); + const queryState = await queryServer(guildID, serverUUID, serverCustomizationSettings); + // add an if statement to stop generating the graph if the setting is disabled + const graphURL = await generateGraph(guildID, serverUUID, serverCustomizationSettings); + try { + if (serverInfo.data.message_id) { + await editMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState, serverInfo); + } else { + await createMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState); + } + } catch { + logger.warn(`Couldn't read message ID for guild ${guildID} and server ${serverUUID}`); + logger.warn(`Creating a new message, for guild ${guildID} in channel ${channel.id}`); + await createMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState); + } +} + +/** + * Validates the server settings to ensure all required fields are present. + * @param {*} serverCustomizationSettings + * @return {boolean} True if all required settings are present, false otherwise. + */ +function areServerSettingsValid(serverCustomizationSettings) { + const botSettings = serverCustomizationSettings.bot_settings; + const serverSettings = serverCustomizationSettings.server_settings; + + return ( + botSettings.channel_id && + serverSettings.ip && + serverSettings.connection_port && + serverSettings.query_port && + serverSettings.game && + serverSettings.query_protocol + ); +} + +/** + * Sets up the query and update cycle for all servers in all guilds. + */ +async function setupAllServers() { const guilds = Object.keys(await getGuilds()); - for (const guild of guilds) { - const guildID = guild; + for (const guildID of guilds) { const servers = await getServerUUIDsForGuild(guildID); if (Object.keys(servers.data).length > 0) { - for (const server of servers.data) { - const serverUUID = server; - const serverCustomizationSettings = await getServerSettings(guildID, serverUUID); - const serverInfo = await getServerInfo(guildID, serverUUID); - const guild = await getGuild(guildID); - const channel = await getChannel(guild, guildID, serverCustomizationSettings); - const queryState = await queryServer(guildID, serverUUID, serverCustomizationSettings); - const graphURL = await generateGraph(guildID, serverUUID, serverCustomizationSettings); - if (serverInfo.data.message_id) { - await editMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState, serverInfo); - } else { - await createMessage(guildID, serverUUID, channel, serverCustomizationSettings, graphURL, queryState); + const serverPromises = servers.data.map(async (serverUUID) => { + if (!initializedServers.has(serverUUID)) { + const serverCustomizationSettings = await getServerSettings(guildID, serverUUID); + if (areServerSettingsValid(serverCustomizationSettings)) { + initializedServers.add(serverUUID); + return initializeServerQueryCycle(guildID, serverUUID); + } else { + logger.warn(`Settings are not valid for guild ${guildID} and server ${serverUUID}`); + } } - } - } else { - continue; + }).filter(Boolean); // Filter out undefined values + await Promise.all(serverPromises); } } +} + +/** + * Initializes the query and update cycle for a specific server. + * @param {*} guildID + * @param {*} serverUUID + */ +async function initializeServerQueryCycle(guildID, serverUUID) { + const serverCustomizationSettings = await getServerSettings(guildID, serverUUID); + const refreshInterval = serverCustomizationSettings.bot_settings.refresh_interval; + + await queryAndUpdateServer(guildID, serverUUID, serverCustomizationSettings); + + setInterval(async () => { + const startTime = Date.now(); + await queryAndUpdateServer(guildID, serverUUID, serverCustomizationSettings); + const endTime = Date.now(); + const executionTime = endTime - startTime; + logger.debug(`Server ${serverUUID} query execution time: ${executionTime/1000}s`); + logger.debug(`Server ${serverUUID}: Waiting ${refreshInterval - executionTime/1000}s before refreshing`); + }, refreshInterval * 1000); +} + +client.on('ready', async () => { + await setupAllServers(); + setInterval(async () => { + await setupAllServers(); + }, 60000); }); client.login(process.env.BOT_TOKEN); diff --git a/discord/index.js b/discord/index.js index ed33609..58b2548 100644 --- a/discord/index.js +++ b/discord/index.js @@ -1,26 +1,26 @@ -const { ShardingManager } = require('discord.js'); -const { getGuildCount } = require('./bot'); +const {ShardingManager} = require('discord.js'); +const {getGuildCount} = require('./bot'); -const SHARDING_THRESHOLD = 2500; // Number of guilds to enable sharding +const SHARDING_THRESHOLD = 2500; +/** + * Checks if the bot is more than 2500 guilds, and will start the bot with sharding or not + */ async function startBot() { - const guildCount = await getGuildCount(); - - if (guildCount < SHARDING_THRESHOLD) { - // Run without sharding - require('./bot'); - } else { - // Run with sharding - const manager = new ShardingManager('./bot.js', { - totalShards: 'auto', - }); - - manager.on('shardCreate', shard => { - console.log(`Launched shard ${shard.id}`); - }); - - manager.spawn(); - } + const guildCount = await getGuildCount(); + if (guildCount < SHARDING_THRESHOLD) { + // Run without sharding + require('./bot'); + } else { + // Run with sharding + const manager = new ShardingManager('./bot.js', { + totalShards: 'auto', + }); + manager.on('shardCreate', (shard) => { + console.log(`Launched shard ${shard.id}`); + }); + manager.spawn(); + } } -startBot(); \ No newline at end of file +startBot();