Recode PteroStats and Support Pelican Panel

This commit is contained in:
hirzidevs
2024-07-29 19:48:25 +07:00
parent fff29725cb
commit 12df9b4acf
18 changed files with 493 additions and 625 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
config-dev.yml
package-lock.json
cache.json
-129
View File
@@ -1,129 +0,0 @@
<div align="center">
<img alt="PteroStats Banner" src="https://cdn.discordapp.com/attachments/626755594526916629/978478722489393153/20220524_090325.png" width="400"/>
## Bahasa / Language
[[Indonesia]](https://github.com/HirziDevs/PteroStats/blob/dev/Indo.md) | [[Inggris]](https://github.com/HirziDevs/PteroStats/blob/dev/README.md)
</div>
## Pengenalan
PteroStats adalah bot yang dirancang untuk memeriksa status panel pterodactyl dan dikirim ke server discord
## Contoh
- Test Panel
<img alt="Example" src="https://cdn.discordapp.com/attachments/988796533430448148/991520721467613224/example.gif" width="200"/>
- [Calvs Cloud](https://discord.gg/ssCQjhrBJN)
<img alt="Calvs Cloud" src="https://media.discordapp.net/attachments/819757140155564062/1037353043487887410/unknown.png" width="200">
## 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`
<img alt="Admin Panel" src="https://usercontent.catto.pictures/hirzi/aabafe57-cbfe-4d7f-9d6d-4a63a7f23d4c.png" width="400"/>
2. Klik tombol `Create New`
<img alt="Application API Page" src="https://usercontent.catto.pictures/hirzi/f916f0c6-0968-4125-8226-ba4daa1de902.png" width="400"/>
3. Set semua permission ke `read` dan untuk description kamu bisa mengisi apa saja
<img alt="Create Application API" src="https://usercontent.catto.pictures/hirzi/3e4575cb-4f52-4bd9-9091-36fda20bedad.png" width="400"/>
4. Copy apikey-nya.
<img alt="Application API List" src="https://usercontent.catto.pictures/hirzi/9142b0b3-0556-4741-840c-6976c3fe3ad4.png" width="400"/>
5. Paste panel apikeynya dan panel urlnya di config
<img alt="Panel Config" src="https://usercontent.catto.pictures/hirzi/2b9365b8-69d2-4fa0-8eac-3efc8591b765.png" width="400"/>
### Membuat Discord Bot
Kalian bisa cek [website ini](https://discordjs.guide/preparations/setting-up-a-bot-application.html)
Paste bot tokennya di config
<img alt="Bot Config" src="https://usercontent.catto.pictures/hirzi/4eb4d5ff-6969-4461-b01d-c45888cfc994.png" width="400"/>
### 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
<img alt="Discord User Settings" src="https://usercontent.catto.pictures/hirzi/36894499-b141-488f-98ed-40245c8f6862.png" width="400"/>
2. Klik kanan teks channel dan pilih `Copy ID`
<img alt="Right Click Channel" src="https://usercontent.catto.pictures/hirzi/9f8352da-df5b-4587-9594-ced9b11a5507.png" width="250"/>
3. Paste id channelnya di config
<img alt="Channel Config" src="https://usercontent.catto.pictures/hirzi/b34cdbee-1e24-49f2-8219-efe0344a24f9.png" width="400"/>
### 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
<img alt="Type \ on the chat" src="https://usercontent.catto.pictures/hirzi/2e3c821f-92f9-4b5c-863a-e020b2fbc426.png" width="350"/>
2. Pilih custom emoji yang kamu mau
<img alt="Select Custom Emoji" src="https://usercontent.catto.pictures/hirzi/7c071727-2adb-4c8c-91d3-21664948a334.png" width="300"/>
3. Copy textnya!
<img alt="Copy Emoji ID" src="https://usercontent.catto.pictures/hirzi/bd0084ac-f11b-413d-8b66-580efc011908.png" width="400"/>
4. Paste id emojinya di config
<img alt="Status Config" src="https://usercontent.catto.pictures/hirzi/458ad1d6-019b-4b27-be60-3cbabfa07c06.png" width="400"/>
### Blacklist Nodes
1. Pilih node yang ada di node list admin page
<img alt="Nodes List" src="https://usercontent.catto.pictures/hirzi/5699fdbd-7c3c-4fa5-ae2c-d0ccb39cb69e.png" width="400"/>
2. Cek urlnya dan copy id nodenya
<img alt="Node Id" src="https://usercontent.catto.pictures/hirzi/45f855fc-6d96-4b23-a96e-892071189d01.png" width="400"/>
3. Masukan ke blacklist di config
<img alt="Blacklist Config" src="https://usercontent.catto.pictures/hirzi/cfb479bf-64da-43e5-b0d1-f7c0c78bf068.png" width="400"/>
Kamu bisa memasukan lebih dari 1 node untuk di blacklist
<img alt="Blacklist Config" src="https://usercontent.catto.pictures/hirzi/85b6a9b1-8ec9-4395-b5b1-6f85d3f52162.png" width="400"/>
## Permission apikey
Jika kamu mengalami error 403 coba aktifkan `read & write` di semua opsi permission
<img alt="Application API Permission" src="https://usercontent.catto.pictures/hirzi/273d5b86-5249-42d0-bfc6-1dcfcae4efe7.png" width="400"/>
## 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)
+25 -28
View File
@@ -1,26 +1,20 @@
<div align="center">
<img alt="PteroStats Banner" src="https://cdn.discordapp.com/attachments/626755594526916629/978478722489393153/20220524_090325.png" width="400"/>
<img alt="PteroStats Banner" src="https://images-ext-2.discordapp.net/external/oRPpwML4JUV0HbsPKtsghvIjS5ZrVwqH2KQ4tevg_Jg/https/repository-images.githubusercontent.com/381250920/e9acc9c2-2fbd-4fb0-8554-9788146d817e" width="400"/>
## Language / Bahasa
[[English]](https://github.com/HirziDevs/PteroStats/blob/dev/README.md) | [[Indonesia]](https://github.com/HirziDevs/PteroStats/blob/dev/Indo.md)
</div>
### 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
<img alt="Example" src="https://cdn.discordapp.com/attachments/988796533430448148/991520721467613224/example.gif" width="200"/>
- [Calvs Cloud](https://discord.gg/ssCQjhrBJN)
<img alt="Calvs Cloud" src="https://media.discordapp.net/attachments/819757140155564062/1037353043487887410/unknown.png" width="200">
<img alt="Example" src="https://i.imgur.com/fzQANo5.gif" width="200"/>
## 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
<img alt="Admin Panel" src="https://usercontent.catto.pictures/hirzi/aabafe57-cbfe-4d7f-9d6d-4a63a7f23d4c.png" width="400"/>
> [!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`.
<img alt="Application API Page" src="https://usercontent.catto.pictures/hirzi/f916f0c6-0968-4125-8226-ba4daa1de902.png" width="400"/>
<img alt="Home" src="https://i.imgur.com/Uu97RJO.png" width="400"/>
3. Set all options permission to `read` and for description you can put whatever you want
2. Click on the `API Credentials` button
<img alt="Create Application API" src="https://usercontent.catto.pictures/hirzi/3e4575cb-4f52-4bd9-9091-36fda20bedad.png" width="400"/>
<img alt="Account Page" src="https://i.imgur.com/sm4THSu.png" width="400"/>
4. Copy the apikey.
3. Fill the `Description` and click on the `Create` button
<img alt="Application API List" src="https://usercontent.catto.pictures/hirzi/9142b0b3-0556-4741-840c-6976c3fe3ad4.png" width="400"/>
<img alt="Create Client API Key" src="https://i.imgur.com/Q5E0PY4.png" width="400"/>
5. Paste the panel apikey and panel url at the config
4. Copy the API key.
<img alt="API Key" src="https://i.imgur.com/7goShy8.png" width="400"/>
5. Paste the panel API key and panel url at the config
<img alt="Panel Config" src="https://usercontent.catto.pictures/hirzi/2b9365b8-69d2-4fa0-8eac-3efc8591b765.png" width="400"/>
@@ -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
<img alt="Type \ on the chat" src="https://usercontent.catto.pictures/hirzi/2e3c821f-92f9-4b5c-863a-e020b2fbc426.png" width="350"/>
@@ -114,16 +112,15 @@ You can add more than one node in the blacklist
<img alt="Blacklist Config" src="https://usercontent.catto.pictures/hirzi/85b6a9b1-8ec9-4395-b5b1-6f85d3f52162.png" width="400"/>
## 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
<img alt="Application API Permission" src="https://usercontent.catto.pictures/hirzi/273d5b86-5249-42d0-bfc6-1dcfcae4efe7.png" width="400"/>
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)
+65 -55
View File
@@ -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
# 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
-29
View File
@@ -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)
}
}
-144
View File
@@ -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)
})
})
}
+24
View File
@@ -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
+20
View File
@@ -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`;
}
}
+13
View File
@@ -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)
}
+44
View File
@@ -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
})
}
+15
View File
@@ -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)
}
+50
View File
@@ -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
}
+15
View File
@@ -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)
}
+12
View File
@@ -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)
}
-176
View File
@@ -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!'))
})
}
+9
View File
@@ -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]);
}
+175 -42
View File
@@ -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);
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();
}
+22 -22
View File
@@ -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"
}
}