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 @@ -
-
-## Bahasa / Language
-[[Indonesia]](https://github.com/HirziDevs/PteroStats/blob/dev/Indo.md) | [[Inggris]](https://github.com/HirziDevs/PteroStats/blob/dev/README.md)
-
-
-
-- [Calvs Cloud](https://discord.gg/ssCQjhrBJN)
-
-
-
-## 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`
-
-
-
-2. Klik tombol `Create New`
-
-
-
-3. Set semua permission ke `read` dan untuk description kamu bisa mengisi apa saja
-
-
-
-4. Copy apikey-nya.
-
-
-
-5. Paste panel apikeynya dan panel urlnya di config
-
-
-
-### Membuat Discord Bot
-Kalian bisa cek [website ini](https://discordjs.guide/preparations/setting-up-a-bot-application.html)
-
-Paste bot tokennya di 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
-
-
-
-2. Klik kanan teks channel dan pilih `Copy ID`
-
-
-
-3. Paste id channelnya di 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
-
-
-
-2. Pilih custom emoji yang kamu mau
-
-
-
-3. Copy textnya!
-
-
-
-4. Paste id emojinya di config
-
-
-
-### Blacklist Nodes
-1. Pilih node yang ada di node list admin page
-
-
-
-2. Cek urlnya dan copy id nodenya
-
-
-
-3. Masukan ke blacklist di config
-
-
-
-Kamu bisa memasukan lebih dari 1 node untuk di blacklist
-
-
-
-## Permission apikey
-
-Jika kamu mengalami error 403 coba aktifkan `read & write` di semua opsi 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 @@
+
-
-- [Calvs Cloud](https://discord.gg/ssCQjhrBJN)
-
-
+
## 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
-
+> [!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`.
-
+
-3. Set all options permission to `read` and for description you can put whatever you want
+2. Click on the `API Credentials` button
-
+
-4. Copy the apikey.
+3. Fill the `Description` and click on the `Create` button
-
+
-5. Paste the panel apikey and panel url at the config
+4. Copy the API key.
+
+
+
+5. Paste the panel API key and panel url at the 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
@@ -114,16 +112,15 @@ You can add more than one node in the blacklist
-## 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
-
-
+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