From c12148bc58adfdc7b5e57eac39032b2e5171e7e8 Mon Sep 17 00:00:00 2001 From: Lezetho <126505858+lezetho@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:13:11 -0400 Subject: [PATCH 01/59] Update package.json --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5aa4a68..8cf92a8 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ }, "dependencies": { "axios": "1.6.3", - "axios-retry": "^3.9.1", - "chalk": "^4.1.2", - "discord.js": "^14.14.1", - "js-yaml": "^4.1.0" + "axios-retry": "3.9.1", + "chalk": "4.1.2", + "discord.js": "14.14.1", + "js-yaml": "4.1.0" }, "engines": { "node": ">=16.9.0" From 12df9b4acf9a2e7e039a74259cd94eac6d6f1875 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Mon, 29 Jul 2024 19:48:25 +0700 Subject: [PATCH 02/59] Recode PteroStats and Support Pelican Panel --- .gitignore | 4 + Indo.md | 129 ------------------ README.md | 53 ++++---- config.yml | 120 +++++++++-------- events/ready.js | 29 ----- handlers/checkStatus.js | 144 -------------------- handlers/config.js | 24 ++++ handlers/convertUnits.js | 20 +++ handlers/getNodeConfiguration.js | 13 ++ handlers/getNodesDetails.js | 44 +++++++ handlers/getServers.js | 15 +++ handlers/getStats.js | 50 +++++++ handlers/getUsers.js | 15 +++ handlers/getWingsStatus.js | 12 ++ handlers/postStatus.js | 176 ------------------------- handlers/promiseTimeout.js | 9 ++ index.js | 217 +++++++++++++++++++++++++------ package.json | 44 +++---- 18 files changed, 493 insertions(+), 625 deletions(-) create mode 100644 .gitignore delete mode 100644 Indo.md delete mode 100644 events/ready.js delete mode 100644 handlers/checkStatus.js create mode 100644 handlers/config.js create mode 100644 handlers/convertUnits.js create mode 100644 handlers/getNodeConfiguration.js create mode 100644 handlers/getNodesDetails.js create mode 100644 handlers/getServers.js create mode 100644 handlers/getStats.js create mode 100644 handlers/getUsers.js create mode 100644 handlers/getWingsStatus.js delete mode 100644 handlers/postStatus.js create mode 100644 handlers/promiseTimeout.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecd64c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +config-dev.yml +package-lock.json +cache.json \ No newline at end of file diff --git a/Indo.md b/Indo.md deleted file mode 100644 index 3436700..0000000 --- a/Indo.md +++ /dev/null @@ -1,129 +0,0 @@ -
- -PteroStats Banner - -## Bahasa / Language -[[Indonesia]](https://github.com/HirziDevs/PteroStats/blob/dev/Indo.md) | [[Inggris]](https://github.com/HirziDevs/PteroStats/blob/dev/README.md) - -
- -## Pengenalan -PteroStats adalah bot yang dirancang untuk memeriksa status panel pterodactyl dan dikirim ke server discord - -## Contoh -- Test Panel - - Example - -- [Calvs Cloud](https://discord.gg/ssCQjhrBJN) - - Calvs Cloud - -## Instalasi - 1. [Mendapatkan apikey dari pterodactyl](#mendapatkan-apikey-dari-pterodactyl) - 2. [Membuat Discord Bot](#membuat-discord-bot) - 3. [Menginvite Discord Bot](#menginvite-discord-bot) - 4. [Mendapatkan Channel ID](#mendapatkan-channel-id) - 5. [Memulai Bot](#memulai-bot) - - - [Mengunakan custom emoji](#mengunakan-custom-emoji) - - [Blacklist Nodes](#blacklist-nodes) - -### Mendapatkan apikey dari pterodactyl -1. Pergi ke `panel admin pterodactyl` dan pergi ke `Application API` - - Admin Panel - -2. Klik tombol `Create New` - - Application API Page - -3. Set semua permission ke `read` dan untuk description kamu bisa mengisi apa saja - - Create Application API - -4. Copy apikey-nya. - - Application API List - -5. Paste panel apikeynya dan panel urlnya di config - - Panel Config - -### Membuat Discord Bot -Kalian bisa cek [website ini](https://discordjs.guide/preparations/setting-up-a-bot-application.html) - -Paste bot tokennya di config - -Bot Config - -### Menginvite Discord Bot -Kalian bisa cek [website ini](https://discordjs.guide/preparations/adding-your-bot-to-servers.html) - -### Mendapatkan Channel ID -1. Aktifkan `Developer Mode` di settings discord kamu - - Discord User Settings - -2. Klik kanan teks channel dan pilih `Copy ID` - - Right Click Channel - -3. Paste id channelnya di config - - Channel Config - -### Memulai Bot -1. Pastikan kamu telah melakukan semua yang ada diatas -2. Jalankan command `npm install` di folder yang berisi file bot -3. Jalankan command `node index` dan kamu selesai! - -Jika kamu mendapat masalah bisa dm `Hirzi#8701` didiscord atau join [server support kami](https://discord.gg/zv6maQRah3) - -### Mengunakan custom emoji -1. ketik `\` di server yang ada custom emojinya - - Type \ on the chat - -2. Pilih custom emoji yang kamu mau - - Select Custom Emoji - -3. Copy textnya! - - Copy Emoji ID - -4. Paste id emojinya di config - - Status Config - -### Blacklist Nodes -1. Pilih node yang ada di node list admin page - - Nodes List - -2. Cek urlnya dan copy id nodenya - - Node Id - -3. Masukan ke blacklist di config - - Blacklist Config - -Kamu bisa memasukan lebih dari 1 node untuk di blacklist - -Blacklist Config - -## Permission apikey - -Jika kamu mengalami error 403 coba aktifkan `read & write` di semua opsi permission - -Application API Permission - -## Links - -- [Pterodactyl Panel](https://pterodactyl.io) -- [Pterodactyl Api Documentation](https://dashflo/docs/api/pterodactyl/v1) -- [Pterodactyl Discord Server](https://discord.gg/pterodactyl) -- [PteroBot Support Server](https://discord.gg/zv6maQRah3) -- [PteroBot Support Server (Indonesia)](https://discord.gg/EYaFB7WSg6) \ No newline at end of file diff --git a/README.md b/README.md index e8a4391..5b11d67 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,20 @@
-PteroStats Banner +PteroStats Banner -## Language / Bahasa -[[English]](https://github.com/HirziDevs/PteroStats/blob/dev/README.md) | [[Indonesia]](https://github.com/HirziDevs/PteroStats/blob/dev/Indo.md)
- +### This is a development build! some features may not work. ## Introduction PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server ## Example - Test Panel - Example - -- [Calvs Cloud](https://discord.gg/ssCQjhrBJN) - - Calvs Cloud + Example ## Installation -1. [Getting apikey from pterodactyl](#getting-apikey-from-pterodactyl) +1. [Getting API key from pterodactyl](#getting-apikey-from-pterodactyl) 2. [Creating Discord Bot](#creating-discord-bot) 3. [Inviting Discord Bot](#inviting-discord-bot) 4. [Getting Channel ID](#getting-channel-id) @@ -29,24 +23,28 @@ PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and pos - [Using Custom Emoji](#using-custom-emoji) - [Blacklist Nodes](#blacklist-nodes) -### Getting apikey from pterodactyl -1. Go to your `pterodactyl admin page` and go to `Application API`. +### Getting API key from pterodactyl - Admin Panel +> [!WARNING] +> The use of Application API keys are **deprecated**, you should use **Client API keys** in the config file -2. Click on the `Create New` button +1. Go to your `Pterodactyl Panel` and go to `Account Page`. - Application API Page + Home -3. Set all options permission to `read` and for description you can put whatever you want +2. Click on the `API Credentials` button - Create Application API + Account Page -4. Copy the apikey. +3. Fill the `Description` and click on the `Create` button - Application API List + Create Client API Key -5. Paste the panel apikey and panel url at the config +4. Copy the API key. + + API Key + +5. Paste the panel API key and panel url at the config Panel Config @@ -78,10 +76,10 @@ Please refer to [this website](https://discordjs.guide/preparations/adding-your- 2. Run `npm install` in the root directory of the bot files. 3. Run `node index` and you are done. -if you need help contact me on discord `Hirzi#8701` or join [our discord support server](https://discord.gg/zv6maQRah3) +if you need help contact me on discord `@hirzidevs` or join [our discord support server](https://discord.gg/zv6maQRah3) ### Using Custom Emoji -1. type `\` in guild that has custom emoji you want +1. type `\` in server that has custom emoji you want Type \ on the chat @@ -114,16 +112,15 @@ You can add more than one node in the blacklist Blacklist Config -## Apikey permission +## The node is online but the embed is read as offline -If you having issue with 403 error try to enable `read & write` on all options - -Application API Permission +If you having this issue, you can enable `log_error` on the config file and report it to our discord server at [Support Server](https://discord.gg/zv6maQRah3) ## Links +- [PteroStats DiscordJS v13](https://github.com/HirziDevs/PteroStats/tree/3d0512c3323ecf079101104c7ecf3c94d265e298) +- [PteroStats DiscordJS v12](https://github.com/HirziDevs/PteroStats/tree/bcfa266be64dda11955f0bf9732da086bcea522c) - [Pterodactyl Panel](https://pterodactyl.io) -- [Pterodactyl Api Documentation](https://dashflo/docs/api/pterodactyl/v1) +- [Pterodactyl API Documentation](https://github.com/devnote-dev/ptero-notes/) - [Pterodactyl Discord Server](https://discord.gg/pterodactyl) - [PteroBot Support Server](https://discord.gg/zv6maQRah3) -- [PteroBot Support Server (Indonesia)](https://discord.gg/EYaFB7WSg6) \ No newline at end of file diff --git a/config.yml b/config.yml index fe1759b..b3e07dd 100644 --- a/config.yml +++ b/config.yml @@ -2,75 +2,85 @@ # If you need help, join our discord server here: https://discord.gg/zv6maQRah3 # Bot Configuration -token: 'Put bot token here' -bot_status: - enable: false - text: 'Hosting Panel' - type: 'WATCHING' # can be 'WATCHING', 'PLAYING', 'LISTENING', or 'COMPETING'. for 'STREAMING' is not working for now +token: "Put Bot Token Here" +presence: + text: "Hosting Panel" + type: "watching" # can be 'watching', 'playing', 'listening', or 'competing'. 'streaming' is not working for now + status: "online" # can be 'online', 'idle', 'dnd', or 'invisible' # Discord Channel and Refresh Time Configuration -channel: 'Put channel id here' -refresh: 60 +channel: "Put Channel ID Here" +refresh: 10 # How much time the bot will refresh the stats +timeout: 5 # How much time to wait for a node to respond to the bot (if you change this, it will add more time to refresh the stats) # Panel Configuration panel: - url: 'Put panel url here' - key: 'Put panel apikey here' + url: "Put Panel URL Here" + key: "Put Panel APIKEY Here" -# Message Configuration -# set to false if you want to disable option +# Message and Embed Configuration +# set the option as '' if you want to disable it message: - content: false - attachment: false # If you enable attachment on message it upload the attachment first before sending or editing message and it will delay the stats + content: "" + attachment: "" # If you enable attachment on message it will upload the attachment first before sending or editing message and will result in delayed stats embed: - title: 'PteroStats' - color: '5865F2' - description: 'Next update {{time}}' # You can use {{time}} to make "in X seconds" time format - footer: 'By Hirzi#8701' - timestamp: true - thumbnail: false - image: false - field: - enable: false - title: 'Custom Field' - description: 'Custom Field' # You can use {{time}} to make "in X seconds" time format + title_nodes: "Nodes Stats" + title_panel: "Panel Stats" + author: "PteroStats" + color: "5865F2" + description: "Next update {{time}}" # You can use {{time}} to make "in X seconds" time format + footer: "By Hirzi#8701" + timestamp: true + thumbnail: "" + image: "" # Message Button Configuration button: - enable: true - btn1: - label: 'PteroStats' - url: 'https://github.com/HirziDevs/PteroStats' - btn2: - label: '' - url: '' - btn3: - label: '' - url: '' - btn4: - label: '' - url: '' - btn5: - label: '' - url: '' + enable: true + btn1: + label: "PteroStats" + url: "https://github.com/HirziDevs/PteroStats" + btn2: + label: "" + url: "" + btn3: + label: "" + url: "" + btn4: + label: "" + url: "" + btn5: + label: "" + url: "" # Status Message Configuration +# How to use custom emoji: https://github.com/HirziDevs/PteroStats#using-custom-emoji status: - online: ':green_circle: Online' - offline: ':red_circle: Offline' + online: ":green_circle: Online" + offline: ":red_circle: Offline" -# Nodes Resource -nodes_resource: - blacklist: [] # You can add node id to remove the node from status embed (Example: "blacklist: [1]") - enable: false - servers: true - location: true - allocations: true - unit: 'gb' # You can use "gb", "mb", "tb", or "percent" +# Nodes Settings +# How to get nodes id: https://github.com/HirziDevs/PteroStats#blacklist-nodes +nodes_settings: + blacklist: [] # You can add node id to remove the node from status embed (Example: "blacklist: [1]") + details: true # enable nodes details i.e memory and disk usage + servers: true + location: true + unit: "mb" # Allowed values- "gb", "mb", "tb", or "%" + limit: 100 -# Panel Users and Servers -panel_resource: - enable: true - servers: true - users: true \ No newline at end of file +# Panel Users and Servers Settings +panel_settings: + status: true # enable panel stats under nodes stats + servers: true + users: true + +# Mentions a User or Role if any node is offline (this feature is still in testing, please report if you have a problem) +mentions: # to enable atleast put 1 ID on user or role bellow + user: [] # Put User ID here (Example: "user: ['548867757517570058', '816219634390663230']") + role: [] # Put Role ID here (Example: "role: ['796083838236622858', '858198863973187585']") + channel: "" # Put Channel ID here for the logging + +# Log error to console if a server offline (enable this when you have a problem that you want to report) +log_error: false # set to "true" to enable \ No newline at end of file diff --git a/events/ready.js b/events/ready.js deleted file mode 100644 index 0927b0e..0000000 --- a/events/ready.js +++ /dev/null @@ -1,29 +0,0 @@ -const chalk = require('chalk') -const checkStatus = require('../handlers/checkStatus') - -module.exports = { - name: 'ready', - once: true, - execute(client) { - console.log(chalk.cyan('[PteroStats] ') + chalk.green('Bot is up!')) - console.log(chalk.cyan('[PteroStats] ') + chalk.green('If you need support you can join our discord server https://discord.gg/zv6maQRah3')) - - if (client.guilds.cache.size < 1) return console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! This bot is not on any discord servers')) - - if (client.config.bot_status.enable && client.config.bot_status.text.length > 0) { - if (!['PLAYING', 'WATCHING', 'LISTENING', 'COMPETING'].includes(client.config.bot_status.type.toUpperCase() || client.config.bot_status.type.length < 1)) { - console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid Status Type!, Can be "WATCHING", "PLAYING", "LISTENING", or "COMPETING"')) - } else { - client.user.setActivity(client.config.bot_status.text, { type: client.config.bot_status.type.toUpperCase() }) - } - } - - if (client.config.refresh < 10) console.log('Refresh lower than 10 seconds is not recommended!') - - checkStatus(client) - - setInterval(() => { - checkStatus(client) - }, client.config.refresh * 1000) - } -} \ No newline at end of file diff --git a/handlers/checkStatus.js b/handlers/checkStatus.js deleted file mode 100644 index 2bab4e7..0000000 --- a/handlers/checkStatus.js +++ /dev/null @@ -1,144 +0,0 @@ -const axios = require('axios') -const chalk = require('chalk') - -const postStatus = require('./postStatus') - -module.exports = function checkStatus(client) { - - if (client.config.channel.startsWith('Put')) { - console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid Channel ID')) - process.exit() - } else if (client.config.panel.url.startsWith('Put')) { - console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid Panel URL')) - process.exit() - } else if (client.config.panel.key.startsWith('Put')) { - console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid Apikey')) - process.exit() - } else if (!client.config.panel.url.startsWith('http')) { - console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid Panel URL')) - console.log(chalk.cyan('[PteroStats] ') + chalk.red('1. Make sure the panel url is starts with "https://" or "http://"')) - process.exit() - } - - if (client.config.panel.url.endsWith('/')) client.config.panel.url = client.config.panel.url.slice(0, -1) - - const nodes = [] - - const panel = { - status: false, - total_servers: -1, - total_users: -1, - } - - console.log(chalk.cyan('[PteroStats] ') + chalk.green('Getting nodes stats')) - - const panelStats = new Promise((resolve, reject) => { - axios(client.config.panel.url + '/api/application/users', { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + client.config.panel.key - } - }).then((usr) => { - axios(client.config.panel.url + '/api/application/servers', { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + client.config.panel.key - } - }).then((ser) => { - panel.total_users = usr.data.meta.pagination.total - panel.total_servers = ser.data.meta.pagination.total - panel.status = true - - resolve() - }) - }).catch((err) => { - if (err.response) { - if (err.response.status === 403) { - console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid apikey')) - console.log(chalk.cyan('[PteroStats] ') + chalk.red('1. Make sure the apikey is from admin page not account page')) - console.log(chalk.cyan('[PteroStats] ') + chalk.red('2. Make sure the apikey has read permission on all options')) - console.log(chalk.cyan('[PteroStats] ') + chalk.red('3. Make sure the apikey is exist')) - } else if (err.response.status === 404) { - console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid Panel URL')) - console.log(chalk.cyan('[PteroStats] ') + chalk.red('1. Make sure the panel url is like "https://panel.example.com"')) - } else console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! ' + err)) - } else console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! ' + err)) - resolve() - }) - }) - - const nodeStats = new Promise((resolve, reject) => { - axios(client.config.panel.url + '/api/application/nodes?include=servers,location,allocations', { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + client.config.panel.key - } - }).then((res) => { - res.data.data.forEach((node, i) => { - axios(client.config.panel.url + '/api/application/nodes/' + node.attributes.id + '/configuration', { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + client.config.panel.key - } - }).then((data) => { - const body = { - id: node.attributes.id, - name: node.attributes.name, - location: node.attributes.relationships.location.attributes.short, - allocations: node.attributes.relationships.allocations.data.length, - status: true, - maintenance: node.attributes.maintenance_mode, - total_servers: node.attributes.relationships.servers.data.length, - memory_min: node.attributes.allocated_resources.memory, - memory_max: node.attributes.memory, - disk_min: node.attributes.allocated_resources.disk, - disk_max: node.attributes.disk, - } - - const stats = new Promise((statsResolve, statsReject) => { - axios(node.attributes.scheme + '://' + node.attributes.fqdn + ':' + node.attributes.daemon_listen + '/api/servers', { - method: 'GET', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + data.data.token - } - }).then((status) => { - return statsResolve() - }).catch((err) => { - body.status = false - return statsResolve() - }) - setTimeout(() => { - body.status = false - return statsResolve() - }, 1000) - }) - stats.then(() => { - nodes.push(body) - if (nodes.length === res.data.data.length) resolve() - }) - }).catch((err) => { - resolve() - }) - }) - }).catch((err) => { - resolve() - }) - }) - - panelStats.then(() => { - nodeStats.then(() => { - nodes.sort(function (a, b) { return a.id - b.id }) - postStatus(client, panel, nodes) - }) - }) -} \ No newline at end of file diff --git a/handlers/config.js b/handlers/config.js new file mode 100644 index 0000000..661329a --- /dev/null +++ b/handlers/config.js @@ -0,0 +1,24 @@ + +const fs = require("node:fs"); +const yaml = require("js-yaml"); +const cliColor = require("cli-color"); + +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...")) + config = yaml.load(fs.readFileSync("./config-dev.yml", "utf8")); +} + +validateURL(config.panel.url); + +function validateURL(url) { + try { + const testURL = new URL(url); + if (!testURL.protocol.startsWith("http")) throw new Error(); + } catch { + console.error('Config Error | Invalid URL Format! Example Correct URL: "https://panel.example.com"'); + process.exit(); + } +} + +module.exports = config \ No newline at end of file diff --git a/handlers/convertUnits.js b/handlers/convertUnits.js new file mode 100644 index 0000000..287f257 --- /dev/null +++ b/handlers/convertUnits.js @@ -0,0 +1,20 @@ +module.exports = function convertUnits(value1, value2, unit) { + unit = unit.toUpperCase(); + const units = ['MB', 'GB', 'TB']; + const conversionFactors = { + 'MB': 1, + 'GB': 1 / 1024, + 'TB': 1 / (1024 * 1024) + }; + + if (unit === '%' || unit === "PERCENTAGE" || unit === "PERCENT") { + const percentage = Math.floor((value1 / value2) * 100); + return `${!percentage ? 0 : percentage}%`; + } else if (units.includes(unit)) { + const formatValue = (value) => Number.isInteger(value) ? value.toLocaleString() : value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + + return `${formatValue(value1 * conversionFactors[unit])} ${unit}/${(value2 === 0 || value2 < 1) ? "Unlimited" : formatValue(value2 * conversionFactors[unit]) + " " + unit}`; + } else { + return `${value1.toLocaleString()} MB/${value2.toLocaleString()} MB`; + } +} \ No newline at end of file diff --git a/handlers/getNodeConfiguration.js b/handlers/getNodeConfiguration.js new file mode 100644 index 0000000..00f2cdc --- /dev/null +++ b/handlers/getNodeConfiguration.js @@ -0,0 +1,13 @@ +const config = require("./config.js"); + +module.exports = async function getNodeConfiguration(id) { + return fetch(`${new URL(config.panel.url).origin}/api/application/nodes/${id}/configuration`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${config.panel.key}` + }, + }) + .then((res) => res.json()) + .then((data) => data) +} \ No newline at end of file diff --git a/handlers/getNodesDetails.js b/handlers/getNodesDetails.js new file mode 100644 index 0000000..b7ad731 --- /dev/null +++ b/handlers/getNodesDetails.js @@ -0,0 +1,44 @@ +const cliColor = require("cli-color"); +const config = require("./config.js"); +const axios = require("axios"); + +module.exports = async function getAllNodes() { + return axios(`${new URL(config.panel.url).origin}/api/application/nodes?include=servers,location,allocations`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${config.panel.key}` + }, + }) + .then((res) => res.data.data) + .catch((error) => { + 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.code === "ECONNREFUSED") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ECONNREFUSED | Connection refused. Ensure the panel is running and reachable.")); + } else if (error.code === "ETIMEDOUT") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ETIMEDOUT | Connection timed out. The panel took too long to respond.")); + } else if (error.code === "ECONNRESET") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ECONNRESET | Connection reset by peer. The panel closed the connection unexpectedly.")); + } else if (error.code === "EHOSTUNREACH") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("EHOSTUNREACH | Host unreachable. The panel is down or not reachable.")); + } else if (error.response) { + if (error.response.status === 401) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("401 | Unauthorized. Invalid Application Key or API Key doesn't have permission to perform this action.")); + } else if (error.response.status === 403) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("403 | Forbidden. Invalid Application Key or API Key doesn't have permission to perform this action.")); + } else if (error.response.status === 404) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("404 | Not Found. Invalid Panel URL or the Panel doesn't exist.")); + } else if (error.response.status === 429) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("429 | Too Many Requests. You have sent too many requests in a given amount of time.")); + } else if ([500, 502, 503, 504].includes(error.response.status)) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("500 | Internal Server Error. This is an error with your panel, PteroStats is not the cause.")); + } else { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`${error.response.status} | Unexpected error: ${error.response.statusText}`)); + } + } else { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Unexpected error: ${error.message}`)); + } + return false + }) +} \ No newline at end of file diff --git a/handlers/getServers.js b/handlers/getServers.js new file mode 100644 index 0000000..18bcbc0 --- /dev/null +++ b/handlers/getServers.js @@ -0,0 +1,15 @@ +const config = require("./config.js"); +const cliColor = require("cli-color"); + +module.exports = async function getServers() { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel servers...")) + return fetch(`${new URL(config.panel.url).origin}/api/application/servers`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${config.panel.key}` + }, + }) + .then((res) => res.json()) + .then((data) => data.data.length) +} \ No newline at end of file diff --git a/handlers/getStats.js b/handlers/getStats.js new file mode 100644 index 0000000..24e73e5 --- /dev/null +++ b/handlers/getStats.js @@ -0,0 +1,50 @@ +const config = require("./config.js"); +const fs = require("node:fs"); +const getNodesDetails = require("./getNodesDetails.js"); +const getNodeConfiguration = require("./getNodeConfiguration.js"); +const getWingsStatus = require("./getWingsStatus.js"); +const getServers = require("./getServers.js"); +const getUsers = require("./getUsers.js"); +const promiseTimeout = require("./promiseTimeout.js"); +const cliColor = require("cli-color"); + +module.exports = async function getStats() { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel nodes...")) + 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(`Getting "${node.attributes.name}" configuration...`)) + const nodeConfig = await getNodeConfiguration(node.attributes.id); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Getting "${node.attributes.name}" wings status...`)) + const nodeStatus = await promiseTimeout(getWingsStatus(node, nodeConfig.token), config.timeout * 1000); + + if (!nodeStatus) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node "${node.attributes.name}" is offline`)) + return { + attributes: { + name: node.attributes.name, + memory: node.attributes.memory, + disk: node.attributes.disk, + cpu: node.attributes.cpu, + allocated_resources: node.attributes.allocated_resources, + relationships: { + allocations: node.attributes.relationships.allocations.data.length, + servers: node.attributes.relationships.servers.data.length + } + }, + status: nodeStatus + }; + }); + + const data = { + servers: await getServers(), + users: await getUsers(), + nodes: await Promise.all(statusPromises), + timestamp: Date.now() + } + + fs.writeFileSync("cache.json", JSON.stringify(data, null, 2), "utf8"); + + return data +} \ No newline at end of file diff --git a/handlers/getUsers.js b/handlers/getUsers.js new file mode 100644 index 0000000..55d72be --- /dev/null +++ b/handlers/getUsers.js @@ -0,0 +1,15 @@ +const config = require("./config.js"); +const cliColor = require("cli-color"); + +module.exports = async function getUsers() { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel users...")) + return fetch(`${new URL(config.panel.url).origin}/api/application/users`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${config.panel.key}` + }, + }) + .then((res) => res.json()) + .then((data) => data.data.length) +} \ No newline at end of file diff --git a/handlers/getWingsStatus.js b/handlers/getWingsStatus.js new file mode 100644 index 0000000..f925eaf --- /dev/null +++ b/handlers/getWingsStatus.js @@ -0,0 +1,12 @@ +module.exports = async function getWingsStatus(node, nodeToken) { + return fetch(`${node.attributes.scheme}://${node.attributes.fqdn}:${node.attributes.daemon_listen}/api/servers`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${nodeToken}` + }, + }) + .then((res) => res.json()) + .then(() => true) + .catch(() => false) +} \ No newline at end of file diff --git a/handlers/postStatus.js b/handlers/postStatus.js deleted file mode 100644 index 7c57495..0000000 --- a/handlers/postStatus.js +++ /dev/null @@ -1,176 +0,0 @@ -const { EmbedBuilder, time, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js') -const chalk = require('chalk') - -module.exports = async function postStatus(client, panel, nodes) { - - if (!client.config.nodes_resource) client.config.nodes_resource = client.config.resource - if (!client.config.nodes_resource.blacklist) client.config.nodes_resource.blacklist = [] - if (!Array.isArray(client.config.nodes_resource.blacklist) && Number.isInteger(client.config.nodes_resource.blacklist)) client.config.nodes_resource.blacklist = [client.config.nodes_resource.blacklist] - if (!client.config.message.attachment) client.config.message.attachment = client.config.message.image - - if (client.guilds.cache.size < 1) return console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! This bot is not on any discord servers')) - - const channel = await client.channels.cache.get(client.config.channel) - - if (!channel) return console.log(chalk.cyan('[PteroStats] ') + chalk.red('Err! Invalid Channel ID')) - - let messages = await channel.messages.fetch({ limit: 10 }) - messages = messages.filter(m => m.author.id === client.user.id).last(); - if (messages && messages.embeds.length < 1) { - messages.delete() - messages = null - } - - const embed = new EmbedBuilder() - - let text = '' - let desc = '' - let blacklist = 0 - - let content = null - const files = [] - - if (client.config.message.content) content = client.config.message.content - if (client.config.message.attachment) files.push(new AttachmentBuilder(client.config.message.attachment)) - - if (client.config.embed.title) embed.setTitle(client.config.embed.title) - if (client.config.embed.description) desc = client.config.embed.description + '\n' - if (client.config.embed.color) embed.setColor(client.config.embed.color) - if (client.config.embed.footer) embed.setFooter({ text: client.config.embed.footer }) - if (client.config.embed.thumbnail) embed.setThumbnail(client.config.embed.thumbnail) - if (client.config.embed.image) embed.setImage(client.config.embed.image) - - panel.total_users = panel.total_users.toLocaleString() - panel.total_servers = panel.total_servers.toLocaleString() - - const stats = new Promise((resolve, reject) => { - if (nodes.length !== 0) { - nodes.forEach((data, i) => { - if (!client.config.nodes_resource.blacklist.includes(data.id)) { - const title = data.name + ': ' + String(data.status).replace('true', client.config.status.online).replace('false', client.config.status.offline) - let description = '```' - switch (client.config.nodes_resource.unit.toLowerCase()) { - case 'tb': - description = description + - '\nMemory : ' + Math.floor(data.memory_min / (1024 * 1000)).toLocaleString() + ' TB / ' + Math.floor(data.memory_max / (1024 * 1000)).toLocaleString() + ' TB' + - '\nDisk : ' + Math.floor(data.disk_min / (1024 * 1000)).toLocaleString() + ' TB / ' + Math.floor(data.disk_max / (1024 * 1000)).toLocaleString() + ' TB' - break; - case 'gb': - description = description + - '\nMemory : ' + Math.floor(data.memory_min / 1024).toLocaleString() + ' GB / ' + Math.floor(data.memory_max / 1024).toLocaleString() + ' GB' + - '\nDisk : ' + Math.floor(data.disk_min / 1024).toLocaleString() + ' GB / ' + Math.floor(data.disk_max / 1024).toLocaleString() + ' GB' - break; - case 'percent': - description = description + - '\nMemory : ' + Math.floor(data.memory_min / data.memory_max * 100) + ' %' + - '\nDisk : ' + Math.floor(data.disk_min / data.disk_max * 100) + ' %' - break; - default: - description = description + - '\nMemory : ' + data.memory_min.toLocaleString() + ' MB / ' + data.memory_max.toLocaleString() + ' MB' + - '\nDisk : ' + data.disk_min.toLocaleString() + ' MB / ' + data.disk_max.toLocaleString() + ' MB' - } - - if (client.config.nodes_resource.servers) description = description + '\nServers : ' + data.total_servers.toLocaleString() - if (client.config.nodes_resource.location) description = description + '\nLocation : ' + data.location - if (client.config.nodes_resource.allocations) description = description + '\nAllocations : ' + data.allocations.toLocaleString() - - description = description + '\n```' - - if (client.config.nodes_resource.enable) { - text = text + '\n**' + title.replace(':', ':**') + '\n' + description - } else { - text = text + '\n**' + title.replace(':', ':**') - } - } else { - blacklist = blacklist + 1 - if (nodes.length - client.config.nodes_resource.blacklist.length < 1) text = '\nThere is no nodes to display' - } - - if (i + 1 === nodes.length) resolve() - }) - } else if (nodes.length < 1) { - if (!messages) { - text = '\nThere is no nodes to display' - resolve() - } else { - text = messages.embeds[0].description.replaceAll(client.config.status.online, client.config.status.offline) - if (!panel.status && String(String(messages.embeds[0].fields[0].value).split('\n')[2]).split('')[String(String(messages.embeds[0].fields[0].value).split('\n')[2]).length - 1] !== '`') { - panel.total_users = String(String(messages.embeds[0].fields[0].value).split('\n')[2]).split('')[String(String(messages.embeds[0].fields[0].value).split('\n')[2]).length - 1] - panel.total_servers = String(String(messages.embeds[0].fields[0].value).split('\n')[3]).split('')[String(String(messages.embeds[0].fields[0].value).split('\n')[3]).length - 1] - } - resolve() - } - } - }) - - stats.then(async () => { - const format = await time(new Date(Date.now() + client.config.refresh * 1000), 'R') - embed.setDescription(desc.replaceAll('{{time}}', format) + '\n**Nodes Stats [' + Math.floor(nodes.length - blacklist) + ']**' + text) - const EmbedFields = [] - - if (client.config.panel_resource.enable) { - let stats = '**Status:** ' + String(panel.status).replace('true', client.config.status.online).replace('false', client.config.status.offline) + '\n\n' - - if (client.config.panel_resource.users) stats = stats + 'Users: ' + String(panel.total_users).replace('-1', '`Unknown`') + '\n' - if (client.config.panel_resource.servers) stats = stats + 'Servers: ' + String(panel.total_servers).replace('-1', '`Unknown`') - - EmbedFields.push({ name: 'Panel Stats', value: stats }) - } - - if (client.config.embed.field.enable) EmbedFields.push({ name: client.config.embed.field.title, value: client.config.embed.field.description.replaceAll('{{time}}', format) }) - if (client.config.embed.timestamp) embed.setTimestamp() - if (EmbedFields.length > 0) embed.addFields(EmbedFields) - - const row = [] - - if (client.config.button.enable) { - const button = new ActionRowBuilder - if (client.config.button.btn1.label.length !== 0 && client.config.button.btn1.url.length !== 0) { - button.addComponents( - new ButtonBuilder() - .setLabel(client.config.button.btn1.label) - .setStyle(ButtonStyle.Link) - .setURL(client.config.button.btn1.url) - ) - } - if (client.config.button.btn2.label.length !== 0 && client.config.button.btn2.url.length !== 0) { - button.addComponents( - new ButtonBuilder() - .setLabel(client.config.button.btn2.label) - .setStyle(ButtonStyle.Link) - .setURL(client.config.button.btn2.url) - ) - } - if (client.config.button.btn3.label.length !== 0 && client.config.button.btn3.url.length !== 0) { - button.addComponents( - new ButtonBuilder() - .setLabel(client.config.button.btn3.label) - .setStyle(ButtonStyle.Link) - .setURL(client.config.button.btn3.url) - ) - } - if (client.config.button.btn4.label.length !== 0 && client.config.button.btn4.url.length !== 0) { - button.addComponents( - new ButtonBuilder() - .setLabel(client.config.button.btn4.label) - .setStyle(ButtonStyle.Link) - .setURL(client.config.button.btn4.url) - ) - } - if (client.config.button.btn5.label.length !== 0 && client.config.button.btn5.url.length !== 0) { - button.addComponents( - new ButtonBuilder() - .setLabel(client.config.button.btn5.label) - .setStyle(ButtonStyle.Link) - .setURL(client.config.button.btn5.url) - ) - } - row.push(button) - } - - if (!messages) channel.send({ content: content, embeds: [embed], components: row, files: files }) - else messages.edit({ content: content, embeds: [embed], components: row, files: files }) - console.log(chalk.cyan('[PteroStats] ') + chalk.green('Stats posted!')) - }) -} \ No newline at end of file diff --git a/handlers/promiseTimeout.js b/handlers/promiseTimeout.js new file mode 100644 index 0000000..510e142 --- /dev/null +++ b/handlers/promiseTimeout.js @@ -0,0 +1,9 @@ +module.exports = function promiseTimeout(promise, ms) { + const timeout = new Promise((resolve) => { + const id = setTimeout(() => { + clearTimeout(id); + resolve(false); + }, ms); + }); + return Promise.race([promise, timeout]); +} \ No newline at end of file diff --git a/index.js b/index.js index 9098250..dc3b354 100644 --- a/index.js +++ b/index.js @@ -1,52 +1,185 @@ -const fs = require('fs'); -const child = require('child_process'); +const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType } = require("discord.js"); +const config = require("./handlers/config.js"); +const convertUnits = require("./handlers/convertUnits.js"); +const getStats = require("./handlers/getStats.js"); +const cachePath = require('node:path').join(__dirname, "./cache.json"); +const fs = require("node:fs"); +const cliColor = require("cli-color"); -if (Number(process.version.split('.')[0]) < 16) { - console.log('Invalid NodeJS Version!, Please use NodeJS 16.x or upper') - process.exit() -} -if (fs.existsSync('./node_modules')) { - const check = require('./node_modules/discord.js/package.json') - if (Number(check.version.split('.')[0]) !== 14) { - console.log('Invalid Discord.JS Version!, Please use Discord.JS 14.x') - process.exit() - } -} else { - console.log('You didn\'t install the required node packages first!') - console.log('Please wait... starting to install all required node packages using child process') - try { - child.execSync('npm i') - console.log('Install complete!, please run "node index" command again!') - process.exit() - } catch (err) { - console.log('Err! ', err) - console.log('Support Server: https://discord.gg/zv6maQRah3') - process.exit() - } -} +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright("Starting bot...")); +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using development build! some features may not work.")); -const chalk = require('chalk'); -const yaml = require('js-yaml'); -const { Client, GatewayIntentBits } = require('discord.js'); -const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +const client = new Client({ + intents: [GatewayIntentBits.Guilds] +}); -const config = yaml.load(fs.readFileSync('./config.yml', 'utf8')); -client.config = config +async function startGetStatus() { + try { + const results = await getStats(); + createMessage({ + panel: true, + nodes: results.nodes, + servers: results.servers, + users: results.users, + }); + } catch { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is offline")); -const eventFiles = fs.readdirSync('./events').filter(file => file.endsWith('.js')); + fs.readFile(cachePath, (err, data) => { + if (err) { + createMessage({ cache: false, panel: false }); + return console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Last cache was not found!")); + } -for (const file of eventFiles) { - const event = require(`./events/${file}`); - if (event.once) { - client.once(event.name, (...args) => event.execute(...args)); - } else { - client.on(event.name, (...args) => event.execute(...args)); + try { + const results = JSON.parse(data); + createMessage({ + cache: true, + panel: false, + nodes: results.nodes, + servers: results.servers, + users: results.users, + }); + } catch { + createMessage({ cache: false, panel: false }); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Something went wrong with cache data...")); + } + }); } } -if (client.config.token.startsWith('Put') || client.config.token.length < 1) { - console.log(chalk.cyan('[PteroStats]') + chalk.red(' Err! Invalid Discord Bot Token')) - process.exit() +client.once("ready", () => { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright(`${client.user.tag} is online!`)); + + if (config.presence.text && config.presence.type) { + switch (config.presence.type.toLowerCase()) { + case "playing": + config.presence.type = ActivityType.Playing; + break; + case "listening": + config.presence.type = ActivityType.Listening; + break; + case "competing": + config.presence.type = ActivityType.Competing; + break; + default: + config.presence.type = ActivityType.Watching; + } + + client.user.setActivity(config.presence.text, { + type: config.presence.type, + }); + } + + if (config.presence.status) { + if (!["idle", "online", "dnd", "invisible"].includes( + config.presence.status.toLowerCase() + )) + config.presence.status = "online"; + + client.user.setStatus(config.presence.status); + } + + startGetStatus(); +}); + +async function createMessage({ cache, panel, nodes, servers, users }) { + let embed = new EmbedBuilder() + .setTitle(config.embed.title_nodes) + .setColor(config.embed.color); + + let embeds = [embed]; + + if (config.nodes_settings.details) { + nodes.forEach((node, index) => { + if (index % 25 === 0 && index !== 0) { + embed = new EmbedBuilder().setColor(config.embed.color); + if (embeds.length < 9) embeds.push(embed); + } + + embed.addFields({ + name: `${node.attributes.name} - ${node.status ? config.status.online : config.status.offline}`, + value: + "```\n" + + `Memory: ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` + + `Disk : ${convertUnits(node.attributes.allocated_resources.disk, node.attributes.disk, config.nodes_settings.unit)}\n` + + `CPU : ${Math.floor(node.attributes.cpu / node.attributes.allocated_resources.cpu * 100) || 0}%` + + "```" + }); + }); + } else { + embeds[0].setDescription(nodes.map(node => `**${node.attributes.name}** - ${node.status ? config.status.online : config.status.offline}`).join("\n")); + } + + let panelEmbed = new EmbedBuilder() + .setColor(config.embed.color) + .setTitle(config.embed.title_panel) + .addFields({ + name: `Panel - ${panel ? config.status.online : config.status.offline}`, + value: + "```\n" + + `Nodes : ${nodes.length}\n` + + (config.panel_settings.servers ? `Servers: ${servers || "Unknown"}\n` : "") + + (config.panel_settings.users ? `Users : ${users || "Unknown"}\n` : "") + + "```" + }); + + if (config.panel_settings.status) embeds.unshift(panelEmbed); + + embeds[0] + .setAuthor(config.embed.title ? { name: config.embed.author } : null) + .setDescription( + embeds[0].data.description ? config.embed.description.replace("{{time}}", time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R")) + "\n\n" + embeds[0].data.description : config.embed.description.replace("{{time}}", time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R")) + ); + + embeds[embeds.length - 1] + .setFooter(config.embed.footer ? { text: config.embed.footer } : null); + + if (config.embed.timestamp) embeds[embeds.length - 1].setTimestamp(); + + if (!cache && !panel) { + embeds[embeds.length - 1].setDescription( + embeds[embeds.length - 1].data.description ? embeds[embeds.length - 1].data.description + "\n\nThere are no nodes to display!" : "There are no nodes to display!" + ); + } + + try { + const channel = await client.channels.fetch(config.channel); + const messages = await channel.messages.fetch({ limit: 10 }); + const botMessage = messages.find(msg => msg.author.id === client.user.id); + + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright("Panel stats have been posted to the channel!")); + + setTimeout(() => startGetStatus(), config.refresh * 1000); + + if (botMessage) { + await botMessage.edit({ content: config.message.content || null, embeds }); + } else { + await channel.send({ content: config.message.content || null, embeds }); + } + } catch (error) { + DiscordErrorHandler(error); + } } -client.login(config.token); \ No newline at end of file +function DiscordErrorHandler(error) { + 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) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!")); + } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code === "MAX_EMBED_SIZE_EXCEEDED") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Embed message limit exceeded! Please limit or decrease the nodes that need to be shown in the config!")); + } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code) { + console.log(Object.values(error.rawError.errors)[0]._errors[0].message); + } else { + console.error(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error"), error); + } + process.exit(); +} + +try { + client.login(config.token); +} catch { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!")); + process.exit(); +} diff --git a/package.json b/package.json index 36fbe00..988b7ad 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,24 @@ { - "name": "pterostats", - "version": "v3.0.0", - "description": "PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server", - "license": "MIT", - "repository": "HirziDevs/PteroStats", - "homepage": "https://pterostats.hirzidevs.net", - "bugs": { - "email": "hirzidevs@gmail.com", - "url": "https://github.com/HirziDevs/PteroStats" - }, - "scripts": { - "start": "node index.js" - }, - "dependencies": { - "axios": "^1.1.3", - "chalk": "4.1.1", - "discord.js": "^14.6.0", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": "^16.x" - } + "name": "pterostats", + "version": "v4.0.0", + "description": "PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server", + "license": "MIT", + "repository": "HirziDevs/PteroStats", + "homepage": "https://pterostats.hirzidevs.xyz", + "bugs": { + "email": "hirzidevs@gmail.com", + "url": "https://github.com/HirziDevs/PteroStats/issues" + }, + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "axios": "^1.7.2", + "cli-color": "^2.0.4", + "discord.js": "^14.15.3", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">=16.11.0" + } } \ No newline at end of file From 86fde40e075f4fdab84d3d2bfb0dd6f92be446ab Mon Sep 17 00:00:00 2001 From: Hirzi <64255651+HirziDevs@users.noreply.github.com> Date: Mon, 29 Jul 2024 19:51:22 +0700 Subject: [PATCH 03/59] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b11d67..7786925 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ -### This is a development build! some features may not work. + +## This is a development build! some features may not work. + ## Introduction PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server From 66709c43e2a5f12318fbdc856b0da5a8bdf64fe2 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Sun, 25 Aug 2024 21:34:33 +0700 Subject: [PATCH 04/59] Better Presence Settings --- config.yml | 1 + index.js | 50 ++++++++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/config.yml b/config.yml index b3e07dd..49ad34d 100644 --- a/config.yml +++ b/config.yml @@ -4,6 +4,7 @@ # Bot Configuration token: "Put Bot Token Here" presence: + enable: true text: "Hosting Panel" type: "watching" # can be 'watching', 'playing', 'listening', or 'competing'. 'streaming' is not working for now status: "online" # can be 'online', 'idle', 'dnd', or 'invisible' diff --git a/index.js b/index.js index dc3b354..830fb30 100644 --- a/index.js +++ b/index.js @@ -51,33 +51,35 @@ async function startGetStatus() { client.once("ready", () => { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright(`${client.user.tag} is online!`)); - if (config.presence.text && config.presence.type) { - switch (config.presence.type.toLowerCase()) { - case "playing": - config.presence.type = ActivityType.Playing; - break; - case "listening": - config.presence.type = ActivityType.Listening; - break; - case "competing": - config.presence.type = ActivityType.Competing; - break; - default: - config.presence.type = ActivityType.Watching; + if (config.presence.enable) { + if (config.presence.text && config.presence.type) { + switch (config.presence.type.toLowerCase()) { + case "playing": + config.presence.type = ActivityType.Playing; + break; + case "listening": + config.presence.type = ActivityType.Listening; + break; + case "competing": + config.presence.type = ActivityType.Competing; + break; + default: + config.presence.type = ActivityType.Watching; + } + + client.user.setActivity(config.presence.text, { + type: config.presence.type, + }); } - client.user.setActivity(config.presence.text, { - type: config.presence.type, - }); - } + if (config.presence.status) { + if (!["idle", "online", "dnd", "invisible"].includes( + config.presence.status.toLowerCase() + )) + config.presence.status = "online"; - if (config.presence.status) { - if (!["idle", "online", "dnd", "invisible"].includes( - config.presence.status.toLowerCase() - )) - config.presence.status = "online"; - - client.user.setStatus(config.presence.status); + client.user.setStatus(config.presence.status); + } } startGetStatus(); From b7981dc39a426b92575239c7569d823734144567 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Sun, 25 Aug 2024 21:35:19 +0700 Subject: [PATCH 05/59] Remove Validate Function --- handlers/config.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/handlers/config.js b/handlers/config.js index 661329a..b444ac2 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -9,16 +9,12 @@ if (fs.existsSync("config-dev.yml")) { config = yaml.load(fs.readFileSync("./config-dev.yml", "utf8")); } -validateURL(config.panel.url); - -function validateURL(url) { - try { - const testURL = new URL(url); - if (!testURL.protocol.startsWith("http")) throw new Error(); - } catch { - console.error('Config Error | Invalid URL Format! Example Correct URL: "https://panel.example.com"'); - process.exit(); - } +try { + const testURL = new URL(config.panel.url); + if (!testURL.protocol.startsWith("http")) throw new Error(); +} catch { + console.error('Config Error | Invalid URL Format! Example Correct URL: "https://panel.example.com"'); + process.exit(); } module.exports = config \ No newline at end of file From d31b99f38f0148d2a92649dc21397338ba444b3c Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Sun, 25 Aug 2024 21:35:38 +0700 Subject: [PATCH 06/59] Filter for blacklisted nodes --- handlers/getNodesDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/getNodesDetails.js b/handlers/getNodesDetails.js index b7ad731..b70ff31 100644 --- a/handlers/getNodesDetails.js +++ b/handlers/getNodesDetails.js @@ -10,7 +10,7 @@ module.exports = async function getAllNodes() { "Authorization": `Bearer ${config.panel.key}` }, }) - .then((res) => res.data.data) + .then((res) => res.data.data.filter((node) => config.nodes_settings.blacklist.includes(node.attributes.id))) .catch((error) => { if (error.code === "ENOTFOUND") { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ENOTFOUND | DNS Error. Ensure your network connection and DNS server are functioning correctly.")); From 75d068a39b2392659d2a98f256db8c35ebc50e4d Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Sun, 25 Aug 2024 21:49:11 +0700 Subject: [PATCH 07/59] Fix: Blacklist Nodes --- handlers/getNodesDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/getNodesDetails.js b/handlers/getNodesDetails.js index b70ff31..0966acd 100644 --- a/handlers/getNodesDetails.js +++ b/handlers/getNodesDetails.js @@ -10,7 +10,7 @@ module.exports = async function getAllNodes() { "Authorization": `Bearer ${config.panel.key}` }, }) - .then((res) => res.data.data.filter((node) => config.nodes_settings.blacklist.includes(node.attributes.id))) + .then((res) => res.data.data.filter((node) => !config.nodes_settings.blacklist.includes(node.attributes.id))) .catch((error) => { if (error.code === "ENOTFOUND") { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ENOTFOUND | DNS Error. Ensure your network connection and DNS server are functioning correctly.")); From d4d7adb57b00c9c73adc4544845fb31ae21483d7 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Tue, 10 Sep 2024 05:10:19 +0700 Subject: [PATCH 08/59] feat: log wings error --- handlers/getWingsStatus.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/handlers/getWingsStatus.js b/handlers/getWingsStatus.js index f925eaf..69975df 100644 --- a/handlers/getWingsStatus.js +++ b/handlers/getWingsStatus.js @@ -1,3 +1,5 @@ +const config = require("./config.js"); + module.exports = async function getWingsStatus(node, nodeToken) { return fetch(`${node.attributes.scheme}://${node.attributes.fqdn}:${node.attributes.daemon_listen}/api/servers`, { method: "GET", @@ -8,5 +10,8 @@ module.exports = async function getWingsStatus(node, nodeToken) { }) .then((res) => res.json()) .then(() => true) - .catch(() => false) + .catch((error) => { + if (config.log_error) console.error(error); + return false + }) } \ No newline at end of file From f7b1c2932cc2d8bb003159a253c0dd84b2ecd739 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Tue, 10 Sep 2024 05:12:07 +0700 Subject: [PATCH 09/59] feat: message button link --- config.yml | 18 +++--------------- index.js | 20 ++++++++++++++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/config.yml b/config.yml index 49ad34d..b9f2ddf 100644 --- a/config.yml +++ b/config.yml @@ -39,21 +39,9 @@ embed: # Message Button Configuration button: enable: true - btn1: - label: "PteroStats" - url: "https://github.com/HirziDevs/PteroStats" - btn2: - label: "" - url: "" - btn3: - label: "" - url: "" - btn4: - label: "" - url: "" - btn5: - label: "" - url: "" + row1: + - label: "Panel" + url: "https://panel.example.com" # Status Message Configuration # How to use custom emoji: https://github.com/HirziDevs/PteroStats#using-custom-emoji diff --git a/index.js b/index.js index 830fb30..fee8e13 100644 --- a/index.js +++ b/index.js @@ -145,8 +145,20 @@ async function createMessage({ cache, panel, nodes, servers, users }) { ); } - try { - const channel = await client.channels.fetch(config.channel); + const components = [] + + if (config.button.enable && config.button.row1?.length > 0) { + components.push( + new ActionRowBuilder().addComponents( + config.button.row1.map(button => + new ButtonBuilder() + .setLabel(button.label) + .setURL(button.url) + .setStyle(ButtonStyle.Link) + ) + ) + ) + } const messages = await channel.messages.fetch({ limit: 10 }); const botMessage = messages.find(msg => msg.author.id === client.user.id); @@ -155,9 +167,9 @@ async function createMessage({ cache, panel, nodes, servers, users }) { setTimeout(() => startGetStatus(), config.refresh * 1000); if (botMessage) { - await botMessage.edit({ content: config.message.content || null, embeds }); + await botMessage.edit({ content: config.message.content || null, embeds, components }); } else { - await channel.send({ content: config.message.content || null, embeds }); + await channel.send({ content: config.message.content || null, embeds, components }); } } catch (error) { DiscordErrorHandler(error); From 8e9f9bcb893c05c6d966b0ca03d30689d2f0ba2c Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Tue, 10 Sep 2024 05:12:55 +0700 Subject: [PATCH 10/59] fix: node cpu stats --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index fee8e13..ba18c82 100644 --- a/index.js +++ b/index.js @@ -104,8 +104,8 @@ async function createMessage({ cache, panel, nodes, servers, users }) { value: "```\n" + `Memory: ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` + - `Disk : ${convertUnits(node.attributes.allocated_resources.disk, node.attributes.disk, config.nodes_settings.unit)}\n` + - `CPU : ${Math.floor(node.attributes.cpu / node.attributes.allocated_resources.cpu * 100) || 0}%` + + `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}%` : "") + "```" }); }); From 9afcc48d4ae4ccb3f480a7b73b5624096bc065be Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Tue, 10 Sep 2024 05:13:40 +0700 Subject: [PATCH 11/59] fix: embed author --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ba18c82..e123485 100644 --- a/index.js +++ b/index.js @@ -129,7 +129,7 @@ async function createMessage({ cache, panel, nodes, servers, users }) { if (config.panel_settings.status) embeds.unshift(panelEmbed); embeds[0] - .setAuthor(config.embed.title ? { name: config.embed.author } : null) + .setAuthor(config.embed.author ? { name: config.embed.author } : null) .setDescription( embeds[0].data.description ? config.embed.description.replace("{{time}}", time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R")) + "\n\n" + embeds[0].data.description : config.embed.description.replace("{{time}}", time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R")) ); From d5d644dc5e083c0c1f749744f65abe8bc773aa29 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Tue, 10 Sep 2024 05:52:54 +0700 Subject: [PATCH 12/59] feat: installer --- .gitignore | 3 +- config.yml | 14 +++--- handlers/config.js | 11 ++++- handlers/convertUnits.js | 2 +- handlers/getNodeConfiguration.js | 4 +- handlers/getNodesDetails.js | 4 +- handlers/getServers.js | 6 +-- handlers/getStats.js | 8 ++-- handlers/getUsers.js | 6 +-- handlers/installer.js | 77 ++++++++++++++++++++++++++++++++ index.js | 36 ++++++++++++--- package.json | 13 +++--- 12 files changed, 145 insertions(+), 39 deletions(-) create mode 100644 handlers/installer.js diff --git a/.gitignore b/.gitignore index ecd64c4..2ef37d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ config-dev.yml package-lock.json -cache.json \ No newline at end of file +cache.json +.env \ No newline at end of file diff --git a/config.yml b/config.yml index b9f2ddf..81dd81d 100644 --- a/config.yml +++ b/config.yml @@ -1,8 +1,8 @@ # PteroStats config # If you need help, join our discord server here: https://discord.gg/zv6maQRah3 +version: 4 # Do not change this unless you know what you are doing! # Bot Configuration -token: "Put Bot Token Here" presence: enable: true text: "Hosting Panel" @@ -10,15 +10,9 @@ presence: status: "online" # can be 'online', 'idle', 'dnd', or 'invisible' # Discord Channel and Refresh Time Configuration -channel: "Put Channel ID Here" refresh: 10 # How much time the bot will refresh the stats timeout: 5 # How much time to wait for a node to respond to the bot (if you change this, it will add more time to refresh the stats) -# Panel Configuration -panel: - url: "Put Panel URL Here" - key: "Put Panel APIKEY Here" - # Message and Embed Configuration # set the option as '' if you want to disable it message: @@ -28,10 +22,10 @@ message: embed: title_nodes: "Nodes Stats" title_panel: "Panel Stats" - author: "PteroStats" + author: "Hosting Panel" color: "5865F2" description: "Next update {{time}}" # You can use {{time}} to make "in X seconds" time format - footer: "By Hirzi#8701" + footer: "By @HirziDevs" timestamp: true thumbnail: "" image: "" @@ -42,6 +36,8 @@ button: row1: - label: "Panel" url: "https://panel.example.com" +# - label: "Dashboard" +# url: "https://dash.example.com" # Status Message Configuration # How to use custom emoji: https://github.com/HirziDevs/PteroStats#using-custom-emoji diff --git a/handlers/config.js b/handlers/config.js index b444ac2..d781a78 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -3,6 +3,8 @@ const fs = require("node:fs"); const yaml = require("js-yaml"); const cliColor = require("cli-color"); +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...")) @@ -10,11 +12,18 @@ if (fs.existsSync("config-dev.yml")) { } try { - const testURL = new URL(config.panel.url); + const testURL = new URL(process.env?.PanelURL); if (!testURL.protocol.startsWith("http")) throw new Error(); } catch { console.error('Config Error | Invalid URL Format! Example Correct URL: "https://panel.example.com"'); process.exit(); } +if (config.version !== 4) { + console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/dev/config.yml'); + process.exit(); +} + +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Configuration loaded")) + module.exports = config \ No newline at end of file diff --git a/handlers/convertUnits.js b/handlers/convertUnits.js index 287f257..d75d46c 100644 --- a/handlers/convertUnits.js +++ b/handlers/convertUnits.js @@ -12,7 +12,7 @@ module.exports = function convertUnits(value1, value2, unit) { return `${!percentage ? 0 : percentage}%`; } else if (units.includes(unit)) { const formatValue = (value) => Number.isInteger(value) ? value.toLocaleString() : value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); - + return `${formatValue(value1 * conversionFactors[unit])} ${unit}/${(value2 === 0 || value2 < 1) ? "Unlimited" : formatValue(value2 * conversionFactors[unit]) + " " + unit}`; } else { return `${value1.toLocaleString()} MB/${value2.toLocaleString()} MB`; diff --git a/handlers/getNodeConfiguration.js b/handlers/getNodeConfiguration.js index 00f2cdc..ebbb4b7 100644 --- a/handlers/getNodeConfiguration.js +++ b/handlers/getNodeConfiguration.js @@ -1,11 +1,11 @@ const config = require("./config.js"); module.exports = async function getNodeConfiguration(id) { - return fetch(`${new URL(config.panel.url).origin}/api/application/nodes/${id}/configuration`, { + return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/nodes/${id}/configuration`, { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.json()) diff --git a/handlers/getNodesDetails.js b/handlers/getNodesDetails.js index 0966acd..a4f75e5 100644 --- a/handlers/getNodesDetails.js +++ b/handlers/getNodesDetails.js @@ -3,11 +3,11 @@ const config = require("./config.js"); const axios = require("axios"); module.exports = async function getAllNodes() { - return axios(`${new URL(config.panel.url).origin}/api/application/nodes?include=servers,location,allocations`, { + return axios(`${new URL(process.env?.PanelURL).origin}/api/application/nodes?include=servers,location,allocations`, { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.data.data.filter((node) => !config.nodes_settings.blacklist.includes(node.attributes.id))) diff --git a/handlers/getServers.js b/handlers/getServers.js index 18bcbc0..5e96b77 100644 --- a/handlers/getServers.js +++ b/handlers/getServers.js @@ -2,12 +2,12 @@ const config = require("./config.js"); const cliColor = require("cli-color"); module.exports = async function getServers() { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel servers...")) - return fetch(`${new URL(config.panel.url).origin}/api/application/servers`, { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel servers...")) + return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/servers`, { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.json()) diff --git a/handlers/getStats.js b/handlers/getStats.js index 24e73e5..47d6549 100644 --- a/handlers/getStats.js +++ b/handlers/getStats.js @@ -9,18 +9,18 @@ const promiseTimeout = require("./promiseTimeout.js"); const cliColor = require("cli-color"); module.exports = async function getStats() { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel nodes...")) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel nodes...")) 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(`Getting "${node.attributes.name}" configuration...`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Fetching "${node.attributes.name}" configuration...`)) const nodeConfig = await getNodeConfiguration(node.attributes.id); - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Getting "${node.attributes.name}" wings status...`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Checking "${node.attributes.name}" wings status...`)) const nodeStatus = await promiseTimeout(getWingsStatus(node, nodeConfig.token), config.timeout * 1000); if (!nodeStatus) - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node "${node.attributes.name}" is offline`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node "${node.attributes.name}" is currently offline`)) return { attributes: { name: node.attributes.name, diff --git a/handlers/getUsers.js b/handlers/getUsers.js index 55d72be..19e403d 100644 --- a/handlers/getUsers.js +++ b/handlers/getUsers.js @@ -2,12 +2,12 @@ const config = require("./config.js"); const cliColor = require("cli-color"); module.exports = async function getUsers() { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel users...")) - return fetch(`${new URL(config.panel.url).origin}/api/application/users`, { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel users...")) + return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/users`, { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.json()) diff --git a/handlers/installer.js b/handlers/installer.js new file mode 100644 index 0000000..4d2a6c0 --- /dev/null +++ b/handlers/installer.js @@ -0,0 +1,77 @@ +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); +const fs = require("fs") +const cliColor = require("cli-color") + +const questions = [ + "Please enter your panel name: ", + "Please enter your panel URL: ", + "Please enter your panel API key: ", + "Please enter your bot token: ", + "Please enter your channel ID: " +]; + +const answers = []; + +const isValidURL = (url) => { + try { + new URL(url); + return true; + } catch (error) { + return false; + } +}; + +module.exports = function Installer() { + console.log("Welcome! Please fill this question to start PteroStats."); + + const askQuestion = (index) => { + if (index < questions.length) { + console.log(questions[index]); + + readline.question('> ', answer => { + let isValid = true; + + if (index === 1 && !isValidURL(answer)) { + console.log(cliColor.redBright("Invalid URL. Please enter a valid URL.")); + isValid = false; + } else if (index === 2 && !/^(plcn_|ptlc_|peli_|ptla_)/.test(answer)) { + console.log(cliColor.redBright("Invalid API key. It must start with 'plcn_' or 'ptlc_'.")); + isValid = false; + } else if (index === 4 && !/^\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 (isValid) { + answers.push(isValidURL(answer) ? new URL(answer).origin : answer); + askQuestion(index + 1); + } else { + askQuestion(index); + } + }); + } else { + console.log(" \n====================================================") + console.log("Thank you! Here are the details you've provided:"); + console.log(`Panel Name : ${cliColor.cyanBright(answers[0])}`); + console.log(`Panel URL : ${cliColor.cyanBright(answers[1])}`); + console.log(`Panel API Key : ${cliColor.cyanBright(answers[2])}`); + console.log(`Discord Bot Token : ${cliColor.cyanBright(answers[3])}`); + console.log(`Discord Channel ID : ${cliColor.cyanBright(answers[4])}`); + console.log("====================================================\n \n") + + fs.writeFileSync(".env", `PanelURL=${answers[1]}\nPanelKEY=${answers[2]}\nDiscordBotToken=${answers[3]}\nDiscordChannel=${answers[4]}`, "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(cliColor.cyanBright("Please restart the bot to continue.")) + readline.close(); + } + }; + + askQuestion(0); +} \ No newline at end of file diff --git a/index.js b/index.js index e123485..4b356a1 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,30 @@ -const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType } = require("discord.js"); -const config = require("./handlers/config.js"); -const convertUnits = require("./handlers/convertUnits.js"); -const getStats = require("./handlers/getStats.js"); +const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const cachePath = require('node:path').join(__dirname, "./cache.json"); const fs = require("node:fs"); const cliColor = require("cli-color"); +console.log(` + _${cliColor.blueBright.bold("Pterodactyl & Pelican")}___ ______ ______ + /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ + \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ + \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ + \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold("4.0.0-dev")}`); + +console.log( + ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + + " \n \nDiscord: https://discord.znproject.my.id" + + " \n Source: https://github.com/HirziDevs/PteroStats" + + " \nLicense: https://github.com/Hirzidevs/PteroStats/blob/main/LICENSE" + + " \n \nPteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. \n \n" +); + +if (!fs.existsSync(".env")) return require("./handlers/installer.js")() + +require("dotenv").config() +const config = require("./handlers/config.js"); +const convertUnits = require("./handlers/convertUnits.js"); +const getStats = require("./handlers/getStats.js"); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright("Starting bot...")); console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using development build! some features may not work.")); @@ -141,7 +160,7 @@ async function createMessage({ cache, panel, nodes, servers, users }) { if (!cache && !panel) { embeds[embeds.length - 1].setDescription( - embeds[embeds.length - 1].data.description ? embeds[embeds.length - 1].data.description + "\n\nThere are no nodes to display!" : "There are no nodes to display!" + embeds[embeds.length - 1].data.description ? embeds[embeds.length - 1].data.description + "\n\nThere are no nodes to be display!" : "There are no nodes to be display!" ); } @@ -159,10 +178,13 @@ async function createMessage({ cache, panel, nodes, servers, users }) { ) ) } + + try { + const channel = await client.channels.fetch(process.env?.DiscordChannel); const messages = await channel.messages.fetch({ limit: 10 }); const botMessage = messages.find(msg => msg.author.id === client.user.id); - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright("Panel stats have been posted to the channel!")); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright(`Panel stats successfuly posted to the "${channel.name}" channel!`)); setTimeout(() => startGetStatus(), config.refresh * 1000); @@ -192,7 +214,7 @@ function DiscordErrorHandler(error) { } try { - client.login(config.token); + client.login(process.env?.DiscordBotToken); } catch { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!")); process.exit(); diff --git a/package.json b/package.json index 988b7ad..f974cf6 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "pterostats", - "version": "v4.0.0", - "description": "PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server", + "version": "4.0.0", + "description": "PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server.", "license": "MIT", "repository": "HirziDevs/PteroStats", - "homepage": "https://pterostats.hirzidevs.xyz", + "homepage": "https://pterostats.znproject.my.id", "bugs": { "email": "hirzidevs@gmail.com", "url": "https://github.com/HirziDevs/PteroStats/issues" @@ -13,12 +13,13 @@ "start": "node index.js" }, "dependencies": { - "axios": "^1.7.2", + "axios": "^1.7.7", "cli-color": "^2.0.4", - "discord.js": "^14.15.3", + "discord.js": "^14.16.1", + "dotenv": "^16.4.5", "js-yaml": "^4.1.0" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" } } \ No newline at end of file From c20f528990b378c26862693b792c815794dbc618 Mon Sep 17 00:00:00 2001 From: Hirzi <64255651+HirziDevs@users.noreply.github.com> Date: Tue, 10 Sep 2024 08:44:35 +0700 Subject: [PATCH 13/59] docs: update installation guide in README --- README.md | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 7786925..fb69e74 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@
-PteroStats Banner +# PteroStats v4 +PteroStats Banner
## This is a development build! some features may not work. ## Introduction -PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server +PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. -## Example -- Test Panel - - Example +## Preview +PteroStats V4 Preview ## Installation -1. [Getting API key from pterodactyl](#getting-apikey-from-pterodactyl) +1. [Getting API key](#getting-api-key) 2. [Creating Discord Bot](#creating-discord-bot) 3. [Inviting Discord Bot](#inviting-discord-bot) 4. [Getting Channel ID](#getting-channel-id) @@ -25,12 +24,12 @@ PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and pos - [Using Custom Emoji](#using-custom-emoji) - [Blacklist Nodes](#blacklist-nodes) -### Getting API key from pterodactyl +### Getting API key > [!WARNING] > The use of Application API keys are **deprecated**, you should use **Client API keys** in the config file -1. Go to your `Pterodactyl Panel` and go to `Account Page`. +1. Go to your `Pterodactyl or Pelican Panel` and go to `Account Page`. Home @@ -46,17 +45,9 @@ PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and pos API Key -5. Paste the panel API key and panel url at the config - - Panel Config - ### Creating Discord Bot Please refer to [this website](https://discordjs.guide/preparations/setting-up-a-bot-application.html) -Paste the bot token at the config - -Bot Config - ### Inviting Discord Bot Please refer to [this website](https://discordjs.guide/preparations/adding-your-bot-to-servers.html) @@ -69,16 +60,16 @@ Please refer to [this website](https://discordjs.guide/preparations/adding-your- Right Click Channel -3. Paste the channel id at the config - - Channel Config - ### Starting bot 1. Make sure you have done the things above 2. Run `npm install` in the root directory of the bot files. -3. Run `node index` and you are done. +3. Run `node index` and fill given question to setup the bot. -if you need help contact me on discord `@hirzidevs` or join [our discord support server](https://discord.gg/zv6maQRah3) + PteroStats V4 Preview + +4. Run `node index` again to start the bot and you are done. + +if you need help contact me on [our discord support server](https://discord.gg/zv6maQRah3) ### Using Custom Emoji 1. type `\` in server that has custom emoji you want @@ -122,7 +113,9 @@ If you having this issue, you can enable `log_error` on the config file and repo - [PteroStats DiscordJS v13](https://github.com/HirziDevs/PteroStats/tree/3d0512c3323ecf079101104c7ecf3c94d265e298) - [PteroStats DiscordJS v12](https://github.com/HirziDevs/PteroStats/tree/bcfa266be64dda11955f0bf9732da086bcea522c) +- [Pelican Panel](https://pelican.dev) - [Pterodactyl Panel](https://pterodactyl.io) +- [Pelican API Documentation](https://demo.pelican.dev/docs/api) - [Pterodactyl API Documentation](https://github.com/devnote-dev/ptero-notes/) - [Pterodactyl Discord Server](https://discord.gg/pterodactyl) -- [PteroBot Support Server](https://discord.gg/zv6maQRah3) +- [Support Server](https://discord.gg/zv6maQRah3) From 1c20949f2086988ee8abbd2231f473636410c82a Mon Sep 17 00:00:00 2001 From: Hirzi <64255651+HirziDevs@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:53:27 +0700 Subject: [PATCH 14/59] docs: update installation guide in README --- README.md | 53 +++++++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index fb69e74..5d526b8 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,35 @@ PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. ## Preview -PteroStats V4 Preview +PteroStats V4 Preview ## Installation -1. [Getting API key](#getting-api-key) -2. [Creating Discord Bot](#creating-discord-bot) -3. [Inviting Discord Bot](#inviting-discord-bot) -4. [Getting Channel ID](#getting-channel-id) -5. [Starting bot](#starting-bot) - +- [Starting the App](#starting-the-app) +- [Getting an Panel API key](#getting-panel-api-key) +- [Getting a Channel ID](#getting-channel-id) - [Using Custom Emoji](#using-custom-emoji) - [Blacklist Nodes](#blacklist-nodes) +- [Reporting a Bug](#reporting-a-bug) -### Getting API key +### Starting the App +1. [Create your Discord App](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). +2. [Invite your Discord App to your Discord server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). +3. Run `npm install` in the root directory of the app files. +4. Run `node index` and answer the provided questions to set up the app. + + PteroStats V4 Preview + + - [Getting Panel API Key](#getting-panel-api-key) + - [Getting a Channel ID](#getting-channel-id) + +5. Run `node index` again to start the app, and you're done! + +If you need help, contact me on [our Discord support server](https://discord.gg/zv6maQRah3). + +### Getting Panel API key > [!WARNING] -> The use of Application API keys are **deprecated**, you should use **Client API keys** in the config file +> The use of Application API keys are **deprecated**, you should use **Client API keys**. 1. Go to your `Pterodactyl or Pelican Panel` and go to `Account Page`. @@ -45,12 +58,6 @@ PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel API Key -### Creating Discord Bot -Please refer to [this website](https://discordjs.guide/preparations/setting-up-a-bot-application.html) - -### Inviting Discord Bot -Please refer to [this website](https://discordjs.guide/preparations/adding-your-bot-to-servers.html) - ### Getting Channel ID 1. Enable Developer Feature at your discord settings @@ -60,17 +67,6 @@ Please refer to [this website](https://discordjs.guide/preparations/adding-your- Right Click Channel -### Starting bot -1. Make sure you have done the things above -2. Run `npm install` in the root directory of the bot files. -3. Run `node index` and fill given question to setup the bot. - - PteroStats V4 Preview - -4. Run `node index` again to start the bot and you are done. - -if you need help contact me on [our discord support server](https://discord.gg/zv6maQRah3) - ### Using Custom Emoji 1. type `\` in server that has custom emoji you want @@ -105,9 +101,9 @@ You can add more than one node in the blacklist Blacklist Config -## The node is online but the embed is read as offline +## Reporting a Bug -If you having this issue, you can enable `log_error` on the config file and report it to our discord server at [Support Server](https://discord.gg/zv6maQRah3) +Enable `log_error` in the `config.yml` file and check the console for the error message. After that, report it to our Discord server at [Support Server](https://discord.gg/zv6maQRah3). ## Links @@ -118,4 +114,5 @@ If you having this issue, you can enable `log_error` on the config file and repo - [Pelican API Documentation](https://demo.pelican.dev/docs/api) - [Pterodactyl API Documentation](https://github.com/devnote-dev/ptero-notes/) - [Pterodactyl Discord Server](https://discord.gg/pterodactyl) +- [Pelican Discord Server](https://discord.gg/pelican-panel) - [Support Server](https://discord.gg/zv6maQRah3) From 32daac1e91492c19922d2a4fb98cf56fecafe304 Mon Sep 17 00:00:00 2001 From: Hirzi <64255651+HirziDevs@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:46:54 +0700 Subject: [PATCH 15/59] feat(installer): remove logs after answering setup questions --- handlers/installer.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/handlers/installer.js b/handlers/installer.js index 4d2a6c0..0c0b0f3 100644 --- a/handlers/installer.js +++ b/handlers/installer.js @@ -55,18 +55,10 @@ module.exports = function Installer() { } }); } else { - console.log(" \n====================================================") - console.log("Thank you! Here are the details you've provided:"); - console.log(`Panel Name : ${cliColor.cyanBright(answers[0])}`); - console.log(`Panel URL : ${cliColor.cyanBright(answers[1])}`); - console.log(`Panel API Key : ${cliColor.cyanBright(answers[2])}`); - console.log(`Discord Bot Token : ${cliColor.cyanBright(answers[3])}`); - console.log(`Discord Channel ID : ${cliColor.cyanBright(answers[4])}`); - console.log("====================================================\n \n") fs.writeFileSync(".env", `PanelURL=${answers[1]}\nPanelKEY=${answers[2]}\nDiscordBotToken=${answers[3]}\nDiscordChannel=${answers[4]}`, "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(cliColor.green(` \nConfiguration saved in ${cliColor.blueBright(".env")} and ${cliColor.blueBright("config.yml")}.\n `)); console.log(cliColor.cyanBright("Please restart the bot to continue.")) readline.close(); From 5b961b6ba815912b6ceab8d48fd4e647f5ebe39c Mon Sep 17 00:00:00 2001 From: Hirzi <64255651+HirziDevs@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:49:42 +0700 Subject: [PATCH 16/59] refactor(logging): change from using quotes to color for nodes, channel, and bot name --- handlers/getStats.js | 6 +++--- index.js | 25 ++++++++++++------------ package.json | 46 ++++++++++++++++++++++---------------------- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/handlers/getStats.js b/handlers/getStats.js index 47d6549..cf62382 100644 --- a/handlers/getStats.js +++ b/handlers/getStats.js @@ -14,13 +14,13 @@ module.exports = async function getStats() { 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 "${node.attributes.name}" configuration...`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Fetching ${cliColor.blueBright(node.attributes.name)} configuration...`)) const nodeConfig = await getNodeConfiguration(node.attributes.id); - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Checking "${node.attributes.name}" wings status...`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Checking ${cliColor.blueBright(node.attributes.name)} wings status...`)) const nodeStatus = await promiseTimeout(getWingsStatus(node, nodeConfig.token), config.timeout * 1000); if (!nodeStatus) - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node "${node.attributes.name}" is currently offline`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node ${cliColor.blueBright(node.attributes.name)} is currently offline.`)) return { attributes: { name: node.attributes.name, diff --git a/index.js b/index.js index 4b356a1..703646b 100644 --- a/index.js +++ b/index.js @@ -2,20 +2,21 @@ const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType, ActionRowBu const cachePath = require('node:path').join(__dirname, "./cache.json"); const fs = require("node:fs"); const cliColor = require("cli-color"); +const package = require("./package.json") console.log(` - _${cliColor.blueBright.bold("Pterodactyl & Pelican")}___ ______ ______ - /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ - \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ - \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ - \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold("4.0.0-dev")}`); + _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ + /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ + \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ + \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ + \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}`); console.log( ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + " \n \nDiscord: https://discord.znproject.my.id" + " \n Source: https://github.com/HirziDevs/PteroStats" + " \nLicense: https://github.com/Hirzidevs/PteroStats/blob/main/LICENSE" + - " \n \nPteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. \n \n" + ` \n \n${package.description}\n ` ); if (!fs.existsSync(".env")) return require("./handlers/installer.js")() @@ -25,8 +26,8 @@ const config = require("./handlers/config.js"); const convertUnits = require("./handlers/convertUnits.js"); const getStats = require("./handlers/getStats.js"); -console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright("Starting bot...")); -console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using development build! some features may not work.")); +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting bot...")); +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using a development build! Some features may not work as intended.")); const client = new Client({ intents: [GatewayIntentBits.Guilds] @@ -42,7 +43,7 @@ async function startGetStatus() { users: results.users, }); } catch { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is offline")); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is currently offline.")); fs.readFile(cachePath, (err, data) => { if (err) { @@ -68,7 +69,7 @@ async function startGetStatus() { } client.once("ready", () => { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright(`${client.user.tag} is online!`)); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`${cliColor.blueBright(client.user.tag)} is online!`)); if (config.presence.enable) { if (config.presence.text && config.presence.type) { @@ -184,7 +185,7 @@ async function createMessage({ cache, panel, nodes, servers, users }) { const messages = await channel.messages.fetch({ limit: 10 }); const botMessage = messages.find(msg => msg.author.id === client.user.id); - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright(`Panel stats successfuly posted to the "${channel.name}" channel!`)); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`Panel stats successfully posted to the ${cliColor.blueBright(channel.name)} channel!`)); setTimeout(() => startGetStatus(), config.refresh * 1000); @@ -218,4 +219,4 @@ try { } catch { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!")); process.exit(); -} +} \ No newline at end of file diff --git a/package.json b/package.json index f974cf6..b6a12b5 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,25 @@ { - "name": "pterostats", - "version": "4.0.0", - "description": "PteroStats is a Discord Bot that 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", - "bugs": { - "email": "hirzidevs@gmail.com", - "url": "https://github.com/HirziDevs/PteroStats/issues" - }, - "scripts": { - "start": "node index.js" - }, - "dependencies": { - "axios": "^1.7.7", - "cli-color": "^2.0.4", - "discord.js": "^14.16.1", - "dotenv": "^16.4.5", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">=18" - } + "name": "pterostats", + "version": "4.0.0", + "description": "PteroStats is a Discord App that 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", + "bugs": { + "email": "hirzidevs@gmail.com", + "url": "https://github.com/HirziDevs/PteroStats/issues" + }, + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "axios": "^1.7.7", + "cli-color": "^2.0.4", + "discord.js": "^14.16.1", + "dotenv": "^16.4.5", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">=18" + } } \ No newline at end of file From 9d3e8dfa31ebedc5a18ff071639c002668f4116a Mon Sep 17 00:00:00 2001 From: Hirzi <64255651+HirziDevs@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:59:11 +0700 Subject: [PATCH 17/59] fix: correct ASCII text formatting --- index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 703646b..398c02e 100644 --- a/index.js +++ b/index.js @@ -6,10 +6,10 @@ const package = require("./package.json") console.log(` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ - /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ - \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ - \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ - \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}`); + /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ + \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ + \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ + \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}`); console.log( ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + @@ -219,4 +219,4 @@ try { } catch { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!")); process.exit(); -} \ No newline at end of file +} From 5041476faecb1455da47b758a297cb6f503781da Mon Sep 17 00:00:00 2001 From: Hirzi <64255651+HirziDevs@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:11:20 +0700 Subject: [PATCH 18/59] docs: fix typo in README --- README.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5d526b8..7b56f57 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ -## This is a development build! some features may not work. +> [!WARNING] +> This is a development build! Some features may not work. ## Introduction PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. @@ -37,20 +38,20 @@ PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel If you need help, contact me on [our Discord support server](https://discord.gg/zv6maQRah3). -### Getting Panel API key +### Getting Panel API Key > [!WARNING] -> The use of Application API keys are **deprecated**, you should use **Client API keys**. +> The use of Application API keys is **deprecated**; you should use **Client API keys**. -1. Go to your `Pterodactyl or Pelican Panel` and go to `Account Page`. +1. Go to your `Pterodactyl` or `Pelican Panel` and navigate to the `Account Page`. Home -2. Click on the `API Credentials` button +2. Click on the `API Credentials` button. Account Page -3. Fill the `Description` and click on the `Create` button +3. Fill in the `Description` and click the `Create` button. Create Client API Key @@ -59,20 +60,22 @@ If you need help, contact me on [our Discord support server](https://discord.gg/ API Key ### Getting Channel ID -1. Enable Developer Feature at your discord settings + +1. Enable Developer Mode in your Discord settings. Discord User Settings -2. Right Click text channel and select `Copy ID` +2. Right-click the text channel and select `Copy ID`. Right Click Channel ### Using Custom Emoji -1. type `\` in server that has custom emoji you want + +1. Type `\` in the server that has the custom emoji you want. Type \ on the chat -2. Select custom emoji you want +2. Select the custom emoji you want. Select Custom Emoji @@ -80,24 +83,25 @@ If you need help, contact me on [our Discord support server](https://discord.gg/ Copy Emoji ID -4. Paste the emoji id at the config +4. Paste the emoji ID into the config. Status Config ### Blacklist Nodes -1. Select node from node list on admin page + +1. Select a node from the node list on the admin page. Nodes List -2. Check the url and copy the node id +2. Check the URL and copy the node ID. - Node Id + Node ID -3. Paste the id to the blacklist on config +3. Paste the ID into the blacklist in the config. Blacklist Config -You can add more than one node in the blacklist +You can add more than one node to the blacklist. Blacklist Config From bd9cdd570672bc26ab251708e3a9c2d57cf77d92 Mon Sep 17 00:00:00 2001 From: Lezetho <126505858+lezetho@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:58:25 +0545 Subject: [PATCH 19/59] Update index.js --- index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 398c02e..ff491c5 100644 --- a/index.js +++ b/index.js @@ -200,8 +200,10 @@ async function createMessage({ cache, panel, nodes, servers, users }) { } function DiscordErrorHandler(error) { - if (error.code === "ENOTFOUND") { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ENOTFOUND | DNS Error. Ensure your network connection and DNS server are functioning correctly.")); + 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.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) { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!")); } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code === "MAX_EMBED_SIZE_EXCEEDED") { From 4e796f275e332ad61522c274b870c165312fb7f1 Mon Sep 17 00:00:00 2001 From: Lezetho <126505858+lezetho@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:00:00 +0545 Subject: [PATCH 20/59] Update index.js --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index ff491c5..c7fe390 100644 --- a/index.js +++ b/index.js @@ -202,6 +202,8 @@ async function createMessage({ cache, panel, nodes, servers, users }) { function DiscordErrorHandler(error) { 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/lezetho/--PteroStats?tab=readme-ov-file#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) { From efcc962fe3ea1a9ba122cdb2b18db5baca4faf9b Mon Sep 17 00:00:00 2001 From: Lezetho <126505858+lezetho@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:38:05 +0545 Subject: [PATCH 21/59] Update package.json --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b6a12b5..c9b69ca 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,13 @@ "start": "node index.js" }, "dependencies": { - "axios": "^1.7.7", - "cli-color": "^2.0.4", - "discord.js": "^14.16.1", - "dotenv": "^16.4.5", - "js-yaml": "^4.1.0" + "axios": "1.7.7", + "cli-color": "2.0.4", + "discord.js": "14.16.1", + "dotenv": "16.4.5", + "js-yaml": "4.1.0" }, "engines": { "node": ">=18" } -} \ No newline at end of file +} From a826f5709eadf92b8d00ece05e1b826a1cdf8a08 Mon Sep 17 00:00:00 2001 From: Hirzi Date: Wed, 11 Sep 2024 08:13:43 +0700 Subject: [PATCH 22/59] feat: separate embed configuration for panel and nodes --- config.yml | 37 +++++++++++++++++++++-------- index.js | 68 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/config.yml b/config.yml index 81dd81d..c4e810b 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ # PteroStats config # If you need help, join our discord server here: https://discord.gg/zv6maQRah3 -version: 4 # Do not change this unless you know what you are doing! +version: 5 # Do not change this unless you know what you are doing! # Bot Configuration presence: @@ -20,15 +20,32 @@ message: attachment: "" # If you enable attachment on message it will upload the attachment first before sending or editing message and will result in delayed stats embed: - title_nodes: "Nodes Stats" - title_panel: "Panel Stats" - author: "Hosting Panel" - color: "5865F2" - description: "Next update {{time}}" # You can use {{time}} to make "in X seconds" time format - footer: "By @HirziDevs" - timestamp: true - thumbnail: "" - image: "" + panel: + author: + name: "Hosting Panel" + icon: "" + title: "Panel Stats" + description: "Next update {{time}}" + timestamp: false + color: "5865F2" + footer: + text: "" + icon: "" + thumbnail: "" + image: "" + nodes: + author: + name: "ZN Panel" + icon: "" + title: "Nodes Stats" + description: "" + timestamp: true + color: "5865F2" + footer: + text: "By @HirziDevs" + icon: "" + thumbnail: "" + image: "" # Message Button Configuration button: diff --git a/index.js b/index.js index c7fe390..6ba3a5f 100644 --- a/index.js +++ b/index.js @@ -6,10 +6,10 @@ const package = require("./package.json") console.log(` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ - /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ - \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ - \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ - \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}`); + /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ + \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ + \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ + \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}`); console.log( ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + @@ -26,7 +26,7 @@ const config = require("./handlers/config.js"); const convertUnits = require("./handlers/convertUnits.js"); const getStats = require("./handlers/getStats.js"); -console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting bot...")); +console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting app...")); console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using a development build! Some features may not work as intended.")); const client = new Client({ @@ -107,15 +107,21 @@ client.once("ready", () => { async function createMessage({ cache, panel, nodes, servers, users }) { let embed = new EmbedBuilder() - .setTitle(config.embed.title_nodes) - .setColor(config.embed.color); + .setAuthor({ + name: config.embed.nodes.author.name || null, + iconURL: config.embed.nodes.author.icon || null + }) + .setTitle(config.embed.nodes.title || null) + .setColor(config.embed.nodes.color || null) + .setURL(config.embed.nodes.url || null) + .setThumbnail(config.embed.nodes.thumbnail || null) let embeds = [embed]; if (config.nodes_settings.details) { nodes.forEach((node, index) => { if (index % 25 === 0 && index !== 0) { - embed = new EmbedBuilder().setColor(config.embed.color); + embed = new EmbedBuilder().setColor(config.embed.nodes.color); if (embeds.length < 9) embeds.push(embed); } @@ -134,8 +140,27 @@ async function createMessage({ cache, panel, nodes, servers, users }) { } let panelEmbed = new EmbedBuilder() - .setColor(config.embed.color) - .setTitle(config.embed.title_panel) + .setAuthor({ + name: config.embed.panel.author.name || null, + iconURL: config.embed.panel.author.icon || null + }) + .setColor(config.embed.panel.color || null) + .setTitle(config.embed.panel.title || null) + .setURL(config.embed.panel.url || null) + .setTimestamp(config.embed.panel.timestamp ? new Date() : null) + .setFooter({ + text: config.embed.panel.footer.text || null, + iconURL: config.embed.panel.footer.icon || null + }) + .setThumbnail(config.embed.panel.thumbnail || null) + .setImage(config.embed.panel.image || null) + .setDescription( + config.embed.panel.description + .replace( + "{{time}}", + time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R") + ) || null + ) .addFields({ name: `Panel - ${panel ? config.status.online : config.status.offline}`, value: @@ -148,16 +173,13 @@ async function createMessage({ cache, panel, nodes, servers, users }) { if (config.panel_settings.status) embeds.unshift(panelEmbed); - embeds[0] - .setAuthor(config.embed.author ? { name: config.embed.author } : null) - .setDescription( - embeds[0].data.description ? config.embed.description.replace("{{time}}", time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R")) + "\n\n" + embeds[0].data.description : config.embed.description.replace("{{time}}", time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R")) - ); - embeds[embeds.length - 1] - .setFooter(config.embed.footer ? { text: config.embed.footer } : null); - - if (config.embed.timestamp) embeds[embeds.length - 1].setTimestamp(); + .setTimestamp(config.embed.nodes.timestamp ? new Date() : null) + .setFooter({ + text: config.embed.nodes.footer.text || null, + iconURL: config.embed.nodes.footer.icon || null + }) + .setImage(config.embed.nodes.image || null) if (!cache && !panel) { embeds[embeds.length - 1].setDescription( @@ -200,12 +222,12 @@ async function createMessage({ cache, panel, nodes, servers, users }) { } function DiscordErrorHandler(error) { - if (error.rawError?.code === 429) { + 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/lezetho/--PteroStats?tab=readme-ov-file#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: 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.")); + 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) { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!")); } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code === "MAX_EMBED_SIZE_EXCEEDED") { @@ -223,4 +245,4 @@ try { } catch { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!")); process.exit(); -} +} \ No newline at end of file From c6996eb80bf3fc623b8bab81f1a6237d90b1cd45 Mon Sep 17 00:00:00 2001 From: Hirzi Date: Wed, 11 Sep 2024 08:19:00 +0700 Subject: [PATCH 23/59] feat: update resource unit conversion, add "byte" and "percentage" as allowed values --- config.yml | 2 +- handlers/convertUnits.js | 33 ++++++++++++++------------------- package.json | 3 ++- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/config.yml b/config.yml index c4e810b..ef7dfb4 100644 --- a/config.yml +++ b/config.yml @@ -69,7 +69,7 @@ nodes_settings: details: true # enable nodes details i.e memory and disk usage servers: true location: true - unit: "mb" # Allowed values- "gb", "mb", "tb", or "%" + unit: "mb" # Allowed values: "byte" or "percentage" limit: 100 # Panel Users and Servers Settings diff --git a/handlers/convertUnits.js b/handlers/convertUnits.js index d75d46c..8c9690b 100644 --- a/handlers/convertUnits.js +++ b/handlers/convertUnits.js @@ -1,20 +1,15 @@ -module.exports = function convertUnits(value1, value2, unit) { - unit = unit.toUpperCase(); - const units = ['MB', 'GB', 'TB']; - const conversionFactors = { - 'MB': 1, - 'GB': 1 / 1024, - 'TB': 1 / (1024 * 1024) - }; - - if (unit === '%' || unit === "PERCENTAGE" || unit === "PERCENT") { - const percentage = Math.floor((value1 / value2) * 100); - return `${!percentage ? 0 : percentage}%`; - } else if (units.includes(unit)) { - const formatValue = (value) => Number.isInteger(value) ? value.toLocaleString() : value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); - - return `${formatValue(value1 * conversionFactors[unit])} ${unit}/${(value2 === 0 || value2 < 1) ? "Unlimited" : formatValue(value2 * conversionFactors[unit]) + " " + unit}`; - } else { - return `${value1.toLocaleString()} MB/${value2.toLocaleString()} MB`; - } +const prettyBytes = require('prettier-bytes'); + +module.exports = function convertUnits(value1, value2, unit) { + unit = unit.toUpperCase(); + switch (unit) { + case 'PERCENTAGE': + case 'PERCENT': + const percentage = Math.floor((value1 / value2) * 100); + return `${!percentage ? 0 : percentage}%`; + case 'BYTE': + return `${prettyBytes(value1)} / ${prettyBytes(value2)}`; + default: + return `${value1.toLocaleString()} ${unit}/${value2.toLocaleString()} ${unit}`; + } } \ No newline at end of file diff --git a/package.json b/package.json index c9b69ca..daf333f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "cli-color": "2.0.4", "discord.js": "14.16.1", "dotenv": "16.4.5", - "js-yaml": "4.1.0" + "js-yaml": "4.1.0", + "prettier-bytes": "1.0.4" }, "engines": { "node": ">=18" From 49134e79c9d7ca1e3fc3ad2f749fdb9ccc2220e4 Mon Sep 17 00:00:00 2001 From: Hirzi Date: Wed, 11 Sep 2024 08:19:55 +0700 Subject: [PATCH 24/59] feat: update config version to 5 --- handlers/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/config.js b/handlers/config.js index d781a78..7609433 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -19,11 +19,11 @@ try { process.exit(); } -if (config.version !== 4) { +if (config.version !== 5) { console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/dev/config.yml'); process.exit(); } console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Configuration loaded")) -module.exports = config \ No newline at end of file +module.exports = configconfig \ No newline at end of file From 62d66942c19b242efb66a403b671e6ce158d530f Mon Sep 17 00:00:00 2001 From: Hirzi Date: Wed, 11 Sep 2024 08:22:15 +0700 Subject: [PATCH 25/59] feat(installer): validate credentials before saving --- handlers/installer.js | 76 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/handlers/installer.js b/handlers/installer.js index 0c0b0f3..24576cc 100644 --- a/handlers/installer.js +++ b/handlers/installer.js @@ -1,9 +1,11 @@ +const axios = require("axios") +const cliColor = require("cli-color") +const { Client, GatewayIntentBits } = require("discord.js") +const fs = require("fs") const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout }); -const fs = require("fs") -const cliColor = require("cli-color") const questions = [ "Please enter your panel name: ", @@ -25,7 +27,8 @@ const isValidURL = (url) => { }; module.exports = function Installer() { - console.log("Welcome! Please fill this question to start PteroStats."); + console.log(cliColor.cyanBright("Welcome to PteroStats!")) + console.log(cliColor.yellow("Please fill in the following credentials to set up the app.\n ")); const askQuestion = (index) => { if (index < questions.length) { @@ -55,13 +58,70 @@ module.exports = function Installer() { } }); } else { + axios(`${new URL(answers[1]).origin}/api/application/nodes?include=servers,location,allocations`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${answers[2]}` + }, + }).then(() => { + console.log(" \n" + cliColor.green("✓ Valid Panel Credentials.")); + const client = new Client({ + intents: [GatewayIntentBits.Guilds] + }) - fs.writeFileSync(".env", `PanelURL=${answers[1]}\nPanelKEY=${answers[2]}\nDiscordBotToken=${answers[3]}\nDiscordChannel=${answers[4]}`, "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(cliColor.green(` \nConfiguration saved in ${cliColor.blueBright(".env")} and ${cliColor.blueBright("config.yml")}.\n `)); + client.login(answers[3]).then(async () => { + console.log(cliColor.green("✓ Valid Discord Bot")); + client.channels.fetch(answers[4]).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("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 `)); - console.log(cliColor.cyanBright("Please restart the bot to continue.")) - readline.close(); + console.log(cliColor.cyanBright("Please restart the app to continue.")); + process.exit() + }).catch(() => { + console.log(cliColor.redBright("❌ Invalid Channel ID.")); + console.log(" \n" + cliColor.redBright("Please run the installer again and fill in the correct credentials.")); + process.exit() + }) + }).catch(() => { + console.log(cliColor.redBright("❌ Invalid Discord Bot Token.")); + console.log(" \n" + cliColor.redBright("Please run the installer again and fill in the correct credentials.")); + process.exit() + }) + }).catch((error) => { + console.log(" \n" + cliColor.redBright("❌ Invalid Panel Credentials.")); + 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.code === "ECONNREFUSED") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ECONNREFUSED | Connection refused. Ensure the panel is running and reachable.")); + } else if (error.code === "ETIMEDOUT") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ETIMEDOUT | Connection timed out. The panel took too long to respond.")); + } else if (error.code === "ECONNRESET") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ECONNRESET | Connection reset by peer. The panel closed the connection unexpectedly.")); + } else if (error.code === "EHOSTUNREACH") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("EHOSTUNREACH | Host unreachable. The panel is down or not reachable.")); + } else if (error.response) { + if (error.response.status === 401) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("401 | Unauthorized. Invalid Application Key or API Key doesn't have permission to perform this action.")); + } else if (error.response.status === 403) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("403 | Forbidden. Invalid Application Key or API Key doesn't have permission to perform this action.")); + } else if (error.response.status === 404) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("404 | Not Found. Invalid Panel URL or the Panel doesn't exist.")); + } else if (error.response.status === 429) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("429 | Too Many Requests. You have sent too many requests in a given amount of time.")); + } else if ([500, 502, 503, 504].includes(error.response.status)) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("500 | Internal Server Error. This is an error with your panel, PteroStats is not the cause.")); + } else { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`${error.response.status} | Unexpected error: ${error.response.statusText}`)); + } + } else { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Unexpected error: ${error.message}`)); + } + console.log(" \n" + cliColor.redBright("Please run the installer again and fill in the correct credentials.")); + process.exit() + }) } }; From 1f4eeae9422934b3462a271f966231fb658fa675 Mon Sep 17 00:00:00 2001 From: Hirzi Date: Wed, 11 Sep 2024 09:50:18 +0700 Subject: [PATCH 26/59] fix: correct unit conversion displaying wrong resource --- handlers/convertUnits.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/handlers/convertUnits.js b/handlers/convertUnits.js index 8c9690b..d0dce92 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) { - unit = unit.toUpperCase(); - switch (unit) { - case 'PERCENTAGE': - case 'PERCENT': - const percentage = Math.floor((value1 / value2) * 100); - return `${!percentage ? 0 : percentage}%`; - case 'BYTE': - return `${prettyBytes(value1)} / ${prettyBytes(value2)}`; - default: - return `${value1.toLocaleString()} ${unit}/${value2.toLocaleString()} ${unit}`; - } +const prettyBytes = require('prettier-bytes'); + +module.exports = function convertUnits(value1, value2, unit) { + unit = unit.toUpperCase(); + switch (unit) { + case 'PERCENTAGE': + case 'PERCENT': + const percentage = Math.floor((value1 / value2) * 100); + return `${!percentage ? 0 : percentage}%`; + case 'BYTE': + return `${prettyBytes(value1 * 1000000)} / ${value2 === 0 ? "Unlimited" : prettyBytes(value2 * 1000000)}`; + default: + return `${value1.toLocaleString()} ${unit}/${value2 === 0 ? "Unlimited" : `${value2.toLocaleString()} ${unit}`}`; + } } \ No newline at end of file From 33d071e6d661082624d9ca804235c550108c52b7 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Wed, 11 Sep 2024 21:20:10 +0700 Subject: [PATCH 27/59] fix: correct ASCII text formatting --- index.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 6ba3a5f..8e34b1a 100644 --- a/index.js +++ b/index.js @@ -4,12 +4,13 @@ const fs = require("node:fs"); const cliColor = require("cli-color"); const package = require("./package.json") -console.log(` - _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ - /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ - \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ - \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ - \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}`); +console.log( + ` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ \n` + + ` /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ \n` + + ` \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ \n` + + ` \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ \n` + + ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}` +); console.log( ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + From 36b4896ff7d193b721f5ff712ce19a76fd82fcb6 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Wed, 11 Sep 2024 21:21:48 +0700 Subject: [PATCH 28/59] feat: set default resource unit to byte --- config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config.yml b/config.yml index ef7dfb4..caf2b33 100644 --- a/config.yml +++ b/config.yml @@ -68,8 +68,7 @@ nodes_settings: blacklist: [] # You can add node id to remove the node from status embed (Example: "blacklist: [1]") details: true # enable nodes details i.e memory and disk usage servers: true - location: true - unit: "mb" # Allowed values: "byte" or "percentage" + unit: "byte" # Allowed values: "byte" or "percentage" limit: 100 # Panel Users and Servers Settings From 5db7930e405dbbb19ea2c409b8d3b9a07b088e8a Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Wed, 11 Sep 2024 21:22:42 +0700 Subject: [PATCH 29/59] chore: temporarily remove down notifier --- config.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/config.yml b/config.yml index caf2b33..5364b8b 100644 --- a/config.yml +++ b/config.yml @@ -35,7 +35,7 @@ embed: image: "" nodes: author: - name: "ZN Panel" + name: "Hosting Panel" icon: "" title: "Nodes Stats" description: "" @@ -77,11 +77,5 @@ panel_settings: servers: true users: true -# Mentions a User or Role if any node is offline (this feature is still in testing, please report if you have a problem) -mentions: # to enable atleast put 1 ID on user or role bellow - user: [] # Put User ID here (Example: "user: ['548867757517570058', '816219634390663230']") - role: [] # Put Role ID here (Example: "role: ['796083838236622858', '858198863973187585']") - channel: "" # Put Channel ID here for the logging - # Log error to console if a server offline (enable this when you have a problem that you want to report) log_error: false # set to "true" to enable \ No newline at end of file From 132e485efc69449930f4716c5a67691800f489f0 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Wed, 11 Sep 2024 21:23:31 +0700 Subject: [PATCH 30/59] fix: repair config loader --- handlers/config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/handlers/config.js b/handlers/config.js index 7609433..452e8ee 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -1,4 +1,3 @@ - const fs = require("node:fs"); const yaml = require("js-yaml"); const cliColor = require("cli-color"); @@ -26,4 +25,4 @@ if (config.version !== 5) { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Configuration loaded")) -module.exports = configconfig \ No newline at end of file +module.exports = config \ No newline at end of file From 84e2d0e9d4bcd1df6ae729bad1b9f564d5c644dd Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Wed, 11 Sep 2024 21:24:39 +0700 Subject: [PATCH 31/59] docs: update installation guide in README --- README.md | 36 ++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7b56f57..5d81078 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,16 @@ -> [!WARNING] -> This is a development build! Some features may not work. - ## Introduction +> [!WARNING] +> This is a development build! Some features may not work as intended. + PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. ## Preview -PteroStats V4 Preview +PteroStats GIF Preview + +PteroStats Image Preview ## Installation - [Starting the App](#starting-the-app) @@ -26,23 +28,27 @@ PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel ### Starting the App 1. [Create your Discord App](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). 2. [Invite your Discord App to your Discord server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). -3. Run `npm install` in the root directory of the app files. -4. Run `node index` and answer the provided questions to set up the app. +3. [Download the repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/dev.zip) and extract it. +4. Run `npm install` in the root directory of the app files. +5. Run `node index` and answer the provided questions to set up the app. - PteroStats V4 Preview + Installer - [Getting Panel API Key](#getting-panel-api-key) - [Getting a Channel ID](#getting-channel-id) -5. Run `node index` again to start the app, and you're done! +6. Run `node index` again to start the app, and you're done! -If you need help, contact me on [our Discord support server](https://discord.gg/zv6maQRah3). + Console Logging ### Getting Panel API Key > [!WARNING] > The use of Application API keys is **deprecated**; you should use **Client API keys**. +> [!TIP] +> Make sure the owner of the Client API key has access to the administrator panel. + 1. Go to your `Pterodactyl` or `Pelican Panel` and navigate to the `Account Page`. Home @@ -85,7 +91,7 @@ If you need help, contact me on [our Discord support server](https://discord.gg/ 4. Paste the emoji ID into the config. - Status Config + Status Config ### Blacklist Nodes @@ -99,11 +105,11 @@ If you need help, contact me on [our Discord support server](https://discord.gg/ 3. Paste the ID into the blacklist in the config. - Blacklist Config + Blacklist Config You can add more than one node to the blacklist. -Blacklist Config +Blacklist Config ## Reporting a Bug @@ -111,12 +117,6 @@ Enable `log_error` in the `config.yml` file and check the console for the error ## Links -- [PteroStats DiscordJS v13](https://github.com/HirziDevs/PteroStats/tree/3d0512c3323ecf079101104c7ecf3c94d265e298) -- [PteroStats DiscordJS v12](https://github.com/HirziDevs/PteroStats/tree/bcfa266be64dda11955f0bf9732da086bcea522c) -- [Pelican Panel](https://pelican.dev) -- [Pterodactyl Panel](https://pterodactyl.io) -- [Pelican API Documentation](https://demo.pelican.dev/docs/api) -- [Pterodactyl API Documentation](https://github.com/devnote-dev/ptero-notes/) - [Pterodactyl Discord Server](https://discord.gg/pterodactyl) - [Pelican Discord Server](https://discord.gg/pelican-panel) - [Support Server](https://discord.gg/zv6maQRah3) diff --git a/package.json b/package.json index daf333f..158b0a8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "repository": "HirziDevs/PteroStats", "homepage": "https://pterostats.znproject.my.id", "bugs": { - "email": "hirzidevs@gmail.com", + "email": "hirzi@znproject.my.id", "url": "https://github.com/HirziDevs/PteroStats/issues" }, "scripts": { From d9157248313b590f40e7822c0b52f6903fa9cab4 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Wed, 11 Sep 2024 22:11:38 +0700 Subject: [PATCH 32/59] feat(installer): update error message --- handlers/installer.js | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/handlers/installer.js b/handlers/installer.js index 24576cc..eac0aa4 100644 --- a/handlers/installer.js +++ b/handlers/installer.js @@ -38,13 +38,13 @@ module.exports = function Installer() { let isValid = true; if (index === 1 && !isValidURL(answer)) { - console.log(cliColor.redBright("Invalid URL. Please enter a valid URL.")); + 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)) { - console.log(cliColor.redBright("Invalid API key. It must start with 'plcn_' or 'ptlc_'.")); + console.log(cliColor.redBright("❌ Invalid Panel API key. It must start with 'plcn_' or 'ptlc_'.")); isValid = false; } else if (index === 4 && !/^\d+$/.test(answer)) { - console.log(cliColor.redBright("Invalid Channel ID. It must be a number.")); + console.log(cliColor.redBright("❌ Invalid Channel ID. It must be a number.")); isValid = false; } diff --git a/package.json b/package.json index 158b0a8..3591ccb 100644 --- a/package.json +++ b/package.json @@ -23,4 +23,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file From 8432533f43acc573c5a415a36b38b5dab695781f Mon Sep 17 00:00:00 2001 From: Hirzi Date: Thu, 12 Sep 2024 17:10:24 +0700 Subject: [PATCH 33/59] docs(config): update comments for better understanding --- config.yml | 102 +++++++++++++++++++++++++++-------------------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/config.yml b/config.yml index 5364b8b..2eafae2 100644 --- a/config.yml +++ b/config.yml @@ -1,81 +1,83 @@ -# PteroStats config -# If you need help, join our discord server here: https://discord.gg/zv6maQRah3 -version: 5 # Do not change this unless you know what you are doing! +# PteroStats Configuration File +# Need help? Join our Discord server: https://discord.gg/zv6maQRah3 +version: 5 # Warning: Do not change this unless you know what you are doing! -# Bot Configuration +# Bot Presence Configuration presence: enable: true - text: "Hosting Panel" - type: "watching" # can be 'watching', 'playing', 'listening', or 'competing'. 'streaming' is not working for now - status: "online" # can be 'online', 'idle', 'dnd', or 'invisible' + text: "Hosting Panel" # The status text shown by the bot. + type: "watching" # Available types: 'watching', 'playing', 'listening', 'competing'. (Note: 'streaming' is currently unsupported) + status: "online" # Available types: 'online', 'idle', 'dnd', or 'invisible' -# Discord Channel and Refresh Time Configuration -refresh: 10 # How much time the bot will refresh the stats -timeout: 5 # How much time to wait for a node to respond to the bot (if you change this, it will add more time to refresh the stats) +# Discord Channel and Refresh Timing Configuration +refresh: 10 # Interval in seconds for refreshing the bot's stats. +timeout: 5 # Timeout in seconds for node responses. Adjusting this will affect the overall refresh time. # Message and Embed Configuration -# set the option as '' if you want to disable it +# To disable an option, leave the value as an empty string: '' message: - content: "" - attachment: "" # If you enable attachment on message it will upload the attachment first before sending or editing message and will result in delayed stats + content: "" # Custom content for bot messages. Set to '' to disable. + attachment: "" # Attachments will delay stats refresh since they are uploaded first. embed: panel: author: name: "Hosting Panel" - icon: "" - title: "Panel Stats" - description: "Next update {{time}}" - timestamp: false - color: "5865F2" + icon: "" # Icon URL for the author of the panel embed. + title: "Panel Stats" # Title of the panel stats embed. + description: "Next update {{time}}" # Description for the panel. {{time}} will display the next refresh time. + timestamp: false # Show the timestamp in the embed (true/false). + color: "5865F2" # Embed color in hex format. footer: - text: "" - icon: "" - thumbnail: "" - image: "" + text: "" # Footer text. Set to '' to disable. + icon: "" # Footer icon URL. + thumbnail: "" # Thumbnail URL for the embed. + image: "" # Image URL for the embed. + nodes: author: name: "Hosting Panel" - icon: "" - title: "Nodes Stats" - description: "" - timestamp: true - color: "5865F2" + icon: "" # Icon URL for the author of the nodes embed. + title: "Nodes Stats" # Title for the node stats embed. + description: "" # Description for the node stats embed. Can be left empty. + timestamp: true # Include a timestamp in the nodes embed (true/false). + color: "5865F2" # Embed color in hex format. footer: - text: "By @HirziDevs" - icon: "" - thumbnail: "" - image: "" + text: "By @HirziDevs" # Footer text for node stats. + icon: "" # Footer icon URL. + thumbnail: "" # Thumbnail URL for the node stats embed. + image: "" # Image URL for the node stats embed. # Message Button Configuration button: - enable: true + enable: true # Enable or disable buttons in messages. row1: - label: "Panel" - url: "https://panel.example.com" -# - label: "Dashboard" -# url: "https://dash.example.com" + url: "https://panel.example.com" # URL for the first button. +# - label: "Dashboard" # Uncomment to add a second button. +# url: "https://dash.example.com" # URL for the second button. # Status Message Configuration -# How to use custom emoji: https://github.com/HirziDevs/PteroStats#using-custom-emoji +# For details on using custom emojis, visit: https://github.com/HirziDevs/PteroStats#using-custom-emoji status: - online: ":green_circle: Online" - offline: ":red_circle: Offline" + online: ":green_circle: Online" # Status message for when a node is online. + offline: ":red_circle: Offline" # Status message for when a node is offline. -# Nodes Settings -# How to get nodes id: https://github.com/HirziDevs/PteroStats#blacklist-nodes +# Node Settings +# Instructions for retrieving node IDs: https://github.com/HirziDevs/PteroStats#blacklist-nodes nodes_settings: - blacklist: [] # You can add node id to remove the node from status embed (Example: "blacklist: [1]") - details: true # enable nodes details i.e memory and disk usage - servers: true - unit: "byte" # Allowed values: "byte" or "percentage" - limit: 100 + blacklist: [] # Add node IDs to exclude them from the status embed (e.g., blacklist: [1]). + details: true # Show node details such as memory and disk usage (true/false). + servers: true # Show server details (true/false). + unit: "byte" # Unit for node usage, Available types: "byte" or "percentage". + limit: 100 # Node limit for usage statistics display. # Panel Users and Servers Settings panel_settings: - status: true # enable panel stats under nodes stats - servers: true - users: true + status: true # Display panel stats under node stats (true/false). + servers: true # Display servers count (true/false). + users: true # Display users count (true/false). -# Log error to console if a server offline (enable this when you have a problem that you want to report) -log_error: false # set to "true" to enable \ No newline at end of file +# Error Logging Configuration +# Enable logging to console if servers go offline, useful for debugging. +log_error: false # Set to true to enable error logging. From f78a6b79f932f855022b86e1c858caa93faf7585 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 08:43:09 +0700 Subject: [PATCH 34/59] feat(nodes): add server counter and set allocations as max servers --- config.yml | 3 ++- index.js | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config.yml b/config.yml index 2eafae2..ac6d947 100644 --- a/config.yml +++ b/config.yml @@ -68,7 +68,8 @@ status: nodes_settings: blacklist: [] # Add node IDs to exclude them from the status embed (e.g., blacklist: [1]). details: true # Show node details such as memory and disk usage (true/false). - servers: true # Show server details (true/false). + servers: false # Show server details (true/false). + allocations_as_max_servers: false # Show allocations as max servers (true/false). unit: "byte" # Unit for node usage, Available types: "byte" or "percentage". limit: 100 # Node limit for usage statistics display. diff --git a/index.js b/index.js index 8e34b1a..dc92355 100644 --- a/index.js +++ b/index.js @@ -130,9 +130,10 @@ async function createMessage({ cache, panel, nodes, servers, users }) { name: `${node.attributes.name} - ${node.status ? config.status.online : config.status.offline}`, value: "```\n" + - `Memory: ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` + - `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}%` : "") + + `Memory : ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` + + `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}` : ""}` : "") + "```" }); }); From 02c8dc13db022a76ab7dafa187b5c9fccc3a2522 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 08:44:20 +0700 Subject: [PATCH 35/59] feat: update config version to 5 --- config.yml | 10 +++++----- handlers/config.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config.yml b/config.yml index ac6d947..6c29576 100644 --- a/config.yml +++ b/config.yml @@ -1,11 +1,11 @@ # PteroStats Configuration File # Need help? Join our Discord server: https://discord.gg/zv6maQRah3 -version: 5 # Warning: Do not change this unless you know what you are doing! +version: 6 # Warning: Do not change this unless you know what you are doing! -# Bot Presence Configuration +# App Presence Configuration presence: - enable: true - text: "Hosting Panel" # The status text shown by the bot. + enable: true # Enable or disable app presence. + text: "Hosting Panel" # The status text shown by the app. type: "watching" # Available types: 'watching', 'playing', 'listening', 'competing'. (Note: 'streaming' is currently unsupported) status: "online" # Available types: 'online', 'idle', 'dnd', or 'invisible' @@ -81,4 +81,4 @@ panel_settings: # Error Logging Configuration # Enable logging to console if servers go offline, useful for debugging. -log_error: false # Set to true to enable error logging. +log_error: false # Set to true to enable error logging. \ No newline at end of file diff --git a/handlers/config.js b/handlers/config.js index 452e8ee..cddef9c 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -18,7 +18,7 @@ try { process.exit(); } -if (config.version !== 5) { +if (config.version !== 6) { console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/dev/config.yml'); process.exit(); } From 3cc57bb9a12b812ec442289ff80228341595d72e Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 09:40:44 +0700 Subject: [PATCH 36/59] feat: add multiple row support for link buttons --- config.yml | 11 ++++++++--- index.js | 25 ++++++++++++++----------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/config.yml b/config.yml index 6c29576..c7eda19 100644 --- a/config.yml +++ b/config.yml @@ -52,10 +52,15 @@ embed: button: enable: true # Enable or disable buttons in messages. row1: - - label: "Panel" + - label: "Panel" # Label for the first button. url: "https://panel.example.com" # URL for the first button. -# - label: "Dashboard" # Uncomment to add a second button. -# url: "https://dash.example.com" # URL for the second button. +# - label: "Dashboard" # Remove "#" at the start of the line to use this button +# url: "https://dash.example.com" # Remove "#" at the start of the line to use this button +# row2: # Remove "#" at the start of the line to use this row +# - label: "Backup Panel" +# url: "https://panel2.example.com" +# - label: "Backup Dashboard" +# url: "https://dash2.example.com" # Status Message Configuration # For details on using custom emojis, visit: https://github.com/HirziDevs/PteroStats#using-custom-emoji diff --git a/index.js b/index.js index dc92355..8250a8f 100644 --- a/index.js +++ b/index.js @@ -191,17 +191,20 @@ async function createMessage({ cache, panel, nodes, servers, users }) { const components = [] - if (config.button.enable && config.button.row1?.length > 0) { - components.push( - new ActionRowBuilder().addComponents( - config.button.row1.map(button => - new ButtonBuilder() - .setLabel(button.label) - .setURL(button.url) - .setStyle(ButtonStyle.Link) - ) - ) - ) + if (config.button.enable) { + for (const row of ["row1", "row2", "row3", "row4", "row5"]) { + if (config.button[row] && config.button[row].length > 0 && config.button[row].label && config.button[row].url) + components.push( + new ActionRowBuilder().addComponents( + config.button[row].slice(0, 5).map(button => + new ButtonBuilder() + .setLabel(button.label) + .setURL(button.url) + .setStyle(ButtonStyle.Link) + ) + ) + ); + } } try { From 229a052201eaade4bdc61d8a8ef66840d56008b1 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 09:41:41 +0700 Subject: [PATCH 37/59] feat(nodes): add custom description for embeds --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 8250a8f..43c9fad 100644 --- a/index.js +++ b/index.js @@ -112,6 +112,7 @@ async function createMessage({ cache, panel, nodes, servers, users }) { name: config.embed.nodes.author.name || null, iconURL: config.embed.nodes.author.icon || null }) + .setDescription(config.embed.nodes.description || null) .setTitle(config.embed.nodes.title || null) .setColor(config.embed.nodes.color || null) .setURL(config.embed.nodes.url || null) @@ -138,7 +139,7 @@ async function createMessage({ cache, panel, nodes, servers, users }) { }); }); } else { - embeds[0].setDescription(nodes.map(node => `**${node.attributes.name}** - ${node.status ? config.status.online : config.status.offline}`).join("\n")); + embeds[0].setDescription((embed.data.description ? (embed.data.description + "\n\n") : "") + nodes.map(node => `**${node.attributes.name}** - ${node.status ? config.status.online : config.status.offline}`).join("\n")); } let panelEmbed = new EmbedBuilder() From 0f68be40a4c7f3747acccf8cfc1135f7cb9ce074 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 10:55:57 +0700 Subject: [PATCH 38/59] refactor: split index.js code into app.js --- handlers/app.js | 236 ++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 235 +---------------------------------------------- 2 files changed, 238 insertions(+), 233 deletions(-) create mode 100644 handlers/app.js diff --git a/handlers/app.js b/handlers/app.js new file mode 100644 index 0000000..4d7310f --- /dev/null +++ b/handlers/app.js @@ -0,0 +1,236 @@ +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 cachePath = require('node:path').join(__dirname, "../cache.json"); +const config = require("./config.js"); +const convertUnits = require("./convertUnits.js"); +const getStats = require("./getStats.js"); + +module.exports = function App() { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting app...")); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using a development build! Some features may not work as intended.")); + + const client = new Client({ + intents: [GatewayIntentBits.Guilds] + }); + + async function startGetStatus() { + try { + const results = await getStats(); + createMessage({ + panel: true, + nodes: results.nodes, + servers: results.servers, + users: results.users, + }); + } catch { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is currently offline.")); + + fs.readFile(cachePath, (err, data) => { + if (err) { + createMessage({ cache: false, panel: false }); + return console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Last cache was not found!")); + } + + try { + const results = JSON.parse(data); + createMessage({ + cache: true, + panel: false, + nodes: results.nodes, + servers: results.servers, + users: results.users, + }); + } catch { + createMessage({ cache: false, panel: false }); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Something went wrong with cache data...")); + } + }); + } + } + + client.once("ready", () => { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`${cliColor.blueBright(client.user.tag)} is online!`)); + + if (config.presence.enable) { + if (config.presence.text && config.presence.type) { + switch (config.presence.type.toLowerCase()) { + case "playing": + config.presence.type = ActivityType.Playing; + break; + case "listening": + config.presence.type = ActivityType.Listening; + break; + case "competing": + config.presence.type = ActivityType.Competing; + break; + default: + config.presence.type = ActivityType.Watching; + } + + client.user.setActivity(config.presence.text, { + type: config.presence.type, + }); + } + + if (config.presence.status) { + if (!["idle", "online", "dnd", "invisible"].includes( + config.presence.status.toLowerCase() + )) + config.presence.status = "online"; + + client.user.setStatus(config.presence.status); + } + } + + startGetStatus(); + }); + + async function createMessage({ cache, panel, nodes, servers, users }) { + let embed = new EmbedBuilder() + .setAuthor({ + name: config.embed.nodes.author.name || null, + iconURL: config.embed.nodes.author.icon || null + }) + .setDescription(config.embed.nodes.description || null) + .setTitle(config.embed.nodes.title || null) + .setColor(config.embed.nodes.color || null) + .setURL(config.embed.nodes.url || null) + .setThumbnail(config.embed.nodes.thumbnail || null) + + let embeds = [embed]; + + if (config.nodes_settings.details) { + nodes.forEach((node, index) => { + if (index % 25 === 0 && index !== 0) { + embed = new EmbedBuilder().setColor(config.embed.nodes.color); + if (embeds.length < 9) embeds.push(embed); + } + + embed.addFields({ + name: `${node.attributes.name} - ${node.status ? config.status.online : config.status.offline}`, + value: + "```\n" + + `Memory : ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` + + `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}` : ""}` : "") + + "```" + }); + }); + } else { + embeds[0].setDescription((embed.data.description ? (embed.data.description + "\n\n") : "") + nodes.map(node => `**${node.attributes.name}** - ${node.status ? config.status.online : config.status.offline}`).join("\n")); + } + + let panelEmbed = new EmbedBuilder() + .setAuthor({ + name: config.embed.panel.author.name || null, + iconURL: config.embed.panel.author.icon || null + }) + .setColor(config.embed.panel.color || null) + .setTitle(config.embed.panel.title || null) + .setURL(config.embed.panel.url || null) + .setTimestamp(config.embed.panel.timestamp ? new Date() : null) + .setFooter({ + text: config.embed.panel.footer.text || null, + iconURL: config.embed.panel.footer.icon || null + }) + .setThumbnail(config.embed.panel.thumbnail || null) + .setImage(config.embed.panel.image || null) + .setDescription( + config.embed.panel.description + .replace( + "{{time}}", + time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R") + ) || null + ) + .addFields({ + name: `Panel - ${panel ? config.status.online : config.status.offline}`, + value: + "```\n" + + `Nodes : ${nodes.length}\n` + + (config.panel_settings.servers ? `Servers: ${servers || "Unknown"}\n` : "") + + (config.panel_settings.users ? `Users : ${users || "Unknown"}\n` : "") + + "```" + }); + + if (config.panel_settings.status) embeds.unshift(panelEmbed); + + embeds[embeds.length - 1] + .setTimestamp(config.embed.nodes.timestamp ? new Date() : null) + .setFooter({ + text: config.embed.nodes.footer.text || null, + iconURL: config.embed.nodes.footer.icon || null + }) + .setImage(config.embed.nodes.image || null) + + if (!cache && !panel) { + embeds[embeds.length - 1].setDescription( + embeds[embeds.length - 1].data.description ? embeds[embeds.length - 1].data.description + "\n\nThere are no nodes to be display!" : "There are no nodes to be display!" + ); + } + + const components = [] + + if (config.button.enable) { + for (const row of ["row1", "row2", "row3", "row4", "row5"]) { + if (config.button[row] && config.button[row].length > 0 && config.button[row].label && config.button[row].url) + components.push( + new ActionRowBuilder().addComponents( + config.button[row].slice(0, 5).map(button => + new ButtonBuilder() + .setLabel(button.label) + .setURL(button.url) + .setStyle(ButtonStyle.Link) + ) + ) + ); + } + } + + try { + const channel = await client.channels.fetch(process.env?.DiscordChannel); + const messages = await channel.messages.fetch({ limit: 10 }); + const botMessage = messages.find(msg => msg.author.id === client.user.id); + + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`Panel stats successfully posted to the ${cliColor.blueBright(channel.name)} channel!`)); + + setTimeout(() => startGetStatus(), config.refresh * 1000); + + if (botMessage) { + await botMessage.edit({ content: config.message.content || null, embeds, components }); + } else { + await channel.send({ content: config.message.content || null, embeds, components }); + } + } catch (error) { + DiscordErrorHandler(error); + } + } + + function DiscordErrorHandler(error) { + 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")); + } 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) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!")); + } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code === "MAX_EMBED_SIZE_EXCEEDED") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Embed message limit exceeded! Please limit or decrease the nodes that need to be shown in the config!")); + } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code) { + console.log(Object.values(error.rawError.errors)[0]._errors[0].message); + } else { + console.error(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error"), error); + } + process.exit(); + } + + try { + client.login(process.env?.DiscordBotToken); + } catch { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!")); + process.exit(); + } +} \ No newline at end of file diff --git a/index.js b/index.js index 43c9fad..19b93c0 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,6 @@ -const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); -const cachePath = require('node:path').join(__dirname, "./cache.json"); const fs = require("node:fs"); const cliColor = require("cli-color"); -const package = require("./package.json") +const package = require("./package.json"); console.log( ` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ \n` + @@ -22,233 +20,4 @@ console.log( if (!fs.existsSync(".env")) return require("./handlers/installer.js")() -require("dotenv").config() -const config = require("./handlers/config.js"); -const convertUnits = require("./handlers/convertUnits.js"); -const getStats = require("./handlers/getStats.js"); - -console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting app...")); -console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using a development build! Some features may not work as intended.")); - -const client = new Client({ - intents: [GatewayIntentBits.Guilds] -}); - -async function startGetStatus() { - try { - const results = await getStats(); - createMessage({ - panel: true, - nodes: results.nodes, - servers: results.servers, - users: results.users, - }); - } catch { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is currently offline.")); - - fs.readFile(cachePath, (err, data) => { - if (err) { - createMessage({ cache: false, panel: false }); - return console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Last cache was not found!")); - } - - try { - const results = JSON.parse(data); - createMessage({ - cache: true, - panel: false, - nodes: results.nodes, - servers: results.servers, - users: results.users, - }); - } catch { - createMessage({ cache: false, panel: false }); - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Something went wrong with cache data...")); - } - }); - } -} - -client.once("ready", () => { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`${cliColor.blueBright(client.user.tag)} is online!`)); - - if (config.presence.enable) { - if (config.presence.text && config.presence.type) { - switch (config.presence.type.toLowerCase()) { - case "playing": - config.presence.type = ActivityType.Playing; - break; - case "listening": - config.presence.type = ActivityType.Listening; - break; - case "competing": - config.presence.type = ActivityType.Competing; - break; - default: - config.presence.type = ActivityType.Watching; - } - - client.user.setActivity(config.presence.text, { - type: config.presence.type, - }); - } - - if (config.presence.status) { - if (!["idle", "online", "dnd", "invisible"].includes( - config.presence.status.toLowerCase() - )) - config.presence.status = "online"; - - client.user.setStatus(config.presence.status); - } - } - - startGetStatus(); -}); - -async function createMessage({ cache, panel, nodes, servers, users }) { - let embed = new EmbedBuilder() - .setAuthor({ - name: config.embed.nodes.author.name || null, - iconURL: config.embed.nodes.author.icon || null - }) - .setDescription(config.embed.nodes.description || null) - .setTitle(config.embed.nodes.title || null) - .setColor(config.embed.nodes.color || null) - .setURL(config.embed.nodes.url || null) - .setThumbnail(config.embed.nodes.thumbnail || null) - - let embeds = [embed]; - - if (config.nodes_settings.details) { - nodes.forEach((node, index) => { - if (index % 25 === 0 && index !== 0) { - embed = new EmbedBuilder().setColor(config.embed.nodes.color); - if (embeds.length < 9) embeds.push(embed); - } - - embed.addFields({ - name: `${node.attributes.name} - ${node.status ? config.status.online : config.status.offline}`, - value: - "```\n" + - `Memory : ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` + - `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}` : ""}` : "") + - "```" - }); - }); - } else { - embeds[0].setDescription((embed.data.description ? (embed.data.description + "\n\n") : "") + nodes.map(node => `**${node.attributes.name}** - ${node.status ? config.status.online : config.status.offline}`).join("\n")); - } - - let panelEmbed = new EmbedBuilder() - .setAuthor({ - name: config.embed.panel.author.name || null, - iconURL: config.embed.panel.author.icon || null - }) - .setColor(config.embed.panel.color || null) - .setTitle(config.embed.panel.title || null) - .setURL(config.embed.panel.url || null) - .setTimestamp(config.embed.panel.timestamp ? new Date() : null) - .setFooter({ - text: config.embed.panel.footer.text || null, - iconURL: config.embed.panel.footer.icon || null - }) - .setThumbnail(config.embed.panel.thumbnail || null) - .setImage(config.embed.panel.image || null) - .setDescription( - config.embed.panel.description - .replace( - "{{time}}", - time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R") - ) || null - ) - .addFields({ - name: `Panel - ${panel ? config.status.online : config.status.offline}`, - value: - "```\n" + - `Nodes : ${nodes.length}\n` + - (config.panel_settings.servers ? `Servers: ${servers || "Unknown"}\n` : "") + - (config.panel_settings.users ? `Users : ${users || "Unknown"}\n` : "") + - "```" - }); - - if (config.panel_settings.status) embeds.unshift(panelEmbed); - - embeds[embeds.length - 1] - .setTimestamp(config.embed.nodes.timestamp ? new Date() : null) - .setFooter({ - text: config.embed.nodes.footer.text || null, - iconURL: config.embed.nodes.footer.icon || null - }) - .setImage(config.embed.nodes.image || null) - - if (!cache && !panel) { - embeds[embeds.length - 1].setDescription( - embeds[embeds.length - 1].data.description ? embeds[embeds.length - 1].data.description + "\n\nThere are no nodes to be display!" : "There are no nodes to be display!" - ); - } - - const components = [] - - if (config.button.enable) { - for (const row of ["row1", "row2", "row3", "row4", "row5"]) { - if (config.button[row] && config.button[row].length > 0 && config.button[row].label && config.button[row].url) - components.push( - new ActionRowBuilder().addComponents( - config.button[row].slice(0, 5).map(button => - new ButtonBuilder() - .setLabel(button.label) - .setURL(button.url) - .setStyle(ButtonStyle.Link) - ) - ) - ); - } - } - - try { - const channel = await client.channels.fetch(process.env?.DiscordChannel); - const messages = await channel.messages.fetch({ limit: 10 }); - const botMessage = messages.find(msg => msg.author.id === client.user.id); - - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`Panel stats successfully posted to the ${cliColor.blueBright(channel.name)} channel!`)); - - setTimeout(() => startGetStatus(), config.refresh * 1000); - - if (botMessage) { - await botMessage.edit({ content: config.message.content || null, embeds, components }); - } else { - await channel.send({ content: config.message.content || null, embeds, components }); - } - } catch (error) { - DiscordErrorHandler(error); - } -} - -function DiscordErrorHandler(error) { - 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")); - } 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) { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!")); - } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code === "MAX_EMBED_SIZE_EXCEEDED") { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Embed message limit exceeded! Please limit or decrease the nodes that need to be shown in the config!")); - } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code) { - console.log(Object.values(error.rawError.errors)[0]._errors[0].message); - } else { - console.error(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error"), error); - } - process.exit(); -} - -try { - client.login(process.env?.DiscordBotToken); -} catch { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!")); - process.exit(); -} \ No newline at end of file +require("./handlers/app.js")() \ No newline at end of file From cbb77ac59d15e954da105f06de0e6992492b54f3 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 14:12:35 +0700 Subject: [PATCH 39/59] feat(installer): start app after validation is complete --- handlers/installer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/handlers/installer.js b/handlers/installer.js index eac0aa4..ff70ac1 100644 --- a/handlers/installer.js +++ b/handlers/installer.js @@ -78,8 +78,7 @@ module.exports = function Installer() { 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 `)); - console.log(cliColor.cyanBright("Please restart the app to continue.")); - process.exit() + require("./app.js")() }).catch(() => { console.log(cliColor.redBright("❌ Invalid Channel ID.")); console.log(" \n" + cliColor.redBright("Please run the installer again and fill in the correct credentials.")); From 144deca5e84719ad96fa5f391038e5d8cd643f63 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 14:16:03 +0700 Subject: [PATCH 40/59] feat: add new executable for changing configuration --- config.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ config.yml | 6 +++--- 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 config.js diff --git a/config.js b/config.js new file mode 100644 index 0000000..1ffbb5a --- /dev/null +++ b/config.js @@ -0,0 +1,46 @@ +const fs = require("node:fs"); +const cliColor = require("cli-color"); +const package = require("./package.json"); +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); + +console.log( + ` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ \n` + + ` /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ \n` + + ` \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ \n` + + ` \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ \n` + + ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}` +); + +console.log( + ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + + " \n \nDiscord: https://discord.znproject.my.id" + + " \n Source: https://github.com/HirziDevs/PteroStats" + + " \nLicense: https://github.com/Hirzidevs/PteroStats/blob/main/LICENSE" + + ` \n \n${package.description}\n ` +); + +if (!fs.existsSync(".env")) return require("./handlers/installer.js")() + +console.log(cliColor.yellowBright( + "Configuration is already set. Please select one of the following options:\n\n" + + `${cliColor.cyanBright("1")} ${cliColor.blueBright("»")} Start the App\n` + + `${cliColor.cyanBright("2")} ${cliColor.blueBright("»")} Change configuration\n ` +)) + +readline.question('> ', async (answer) => { + readline.close(); + + switch(answer) { + case '2': + require('./handlers/installer.js')(); + break; + case '1': + require('./handlers/app.js')(); + break; + default: + console.log(cliColor.redBright('Invalid input. Please type either 1 or 2.')); + } +}); \ No newline at end of file diff --git a/config.yml b/config.yml index c7eda19..332b35c 100644 --- a/config.yml +++ b/config.yml @@ -10,13 +10,13 @@ presence: status: "online" # Available types: 'online', 'idle', 'dnd', or 'invisible' # Discord Channel and Refresh Timing Configuration -refresh: 10 # Interval in seconds for refreshing the bot's stats. +refresh: 10 # Interval in seconds for refreshing the panel's stats. timeout: 5 # Timeout in seconds for node responses. Adjusting this will affect the overall refresh time. # Message and Embed Configuration # To disable an option, leave the value as an empty string: '' message: - content: "" # Custom content for bot messages. Set to '' to disable. + content: "" # Custom content for app messages. Set to '' to disable. attachment: "" # Attachments will delay stats refresh since they are uploaded first. embed: @@ -36,7 +36,7 @@ embed: nodes: author: - name: "Hosting Panel" + name: "" icon: "" # Icon URL for the author of the nodes embed. title: "Nodes Stats" # Title for the node stats embed. description: "" # Description for the node stats embed. Can be left empty. From 023745dfdc4f138d1211dd976a3a365f707c8418 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 16:01:51 +0700 Subject: [PATCH 41/59] docs: update README and installer messages --- README.md | 43 ++++++++++++++++++++++++------------------- config.js | 10 +++++----- config.yml | 2 +- index.js | 8 ++++---- package.json | 2 +- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 5d81078..0222945 100644 --- a/README.md +++ b/README.md @@ -10,39 +10,49 @@ > [!WARNING] > This is a development build! Some features may not work as intended. -PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. +PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. ## Preview PteroStats GIF Preview PteroStats Image Preview -## Installation -- [Starting the App](#starting-the-app) +## Guide +- [Starting the App/Bot](#starting-the-app--bot) +- [Changing Env Configuration](#changing-env-configuration) - [Getting an Panel API key](#getting-panel-api-key) - [Getting a Channel ID](#getting-channel-id) - [Using Custom Emoji](#using-custom-emoji) - [Blacklist Nodes](#blacklist-nodes) -- [Reporting a Bug](#reporting-a-bug) -### Starting the App -1. [Create your Discord App](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). -2. [Invite your Discord App to your Discord server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). -3. [Download the repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/dev.zip) and extract it. -4. Run `npm install` in the root directory of the app files. -5. Run `node index` and answer the provided questions to set up the app. +### Starting the App/Bot +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 by: + - [Downloading this repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/dev.zip) and extract it. + - Using Git: Run `git clone - b dev 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. Installer - [Getting Panel API Key](#getting-panel-api-key) - [Getting a Channel ID](#getting-channel-id) -6. Run `node index` again to start the app, and you're done! +6. Run `node index` if you want to start the app/bot again, and you're done! Console Logging -### Getting Panel API Key +### Changing Env Configuration +1. Run `node config` in the root directory of the app/bot files. +2. Enter `2` to change configuration. + Change Configuration + +3. Answer the provided question to set up the app/bot. +4. Run `node index` if you want ti start the app/bot again, and you're done! + +### Getting Panel API Key > [!WARNING] > The use of Application API keys is **deprecated**; you should use **Client API keys**. @@ -66,7 +76,6 @@ PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel API Key ### Getting Channel ID - 1. Enable Developer Mode in your Discord settings. Discord User Settings @@ -76,7 +85,6 @@ PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel Right Click Channel ### Using Custom Emoji - 1. Type `\` in the server that has the custom emoji you want. Type \ on the chat @@ -94,7 +102,6 @@ PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel Status Config ### Blacklist Nodes - 1. Select a node from the node list on the admin page. Nodes List @@ -112,11 +119,9 @@ You can add more than one node to the blacklist. Blacklist Config ## Reporting a Bug - -Enable `log_error` in the `config.yml` file and check the console for the error message. After that, report it to our Discord server at [Support Server](https://discord.gg/zv6maQRah3). +Enable `log_error` in the `config.yml` file and check the console for the error message. After that, report it to our Discord server at [Support Server](https://discord.znproject.my.id). ## Links - - [Pterodactyl Discord Server](https://discord.gg/pterodactyl) - [Pelican Discord Server](https://discord.gg/pelican-panel) -- [Support Server](https://discord.gg/zv6maQRah3) +- [Support Server](https://discord.znproject.my.id) \ No newline at end of file diff --git a/config.js b/config.js index 1ffbb5a..ee1c62c 100644 --- a/config.js +++ b/config.js @@ -15,20 +15,20 @@ console.log( ); console.log( - ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + - " \n \nDiscord: https://discord.znproject.my.id" + + ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors\n ` + + " \nDiscord: https://discord.znproject.my.id" + " \n Source: https://github.com/HirziDevs/PteroStats" + " \nLicense: https://github.com/Hirzidevs/PteroStats/blob/main/LICENSE" + ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return require("./handlers/installer.js")() +if (!fs.existsSync(".env")) return require("./handlers/installer.js")(); console.log(cliColor.yellowBright( - "Configuration is already set. Please select one of the following options:\n\n" + + "Configuration is already set. Please select one of the following options:\n \n" + `${cliColor.cyanBright("1")} ${cliColor.blueBright("»")} Start the App\n` + `${cliColor.cyanBright("2")} ${cliColor.blueBright("»")} Change configuration\n ` -)) +)); readline.question('> ', async (answer) => { readline.close(); diff --git a/config.yml b/config.yml index 332b35c..8492f8c 100644 --- a/config.yml +++ b/config.yml @@ -1,5 +1,5 @@ # PteroStats Configuration File -# Need help? Join our Discord server: https://discord.gg/zv6maQRah3 +# Need help? Join our Discord server: https://discord.znproject.my.id version: 6 # Warning: Do not change this unless you know what you are doing! # App Presence Configuration diff --git a/index.js b/index.js index 19b93c0..6a5d066 100644 --- a/index.js +++ b/index.js @@ -11,13 +11,13 @@ console.log( ); console.log( - ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors` + - " \n \nDiscord: https://discord.znproject.my.id" + + ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors\n ` + + " \nDiscord: https://discord.znproject.my.id" + " \n Source: https://github.com/HirziDevs/PteroStats" + " \nLicense: https://github.com/Hirzidevs/PteroStats/blob/main/LICENSE" + ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return require("./handlers/installer.js")() +if (!fs.existsSync(".env")) return require("./handlers/installer.js")(); -require("./handlers/app.js")() \ No newline at end of file +require("./handlers/app.js")(); \ No newline at end of file diff --git a/package.json b/package.json index 9de45fc..7419d15 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pterostats", "version": "4.0.0", - "description": "PteroStats is a Discord App that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server.", + "description": "PteroStats is a Discord App/Bot that 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", From f6f59d38b65a8087beefa9f82fb7f5b7375d6dd9 Mon Sep 17 00:00:00 2001 From: Hirzi Date: Fri, 13 Sep 2024 16:45:52 +0700 Subject: [PATCH 42/59] docs: fix typo in README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0222945..0ee218b 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. ## Preview -PteroStats GIF Preview - PteroStats Image Preview +PteroStats GIF Preview + ## Guide -- [Starting the App/Bot](#starting-the-app--bot) +- [Starting the App/Bot](#starting-the-appbot) - [Changing Env Configuration](#changing-env-configuration) - [Getting an Panel API key](#getting-panel-api-key) - [Getting a Channel ID](#getting-channel-id) @@ -30,7 +30,7 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa 2. [Invite your Discord App/Bot to your Discord server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). 3. Download this repository by: - [Downloading this repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/dev.zip) and extract it. - - Using Git: Run `git clone - b dev https://github.com/HirziDevs/PteroStats.git .` in the command line. + - Using Git: Run `git clone -b dev 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. @@ -124,4 +124,4 @@ Enable `log_error` in the `config.yml` file and check the console for the error ## Links - [Pterodactyl Discord Server](https://discord.gg/pterodactyl) - [Pelican Discord Server](https://discord.gg/pelican-panel) -- [Support Server](https://discord.znproject.my.id) \ No newline at end of file +- [Support Server](https://discord.znproject.my.id) From ba73378f3536de26bd1ea2f99d3d2e864fbc3a7d Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 18:36:02 +0700 Subject: [PATCH 43/59] feat: update config version to 7 --- config.yml | 2 +- handlers/config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yml b/config.yml index 8492f8c..2768328 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ # PteroStats Configuration File # Need help? Join our Discord server: https://discord.znproject.my.id -version: 6 # Warning: Do not change this unless you know what you are doing! +version: 7 # Warning: Do not change this unless you know what you are doing! # App Presence Configuration presence: diff --git a/handlers/config.js b/handlers/config.js index cddef9c..488f837 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -18,7 +18,7 @@ try { process.exit(); } -if (config.version !== 6) { +if (config.version !== 7) { console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/dev/config.yml'); process.exit(); } From fa1fc679de5eae9418595a9a8c069306b2ddf5dd Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 19:41:11 +0700 Subject: [PATCH 44/59] fix: resolve issue where message buttons don't display --- handlers/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/handlers/app.js b/handlers/app.js index 4d7310f..d0a94bd 100644 --- a/handlers/app.js +++ b/handlers/app.js @@ -175,10 +175,10 @@ 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 && config.button[row].label && config.button[row].url) - components.push( + 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( new ActionRowBuilder().addComponents( - config.button[row].slice(0, 5).map(button => + config.button[row].slice(0, 5).filter(button => button.label && button.url).map(button => new ButtonBuilder() .setLabel(button.label) .setURL(button.url) From 1fbd991fcbc7790d64f59744889e775e5e3554fa Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 19:43:43 +0700 Subject: [PATCH 45/59] feat: add uptime tracking for panel and nodes --- config.yml | 2 ++ handlers/UptimeFormatter.js | 13 +++++++++++ handlers/app.js | 45 ++++++++++++++++++++++--------------- handlers/getStats.js | 40 +++++++++++++++++++++++---------- 4 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 handlers/UptimeFormatter.js diff --git a/config.yml b/config.yml index 2768328..d081870 100644 --- a/config.yml +++ b/config.yml @@ -76,10 +76,12 @@ nodes_settings: servers: false # Show server details (true/false). allocations_as_max_servers: false # Show allocations as max servers (true/false). unit: "byte" # Unit for node usage, Available types: "byte" or "percentage". + uptime: true # Enable or disable node uptime (true/false). limit: 100 # Node limit for usage statistics display. # Panel Users and Servers Settings panel_settings: + uptime: true # Enable or disable node uptime (true/false). status: true # Display panel stats under node stats (true/false). servers: true # Display servers count (true/false). users: true # Display users count (true/false). diff --git a/handlers/UptimeFormatter.js b/handlers/UptimeFormatter.js new file mode 100644 index 0000000..711d9e9 --- /dev/null +++ b/handlers/UptimeFormatter.js @@ -0,0 +1,13 @@ +module.exports = function UptimeFormatter(time) { + let text = [] + const days = Math.floor(time / 86400000); + const hours = Math.floor(time / 3600000) % 24; + const minutes = Math.floor(time / 60000) % 60; + const seconds = Math.floor(time / 1000) % 60; + if (days > 0) text.push(`${days} days`) + if (hours > 0) text.push(`${hours} hours`) + if (minutes > 0) text.push(`${minutes} minutes`) + if (text.length > 0) text.push(`and ${seconds} seconds`) + else text.push(`${seconds} seconds`) + return text.join(", ").replace(", and", " and") +} \ No newline at end of file diff --git a/handlers/app.js b/handlers/app.js index d0a94bd..c2234da 100644 --- a/handlers/app.js +++ b/handlers/app.js @@ -2,7 +2,6 @@ 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 cachePath = require('node:path').join(__dirname, "../cache.json"); const config = require("./config.js"); const convertUnits = require("./convertUnits.js"); const getStats = require("./getStats.js"); @@ -20,6 +19,7 @@ module.exports = function App() { const results = await getStats(); createMessage({ panel: true, + uptime: results.uptime, nodes: results.nodes, servers: results.servers, users: results.users, @@ -27,7 +27,7 @@ module.exports = function App() { } catch { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is currently offline.")); - fs.readFile(cachePath, (err, data) => { + fs.readFile(require('node: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!")); @@ -35,6 +35,8 @@ module.exports = function App() { try { const results = JSON.parse(data); + results.uptime = false + fs.writeFileSync("cache.json", JSON.stringify(results, null, 2), "utf8"); createMessage({ cache: true, panel: false, @@ -87,7 +89,7 @@ module.exports = function App() { startGetStatus(); }); - async function createMessage({ cache, panel, nodes, servers, users }) { + async function createMessage({ cache, panel, uptime, nodes, servers, users }) { let embed = new EmbedBuilder() .setAuthor({ name: config.embed.nodes.author.name || null, @@ -116,6 +118,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"}` : "") + "```" }); }); @@ -152,6 +155,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` : "") + "```" }); @@ -209,22 +213,27 @@ module.exports = function App() { } function DiscordErrorHandler(error) { - 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")); - } 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) { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!")); - } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code === "MAX_EMBED_SIZE_EXCEEDED") { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Embed message limit exceeded! Please limit or decrease the nodes that need to be shown in the config!")); - } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]._errors[0].code) { - console.log(Object.values(error.rawError.errors)[0]._errors[0].message); - } else { - console.error(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error"), 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")); + } 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) { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!")); + } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]?._errors[0]?.code === "MAX_EMBED_SIZE_EXCEEDED") { + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Embed message limit exceeded! Please limit or decrease the nodes that need to be shown in the config!")); + } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]?._errors[0]?.code) { + console.log(Object.values(error.rawError.errors)[0]._errors[0].message); + } else { + console.error(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error"), error); + } + process.exit(); + } catch (err) { + console.log(error) + process.exit(); } - process.exit(); } try { diff --git a/handlers/getStats.js b/handlers/getStats.js index cf62382..b1c4280 100644 --- a/handlers/getStats.js +++ b/handlers/getStats.js @@ -1,28 +1,40 @@ const config = require("./config.js"); const fs = require("node:fs"); -const getNodesDetails = require("./getNodesDetails.js"); -const getNodeConfiguration = require("./getNodeConfiguration.js"); -const getWingsStatus = require("./getWingsStatus.js"); -const getServers = require("./getServers.js"); -const getUsers = require("./getUsers.js"); -const promiseTimeout = require("./promiseTimeout.js"); const cliColor = require("cli-color"); module.exports = async function getStats() { + let cache = (() => { + try { + return JSON.parse(fs.readFileSync(require('node:path').join(__dirname, "../cache.json"))) + } catch { + return false + } + })() + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel nodes...")) - const nodesStats = await getNodesDetails(); + const nodesStats = await require("./getNodesDetails.js")(); 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 getNodeConfiguration(node.attributes.id); + const nodeConfig = await require("./getNodeConfiguration.js")(node.attributes.id); console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Checking ${cliColor.blueBright(node.attributes.name)} wings status...`)) - const nodeStatus = await promiseTimeout(getWingsStatus(node, nodeConfig.token), config.timeout * 1000); + const nodeStatus = await require("./promiseTimeout.js")(require("./getWingsStatus.js")(node, nodeConfig.token), config.timeout * 1000); - if (!nodeStatus) + let nodeUptime = cache ? (() => { + return cache.nodes.find((n) => n.attributes.id === node.attributes.id)?.uptime || Date.now() + })() : Date.now() + + if (!nodeUptime && nodeStatus) nodeUptime = Date.now() + + if (!nodeStatus) { + nodeUptime = false console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node ${cliColor.blueBright(node.attributes.name)} is currently offline.`)) + } + return { attributes: { + id: node.attributes.id, name: node.attributes.name, memory: node.attributes.memory, disk: node.attributes.disk, @@ -33,13 +45,17 @@ module.exports = async function getStats() { servers: node.attributes.relationships.servers.data.length } }, + uptime: nodeUptime, status: nodeStatus }; }); const data = { - servers: await getServers(), - users: await getUsers(), + uptime: cache ? (() => { + return cache.uptime || Date.now() + })() : Date.now(), + servers: await require("./getServers.js")(), + users: await require("./getUsers.js")(), nodes: await Promise.all(statusPromises), timestamp: Date.now() } From 820dfd9d8a12cffc4274e503992a9e3613e6692e Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 19:52:39 +0700 Subject: [PATCH 46/59] refactor: rename installer to setup --- README.md | 4 ++-- handlers/{installer.js => setup.js} | 8 ++++---- index.js | 2 +- config.js => setup.js | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename handlers/{installer.js => setup.js} (96%) rename config.js => setup.js (93%) diff --git a/README.md b/README.md index 0ee218b..69bf81a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa 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. - Installer + Setup - [Getting Panel API Key](#getting-panel-api-key) - [Getting a Channel ID](#getting-channel-id) @@ -44,7 +44,7 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa Console Logging ### Changing Env Configuration -1. Run `node config` in the root directory of the app/bot files. +1. Run `node setup` in the root directory of the app/bot files. 2. Enter `2` to change configuration. Change Configuration diff --git a/handlers/installer.js b/handlers/setup.js similarity index 96% rename from handlers/installer.js rename to handlers/setup.js index ff70ac1..78a88d8 100644 --- a/handlers/installer.js +++ b/handlers/setup.js @@ -26,7 +26,7 @@ const isValidURL = (url) => { } }; -module.exports = function Installer() { +module.exports = function Setup() { console.log(cliColor.cyanBright("Welcome to PteroStats!")) console.log(cliColor.yellow("Please fill in the following credentials to set up the app.\n ")); @@ -81,12 +81,12 @@ module.exports = function Installer() { require("./app.js")() }).catch(() => { console.log(cliColor.redBright("❌ Invalid Channel ID.")); - console.log(" \n" + cliColor.redBright("Please run the installer again and fill in the correct credentials.")); + console.log(" \n" + cliColor.redBright("Please run the setup again and fill in the correct credentials.")); process.exit() }) }).catch(() => { console.log(cliColor.redBright("❌ Invalid Discord Bot Token.")); - console.log(" \n" + cliColor.redBright("Please run the installer again and fill in the correct credentials.")); + console.log(" \n" + cliColor.redBright("Please run the setup again and fill in the correct credentials.")); process.exit() }) }).catch((error) => { @@ -118,7 +118,7 @@ module.exports = function Installer() { } else { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Unexpected error: ${error.message}`)); } - console.log(" \n" + cliColor.redBright("Please run the installer again and fill in the correct credentials.")); + console.log(" \n" + cliColor.redBright("Please run the setup again and fill in the correct credentials.")); process.exit() }) } diff --git a/index.js b/index.js index 6a5d066..f8dead1 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,6 @@ console.log( ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return require("./handlers/installer.js")(); +if (!fs.existsSync(".env")) return require("./handlers/setup.js")(); require("./handlers/app.js")(); \ No newline at end of file diff --git a/config.js b/setup.js similarity index 93% rename from config.js rename to setup.js index ee1c62c..f5d2c94 100644 --- a/config.js +++ b/setup.js @@ -22,7 +22,7 @@ console.log( ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return require("./handlers/installer.js")(); +if (!fs.existsSync(".env")) return require("./handlers/setup.js")(); console.log(cliColor.yellowBright( "Configuration is already set. Please select one of the following options:\n \n" + @@ -35,7 +35,7 @@ readline.question('> ', async (answer) => { switch(answer) { case '2': - require('./handlers/installer.js')(); + require('./handlers/setup.js')(); break; case '1': require('./handlers/app.js')(); From d1ef562431394977473a7381a37668224b03c561 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 20:31:58 +0700 Subject: [PATCH 47/59] docs: update README --- README.md | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 69bf81a..54162c5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa ## Preview PteroStats Image Preview +PteroStats Image Preview + PteroStats GIF Preview ## Guide @@ -28,29 +30,29 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa ### Starting the App/Bot 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 by: +3. Download this repository: - [Downloading this repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/dev.zip) and extract it. - - Using Git: Run `git clone -b dev https://github.com/HirziDevs/PteroStats.git .` in the command line. + - Using Git: Run `git clone -b dev 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. - Setup + Setup - [Getting Panel API Key](#getting-panel-api-key) - [Getting a Channel ID](#getting-channel-id) 6. Run `node index` if you want to start the app/bot again, and you're done! - Console Logging + Console Logging ### Changing Env Configuration 1. Run `node setup` in the root directory of the app/bot files. 2. Enter `2` to change configuration. - Change Configuration + Change Configuration 3. Answer the provided question to set up the app/bot. -4. Run `node index` if you want ti start the app/bot again, and you're done! +4. Run `node index` if you want to start the app/bot again, and you're done! ### Getting Panel API Key > [!WARNING] @@ -59,21 +61,21 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa > [!TIP] > Make sure the owner of the Client API key has access to the administrator panel. -1. Go to your `Pterodactyl` or `Pelican Panel` and navigate to the `Account Page`. +1. Go to your `Pterodactyl` or `Pelican` Panel and navigate to the `Account Page`. - Home + Home 2. Click on the `API Credentials` button. - Account Page + Account Page 3. Fill in the `Description` and click the `Create` button. - Create Client API Key + Create Client API Key 4. Copy the API key. - API Key + API Key ### Getting Channel ID 1. Enable Developer Mode in your Discord settings. @@ -104,19 +106,23 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa ### Blacklist Nodes 1. Select a node from the node list on the admin page. - Nodes List + Pterodactyl Nodes List + + Pelican Nodes List 2. Check the URL and copy the node ID. - Node ID + Pterodactyl Node ID + + Pelican Node ID 3. Paste the ID into the blacklist in the config. - Blacklist Config + Blacklist Config You can add more than one node to the blacklist. -Blacklist Config +Blacklist Config ## Reporting a Bug Enable `log_error` in the `config.yml` file and check the console for the error message. After that, report it to our Discord server at [Support Server](https://discord.znproject.my.id). From 42cfdf6f1c17ce8baba69b42b665367cb3f2a49c Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 20:33:36 +0700 Subject: [PATCH 48/59] chore: remove -dev from version and development build notice --- README.md | 3 --- handlers/app.js | 1 - index.js | 2 +- setup.js | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 54162c5..a5b2668 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,6 @@ ## Introduction -> [!WARNING] -> This is a development build! Some features may not work as intended. - PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. ## Preview diff --git a/handlers/app.js b/handlers/app.js index c2234da..26edaa3 100644 --- a/handlers/app.js +++ b/handlers/app.js @@ -8,7 +8,6 @@ const getStats = require("./getStats.js"); module.exports = function App() { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting app...")); - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using a development build! Some features may not work as intended.")); const client = new Client({ intents: [GatewayIntentBits.Guilds] diff --git a/index.js b/index.js index f8dead1..64d16cd 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ console.log( ` /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ \n` + ` \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ \n` + ` \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ \n` + - ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}` + ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}`)}` ); console.log( diff --git a/setup.js b/setup.js index f5d2c94..cdda4ca 100644 --- a/setup.js +++ b/setup.js @@ -11,7 +11,7 @@ console.log( ` /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ \n` + ` \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ \n` + ` \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ \n` + - ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}-dev`)}` + ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}`)}` ); console.log( From 5cacb4c9c4efccec3f3c422de6453ddf4c34948f Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 21:18:48 +0700 Subject: [PATCH 49/59] docs: update README --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a5b2668..e9e59a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-# PteroStats v4 +# PteroStats PteroStats Banner @@ -10,9 +10,9 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. ## Preview -PteroStats Image Preview +PteroStats Image Preview -PteroStats Image Preview +PteroStats Console Preview PteroStats GIF Preview @@ -33,7 +33,7 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa 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. - Setup + Setup - [Getting Panel API Key](#getting-panel-api-key) - [Getting a Channel ID](#getting-channel-id) @@ -43,6 +43,9 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa Console Logging ### Changing Env Configuration +> [!TIP] +> You can change other configuration at the `config.yml` file. + 1. Run `node setup` in the root directory of the app/bot files. 2. Enter `2` to change configuration. From f7f1ba850f744d88926b6c5a81374272a3320248 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 21:20:35 +0700 Subject: [PATCH 50/59] feat: display panel or nodes host --- config.yml | 4 +++- handlers/app.js | 7 +++++-- handlers/getStats.js | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/config.yml b/config.yml index d081870..8e0b7f2 100644 --- a/config.yml +++ b/config.yml @@ -75,14 +75,16 @@ nodes_settings: details: true # Show node details such as memory and disk usage (true/false). servers: false # Show server details (true/false). allocations_as_max_servers: false # Show allocations as max servers (true/false). + host: false # Show node host (true/false). unit: "byte" # Unit for node usage, Available types: "byte" or "percentage". uptime: true # Enable or disable node uptime (true/false). limit: 100 # Node limit for usage statistics display. # Panel Users and Servers Settings panel_settings: + status: true # Display panel stats above node stats (true/false). + host: false # Show panel host (true/false). uptime: true # Enable or disable node uptime (true/false). - status: true # Display panel stats under node stats (true/false). servers: true # Display servers count (true/false). users: true # Display users count (true/false). diff --git a/handlers/app.js b/handlers/app.js index 26edaa3..7a57e63 100644 --- a/handlers/app.js +++ b/handlers/app.js @@ -23,7 +23,8 @@ module.exports = function App() { servers: results.servers, users: results.users, }); - } catch { + } catch (error) { + 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) => { @@ -113,6 +114,7 @@ module.exports = function App() { name: `${node.attributes.name} - ${node.status ? config.status.online : config.status.offline}`, value: "```\n" + + (config.nodes_settings.host ? `Host : ${node.attributes.fqdn}\n` : "") + `Memory : ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` + `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}%` : "") + @@ -151,6 +153,7 @@ module.exports = function App() { name: `Panel - ${panel ? config.status.online : config.status.offline}`, value: "```\n" + + (config.panel_settings.host ? `Host : ${new URL(process.env.PanelURL).host}\n` : "") + `Nodes : ${nodes.length}\n` + (config.panel_settings.servers ? `Servers: ${servers || "Unknown"}\n` : "") + (config.panel_settings.users ? `Users : ${users || "Unknown"}\n` : "") + @@ -230,7 +233,7 @@ module.exports = function App() { } process.exit(); } catch (err) { - console.log(error) + console.error(error) process.exit(); } } diff --git a/handlers/getStats.js b/handlers/getStats.js index b1c4280..bced1a9 100644 --- a/handlers/getStats.js +++ b/handlers/getStats.js @@ -39,6 +39,7 @@ module.exports = async function getStats() { memory: node.attributes.memory, disk: node.attributes.disk, cpu: node.attributes.cpu, + fqdn: node.attributes.fqdn, allocated_resources: node.attributes.allocated_resources, relationships: { allocations: node.attributes.relationships.allocations.data.length, From d5d9b4c02632e5ecff5b6a107978877aee19071c Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 21:22:14 +0700 Subject: [PATCH 51/59] feat: updaste config version to 8 --- config.yml | 2 +- handlers/config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yml b/config.yml index 8e0b7f2..aa60597 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ # PteroStats Configuration File # Need help? Join our Discord server: https://discord.znproject.my.id -version: 7 # Warning: Do not change this unless you know what you are doing! +version: 8 # Warning: Do not change this unless you know what you are doing! # App Presence Configuration presence: diff --git a/handlers/config.js b/handlers/config.js index 488f837..af9fa40 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -18,7 +18,7 @@ try { process.exit(); } -if (config.version !== 7) { +if (config.version !== 8) { console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/dev/config.yml'); process.exit(); } From 10c685ee9450fd7d7ab94e36be2a860d7f1c3554 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 21:35:21 +0700 Subject: [PATCH 52/59] chore: remove development text --- README.md | 4 ++-- handlers/config.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e9e59a6..e35dc9a 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ 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/dev.zip) and extract it. - - Using Git: Run `git clone -b dev https://github.com/HirziDevs/PteroStats.git` in the command line. + - [Downloading 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. diff --git a/handlers/config.js b/handlers/config.js index af9fa40..b7a9de8 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -19,7 +19,7 @@ try { } if (config.version !== 8) { - console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/dev/config.yml'); + 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'); process.exit(); } From 3d3604df052279e81e6d79d1b807dfc624a7fd64 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 23:00:16 +0700 Subject: [PATCH 53/59] feat: update config version to 9 --- config.yml | 16 ++++++++-------- handlers/config.js | 2 +- setup.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config.yml b/config.yml index aa60597..98da611 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ # PteroStats Configuration File # Need help? Join our Discord server: https://discord.znproject.my.id -version: 8 # Warning: Do not change this unless you know what you are doing! +version: 9 # Warning: Do not change this unless you know what you are doing! # App Presence Configuration presence: @@ -22,7 +22,7 @@ message: embed: panel: author: - name: "Hosting Panel" + name: "Hosting Panel" # Author name for the panel embed. icon: "" # Icon URL for the author of the panel embed. title: "Panel Stats" # Title of the panel stats embed. description: "Next update {{time}}" # Description for the panel. {{time}} will display the next refresh time. @@ -36,17 +36,17 @@ embed: nodes: author: - name: "" + name: "" # Author name for the nodes embed. icon: "" # Icon URL for the author of the nodes embed. - title: "Nodes Stats" # Title for the node stats embed. - description: "" # Description for the node stats embed. Can be left empty. + title: "Nodes Stats" # Title for the nodes stats embed. + description: "" # Description for the nodes stats embed. Can be left empty. timestamp: true # Include a timestamp in the nodes embed (true/false). color: "5865F2" # Embed color in hex format. footer: - text: "By @HirziDevs" # Footer text for node stats. + text: "By @HirziDevs" # Footer text for nodes stats. icon: "" # Footer icon URL. - thumbnail: "" # Thumbnail URL for the node stats embed. - image: "" # Image URL for the node stats embed. + thumbnail: "" # Thumbnail URL for the nodes stats embed. + image: "" # Image URL for the nodes stats embed. # Message Button Configuration button: diff --git a/handlers/config.js b/handlers/config.js index b7a9de8..86322fc 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -18,7 +18,7 @@ try { process.exit(); } -if (config.version !== 8) { +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'); process.exit(); } diff --git a/setup.js b/setup.js index cdda4ca..0de3947 100644 --- a/setup.js +++ b/setup.js @@ -33,7 +33,7 @@ console.log(cliColor.yellowBright( readline.question('> ', async (answer) => { readline.close(); - switch(answer) { + switch (answer) { case '2': require('./handlers/setup.js')(); break; From 2b751c86e749f8305cd0a772bbf872d0994a8a39 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 23:04:24 +0700 Subject: [PATCH 54/59] feat: webhook notifier --- README.md | 35 +++++++++++++++++++++++++++++++++++ config.yml | 15 +++++++++++++++ handlers/app.js | 13 +++++++++++++ handlers/getStats.js | 18 ++++++++++++++++++ handlers/webhook.js | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 handlers/webhook.js diff --git a/README.md b/README.md index e35dc9a..d2a9b22 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa - [Getting a Channel ID](#getting-channel-id) - [Using Custom Emoji](#using-custom-emoji) - [Blacklist Nodes](#blacklist-nodes) +- [Notifier](#notifier) ### Starting the App/Bot 1. [Create your Discord App/Bot](https://discordjs.guide/preparations/adding-your-bot-to-servers.html). @@ -124,6 +125,40 @@ You can add more than one node to the blacklist. Blacklist Config +### Notifier +Get a notification on Discord when your panel or specific nodes are currently down. + +Notifier Preview + +### Enabling Notifier +Open `config.yml` and set `enable` at the notifier configuration to `true` + +Notifier Config + +#### Getting Discord Webhook URL +1. Go to the channel settings of the channel you want to set for the notifier. + + Notifier Config + +2. Go to integrations and select `View Webhooks` or `Create Webhook`. + + Notifier Config + +3. Create a new webhook and copy the Webhook URL + + Notifier Config + +4. Paste the Webhook URL on the webhook notifier configuration. + + Notifier Config + + +> [!TIP] +> You can change the webhook icon and username on the webhook settings. + +Notifier Config + + ## Reporting a Bug Enable `log_error` in the `config.yml` file and check the console for the error message. After that, report it to our Discord server at [Support Server](https://discord.znproject.my.id). diff --git a/config.yml b/config.yml index 98da611..4760a7f 100644 --- a/config.yml +++ b/config.yml @@ -88,6 +88,21 @@ panel_settings: servers: true # Display servers count (true/false). users: true # Display users count (true/false). +# Notifier Configuration +notifier: + enable: false # Enable or disable notifier. + webhook: "" # Discord Webhook URL for the notifier. + embed: + author: + name: "" # Author name for the notifier embed. + icon: "" # Icon URL for the author of the notifier embed. + timestamp: true # Include a timestamp in the notifier embed (true/false). + footer: + text: "PteroStats Notifier" # Footer text for notifier stats. + icon: "" # Footer icon URL. + thumbnail: "" # Thumbnail URL for the notifier stats embed. + image: "" # Image URL for the notifier stats embed. + # Error Logging Configuration # Enable logging to console if servers go offline, useful for debugging. log_error: false # Set to true to enable error logging. \ No newline at end of file diff --git a/handlers/app.js b/handlers/app.js index 7a57e63..69105e1 100644 --- a/handlers/app.js +++ b/handlers/app.js @@ -5,6 +5,7 @@ const cliColor = require("cli-color"); const config = require("./config.js"); const convertUnits = require("./convertUnits.js"); const getStats = require("./getStats.js"); +const webhook = require("./webhook.js"); module.exports = function App() { console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting app...")); @@ -16,6 +17,12 @@ module.exports = function App() { async function startGetStatus() { try { const results = await getStats(); + if (results.isPanelDown) webhook( + new EmbedBuilder() + .setTitle("Panel Online") + .setColor("57F287") + .setDescription(`Panel is back online`) + ) createMessage({ panel: true, uptime: results.uptime, @@ -35,6 +42,12 @@ module.exports = function App() { try { const results = JSON.parse(data); + if (results.uptime) webhook( + new EmbedBuilder() + .setTitle("Panel Offline") + .setColor("ED4245") + .setDescription(`Panel is currently offline`) + ) results.uptime = false fs.writeFileSync("cache.json", JSON.stringify(results, null, 2), "utf8"); createMessage({ diff --git a/handlers/getStats.js b/handlers/getStats.js index bced1a9..7579e9d 100644 --- a/handlers/getStats.js +++ b/handlers/getStats.js @@ -1,6 +1,8 @@ const config = require("./config.js"); const fs = require("node:fs"); const cliColor = require("cli-color"); +const webhook = require("./webhook.js"); +const { EmbedBuilder } = require("discord.js"); module.exports = async function getStats() { let cache = (() => { @@ -29,7 +31,22 @@ module.exports = async function getStats() { if (!nodeStatus) { nodeUptime = false + if (cache && cache.nodes.find((n) => n.attributes.id === node.attributes.id)?.status) + webhook( + new EmbedBuilder() + .setTitle("Node Offline") + .setColor("ED4245") + .setDescription(`Node \`${node.attributes.name}\` is currently offline`) + ) console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node ${cliColor.blueBright(node.attributes.name)} is currently offline.`)) + } else { + if (cache && !cache.nodes.find((n) => n.attributes.id === node.attributes.id)?.status) + webhook( + new EmbedBuilder() + .setTitle("Node Online") + .setColor("57F287") + .setDescription(`Node \`${node.attributes.name}\` is back online`) + ) } return { @@ -58,6 +75,7 @@ module.exports = async function getStats() { servers: await require("./getServers.js")(), users: await require("./getUsers.js")(), nodes: await Promise.all(statusPromises), + isPanelDown: !cache.uptime, timestamp: Date.now() } diff --git a/handlers/webhook.js b/handlers/webhook.js new file mode 100644 index 0000000..84db3e0 --- /dev/null +++ b/handlers/webhook.js @@ -0,0 +1,33 @@ +const { WebhookClient, EmbedBuilder } = require("discord.js") +const config = require("./config") +const cliColor = require("cli-color") + +module.exports = function Webhook(embed) { + if (config.notifier.enable) { + try { + const webhook = new WebhookClient({ + url: config.notifier.webhook + }) + webhook.send({ + embeds: [ + new EmbedBuilder(embed.data) + .setAuthor({ + name: config.notifier.embed.author.name || null, + iconURL: config.notifier.embed.author.icon || null + }) + .setFooter({ + text: config.notifier.embed.footer.text || null, + iconURL: config.notifier.embed.footer.icon || null + }) + .setURL(config.notifier.embed.url || null) + .setTimestamp(config.notifier.embed.timestamp ? new Date() : null) + .setThumbnail(config.notifier.embed.thumbnail || null) + .setImage(config.notifier.embed.image || null) + ] + }) + } catch (error) { + console.log(error) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Invalid Webhook URL")) + } + } +} \ No newline at end of file From 9f2660d3370d45c4544304f3833759e3ffe83a9b Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Fri, 13 Sep 2024 23:16:36 +0700 Subject: [PATCH 55/59] docs: fix typo in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d2a9b22..a031c91 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,8 @@ PteroStats is a Discord App/Bot that designed to check Pterodactyl or Pelican Pa Blacklist Config -You can add more than one node to the blacklist. +> [!TIP] +> You can add more than one node to the blacklist. Blacklist Config @@ -130,7 +131,7 @@ Get a notification on Discord when your panel or specific nodes are currently do Notifier Preview -### Enabling Notifier +#### Enabling Notifier Open `config.yml` and set `enable` at the notifier configuration to `true` Notifier Config @@ -158,11 +159,10 @@ Open `config.yml` and set `enable` at the notifier configuration to `true` Notifier Config - ## Reporting a Bug Enable `log_error` in the `config.yml` file and check the console for the error message. After that, report it to our Discord server at [Support Server](https://discord.znproject.my.id). ## Links - [Pterodactyl Discord Server](https://discord.gg/pterodactyl) - [Pelican Discord Server](https://discord.gg/pelican-panel) -- [Support Server](https://discord.znproject.my.id) +- [Support Server](https://discord.znproject.my.id) \ No newline at end of file From e6374da63d8f4edd7f15465ba191a31f0536aec7 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Sat, 14 Sep 2024 09:39:08 +0700 Subject: [PATCH 56/59] refactor: improve function names, code readability, and error handling --- README.md | 6 ++--- handlers/UptimeFormatter.js | 2 +- handlers/{app.js => application.js} | 31 ++++++++++++++---------- handlers/{config.js => configuration.js} | 10 ++++---- handlers/convertUnits.js | 8 +++--- handlers/getNodeConfiguration.js | 2 +- handlers/getNodesDetails.js | 2 +- handlers/getServers.js | 2 +- handlers/getStats.js | 23 ++++++++++++------ handlers/getUsers.js | 2 +- handlers/getWingsStatus.js | 2 +- handlers/setup.js | 29 ++++++++++++++-------- handlers/webhook.js | 2 +- index.js | 6 +++-- package.json | 2 +- setup.js | 10 +++++--- 16 files changed, 82 insertions(+), 57 deletions(-) rename handlers/{app.js => application.js} (93%) rename handlers/{config.js => configuration.js} (76%) 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 +}); From 49470d690d8f8ad6edba84e2c5c1ca82ef849316 Mon Sep 17 00:00:00 2001 From: Hirzi Date: Sat, 14 Sep 2024 09:41:25 +0700 Subject: [PATCH 57/59] refactor: ename UptimeFormatter.js to uptimeFormatter.js --- handlers/{UptimeFormatter.js => uptimeFormatter.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename handlers/{UptimeFormatter.js => uptimeFormatter.js} (99%) diff --git a/handlers/UptimeFormatter.js b/handlers/uptimeFormatter.js similarity index 99% rename from handlers/UptimeFormatter.js rename to handlers/uptimeFormatter.js index 1d214ca..dd26d42 100644 --- a/handlers/UptimeFormatter.js +++ b/handlers/uptimeFormatter.js @@ -10,4 +10,4 @@ module.exports = function uptimeFormatter(time) { if (text.length > 0) text.push(`and ${seconds} seconds`) else text.push(`${seconds} seconds`) return text.join(", ").replace(", and", " and") -} \ No newline at end of file +} From 22fc0017e7be670384794d62b2f681d97da73f41 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Sat, 14 Sep 2024 09:43:00 +0700 Subject: [PATCH 58/59] refactor: rename UptimeFormatter.js to uptimeFormatter.js --- handlers/{UptimeFormatter.js => uptimeFormatter.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename handlers/{UptimeFormatter.js => uptimeFormatter.js} (99%) diff --git a/handlers/UptimeFormatter.js b/handlers/uptimeFormatter.js similarity index 99% rename from handlers/UptimeFormatter.js rename to handlers/uptimeFormatter.js index 1d214ca..dd26d42 100644 --- a/handlers/UptimeFormatter.js +++ b/handlers/uptimeFormatter.js @@ -10,4 +10,4 @@ module.exports = function uptimeFormatter(time) { if (text.length > 0) text.push(`and ${seconds} seconds`) else text.push(`${seconds} seconds`) return text.join(", ").replace(", and", " and") -} \ No newline at end of file +} From 5a31e9edede3b2f079395bd8a22c1511727d2d71 Mon Sep 17 00:00:00 2001 From: hirzidevs Date: Sat, 14 Sep 2024 11:06:20 +0700 Subject: [PATCH 59/59] fix: resolve issue where config validation starts before setup is running --- index.js | 6 ++---- setup.js | 8 +++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index fd4383c..2d73992 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,6 @@ 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` + @@ -20,6 +18,6 @@ console.log( ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return setup(); +if (!fs.existsSync(".env")) return require("./handlers/setup.js")(); -application(); +require("./handlers/application.js")(); diff --git a/setup.js b/setup.js index ae62cbf..6b026a6 100644 --- a/setup.js +++ b/setup.js @@ -1,8 +1,6 @@ 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 @@ -24,7 +22,7 @@ console.log( ` \n \n${package.description}\n ` ); -if (!fs.existsSync(".env")) return setup(); +if (!fs.existsSync(".env")) return require("./handlers/setup.js")(); console.log(cliColor.yellowBright( "Configuration is already set. Please select one of the following options:\n \n" + @@ -37,10 +35,10 @@ readline.question('> ', async (answer) => { switch (answer) { case '2': - setup(); + require("./handlers/setup.js")(); break; case '1': - application(); + require("./handlers/application.js")(); break; default: console.log(cliColor.redBright('Invalid input. Please type either 1 or 2.'));