diff --git a/.gitignore b/.gitignore index 6134a9d..4525073 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +node_modules/ +config-dev.yml package-lock.json .vscode node_modules +cache.json +.env diff --git a/Indo.md b/Indo.md deleted file mode 100644 index a41df97..0000000 --- a/Indo.md +++ /dev/null @@ -1,129 +0,0 @@ -
-
-## Bahasa / Language
-[[Indonesia]](https://github.com/HirziDevs/PteroStats/blob/main/Indo.md) | [[Inggris]](https://github.com/HirziDevs/PteroStats/blob/main/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
-
-
-
-## Nodenya online tapi di embed dibilang offline
-
-Jika kamu mengalami isu ini, atur `log_error` menjadi true di file config dan beri tahu kami di [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 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 26e966a..03c89fe 100644
--- a/README.md
+++ b/README.md
@@ -1,97 +1,102 @@
-
+
-## Installation
-- [Introduction](#introduction)
-- [Example](#example)
-- [Installation](#installation)
- - [Getting API key from pterodactyl](#getting-api-key-from-pterodactyl)
- - [Creating Discord Bot](#creating-discord-bot)
- - [Inviting Discord Bot](#inviting-discord-bot)
- - [Getting Channel ID](#getting-channel-id)
- - [Starting bot](#starting-bot)
- - [Using Custom Emoji](#using-custom-emoji)
- - [Blacklist Nodes](#blacklist-nodes)
-- [The node is online but the embed is read as offline](#the-node-is-online-but-the-embed-is-read-as-offline)
-- [Docker](#docker)
+
+
+## Guide
+- [Starting the App/Bot](#starting-the-appbot)
+- [Changing Env Configuration](#changing-env-configuration)
+- [Getting an Panel API key](#getting-panel-api-key)
+- [Getting a Channel ID](#getting-channel-id)
+- [Using Custom Emoji](#using-custom-emoji)
+- [Blacklist Nodes](#blacklist-nodes)
+- [Notifier](#notifier)
+- - [Docker](#docker)
- [Installation](#installation-1)
- [Docker Compose](#docker-compose)
- [Docker Run](#docker-run)
-- [Links](#links)
-### Getting API key from pterodactyl
+### Starting the App/Bot
+1. [Create your Discord App/Bot](https://discordjs.guide/preparations/adding-your-bot-to-servers.html).
+2. [Invite your Discord App/Bot to your Discord server](https://discordjs.guide/preparations/adding-your-bot-to-servers.html).
+3. Download this repository:
+ - [Download this repository](https://github.com/HirziDevs/PteroStats/archive/refs/heads/main.zip) and extract it.
+ - Using Git: Run `git clone https://github.com/HirziDevs/PteroStats.git` in the command line.
+4. Run `npm install` in the root directory of the app/bot files.
+5. Run `node index` and answer the prompted questions to set up the app/bot.
+
+
+ - [Getting Panel API Key](#getting-panel-api-key)
+ - [Getting a Channel ID](#getting-channel-id)
+
+6. Run `node index` if you want to start the app/bot again, and you're done!
+
+
+
+### Changing Env Configuration
+> [!TIP]
+> You can change other configuration at the `config.yml` file.
+
+1. Run `node setup` in the root directory of the app/bot files.
+2. Enter `2` to change configuration.
+
+
+
+3. Answer the provided question to set up the app/bot.
+4. Run `node index` if you want to start the app/bot again, and you're done!
+
+### Getting Panel API Key
> [!WARNING]
-> The use of Application API keys are **deprecated**, you should use **Client API keys** in the config file
+> The use of Application API keys is **deprecated**; you should use **Client API keys**.
-1. Go to your `Pterodactyl Panel` and go to `Account Page`.
+> [!TIP]
+> Make sure the owner of the Client API key has access to the administrator panel.
-
+1. Go to your `Pterodactyl` or `Pelican` Panel and navigate to the `Account Page`.
-2. Click on the `API Credentials` button
+
-
+2. Click on the `API Credentials` button.
-3. Fill the `Description` and click on the `Create` button
+
-
+3. Fill in the `Description` and click the `Create` button.
+
+
4. Copy the API key.
-
-
-5. Paste the panel API key and panel url at the config
-
-
-
-### Creating Discord Bot
-Please refer to [this website](https://discordjs.guide/preparations/setting-up-a-bot-application.html)
-
-Paste the bot token at the config
-
-
-
-### Inviting Discord Bot
-Please refer to [this website](https://discordjs.guide/preparations/adding-your-bot-to-servers.html)
+
### Getting Channel ID
-1. Enable Developer Feature at your discord settings
+1. Enable Developer Mode in your Discord settings.
-2. Right Click text channel and select `Copy ID`
+2. Right-click the text channel and select `Copy ID`.
-3. Paste the channel id at the config
-
-
-
-### Starting bot
-1. Make sure you have done the things above
-2. Run `npm install` in the root directory of the bot files.
-3. Run `node index` and you are done.
-
-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 server that has custom emoji you want
+1. Type `\` in the server that has the custom emoji you want.
-2. Select custom emoji you want
+2. Select the custom emoji you want.
@@ -99,30 +104,67 @@ if you need help contact me on discord `@hirzidevs` or join [our discord support
-4. Paste the emoji id at the config
+4. Paste the emoji ID into the config.
-
+
### Blacklist Nodes
-1. Select node from node list on admin page
+1. Select a node from the node list on the admin page.
-
+
-2. Check the url and copy the node id
+
-
+2. Check the URL and copy the node ID.
-3. Paste the id to the blacklist on config
+
-
+
-You can add more than one node in the blacklist
+3. Paste the ID into the blacklist in the config.
-
+
-## The node is online but the embed is read as offline
+> [!TIP]
+> You can add more than one node to the blacklist.
-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
+=======
+### Notifier
+Get a notification on Discord when your panel or specific nodes are currently down.
+
+
+
+
+#### Enabling Notifier
+Open `config.yml` and set `enable` at the notifier configuration to `true`
+
+
+
+#### Getting Discord Webhook URL
+1. Go to the channel settings of the channel you want to set for the notifier.
+
+
+
+2. Go to integrations and select `View Webhooks` or `Create Webhook`.
+
+
+
+3. Create a new webhook and copy the Webhook URL
+
+
+
+4. Paste the Webhook URL on the webhook notifier configuration.
+
+
+
+
+> [!TIP]
+> You can change the webhook icon and username on the webhook settings.
+
+
## Docker
@@ -151,11 +193,10 @@ docker run -d --name pterostats -v $(pwd)/config.yml:/app/config.yml ghcr.io/hir
docker logs -f pterostats
```
-## Links
+## Reporting a Bug
+Enable `log_error` in the `config.yml` file and check the console for the error message. After that, report it to our Discord server at [Support Server](https://discord.znproject.my.id).
-- [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://github.com/devnote-dev/ptero-notes/)
+## Links
- [Pterodactyl Discord Server](https://discord.gg/pterodactyl)
-- [PteroBot Support Server](https://discord.gg/zv6maQRah3)
+- [Pelican Discord Server](https://discord.gg/pelican-panel)
+- [Support Server](https://discord.znproject.my.id)
\ No newline at end of file
diff --git a/config.yml b/config.yml
index a4e6a9b..4760a7f 100644
--- a/config.yml
+++ b/config.yml
@@ -1,87 +1,108 @@
-# PteroStats config
-# If you need help, join our discord server here: https://discord.gg/zv6maQRah3
+# PteroStats Configuration File
+# Need help? Join our Discord server: https://discord.znproject.my.id
+version: 9 # Warning: Do not change this unless you know what you are doing!
-# Bot Configuration
-token: "Put bot token here"
+# App Presence Configuration
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'
+ enable: true # Enable or disable app presence.
+ text: "Hosting Panel" # The status text shown by the app.
+ type: "watching" # Available types: 'watching', 'playing', 'listening', 'competing'. (Note: 'streaming' is currently unsupported)
+ status: "online" # Available types: 'online', 'idle', 'dnd', or 'invisible'
-# Discord Channel and Refresh Time Configuration
-channel: "Put channel id here"
-refresh: 60 # How much time the bot will refresh the stats
-timeout: 1 # 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"
+# Discord Channel and Refresh Timing Configuration
+refresh: 10 # Interval in seconds for refreshing the panel's stats.
+timeout: 5 # Timeout in seconds for node responses. Adjusting this will affect the overall refresh time.
# Message and Embed Configuration
-# set the option as '' if you want to disable it
+# To disable an option, leave the value as an empty string: ''
message:
- content: ""
- attachment: "" # If you enable attachment on message it will upload the attachment first before sending or editing message and will result in delayed stats
+ content: "" # Custom content for app messages. Set to '' to disable.
+ attachment: "" # Attachments will delay stats refresh since they are uploaded first.
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: ""
- image: ""
- field:
- title: ""
- description: "" # You can use {{time}} to make "in X seconds" time format
+ panel:
+ author:
+ name: "Hosting Panel" # Author name for the panel embed.
+ icon: "" # Icon URL for the author of the panel embed.
+ title: "Panel Stats" # Title of the panel stats embed.
+ description: "Next update {{time}}" # Description for the panel. {{time}} will display the next refresh time.
+ timestamp: false # Show the timestamp in the embed (true/false).
+ color: "5865F2" # Embed color in hex format.
+ footer:
+ text: "" # Footer text. Set to '' to disable.
+ icon: "" # Footer icon URL.
+ thumbnail: "" # Thumbnail URL for the embed.
+ image: "" # Image URL for the embed.
+
+ nodes:
+ author:
+ name: "" # Author name for the nodes embed.
+ icon: "" # Icon URL for the author of the nodes embed.
+ title: "Nodes Stats" # Title for the nodes stats embed.
+ description: "" # Description for the nodes stats embed. Can be left empty.
+ timestamp: true # Include a timestamp in the nodes embed (true/false).
+ color: "5865F2" # Embed color in hex format.
+ footer:
+ text: "By @HirziDevs" # Footer text for nodes stats.
+ icon: "" # Footer icon URL.
+ thumbnail: "" # Thumbnail URL for the nodes stats embed.
+ image: "" # Image URL for the nodes stats embed.
# Message Button Configuration
button:
- enable: true
- btn1:
- label: "PteroStats"
- url: "https://github.com/HirziDevs/PteroStats"
- btn2:
- label: ""
- url: ""
- btn3:
- label: ""
- url: ""
- btn4:
- label: ""
- url: ""
- btn5:
- label: ""
- url: ""
+ enable: true # Enable or disable buttons in messages.
+ row1:
+ - label: "Panel" # Label for the first button.
+ url: "https://panel.example.com" # URL for the first button.
+# - label: "Dashboard" # Remove "#" at the start of the line to use this button
+# url: "https://dash.example.com" # Remove "#" at the start of the line to use this button
+# row2: # Remove "#" at the start of the line to use this row
+# - label: "Backup Panel"
+# url: "https://panel2.example.com"
+# - label: "Backup Dashboard"
+# url: "https://dash2.example.com"
# Status Message Configuration
-# How to use custom emoji: https://github.com/HirziDevs/PteroStats#using-custom-emoji
+# For details on using custom emojis, visit: https://github.com/HirziDevs/PteroStats#using-custom-emoji
status:
- online: ":green_circle: Online"
- offline: ":red_circle: Offline"
+ online: ":green_circle: Online" # Status message for when a node is online.
+ offline: ":red_circle: Offline" # Status message for when a node is offline.
-# Nodes Settings
-# How to get nodes id: https://github.com/HirziDevs/PteroStats#blacklist-nodes
+# Node Settings
+# Instructions for retrieving node IDs: https://github.com/HirziDevs/PteroStats#blacklist-nodes
nodes_settings:
- blacklist: [] # You can add node id to remove the node from status embed (Example: "blacklist: [1]")
- details: false # enable nodes details i.e memory and disk usage
- servers: true
- location: true
- allocations: true
- unit: "gb" # Allowed values- "gb", "mb", "tb", or "percent"
+ blacklist: [] # Add node IDs to exclude them from the status embed (e.g., blacklist: [1]).
+ details: true # Show node details such as memory and disk usage (true/false).
+ servers: false # Show server details (true/false).
+ allocations_as_max_servers: false # Show allocations as max servers (true/false).
+ host: false # Show node host (true/false).
+ unit: "byte" # Unit for node usage, Available types: "byte" or "percentage".
+ uptime: true # Enable or disable node uptime (true/false).
+ limit: 100 # Node limit for usage statistics display.
# Panel Users and Servers Settings
panel_settings:
- status: true # enable panel stats under nodes stats
- servers: true
- users: true
+ status: true # Display panel stats above node stats (true/false).
+ host: false # Show panel host (true/false).
+ uptime: true # Enable or disable node uptime (true/false).
+ servers: true # Display servers count (true/false).
+ users: true # Display users count (true/false).
-# 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
+# Notifier Configuration
+notifier:
+ enable: false # Enable or disable notifier.
+ webhook: "" # Discord Webhook URL for the notifier.
+ embed:
+ author:
+ name: "" # Author name for the notifier embed.
+ icon: "" # Icon URL for the author of the notifier embed.
+ timestamp: true # Include a timestamp in the notifier embed (true/false).
+ footer:
+ text: "PteroStats Notifier" # Footer text for notifier stats.
+ icon: "" # Footer icon URL.
+ thumbnail: "" # Thumbnail URL for the notifier stats embed.
+ image: "" # Image URL for the notifier stats embed.
-# 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
+# Error Logging Configuration
+# Enable logging to console if servers go offline, useful for debugging.
+log_error: false # Set to true to enable error logging.
\ No newline at end of file
diff --git a/events/ready.js b/events/ready.js
deleted file mode 100644
index 80091d2..0000000
--- a/events/ready.js
+++ /dev/null
@@ -1,92 +0,0 @@
-const { ActivityType } = require("discord.js");
-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"
- )
- );
- console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.yellow(
- "If some node is online but the embed is read as offline, please enable "
- ) +
- chalk.green("log_error") +
- chalk.yellow(
- " on config file and report it at https://discord.gg/zv6maQRah3"
- )
- );
-
- if (client.guilds.cache.size < 1) {
- return console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.red("Error! This bot is not on any discord servers")
- );
- }
-
- if (client.config.timeout < 1) {
- console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.red("Timeout cannot be less than 1 seconds!")
- );
- client.config.timeout = 1;
- }
-
- if (client.config.refresh >= 1 && client.config.refresh <= 10) {
- console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.red("Refresh Time below 10 seconds is not recommended!")
- );
- } else if (client.config.refresh < 1) {
- console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.red("Refresh Time cannot be less than 1 seconds!")
- );
- client.config.refresh = 10;
- }
-
- if (client.config.presence.text && client.config.presence.type) {
- switch (client.config.presence.type.toLowerCase()) {
- case "playing":
- client.config.presence.type = ActivityType.Playing;
- break;
- case "listening":
- client.config.presence.type = ActivityType.Listening;
- break;
- case "competing":
- client.config.presence.type = ActivityType.Competing;
- break;
- default:
- client.config.presence.type = ActivityType.Watching;
- }
-
- client.user.setActivity(client.config.presence.text, {
- type: client.config.presence.type,
- });
- }
-
- if (client.config.presence.status) {
- if (
- !["idle", "online", "dnd", "invisible"].includes(
- client.config.presence.status.toLowerCase()
- )
- )
- client.config.presence.status = "online";
-
- client.user.setStatus(client.config.presence.status);
- }
-
- checkStatus({ client: client });
-
- setInterval(async () => {
- await checkStatus({ client: client });
- }, client.config.refresh * 1000);
- },
-};
diff --git a/handlers/application.js b/handlers/application.js
new file mode 100644
index 0000000..3cbc9cf
--- /dev/null
+++ b/handlers/application.js
@@ -0,0 +1,265 @@
+require("dotenv").config();
+const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
+const fs = require("node:fs");
+const cliColor = require("cli-color");
+const path = require("node:path");
+const config = require("./configuration.js");
+const convertUnits = require("./convertUnits.js");
+const getStats = require("./getStats.js");
+const webhook = require("./webhook.js");
+const uptimeFormatter = require("./uptimeFormatter.js");
+
+module.exports = function App() {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green("Starting app..."));
+
+ const client = new Client({
+ intents: [GatewayIntentBits.Guilds]
+ });
+
+ async function startGetStatus() {
+ try {
+ const results = await getStats();
+ if (results.isPanelDown) webhook(
+ new EmbedBuilder()
+ .setTitle("Panel Online")
+ .setColor("57F287")
+ .setDescription(`Panel is back online`)
+ )
+ createMessage({
+ panel: true,
+ uptime: results.uptime,
+ nodes: results.nodes,
+ servers: results.servers,
+ users: results.users,
+ });
+ } catch (error) {
+ if (config.log_error) console.error(error)
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Panel is currently offline."));
+
+ fs.readFile(path.join(__dirname, "../cache.json"), (err, data) => {
+ if (err) {
+ createMessage({ cache: false, panel: false });
+ return console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Last cache was not found!"));
+ }
+
+ try {
+ const results = JSON.parse(data);
+ if (results.uptime) webhook(
+ new EmbedBuilder()
+ .setTitle("Panel Offline")
+ .setColor("ED4245")
+ .setDescription(`Panel is currently offline`)
+ );
+ results.uptime = false;
+ fs.writeFileSync("cache.json", JSON.stringify(results, null, 2), "utf8");
+ createMessage({
+ cache: true,
+ panel: false,
+ nodes: results.nodes,
+ servers: results.servers,
+ users: results.users,
+ });
+ } catch {
+ createMessage({ cache: false, panel: false });
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Something went wrong with cache data..."));
+ }
+ });
+ }
+ }
+
+ client.once("ready", () => {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`${cliColor.blueBright(client.user.tag)} is online!`));
+
+ if (config.presence.enable) {
+ if (config.presence.text && config.presence.type) {
+ switch (config.presence.type.toLowerCase()) {
+ case "playing":
+ config.presence.type = ActivityType.Playing;
+ break;
+ case "listening":
+ config.presence.type = ActivityType.Listening;
+ break;
+ case "competing":
+ config.presence.type = ActivityType.Competing;
+ break;
+ default:
+ config.presence.type = ActivityType.Watching;
+ }
+
+ client.user.setActivity(config.presence.text, {
+ type: config.presence.type,
+ });
+ }
+
+ if (config.presence.status) {
+ if (!["idle", "online", "dnd", "invisible"].includes(
+ config.presence.status.toLowerCase()
+ ))
+ config.presence.status = "online";
+
+ client.user.setStatus(config.presence.status);
+ }
+ }
+
+ startGetStatus();
+ });
+
+ async function createMessage({ cache, panel, uptime, nodes, servers, users }) {
+ let embed = new EmbedBuilder()
+ .setAuthor({
+ name: config.embed.nodes.author.name || null,
+ iconURL: config.embed.nodes.author.icon || null
+ })
+ .setDescription(config.embed.nodes.description || null)
+ .setTitle(config.embed.nodes.title || null)
+ .setColor(config.embed.nodes.color || null)
+ .setURL(config.embed.nodes.url || null)
+ .setThumbnail(config.embed.nodes.thumbnail || null)
+
+ let embeds = [embed];
+
+ if (config.nodes_settings.details) {
+ nodes.forEach((node, index) => {
+ if (index % 25 === 0 && index !== 0) {
+ embed = new EmbedBuilder().setColor(config.embed.nodes.color);
+ if (embeds.length < 9) embeds.push(embed);
+ }
+
+ embed.addFields({
+ name: `${node.attributes.name} - ${node.status ? config.status.online : config.status.offline}`,
+ value:
+ "```\n" +
+ (config.nodes_settings.host ? `Host : ${node.attributes.fqdn}\n` : "") +
+ `Memory : ${convertUnits(node.attributes.allocated_resources.memory, node.attributes.memory, config.nodes_settings.unit)}\n` +
+ `Disk : ${convertUnits(node.attributes.allocated_resources.disk, node.attributes.disk, config.nodes_settings.unit)}` +
+ (node.attributes?.allocated_resources?.cpu ? `\nCPU : ${node.attributes?.allocated_resources?.cpu || 0}%` : "") +
+ (config.nodes_settings.servers ? `\nServers: ${node.attributes.relationships.servers}${config.nodes_settings.allocations_as_max_servers ? ` / ${node.attributes.relationships.allocations}` : ""}` : "") +
+ (config.nodes_settings.uptime ? `\nUptime : ${node.uptime ? uptimeFormatter(Date.now() - node.uptime) : "N/A"}` : "") +
+ "```"
+ });
+ });
+ } else {
+ embeds[0].setDescription((embed.data.description ? (embed.data.description + "\n\n") : "") + nodes.map(node => `**${node.attributes.name}** - ${node.status ? config.status.online : config.status.offline}`).join("\n"));
+ }
+
+ let panelEmbed = new EmbedBuilder()
+ .setAuthor({
+ name: config.embed.panel.author.name || null,
+ iconURL: config.embed.panel.author.icon || null
+ })
+ .setColor(config.embed.panel.color || null)
+ .setTitle(config.embed.panel.title || null)
+ .setURL(config.embed.panel.url || null)
+ .setTimestamp(config.embed.panel.timestamp ? new Date() : null)
+ .setFooter({
+ text: config.embed.panel.footer.text || null,
+ iconURL: config.embed.panel.footer.icon || null
+ })
+ .setThumbnail(config.embed.panel.thumbnail || null)
+ .setImage(config.embed.panel.image || null)
+ .setDescription(
+ config.embed.panel.description
+ .replace(
+ "{{time}}",
+ time(new Date(Date.now() + (config.refresh * 1000) + 1000), "R")
+ ) || null
+ )
+ .addFields({
+ name: `Panel - ${panel ? config.status.online : config.status.offline}`,
+ value:
+ "```\n" +
+ (config.panel_settings.host ? `Host : ${new URL(process.env.PanelURL).host}\n` : "") +
+ `Nodes : ${nodes.length}\n` +
+ (config.panel_settings.servers ? `Servers: ${servers || "Unknown"}\n` : "") +
+ (config.panel_settings.users ? `Users : ${users || "Unknown"}\n` : "") +
+ (config.panel_settings.uptime ? `Uptime : ${uptime ? uptimeFormatter(Date.now() - uptime) : "N/A"}\n` : "") +
+ "```"
+ });
+
+ if (config.panel_settings.status) embeds.unshift(panelEmbed);
+
+ embeds[embeds.length - 1]
+ .setTimestamp(config.embed.nodes.timestamp ? new Date() : null)
+ .setFooter({
+ text: config.embed.nodes.footer.text || null,
+ iconURL: config.embed.nodes.footer.icon || null
+ })
+ .setImage(config.embed.nodes.image || null)
+
+ if (!cache && !panel) {
+ embeds[embeds.length - 1].setDescription(
+ embeds[embeds.length - 1].data.description ? embeds[embeds.length - 1].data.description + "\n\nThere are no nodes to be display!" : "There are no nodes to be display!"
+ );
+ }
+
+ const components = []
+
+ if (config.button.enable) {
+ for (const row of ["row1", "row2", "row3", "row4", "row5"]) {
+ const buttons = config.button[row]?.slice(0, 5).filter(button => button.label && button.url);
+
+ if (buttons && buttons.length > 0) {
+ components.push(
+ new ActionRowBuilder().addComponents(
+ buttons.map(button =>
+ new ButtonBuilder()
+ .setLabel(button.label)
+ .setURL(button.url)
+ .setStyle(ButtonStyle.Link)
+ )
+ )
+ );
+ }
+ }
+ }
+
+ try {
+ const channel = await client.channels.fetch(process.env?.DiscordChannel);
+ const messages = await channel.messages.fetch({ limit: 10 });
+ const botMessage = messages.find(msg => msg.author.id === client.user.id);
+
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.green(`Panel stats successfully posted to the ${cliColor.blueBright(channel.name)} channel!`));
+
+ setTimeout(() => startGetStatus(), config.refresh * 1000);
+
+ if (botMessage) {
+ await botMessage.edit({ content: config.message.content || null, embeds, components });
+ } else {
+ await channel.send({ content: config.message.content || null, embeds, components });
+ }
+ } catch (error) {
+ handleDiscordError(error);
+ }
+ }
+
+ function handleDiscordError(error) {
+ try {
+ if (error.rawError?.code === 429) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Error 429 | Your IP has been rate limited by either Discord or your website. If it's a rate limit with Discord, you must wait. If it's a issue with your website, consider whitelisting your server IP."));
+ } else if (error.rawError?.code === 403) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("FORBIDDEN | The channel ID you provided is incorrect. Please double check you have the right ID. If you're not sure, read our documentation: \n>>https://github.com/HirziDevs/PteroStats#getting-channel-id<<"));
+ } else if (error.code === "ENOTFOUND") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ENOTFOUND | DNS Error. Ensure your network connection and DNS server are functioning correctly."));
+ } else if (error.rawError?.code === 50001) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Your discord bot doesn't have access to see/send message/edit message in the channel!"));
+ } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]?._errors[0]?.code === "MAX_EMBED_SIZE_EXCEEDED") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Embed message limit exceeded! Please limit or decrease the nodes that need to be shown in the config!"));
+ } else if (error.rawError?.errors && Object?.values(error.rawError.errors)[0]?._errors[0]?.code) {
+ console.log(Object.values(error.rawError.errors)[0]._errors[0].message);
+ } else {
+ console.error(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error"), error);
+ }
+ process.exit();
+ } catch (err) {
+ console.error(error)
+ process.exit();
+ }
+ }
+
+ try {
+ client.login(process.env?.DiscordBotToken);
+ } catch {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Discord Error | Invalid Discord Bot Token! Make sure you have the correct token in the config!"));
+ process.exit();
+ }
+}
\ No newline at end of file
diff --git a/handlers/checkStatus.js b/handlers/checkStatus.js
deleted file mode 100644
index 5d4e915..0000000
--- a/handlers/checkStatus.js
+++ /dev/null
@@ -1,367 +0,0 @@
-const { EmbedBuilder } = require("discord.js");
-const axios = require("axios");
-const axiosRetry = require("axios-retry");
-const chalk = require("chalk");
-
-const postStatus = require("./postStatus");
-
-axiosRetry(axios, { retries: 5 });
-
-const EMBED_TITLE = "Node Loggin";
-const MENTION_DELETE_TIMEOUT = 1;
-
-module.exports = async ({ client }) => {
- function Embed({ node }) {
- return new EmbedBuilder()
- .setTitle(EMBED_TITLE)
- .setDescription("`" + node.name + "` is down!")
- .setFooter({ text: "Please see console for more details" })
- .setTimestamp()
- .setColor("ED4245");
- }
-
- if (client.config.channel.startsWith("Put")) {
- console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! Invalid Channel ID")
- );
-
- process.exit();
- } else if (client.config.panel.url.startsWith("Put")) {
- console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! Invalid Panel URL")
- );
-
- process.exit();
- } else if (client.config.panel.key.startsWith("Put")) {
- console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! Invalid Apikey")
- );
-
- process.exit();
- } else if (!client.config.panel.url.startsWith("http")) {
- console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! 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 embeds = [];
-
- const panel = {
- status: false,
- total_servers: -1,
- total_users: -1,
- };
-
- console.log(chalk.cyan("[PteroStats] ") + chalk.green("Getting nodes stats"));
-
- try {
- const users = await axios(
- client.config.panel.url + "/api/application/users",
- {
- method: "GET",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: "Bearer " + client.config.panel.key,
- },
- }
- );
-
- if (users?.status === 200 && users?.data) {
- const servers = await axios(
- client.config.panel.url + "/api/application/servers",
- {
- method: "GET",
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json",
- Authorization: "Bearer " + client.config.panel.key,
- },
- }
- );
-
- if (servers?.status === 200 && users?.data) {
- panel.total_users = users?.data?.meta?.pagination?.total || "ERROR";
- panel.total_servers = servers?.data.meta?.pagination?.total || "ERROR";
- panel.status = true;
-
- const res = await 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,
- },
- }
- );
-
- if (res?.status === 200 && res?.data?.data) {
- for (const node of res.data.data) {
- const data = await 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,
- },
- }
- );
-
- if (data?.status === 200 && data?.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,
- 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,
- };
-
- try {
- const stats = await 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,
- },
- }
- );
-
- if (stats?.status === 200 && stats?.data) {
- body.status = true;
- } else {
- body.status = false;
- }
-
- setTimeout(() => {
- if (!body?.status) {
- if (client.config.log_error)
- console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.yellow(
- "[Node: " + node.attributes.name + "] "
- ) +
- chalk.red("Timeout!")
- );
- embeds.push(Embed({ node: body }));
-
- body.status = false;
- }
- }, client.config.timeout * 1000);
- } catch (error) {
- if (client.config.log_error)
- console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.yellow("[Node: " + node.attributes.name + "] ") +
- chalk.red(error)
- );
-
- embeds.push(Embed({ node: body }));
-
- body.status = false;
- }
-
- nodes.push(body);
- } else {
- throw new Error(
- JSON.stringify({
- response: {
- status: data.status,
- },
- })
- );
- }
- }
-
- nodes.sort(function (a, b) {
- return a.id - b.id;
- });
-
- await postStatus({ client: client, panel: panel, nodes: nodes });
-
- if (
- (client.config.mentions.user.length > 0 ||
- client.config.mentions.role.length > 0) &&
- client.config.mentions.channel
- ) {
- if (
- Array.isArray(client.config.mentions.user) ||
- Array.isArray(client.config.mentions.role)
- ) {
- let mentions = "";
-
- for (const user of client.config.mentions.user) {
- if (!isNaN(Number(user))) {
- mentions += " <@" + user + ">";
- }
- }
-
- for (const role of client.config.mentions.role) {
- if (!isNaN(Number(role))) {
- mentions += " <@&" + role + ">";
- }
- }
-
- const channel = await client.channels.cache.get(
- client.config.mentions.channel
- );
-
- if (channel) {
- const messages = await channel.messages
- .fetch({ limit: 10 })
- .then((msg) =>
- msg
- .filter(
- (m) =>
- m.author.id === client.user.id &&
- m.embeds[0].data.title === EMBED_TITLE
- )
- .first()
- );
-
- if (messages) {
- for (const MsgEmbed of messages.embeds) {
- for (const [index, embed] of embeds.entries()) {
- if (
- MsgEmbed.data.description === embed.data.description
- ) {
- embeds.splice(index, 1);
- }
-
- for (const node of nodes) {
- if (
- MsgEmbed.data.description.startsWith(
- "`" + node.name
- ) &&
- node.status
- ) {
- await messages.delete();
- }
- }
- }
- }
- }
-
- if (embeds.length > 0) {
- await channel.send({ embeds: embeds });
- }
-
- await channel.send({ content: mentions }).then(async (msg) => {
- setTimeout(async () => {
- if (msg) {
- await msg.delete();
- }
- }, MENTION_DELETE_TIMEOUT * 1000);
- });
- }
- }
- }
- } else {
- throw new Error(
- JSON.stringify({
- response: {
- status: res.status,
- },
- })
- );
- }
- } else {
- throw new Error(
- JSON.stringify({
- response: {
- status: servers.status,
- },
- })
- );
- }
- } else {
- throw new Error(
- JSON.stringify({
- response: {
- status: users.status,
- },
- })
- );
- }
- } catch (error) {
- try {
- if (typeof error === "string") {
- error = JSON.parse(error);
- }
- } catch {}
-
- if (error?.response) {
- if (error.response?.status === 403) {
- console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! 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 (error.response?.status === 404) {
- console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! 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("Error! " + error));
- }
- } else {
- console.log(chalk.cyan("[PteroStats] ") + chalk.red("Error! " + error));
- }
- }
-};
diff --git a/handlers/configuration.js b/handlers/configuration.js
new file mode 100644
index 0000000..ccf1cce
--- /dev/null
+++ b/handlers/configuration.js
@@ -0,0 +1,28 @@
+const fs = require("node:fs");
+const yaml = require("js-yaml");
+const cliColor = require("cli-color");
+
+console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Loading configuration..."));
+
+let config = yaml.load(fs.readFileSync("./config.yml", "utf8"));
+if (fs.existsSync("config-dev.yml")) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Using development configuration..."));
+ config = yaml.load(fs.readFileSync("./config-dev.yml", "utf8"));
+}
+
+try {
+ const testURL = new URL(process.env?.PanelURL);
+ if (!testURL.protocol.startsWith("http")) throw new Error();
+} catch {
+ console.error('Config Error | Invalid URL Format! Example Correct URL: "https://panel.example.com"');
+ process.exit();
+}
+
+if (config.version !== 9) {
+ console.error('Config Error | Invalid config version! The config has been updated. Please get the new config format from: \n>> https://github.com/HirziDevs/PteroStats/blob/main/config.yml <<');
+ process.exit();
+}
+
+console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Configuration loaded"));
+
+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..6a116f1
--- /dev/null
+++ b/handlers/convertUnits.js
@@ -0,0 +1,15 @@
+const prettyBytes = require('prettier-bytes');
+
+module.exports = function convertUnits(value, max, unit) {
+ unit = unit.toUpperCase();
+ switch (unit) {
+ case 'PERCENTAGE':
+ case 'PERCENT':
+ const percentage = Math.floor((value / max) * 100);
+ return `${!percentage ? 0 : percentage}%`;
+ case 'BYTE':
+ return `${prettyBytes(value * 1000000)} / ${max === 0 ? "Unlimited" : prettyBytes(max * 1000000)}`;
+ default:
+ return `${value.toLocaleString()} ${unit}/${max === 0 ? "Unlimited" : `${max.toLocaleString()} ${unit}`}`;
+ }
+}
\ No newline at end of file
diff --git a/handlers/getNodeConfiguration.js b/handlers/getNodeConfiguration.js
new file mode 100644
index 0000000..c704d15
--- /dev/null
+++ b/handlers/getNodeConfiguration.js
@@ -0,0 +1,13 @@
+const config = require("./configuration.js");
+
+module.exports = async function getNodeConfiguration(id) {
+ return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/nodes/${id}/configuration`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${process.env?.PanelKEY}`
+ },
+ })
+ .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..869ee27
--- /dev/null
+++ b/handlers/getNodesDetails.js
@@ -0,0 +1,44 @@
+const cliColor = require("cli-color");
+const config = require("./configuration.js");
+const axios = require("axios");
+
+module.exports = async function getAllNodes() {
+ return axios(`${new URL(process.env?.PanelURL).origin}/api/application/nodes?include=servers,location,allocations`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${process.env?.PanelKEY}`
+ },
+ })
+ .then((res) => res.data.data.filter((node) => !config.nodes_settings.blacklist.includes(node.attributes.id)))
+ .catch((error) => {
+ if (error.code === "ENOTFOUND") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ENOTFOUND | DNS Error. Ensure your network connection and DNS server are functioning correctly."));
+ } 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..1a8aab5
--- /dev/null
+++ b/handlers/getServers.js
@@ -0,0 +1,15 @@
+const config = require("./configuration.js");
+const cliColor = require("cli-color");
+
+module.exports = async function getServers() {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel servers..."))
+ return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/servers`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${process.env?.PanelKEY}`
+ },
+ })
+ .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..58aa52a
--- /dev/null
+++ b/handlers/getStats.js
@@ -0,0 +1,92 @@
+const { EmbedBuilder } = require("discord.js");
+const fs = require("node:fs");
+const cliColor = require("cli-color");
+const path = require('node:path');
+const webhook = require("./webhook.js");
+const config = require("./configuration.js");
+const getNodesDetails = require("./getNodesDetails.js");
+const getNodeConfiguration = require("./getNodeConfiguration.js");
+const getWingsStatus = require("./getWingsStatus.js");
+const promiseTimeout = require("./promiseTimeout.js");
+const getServers = require("./getServers.js");
+const getUsers = require("./getUsers.js");
+
+module.exports = async function getStats() {
+ let cache = (() => {
+ try {
+ return JSON.parse(fs.readFileSync(path.join(__dirname, "../cache.json")))
+ } catch {
+ return false
+ }
+ })()
+
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel nodes..."))
+ const nodesStats = await getNodesDetails();
+ if (!nodesStats) throw new Error("Failed to get nodes attributes");
+
+ const statusPromises = nodesStats.slice(0, config.nodes_settings.limit).map(async (node) => {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Fetching ${cliColor.blueBright(node.attributes.name)} configuration...`))
+ const nodeConfig = await getNodeConfiguration(node.attributes.id);
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Checking ${cliColor.blueBright(node.attributes.name)} wings status...`))
+ const nodeStatus = await promiseTimeout(getWingsStatus(node, nodeConfig.token), config.timeout * 1000);
+
+ let nodeUptime = cache ? (() => {
+ return cache.nodes.find((n) => n.attributes.id === node.attributes.id)?.uptime || Date.now()
+ })() : Date.now()
+
+ if (!nodeUptime && nodeStatus) nodeUptime = Date.now()
+
+ if (!nodeStatus) {
+ nodeUptime = false
+ if (cache && cache.nodes.find((n) => n.attributes.id === node.attributes.id)?.status)
+ webhook(
+ new EmbedBuilder()
+ .setTitle("Node Offline")
+ .setColor("ED4245")
+ .setDescription(`Node \`${node.attributes.name}\` is currently offline`)
+ )
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node ${cliColor.blueBright(node.attributes.name)} is currently offline.`))
+ } else {
+ if (cache && !cache.nodes.find((n) => n.attributes.id === node.attributes.id)?.status)
+ webhook(
+ new EmbedBuilder()
+ .setTitle("Node Online")
+ .setColor("57F287")
+ .setDescription(`Node \`${node.attributes.name}\` is back online`)
+ )
+ }
+
+ return {
+ attributes: {
+ id: node.attributes.id,
+ name: node.attributes.name,
+ memory: node.attributes.memory,
+ disk: node.attributes.disk,
+ cpu: node.attributes.cpu,
+ fqdn: node.attributes.fqdn,
+ allocated_resources: node.attributes.allocated_resources,
+ relationships: {
+ allocations: node.attributes.relationships.allocations.data.length,
+ servers: node.attributes.relationships.servers.data.length
+ }
+ },
+ uptime: nodeUptime,
+ status: nodeStatus
+ };
+ });
+
+ const data = {
+ uptime: cache ? (() => {
+ return cache.uptime || Date.now()
+ })() : Date.now(),
+ servers: await getServers(),
+ users: await getUsers(),
+ nodes: await Promise.all(statusPromises),
+ isPanelDown: !cache.uptime,
+ 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..1b71b3d
--- /dev/null
+++ b/handlers/getUsers.js
@@ -0,0 +1,15 @@
+const config = require("./configuration.js");
+const cliColor = require("cli-color");
+
+module.exports = async function getUsers() {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Retrieving panel users..."))
+ return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/users`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${process.env?.PanelKEY}`
+ },
+ })
+ .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..68d5c10
--- /dev/null
+++ b/handlers/getWingsStatus.js
@@ -0,0 +1,17 @@
+const config = require("./configuration.js");
+
+module.exports = async function getWingsStatus(node, nodeToken) {
+ return fetch(`${node.attributes.scheme}://${node.attributes.fqdn}:${node.attributes.daemon_listen}/api/servers`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${nodeToken}`
+ },
+ })
+ .then((res) => res.json())
+ .then(() => true)
+ .catch((error) => {
+ if (config.log_error) console.error(error);
+ return false
+ })
+}
\ No newline at end of file
diff --git a/handlers/postStatus.js b/handlers/postStatus.js
deleted file mode 100644
index 22102f7..0000000
--- a/handlers/postStatus.js
+++ /dev/null
@@ -1,356 +0,0 @@
-const {
- EmbedBuilder,
- time,
- ActionRowBuilder,
- ButtonBuilder,
- ButtonStyle,
- AttachmentBuilder,
-} = require("discord.js");
-const chalk = require("chalk");
-
-const memorySizeConverter = require("../modules/memorySizeConverter");
-const percentageCalculator = require("../modules/percentageCalculator");
-
-const BUFFER_MS = 2000; //Added these extra milliseconds to prevent the stats timer from showing "1 or 2 seconds ago" before updating the stats.
-
-module.exports = async ({ client, panel, nodes }) => {
- const channel = await client.channels.cache.get(client.config.channel);
- if (!channel) {
- return console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! Invalid Channel ID")
- );
- }
-
- const files = [];
-
- const embed = new EmbedBuilder();
-
- let messages = await channel.messages
- .fetch({ limit: 10 })
- .then((msg) => msg.filter((m) => m.author.id === client.user.id).last());
-
- let text = "";
- let desc = "";
- let blacklist = 0;
- let content = null;
-
- if (!client.config.nodes_settings.blacklist) {
- client.config.nodes_settings.blacklist = [];
- }
-
- if (
- !Array.isArray(client.config.nodes_settings.blacklist) &&
- Number.isInteger(client.config.nodes_settings.blacklist)
- ) {
- client.config.nodes_settings.blacklist = [
- client.config.nodes_settings.blacklist,
- ];
- }
-
- if (client.guilds.cache.size < 1) {
- return console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.red("Error! This bot is not on any discord servers")
- );
- }
-
- if (messages && messages.embeds.length < 1) {
- messages.delete();
- messages = null;
- }
-
- 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();
-
- if (nodes?.length >= 1) {
- for (const data of nodes) {
- if (!client.config.nodes_settings.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_settings.unit.toLowerCase()) {
- case "percent":
- description =
- description +
- "\nMemory : " +
- percentageCalculator(data.memory_min, data.memory_max) +
- "\nDisk : " +
- percentageCalculator(data.disk_min, data.disk_max);
- break;
- default:
- description =
- description +
- "\nMemory : " +
- memorySizeConverter(data.memory_min, client.config.nodes_settings.unit.toLowerCase()) +
- " / " +
- memorySizeConverter(data.memory_max, client.config.nodes_settings.unit.toLowerCase()) +
- "\nDisk : " +
- memorySizeConverter(data.disk_min, client.config.nodes_settings.unit.toLowerCase()) +
- " / " +
- memorySizeConverter(data.disk_max, client.config.nodes_settings.unit.toLowerCase());
- }
-
- if (client.config.nodes_settings.servers) {
- description =
- description + "\nServers : " + data.total_servers.toLocaleString();
- }
-
- if (client.config.nodes_settings.location) {
- description = description + "\nLocation : " + data.location;
- }
-
- if (client.config.nodes_settings.allocations) {
- description =
- description +
- "\nAllocations : " +
- data.allocations.toLocaleString();
- }
-
- description = description + "\n```";
-
- if (client.config.nodes_settings.details) {
- text = text + "\n**" + title.replace(":", ":**") + "\n" + description;
- } else {
- text = text + "\n**" + title.replace(":", ":**");
- }
- } else {
- blacklist = blacklist + 1;
- if (nodes.length - client.config.nodes_settings.blacklist.length < 1) {
- text = "\nThere are no nodes to display";
- }
- }
- }
-
- const format = time(
- new Date(Date.now() + client.config.refresh * 1000 + BUFFER_MS),
- "R"
- );
-
- embed.setDescription(
- desc.replaceAll("{{time}}", format) +
- "\n**Nodes Stats [" +
- Math.floor(nodes.length - blacklist) +
- "]**" +
- text
- );
-
- const EmbedFields = [];
-
- if (client.config.panel_settings.status) {
- let stats =
- "**Status:** " +
- String(panel.status)
- .replace("true", client.config.status.online)
- .replace("false", client.config.status.offline) +
- "\n\n";
-
- if (client.config.panel_settings.users) {
- stats =
- stats +
- "Users: " +
- String(panel.total_users).replace("-1", "`Unknown`") +
- "\n";
- }
-
- if (client.config.panel_settings.servers) {
- stats =
- stats +
- "Servers: " +
- String(panel.total_servers).replace("-1", "`Unknown`");
- }
-
- EmbedFields.push({ name: "Panel Stats", value: stats });
- }
-
- if (
- client.config.embed.field.title &&
- client.config.embed.field.description
- ) {
- 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.setFields(EmbedFields); //try it and see
- }
-
- const row = [];
-
- if (client.config.button.enable) {
- const button = new ActionRowBuilder();
-
- if (
- client.config.button.btn1.label.length >= 1 &&
- client.config.button.btn1.url.length >= 1
- ) {
- 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 >= 1 &&
- client.config.button.btn2.url.length >= 1
- ) {
- 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 >= 1 &&
- client.config.button.btn3.url.length >= 1
- ) {
- 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 >= 1 &&
- client.config.button.btn4.url.length >= 1
- ) {
- 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 >= 1 &&
- client.config.button.btn5.url.length >= 1
- ) {
- button.addComponents(
- new ButtonBuilder()
- .setLabel(client.config.button.btn5.label)
- .setStyle(ButtonStyle.Link)
- .setURL(client.config.button.btn5.url)
- );
- }
-
- row.push(button);
- }
-
- try {
- if (!messages) {
- channel.send({
- content: content,
- embeds: [embed],
- components: row,
- files: files,
- });
- } else {
- messages.edit({
- content: content,
- embeds: [embed],
- components: row,
- files: files,
- });
- }
- } catch (error) {
- console.log(error);
- }
-
- console.log(chalk.cyan("[PteroStats] ") + chalk.green("Stats posted!"));
- } else {
- if (!messages) {
- text = "\nThere are no nodes to display.";
- } else {
- if (
- messages?.embeds?.length > 0 &&
- client.config.embed.title &&
- messages?.embeds[0]?.data?.title === client.config.embed.title
- ) {
- 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
- ] || 0;
- 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
- ] || 0;
- }
- }
- }
- }
-};
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/handlers/setup.js b/handlers/setup.js
new file mode 100644
index 0000000..37cbb13
--- /dev/null
+++ b/handlers/setup.js
@@ -0,0 +1,137 @@
+const axios = require("axios")
+const cliColor = require("cli-color")
+const { Client, GatewayIntentBits } = require("discord.js")
+const fs = require("fs")
+const application = require("./application.js");
+const readline = require('readline').createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+const questions = [
+ "Please enter your panel name: ",
+ "Please enter your panel URL: ",
+ "Please enter your panel API key: ",
+ "Please enter your bot token: ",
+ "Please enter your channel ID: "
+];
+
+const Question = {
+ panelName: 0,
+ panelUrl: 1,
+ panelApiKey: 2,
+ botToken: 3,
+ channelId: 4,
+}
+
+const answers = [];
+
+const isValidURL = (url) => {
+ try {
+ new URL(url);
+ return true;
+ } catch (error) {
+ return false;
+ }
+};
+
+module.exports = function Setup() {
+ console.log(cliColor.cyanBright("Welcome to PteroStats!"))
+ console.log(cliColor.yellow("Please fill in the following credentials to set up the app.\n "));
+
+ const askQuestion = (index) => {
+ if (index < questions.length) {
+ console.log(questions[index]);
+
+ readline.question('> ', answer => {
+ let isValid = true;
+
+ if (index === Question.panelUrl && !isValidURL(answer)) {
+ console.log(cliColor.redBright('❌ Invalid Panel URL. Please enter a valid URL. Example Correct URL: "https://panel.example.com"'));
+ isValid = false;
+ } else if (index === Question.panelApiKey && !/^(plcn_|ptlc_|peli_|ptla_)/.test(answer)) {
+ console.log(cliColor.redBright("❌ Invalid Panel API key. It must start with 'plcn_' or 'ptlc_'."));
+ isValid = false;
+ } else if (index === Question.channelId && !/^\d+$/.test(answer)) {
+ console.log(cliColor.redBright("❌ Invalid Channel ID. It must be a number."));
+ isValid = false;
+ }
+
+ if (index === Question.panelApiKey && /^(peli_|ptla_)/.test(answer)) console.log(cliColor.yellow("The use of Application API keys are deprecated, you should use Client API keys"));
+
+ if (isValid) {
+ answers.push(isValidURL(answer) ? new URL(answer).origin : answer);
+ askQuestion(index + 1);
+ } else {
+ askQuestion(index);
+ }
+ });
+ } else {
+ axios(`${new URL(answers[Question.panelUrl]).origin}/api/application/nodes?include=servers,location,allocations`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${answers[Question.panelApiKey]}`
+ },
+ }).then(() => {
+ console.log(" \n" + cliColor.green("✓ Valid Panel Credentials."));
+ const client = new Client({
+ intents: [GatewayIntentBits.Guilds]
+ })
+
+ client.login(answers[Question.botToken]).then(async () => {
+ console.log(cliColor.green("✓ Valid Discord Bot"));
+ client.channels.fetch(answers[Question.channelId]).then(() => {
+ console.log(cliColor.green("✓ Valid Discord Channel"));
+ fs.writeFileSync(".env", `PanelURL=${answers[Question.panelUrl]}\nPanelKEY=${answers[Question.panelApiKey]}\nDiscordBotToken=${answers[Question.botToken]}\nDiscordChannel=${answers[Question.channelId]}`, "utf8")
+ fs.writeFileSync("config.yml", fs.readFileSync("./config.yml", "utf8").replaceAll("Hosting Panel", answers[0]).replaceAll("https://panel.example.com", answers[1]), "utf-8")
+ console.log(" \n" + cliColor.green(`Configuration saved in ${cliColor.blueBright(".env")} and ${cliColor.blueBright("config.yml")}.\n `));
+
+ application()
+ }).catch(() => {
+ console.log(cliColor.redBright("❌ Invalid Channel ID."));
+ console.log(" \n" + cliColor.redBright("Please run the setup again and fill in the correct credentials."));
+ process.exit()
+ })
+ }).catch(() => {
+ console.log(cliColor.redBright("❌ Invalid Discord Bot Token."));
+ console.log(" \n" + cliColor.redBright("Please run the setup again and fill in the correct credentials."));
+ process.exit()
+ })
+ }).catch((error) => {
+ console.log(" \n" + cliColor.redBright("❌ Invalid Panel Credentials."));
+ if (error.code === "ENOTFOUND") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ENOTFOUND | DNS Error. Ensure your network connection and DNS server are functioning correctly."));
+ } else if (error.code === "ECONNREFUSED") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ECONNREFUSED | Connection refused. Ensure the panel is running and reachable."));
+ } else if (error.code === "ETIMEDOUT") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ETIMEDOUT | Connection timed out. The panel took too long to respond."));
+ } else if (error.code === "ECONNRESET") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("ECONNRESET | Connection reset by peer. The panel closed the connection unexpectedly."));
+ } else if (error.code === "EHOSTUNREACH") {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("EHOSTUNREACH | Host unreachable. The panel is down or not reachable."));
+ } else if (error.response) {
+ if (error.response.status === 401) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("401 | Unauthorized. Invalid Application Key or API Key doesn't have permission to perform this action."));
+ } else if (error.response.status === 403) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("403 | Forbidden. Invalid Application Key or API Key doesn't have permission to perform this action."));
+ } else if (error.response.status === 404) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("404 | Not Found. Invalid Panel URL or the Panel doesn't exist."));
+ } else if (error.response.status === 429) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("429 | Too Many Requests. You have sent too many requests in a given amount of time."));
+ } else if ([500, 502, 503, 504].includes(error.response.status)) {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("500 | Internal Server Error. This is an error with your panel, PteroStats is not the cause."));
+ } else {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`${error.response.status} | Unexpected error: ${error.response.statusText}`));
+ }
+ } else {
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Unexpected error: ${error.message}`));
+ }
+ console.log(" \n" + cliColor.redBright("Please run the setup again and fill in the correct credentials."));
+ process.exit()
+ })
+ }
+ };
+
+ askQuestion(0);
+}
\ No newline at end of file
diff --git a/handlers/uptimeFormatter.js b/handlers/uptimeFormatter.js
new file mode 100644
index 0000000..dd26d42
--- /dev/null
+++ b/handlers/uptimeFormatter.js
@@ -0,0 +1,13 @@
+module.exports = function uptimeFormatter(time) {
+ let text = []
+ const days = Math.floor(time / 86400000);
+ const hours = Math.floor(time / 3600000) % 24;
+ const minutes = Math.floor(time / 60000) % 60;
+ const seconds = Math.floor(time / 1000) % 60;
+ if (days > 0) text.push(`${days} days`)
+ if (hours > 0) text.push(`${hours} hours`)
+ if (minutes > 0) text.push(`${minutes} minutes`)
+ if (text.length > 0) text.push(`and ${seconds} seconds`)
+ else text.push(`${seconds} seconds`)
+ return text.join(", ").replace(", and", " and")
+}
diff --git a/handlers/webhook.js b/handlers/webhook.js
new file mode 100644
index 0000000..b1a79bf
--- /dev/null
+++ b/handlers/webhook.js
@@ -0,0 +1,33 @@
+const { WebhookClient, EmbedBuilder } = require("discord.js")
+const config = require("./configuration")
+const cliColor = require("cli-color")
+
+module.exports = function Webhook(embed) {
+ if (config.notifier.enable) {
+ try {
+ const webhook = new WebhookClient({
+ url: config.notifier.webhook
+ })
+ webhook.send({
+ embeds: [
+ new EmbedBuilder(embed.data)
+ .setAuthor({
+ name: config.notifier.embed.author.name || null,
+ iconURL: config.notifier.embed.author.icon || null
+ })
+ .setFooter({
+ text: config.notifier.embed.footer.text || null,
+ iconURL: config.notifier.embed.footer.icon || null
+ })
+ .setURL(config.notifier.embed.url || null)
+ .setTimestamp(config.notifier.embed.timestamp ? new Date() : null)
+ .setThumbnail(config.notifier.embed.thumbnail || null)
+ .setImage(config.notifier.embed.image || null)
+ ]
+ })
+ } catch (error) {
+ console.log(error)
+ console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("Invalid Webhook URL"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/index.js b/index.js
index 940587e..2d73992 100644
--- a/index.js
+++ b/index.js
@@ -1,52 +1,23 @@
-const intigrityCheck = require("./modules/intigrityCheck");
-
-if(!intigrityCheck()){
- return console.log("Intigrity check failed!");
-}
-
const fs = require("node:fs");
-const chalk = require("chalk");
-const yaml = require("js-yaml");
-const { Client, GatewayIntentBits } = require("discord.js");
-const client = new Client({ intents: [GatewayIntentBits.Guilds] });
+const cliColor = require("cli-color");
+const package = require("./package.json");
-client.config = yaml.load(fs.readFileSync("./config.yml", "utf8"));
+console.log(
+ ` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ \n` +
+ ` /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ \n` +
+ ` \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ \n` +
+ ` \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ \n` +
+ ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}`)}`
+);
-if (
- client.config.panel.adminkey ||
- client.config.resource ||
- client.config.message.image
-) {
- console.log(
- chalk.cyan("[PteroStats] ") +
- chalk.red(
- "You are using old config file, please update your config file at "
- ) +
- chalk.green(
- "https://github.com/HirziDevs/PteroStats/blob/main/config.yml"
- )
- );
- process.exit();
-}
+console.log(
+ ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors\n ` +
+ " \nDiscord: https://discord.znproject.my.id" +
+ " \n Source: https://github.com/HirziDevs/PteroStats" +
+ " \nLicense: https://github.com/Hirzidevs/PteroStats/blob/main/LICENSE" +
+ ` \n \n${package.description}\n `
+);
-if (client.config.token.startsWith("Put") || !client.config.token.length) {
- console.log(
- chalk.cyan("[PteroStats] ") + chalk.red("Error! Invalid Discord Bot Token")
- );
- process.exit();
-}
+if (!fs.existsSync(".env")) return require("./handlers/setup.js")();
-const eventFiles = fs
- .readdirSync("./events")
- .filter((file) => file.endsWith(".js"));
-
-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));
- }
-}
-
-client.login(client.config.token);
+require("./handlers/application.js")();
diff --git a/modules/intigrityCheck.js b/modules/intigrityCheck.js
deleted file mode 100644
index 93707ac..0000000
--- a/modules/intigrityCheck.js
+++ /dev/null
@@ -1,131 +0,0 @@
-const fs = require("node:fs");
-const child = require("node:child_process");
-const path = require("node:path");
-
-const REQUIRED_PACKAGES = [
- {
- name: "axios",
- version: "1.6.3",
- },
- {
- name: "axios-retry",
- version: "3.9.1",
- },
- {
- name: "chalk",
- version: "4.1.2",
- },
- {
- name: "discord.js",
- version: "14.14.1",
- },
- {
- name: "js-yaml",
- version: "4.1.0",
- },
-];
-const NODE_VERSION = 16;
-
-function InstallPackages() {
- console.log("Required nodejs packages not found!");
- console.log("Please wait... starting to install all required node packages.");
-
- let packagesList = "";
- for(const package of REQUIRED_PACKAGES){
- packagesList += `\n- ${package.name}@${package.version}`;
- }
- console.log("The following packages will be installed:", packagesList);
-
- console.log(
- "If the bot can't install the packages, please install them manually."
- );
-
- try {
- let packagesList = "";
-
- for (const package of REQUIRED_PACKAGES) {
- packagesList += ` ${package.name}@${package.version}`;
- }
-
- child.execSync(`npm i${packagesList}`);
- console.log('Install complete!, please run "node index" command again!');
-
- process.exit();
- } catch (err) {
- console.log("Error! ", err);
- console.log("Support Server: https://discord.gg/zv6maQRah3");
-
- process.exit();
- }
-}
-
-module.exports = () => {
- if (Number(process.version.split(".")[0]) < NODE_VERSION) {
- console.log(
- `Unsupported NodeJS Version!, Please use NodeJS ${NODE_VERSION}.x or higher.`
- );
-
- process.exit();
- }
-
- const nodeModulesFolderPath = path.join(__dirname, "../", "node_modules");
-
- if (fs.existsSync(nodeModulesFolderPath)) {
- let success = false;
- let errorMessage = "";
-
- for (const package of REQUIRED_PACKAGES) {
- const packageFilePath = path.join(
- __dirname,
- "../",
- "node_modules",
- package.name,
- "package.json"
- );
-
- if (fs.existsSync(packageFilePath)) {
- let packageFile = fs.readFileSync(packageFilePath, "utf-8");
-
- if (packageFile) {
- packageFile = JSON.parse(packageFile);
-
- if (packageFile.version !== package.version) {
- console.log(
- `Unsupported "${package.name}" version!.\nPlease delete your "node_modules" and "package-lock.json".\nAnd restart the bot.\nPlease make sure to check and remove "npm install" command from your startup params.`
- );
-
- process.exit();
- } else {
- success = true;
-
- continue;
- }
- } else {
- success = false;
-
- errorMessage = `Unknown package version- "${package.name}".`;
-
- break;
- }
- } else {
- success = false;
-
- errorMessage = `Missing package- "${package.name}".`;
-
- break;
- }
- }
-
- if (!success) {
- if (errorMessage) {
- console.log(errorMessage);
- }
-
- InstallPackages();
- }
- } else {
- InstallPackages();
- }
-
- return true;
-};
diff --git a/modules/memorySizeConverter.js b/modules/memorySizeConverter.js
deleted file mode 100644
index d20edce..0000000
--- a/modules/memorySizeConverter.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const SUPPORTED_TYPES = ["mb", "gb", "tb"];
-
-module.exports = (value, type) => {
- if (value) {
- value = parseInt(value);
-
- if (value > 0) {
- if (!type) {
- type = "mb";
- } else {
- type = type?.toLowerCase() || "mb";
-
- if (!SUPPORTED_TYPES.includes(type)) {
- type = "mb";
- }
- }
-
- let result = "";
-
- switch (type) {
- case "mb":
- result = value.toFixed(2).toLocaleString().replace(".00", "") + " MB";
- break;
- case "gb":
- result =
- (value / 1024).toFixed(2).toLocaleString().replace(".00", "") +
- " GB";
- break;
- case "tb":
- result =
- (value / (1024 * 1000))
- .toFixed(2)
- .toLocaleString()
- .replace(".00", "") + " TB";
- break;
- }
-
- return result;
- } else {
- return "0";
- }
- } else {
- return "0";
- }
-};
diff --git a/modules/percentageCalculator.js b/modules/percentageCalculator.js
deleted file mode 100644
index 7fa9462..0000000
--- a/modules/percentageCalculator.js
+++ /dev/null
@@ -1,16 +0,0 @@
-module.exports = (used, total) => {
- if (used && total) {
- used = parseInt(used);
- total = parseInt(total);
-
- if (used >= 1 && total >= 1) {
- let percentage = ((used / total) * 100).toFixed(2).toLocaleString().replace(".00", "") + " %";
-
- return percentage;
- } else {
- return "ERROR";
- }
- } else {
- return "ERROR";
- }
-};
diff --git a/package.json b/package.json
index 5aa4a68..647f718 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,26 @@
{
- "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.xyz",
- "bugs": {
- "email": "hirzidevs@gmail.com",
- "url": "https://github.com/HirziDevs/PteroStats/issues"
- },
- "scripts": {
- "start": "node index.js"
- },
- "dependencies": {
- "axios": "1.6.3",
- "axios-retry": "^3.9.1",
- "chalk": "^4.1.2",
- "discord.js": "^14.14.1",
- "js-yaml": "^4.1.0"
- },
- "engines": {
- "node": ">=16.9.0"
- }
-}
+ "name": "pterostats",
+ "version": "4.0.0",
+ "description": "PteroStats is a Discord App/Bot designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server.",
+ "license": "MIT",
+ "repository": "HirziDevs/PteroStats",
+ "homepage": "https://pterostats.znproject.my.id",
+ "bugs": {
+ "email": "hirzi@znproject.my.id",
+ "url": "https://github.com/HirziDevs/PteroStats/issues"
+ },
+ "scripts": {
+ "start": "node index.js"
+ },
+ "dependencies": {
+ "axios": "1.7.7",
+ "cli-color": "2.0.4",
+ "discord.js": "14.16.2",
+ "dotenv": "16.4.5",
+ "js-yaml": "4.1.0",
+ "prettier-bytes": "1.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+}
\ No newline at end of file
diff --git a/setup.js b/setup.js
new file mode 100644
index 0000000..6b026a6
--- /dev/null
+++ b/setup.js
@@ -0,0 +1,46 @@
+const fs = require("node:fs");
+const cliColor = require("cli-color");
+const package = require("./package.json");
+const readline = require('readline').createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+console.log(
+ ` _${cliColor.blueBright.bold(`${cliColor.underline("Ptero")}dact${cliColor.underline("yl & P")}eli${cliColor.underline("can")}`)}___ ______ ______ \n` +
+ ` /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ \n` +
+ ` \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ \n` +
+ ` \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ \n` +
+ ` \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold(`${package.version}`)}`
+);
+
+console.log(
+ ` \nCopyright © 2022 - ${new Date().getFullYear()} HirziDevs & Contributors\n ` +
+ " \nDiscord: https://discord.znproject.my.id" +
+ " \n Source: https://github.com/HirziDevs/PteroStats" +
+ " \nLicense: https://github.com/Hirzidevs/PteroStats/blob/main/LICENSE" +
+ ` \n \n${package.description}\n `
+);
+
+if (!fs.existsSync(".env")) return require("./handlers/setup.js")();
+
+console.log(cliColor.yellowBright(
+ "Configuration is already set. Please select one of the following options:\n \n" +
+ `${cliColor.cyanBright("1")} ${cliColor.blueBright("»")} Start the App\n` +
+ `${cliColor.cyanBright("2")} ${cliColor.blueBright("»")} Change configuration\n `
+));
+
+readline.question('> ', async (answer) => {
+ readline.close();
+
+ switch (answer) {
+ case '2':
+ require("./handlers/setup.js")();
+ break;
+ case '1':
+ require("./handlers/application.js")();
+ break;
+ default:
+ console.log(cliColor.redBright('Invalid input. Please type either 1 or 2.'));
+ }
+});