diff --git a/README.md b/README.md index a031c91..1bc9e65 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## Introduction -PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. +PteroStats is a Discord App/Bot designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. ## Preview PteroStats Image Preview @@ -29,10 +29,10 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa 1. [Create your Discord App/Bot](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). 2. [Invite your Discord App/Bot to your Discord server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). 3. Download this repository: - - [Downloading this repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/main.zip) and extract it. + - [Download this repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/main.zip) and extract it. - Using Git: Run `git clone https://github.com/HirziDevs/PteroStats.git` in the command line. 4. Run `npm install` in the root directory of the app/bot files. -5. Run `node index` and answer the provided questions to set up the app/bot. +5. Run `node index` and answer the prompted questions to set up the app/bot. Setup diff --git a/handlers/UptimeFormatter.js b/handlers/UptimeFormatter.js index 711d9e9..1d214ca 100644 --- a/handlers/UptimeFormatter.js +++ b/handlers/UptimeFormatter.js @@ -1,4 +1,4 @@ -module.exports = function UptimeFormatter(time) { +module.exports = function uptimeFormatter(time) { let text = [] const days = Math.floor(time / 86400000); const hours = Math.floor(time / 3600000) % 24; diff --git a/handlers/app.js b/handlers/application.js similarity index 93% rename from handlers/app.js rename to handlers/application.js index 69105e1..3cbc9cf 100644 --- a/handlers/app.js +++ b/handlers/application.js @@ -1,11 +1,13 @@ -require("dotenv").config() +require("dotenv").config(); const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const fs = require("node:fs"); const cliColor = require("cli-color"); -const config = require("./config.js"); +const path = require("node:path"); +const config = require("./configuration.js"); const convertUnits = require("./convertUnits.js"); const getStats = require("./getStats.js"); const webhook = require("./webhook.js"); +const uptimeFormatter = require("./uptimeFormatter.js"); module.exports = function App() { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting app...")); @@ -34,7 +36,7 @@ module.exports = function App() { if (config.log_error) console.error(error) console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is currently offline.")); - fs.readFile(require('node:path').join(__dirname, "../cache.json"), (err, data) => { + fs.readFile(path.join(__dirname, "../cache.json"), (err, data) => { if (err) { createMessage({ cache: false, panel: false }); return console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Last cache was not found!")); @@ -47,8 +49,8 @@ module.exports = function App() { .setTitle("Panel Offline") .setColor("ED4245") .setDescription(`Panel is currently offline`) - ) - results.uptime = false + ); + results.uptime = false; fs.writeFileSync("cache.json", JSON.stringify(results, null, 2), "utf8"); createMessage({ cache: true, @@ -132,7 +134,7 @@ module.exports = function App() { `Disk : ${convertUnits(node.attributes.allocated_resources.disk, node.attributes.disk, config.nodes_settings.unit)}` + (node.attributes?.allocated_resources?.cpu ? `\nCPU : ${node.attributes?.allocated_resources?.cpu || 0}%` : "") + (config.nodes_settings.servers ? `\nServers: ${node.attributes.relationships.servers}${config.nodes_settings.allocations_as_max_servers ? ` / ${node.attributes.relationships.allocations}` : ""}` : "") + - (config.nodes_settings.uptime ? `\nUptime : ${node.uptime ? require("./UptimeFormatter.js")(Date.now() - node.uptime) : "N/A"}` : "") + + (config.nodes_settings.uptime ? `\nUptime : ${node.uptime ? uptimeFormatter(Date.now() - node.uptime) : "N/A"}` : "") + "```" }); }); @@ -170,7 +172,7 @@ module.exports = function App() { `Nodes : ${nodes.length}\n` + (config.panel_settings.servers ? `Servers: ${servers || "Unknown"}\n` : "") + (config.panel_settings.users ? `Users : ${users || "Unknown"}\n` : "") + - (config.panel_settings.uptime ? `Uptime : ${uptime ? require("./UptimeFormatter.js")(Date.now() - uptime) : "N/A"}\n` : "") + + (config.panel_settings.uptime ? `Uptime : ${uptime ? uptimeFormatter(Date.now() - uptime) : "N/A"}\n` : "") + "```" }); @@ -194,10 +196,12 @@ module.exports = function App() { if (config.button.enable) { for (const row of ["row1", "row2", "row3", "row4", "row5"]) { - if (config.button[row] && config.button[row].length > 0) - if (config.button[row].slice(0, 5).filter(button => button.label && button.url).length > 0) components.push( + const buttons = config.button[row]?.slice(0, 5).filter(button => button.label && button.url); + + if (buttons && buttons.length > 0) { + components.push( new ActionRowBuilder().addComponents( - config.button[row].slice(0, 5).filter(button => button.label && button.url).map(button => + buttons.map(button => new ButtonBuilder() .setLabel(button.label) .setURL(button.url) @@ -205,6 +209,7 @@ module.exports = function App() { ) ) ); + } } } @@ -223,16 +228,16 @@ module.exports = function App() { await channel.send({ content: config.message.content || null, embeds, components }); } } catch (error) { - DiscordErrorHandler(error); + handleDiscordError(error); } } - function DiscordErrorHandler(error) { + function handleDiscordError(error) { try { if (error.rawError?.code === 429) { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Error 429 | Your IP has been rate limited by either Discord or your website. If it's a rate limit with Discord, you must wait. If it's a issue with your website, consider whitelisting your server IP.")); } else if (error.rawError?.code === 403) { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("FORBIDDEN | The channel ID you provided is incorrect. Please double check you have the right ID. If you're not sure, read our documentation: https://github.com/HirziDevs/PteroStats#getting-channel-id")); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("FORBIDDEN | The channel ID you provided is incorrect. Please double check you have the right ID. If you're not sure, read our documentation: \n>>https://github.com/HirziDevs/PteroStats#getting-channel-id<<")); } else if (error.code === "ENOTFOUND") { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ENOTFOUND | DNS Error. Ensure your network connection and DNS server are functioning correctly.")); } else if (error.rawError?.code === 50001) { diff --git a/handlers/config.js b/handlers/configuration.js similarity index 76% rename from handlers/config.js rename to handlers/configuration.js index 86322fc..ccf1cce 100644 --- a/handlers/config.js +++ b/handlers/configuration.js @@ -2,11 +2,11 @@ const fs = require("node:fs"); const yaml = require("js-yaml"); const cliColor = require("cli-color"); -console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Loading configuration...")) +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Loading configuration...")); let config = yaml.load(fs.readFileSync("./config.yml", "utf8")); if (fs.existsSync("config-dev.yml")) { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Using development configuration...")) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Using development configuration...")); config = yaml.load(fs.readFileSync("./config-dev.yml", "utf8")); } @@ -19,10 +19,10 @@ try { } if (config.version !== 9) { - console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/main/config.yml'); + console.error('Config Error | Invalid config version! The config has been updated. Please get the new config format from: \n>> https://github.com/HirziDevs/PteroStats/blob/main/config.yml <<'); process.exit(); } -console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Configuration loaded")) +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Configuration loaded")); -module.exports = config \ No newline at end of file +module.exports = config; \ No newline at end of file diff --git a/handlers/convertUnits.js b/handlers/convertUnits.js index d0dce92..6a116f1 100644 --- a/handlers/convertUnits.js +++ b/handlers/convertUnits.js @@ -1,15 +1,15 @@ const prettyBytes = require('prettier-bytes'); -module.exports = function convertUnits(value1, value2, unit) { +module.exports = function convertUnits(value, max, unit) { unit = unit.toUpperCase(); switch (unit) { case 'PERCENTAGE': case 'PERCENT': - const percentage = Math.floor((value1 / value2) * 100); + const percentage = Math.floor((value / max) * 100); return `${!percentage ? 0 : percentage}%`; case 'BYTE': - return `${prettyBytes(value1 * 1000000)} / ${value2 === 0 ? "Unlimited" : prettyBytes(value2 * 1000000)}`; + return `${prettyBytes(value * 1000000)} / ${max === 0 ? "Unlimited" : prettyBytes(max * 1000000)}`; default: - return `${value1.toLocaleString()} ${unit}/${value2 === 0 ? "Unlimited" : `${value2.toLocaleString()} ${unit}`}`; + return `${value.toLocaleString()} ${unit}/${max === 0 ? "Unlimited" : `${max.toLocaleString()} ${unit}`}`; } } \ No newline at end of file diff --git a/handlers/getNodeConfiguration.js b/handlers/getNodeConfiguration.js index ebbb4b7..c704d15 100644 --- a/handlers/getNodeConfiguration.js +++ b/handlers/getNodeConfiguration.js @@ -1,4 +1,4 @@ -const config = require("./config.js"); +const config = require("./configuration.js"); module.exports = async function getNodeConfiguration(id) { return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/nodes/${id}/configuration`, { diff --git a/handlers/getNodesDetails.js b/handlers/getNodesDetails.js index a4f75e5..869ee27 100644 --- a/handlers/getNodesDetails.js +++ b/handlers/getNodesDetails.js @@ -1,5 +1,5 @@ const cliColor = require("cli-color"); -const config = require("./config.js"); +const config = require("./configuration.js"); const axios = require("axios"); module.exports = async function getAllNodes() { diff --git a/handlers/getServers.js b/handlers/getServers.js index 5e96b77..1a8aab5 100644 --- a/handlers/getServers.js +++ b/handlers/getServers.js @@ -1,4 +1,4 @@ -const config = require("./config.js"); +const config = require("./configuration.js"); const cliColor = require("cli-color"); module.exports = async function getServers() { diff --git a/handlers/getStats.js b/handlers/getStats.js index 7579e9d..58aa52a 100644 --- a/handlers/getStats.js +++ b/handlers/getStats.js @@ -1,27 +1,34 @@ -const config = require("./config.js"); +const { EmbedBuilder } = require("discord.js"); const fs = require("node:fs"); const cliColor = require("cli-color"); +const path = require('node:path'); const webhook = require("./webhook.js"); -const { EmbedBuilder } = require("discord.js"); +const config = require("./configuration.js"); +const getNodesDetails = require("./getNodesDetails.js"); +const getNodeConfiguration = require("./getNodeConfiguration.js"); +const getWingsStatus = require("./getWingsStatus.js"); +const promiseTimeout = require("./promiseTimeout.js"); +const getServers = require("./getServers.js"); +const getUsers = require("./getUsers.js"); module.exports = async function getStats() { let cache = (() => { try { - return JSON.parse(fs.readFileSync(require('node:path').join(__dirname, "../cache.json"))) + return JSON.parse(fs.readFileSync(path.join(__dirname, "../cache.json"))) } catch { return false } })() console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel nodes...")) - const nodesStats = await require("./getNodesDetails.js")(); + const nodesStats = await getNodesDetails(); if (!nodesStats) throw new Error("Failed to get nodes attributes"); const statusPromises = nodesStats.slice(0, config.nodes_settings.limit).map(async (node) => { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Fetching ${cliColor.blueBright(node.attributes.name)} configuration...`)) - const nodeConfig = await require("./getNodeConfiguration.js")(node.attributes.id); + const nodeConfig = await getNodeConfiguration(node.attributes.id); console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Checking ${cliColor.blueBright(node.attributes.name)} wings status...`)) - const nodeStatus = await require("./promiseTimeout.js")(require("./getWingsStatus.js")(node, nodeConfig.token), config.timeout * 1000); + const nodeStatus = await promiseTimeout(getWingsStatus(node, nodeConfig.token), config.timeout * 1000); let nodeUptime = cache ? (() => { return cache.nodes.find((n) => n.attributes.id === node.attributes.id)?.uptime || Date.now() @@ -72,8 +79,8 @@ module.exports = async function getStats() { uptime: cache ? (() => { return cache.uptime || Date.now() })() : Date.now(), - servers: await require("./getServers.js")(), - users: await require("./getUsers.js")(), + servers: await getServers(), + users: await getUsers(), nodes: await Promise.all(statusPromises), isPanelDown: !cache.uptime, timestamp: Date.now() diff --git a/handlers/getUsers.js b/handlers/getUsers.js index 19e403d..1b71b3d 100644 --- a/handlers/getUsers.js +++ b/handlers/getUsers.js @@ -1,4 +1,4 @@ -const config = require("./config.js"); +const config = require("./configuration.js"); const cliColor = require("cli-color"); module.exports = async function getUsers() { diff --git a/handlers/getWingsStatus.js b/handlers/getWingsStatus.js index 69975df..68d5c10 100644 --- a/handlers/getWingsStatus.js +++ b/handlers/getWingsStatus.js @@ -1,4 +1,4 @@ -const config = require("./config.js"); +const config = require("./configuration.js"); module.exports = async function getWingsStatus(node, nodeToken) { return fetch(`${node.attributes.scheme}://${node.attributes.fqdn}:${node.attributes.daemon_listen}/api/servers`, { diff --git a/handlers/setup.js b/handlers/setup.js index 78a88d8..37cbb13 100644 --- a/handlers/setup.js +++ b/handlers/setup.js @@ -2,6 +2,7 @@ const axios = require("axios") const cliColor = require("cli-color") const { Client, GatewayIntentBits } = require("discord.js") const fs = require("fs") +const application = require("./application.js"); const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout @@ -15,6 +16,14 @@ const questions = [ "Please enter your channel ID: " ]; +const Question = { + panelName: 0, + panelUrl: 1, + panelApiKey: 2, + botToken: 3, + channelId: 4, +} + const answers = []; const isValidURL = (url) => { @@ -37,18 +46,18 @@ module.exports = function Setup() { readline.question('> ', answer => { let isValid = true; - if (index === 1 && !isValidURL(answer)) { + if (index === Question.panelUrl && !isValidURL(answer)) { console.log(cliColor.redBright('❌ Invalid Panel URL. Please enter a valid URL. Example Correct URL: "https://panel.example.com"')); isValid = false; - } else if (index === 2 && !/^(plcn_|ptlc_|peli_|ptla_)/.test(answer)) { + } else if (index === Question.panelApiKey && !/^(plcn_|ptlc_|peli_|ptla_)/.test(answer)) { console.log(cliColor.redBright("❌ Invalid Panel API key. It must start with 'plcn_' or 'ptlc_'.")); isValid = false; - } else if (index === 4 && !/^\d+$/.test(answer)) { + } else if (index === Question.channelId && !/^\d+$/.test(answer)) { console.log(cliColor.redBright("❌ Invalid Channel ID. It must be a number.")); isValid = false; } - if (index === 2 && /^(peli_|ptla_)/.test(answer)) console.log(cliColor.yellow("The use of Application API keys are deprecated, you should use Client API keys")); + if (index === Question.panelApiKey && /^(peli_|ptla_)/.test(answer)) console.log(cliColor.yellow("The use of Application API keys are deprecated, you should use Client API keys")); if (isValid) { answers.push(isValidURL(answer) ? new URL(answer).origin : answer); @@ -58,11 +67,11 @@ module.exports = function Setup() { } }); } else { - axios(`${new URL(answers[1]).origin}/api/application/nodes?include=servers,location,allocations`, { + axios(`${new URL(answers[Question.panelUrl]).origin}/api/application/nodes?include=servers,location,allocations`, { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${answers[2]}` + "Authorization": `Bearer ${answers[Question.panelApiKey]}` }, }).then(() => { console.log(" \n" + cliColor.green("✓ Valid Panel Credentials.")); @@ -70,15 +79,15 @@ module.exports = function Setup() { intents: [GatewayIntentBits.Guilds] }) - client.login(answers[3]).then(async () => { + client.login(answers[Question.botToken]).then(async () => { console.log(cliColor.green("✓ Valid Discord Bot")); - client.channels.fetch(answers[4]).then(() => { + client.channels.fetch(answers[Question.channelId]).then(() => { console.log(cliColor.green("✓ Valid Discord Channel")); - fs.writeFileSync(".env", `PanelURL=${answers[1]}\nPanelKEY=${answers[2]}\nDiscordBotToken=${answers[3]}\nDiscordChannel=${answers[4]}`, "utf8") + fs.writeFileSync(".env", `PanelURL=${answers[Question.panelUrl]}\nPanelKEY=${answers[Question.panelApiKey]}\nDiscordBotToken=${answers[Question.botToken]}\nDiscordChannel=${answers[Question.channelId]}`, "utf8") fs.writeFileSync("config.yml", fs.readFileSync("./config.yml", "utf8").replaceAll("Hosting Panel", answers[0]).replaceAll("https://panel.example.com", answers[1]), "utf-8") console.log(" \n" + cliColor.green(`Configuration saved in ${cliColor.blueBright(".env")} and ${cliColor.blueBright("config.yml")}.\n `)); - require("./app.js")() + application() }).catch(() => { console.log(cliColor.redBright("❌ Invalid Channel ID.")); console.log(" \n" + cliColor.redBright("Please run the setup again and fill in the correct credentials.")); diff --git a/handlers/webhook.js b/handlers/webhook.js index 84db3e0..b1a79bf 100644 --- a/handlers/webhook.js +++ b/handlers/webhook.js @@ -1,5 +1,5 @@ const { WebhookClient, EmbedBuilder } = require("discord.js") -const config = require("./config") +const config = require("./configuration") const cliColor = require("cli-color") module.exports = function Webhook(embed) { diff --git a/index.js b/index.js index 64d16cd..fd4383c 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,8 @@ const fs = require("node:fs"); const cliColor = require("cli-color"); const package = require("./package.json"); +const setup = require("./handlers/setup.js"); +const application = require("./handlers/application.js"); console.log( ` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ \n` + @@ -18,6 +20,6 @@ console.log( ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return require("./handlers/setup.js")(); +if (!fs.existsSync(".env")) return setup(); -require("./handlers/app.js")(); \ No newline at end of file +application(); diff --git a/package.json b/package.json index 7419d15..647f718 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pterostats", "version": "4.0.0", - "description": "PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server.", + "description": "PteroStats is a Discord App/Bot designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server.", "license": "MIT", "repository": "HirziDevs/PteroStats", "homepage": "https://pterostats.znproject.my.id", diff --git a/setup.js b/setup.js index 0de3947..ae62cbf 100644 --- a/setup.js +++ b/setup.js @@ -1,6 +1,8 @@ const fs = require("node:fs"); const cliColor = require("cli-color"); const package = require("./package.json"); +const setup = require("./handlers/setup.js"); +const application = require("./handlers/application.js"); const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout @@ -22,7 +24,7 @@ console.log( ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return require("./handlers/setup.js")(); +if (!fs.existsSync(".env")) return setup(); console.log(cliColor.yellowBright( "Configuration is already set. Please select one of the following options:\n \n" + @@ -35,12 +37,12 @@ readline.question('> ', async (answer) => { switch (answer) { case '2': - require('./handlers/setup.js')(); + setup(); break; case '1': - require('./handlers/app.js')(); + application(); break; default: console.log(cliColor.redBright('Invalid input. Please type either 1 or 2.')); } -}); \ No newline at end of file +});