refactor: improve function names, code readability, and error handling

This commit is contained in:
hirzidevs
2024-09-14 09:39:08 +07:00
parent 9f2660d337
commit e6374da63d
16 changed files with 82 additions and 57 deletions

View File

@@ -7,7 +7,7 @@
</div>
## 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
<img alt="PteroStats Image Preview" src="https://usercontent.catto.pictures/hirzi/e6f6fe6a-8c0e-4c7a-8b73-d4af752324f4.png" width="300"/>
@@ -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.
<img alt="Setup" src="https://usercontent.catto.pictures/hirzi/b8645828-591d-4d52-b6d8-51f8df60440c.png" width="300"/>

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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
module.exports = config;

View File

@@ -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}`}`;
}
}

View File

@@ -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`, {

View File

@@ -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() {

View File

@@ -1,4 +1,4 @@
const config = require("./config.js");
const config = require("./configuration.js");
const cliColor = require("cli-color");
module.exports = async function getServers() {

View File

@@ -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()

View File

@@ -1,4 +1,4 @@
const config = require("./config.js");
const config = require("./configuration.js");
const cliColor = require("cli-color");
module.exports = async function getUsers() {

View File

@@ -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`, {

View File

@@ -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."));

View File

@@ -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) {

View File

@@ -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")();
application();

View File

@@ -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",

View File

@@ -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.'));
}
});
});