Merge branch 'main' into main

This commit is contained in:
Nard Théo
2024-09-16 11:04:50 +02:00
committed by GitHub
26 changed files with 992 additions and 1348 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
node_modules/
config-dev.yml
package-lock.json
.vscode
node_modules
cache.json
.env

129
Indo.md
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/main/Indo.md) | [[Inggris]](https://github.com/HirziDevs/PteroStats/blob/main/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"/>
## 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)

191
README.md
View File

@@ -1,97 +1,102 @@
<div align="center">
<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"/>
# PteroStats
<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"/>
</div>
## Introduction
PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server
PteroStats is a Discord App/Bot designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server.
## Example
- Test Panel
## Preview
<img alt="PteroStats Image Preview" src="https://usercontent.catto.pictures/hirzi/e6f6fe6a-8c0e-4c7a-8b73-d4af752324f4.png" width="300"/>
<img alt="Example" src="https://i.imgur.com/fzQANo5.gif" width="200"/>
<img alt="PteroStats Console Preview" src="https://usercontent.catto.pictures/hirzi/8ce3aac6-5c46-4626-bd14-af994b602f8e.png" width="300"/>
## 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)
<img alt="PteroStats GIF Preview" src="https://usercontent.catto.pictures/hirzi/ad6e36cc-b582-460b-ab4e-b5e1dacd8b02.gif" width="300"/>
## 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.
<img alt="Setup" src="https://usercontent.catto.pictures/hirzi/b8645828-591d-4d52-b6d8-51f8df60440c.png" width="300"/>
- [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!
<img alt="Console Logging" src="https://usercontent.catto.pictures/hirzi/8ce3aac6-5c46-4626-bd14-af994b602f8e.png" width="300"/>
### 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.
<img alt="Change Configuration" src="https://usercontent.catto.pictures/hirzi/f61ebf43-3df8-4b86-93ac-166e2de1edcd.png" width="300"/>
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.
<img alt="Home" src="https://i.imgur.com/Uu97RJO.png" width="400"/>
1. Go to your `Pterodactyl` or `Pelican` Panel and navigate to the `Account Page`.
2. Click on the `API Credentials` button
<img alt="Home" src="https://usercontent.catto.pictures/hirzi/6d3e4c63-c5e8-4d94-9d78-07bb937b1dbd.png" width="400"/>
<img alt="Account Page" src="https://i.imgur.com/sm4THSu.png" width="400"/>
2. Click on the `API Credentials` button.
3. Fill the `Description` and click on the `Create` button
<img alt="Account Page" src="https://usercontent.catto.pictures/hirzi/0a2327ee-243a-4dd1-86f4-549f1ab8a91c.png" width="400"/>
<img alt="Create Client API Key" src="https://i.imgur.com/Q5E0PY4.png" width="400"/>
3. Fill in the `Description` and click the `Create` button.
<img alt="Create Client API Key" src="https://usercontent.catto.pictures/hirzi/7fcf5b7e-0087-4cf2-9e57-fed01292fd10.png" width="400"/>
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"/>
### 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
<img alt="Bot Config" src="https://usercontent.catto.pictures/hirzi/4eb4d5ff-6969-4461-b01d-c45888cfc994.png" width="400"/>
### Inviting Discord Bot
Please refer to [this website](https://discordjs.guide/preparations/adding-your-bot-to-servers.html)
<img alt="API Key" src="https://usercontent.catto.pictures/hirzi/267cf48a-7229-4bb6-8c77-7d0c0884f6ba.png" width="400"/>
### Getting Channel ID
1. Enable Developer Feature at your discord settings
1. Enable Developer Mode in your Discord settings.
<img alt="Discord User Settings" src="https://usercontent.catto.pictures/hirzi/36894499-b141-488f-98ed-40245c8f6862.png" width="400"/>
2. Right Click text channel and select `Copy ID`
2. Right-click the text channel and select `Copy ID`.
<img alt="Right Click Channel" src="https://usercontent.catto.pictures/hirzi/9f8352da-df5b-4587-9594-ced9b11a5507.png" width="250"/>
3. Paste the channel id at the config
<img alt="Channel Config" src="https://usercontent.catto.pictures/hirzi/b34cdbee-1e24-49f2-8219-efe0344a24f9.png" width="400"/>
### 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.
<img alt="Type \ on the chat" src="https://usercontent.catto.pictures/hirzi/2e3c821f-92f9-4b5c-863a-e020b2fbc426.png" width="350"/>
2. Select custom emoji you want
2. Select the custom emoji you want.
<img alt="Select Custom Emoji" src="https://usercontent.catto.pictures/hirzi/7c071727-2adb-4c8c-91d3-21664948a334.png" width="300"/>
@@ -99,30 +104,67 @@ if you need help contact me on discord `@hirzidevs` or join [our discord support
<img alt="Copy Emoji ID" src="https://usercontent.catto.pictures/hirzi/bd0084ac-f11b-413d-8b66-580efc011908.png" width="400"/>
4. Paste the emoji id at the config
4. Paste the emoji ID into the config.
<img alt="Status Config" src="https://usercontent.catto.pictures/hirzi/458ad1d6-019b-4b27-be60-3cbabfa07c06.png" width="400"/>
<img alt="Status Config" src="https://usercontent.catto.pictures/hirzi/369cf7af-ae32-4193-9b09-195ba6f71f62.png" width="400"/>
### Blacklist Nodes
1. Select node from node list on admin page
1. Select a node from the node list on the admin page.
<img alt="Nodes List" src="https://usercontent.catto.pictures/hirzi/5699fdbd-7c3c-4fa5-ae2c-d0ccb39cb69e.png" width="400"/>
<img alt="Pterodactyl Nodes List" src="https://usercontent.catto.pictures/hirzi/5699fdbd-7c3c-4fa5-ae2c-d0ccb39cb69e.png" width="400"/>
2. Check the url and copy the node id
<img alt="Pelican Nodes List" src="https://usercontent.catto.pictures/hirzi/5994fbf0-03ac-4196-9bb5-e945401f204e.png" width="400"/>
<img alt="Node Id" src="https://usercontent.catto.pictures/hirzi/45f855fc-6d96-4b23-a96e-892071189d01.png" width="400"/>
2. Check the URL and copy the node ID.
3. Paste the id to the blacklist on config
<img alt="Pterodactyl Node ID" src="https://usercontent.catto.pictures/hirzi/45f855fc-6d96-4b23-a96e-892071189d01.png" width="400"/>
<img alt="Blacklist Config" src="https://usercontent.catto.pictures/hirzi/cfb479bf-64da-43e5-b0d1-f7c0c78bf068.png" width="400"/>
<img alt="Pelican Node ID" src="https://usercontent.catto.pictures/hirzi/0ff8d9fc-6125-4fbb-8e19-ff8743cd365c.png" width="400"/>
You can add more than one node in the blacklist
3. Paste the ID into the blacklist in the config.
<img alt="Blacklist Config" src="https://usercontent.catto.pictures/hirzi/85b6a9b1-8ec9-4395-b5b1-6f85d3f52162.png" width="400"/>
<img alt="Blacklist Config" src="https://usercontent.catto.pictures/hirzi/bfae6a04-8dad-4db1-b3d8-05e6db691516.png" width="400"/>
## 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)
<img alt="Blacklist Config" src="https://usercontent.catto.pictures/hirzi/7b5d6c7f-54d9-40ea-b5a6-9192325ba2a0.png" width="400"/>
## Links
=======
### Notifier
Get a notification on Discord when your panel or specific nodes are currently down.
<img alt="Notifier Preview" src="https://usercontent.catto.pictures/hirzi/a2b8e36f-7448-4849-a14a-b1eb4ec8fb26.png" width="250"/>
#### Enabling Notifier
Open `config.yml` and set `enable` at the notifier configuration to `true`
<img alt="Notifier Config" src="https://usercontent.catto.pictures/hirzi/b4c3f1d0-e053-402c-8401-4de44926fce6.png" width="300"/>
#### Getting Discord Webhook URL
1. Go to the channel settings of the channel you want to set for the notifier.
<img alt="Notifier Config" src="https://usercontent.catto.pictures/hirzi/7d7712b9-d9ac-4650-83ac-21dc3f20c3fe.png" width="300"/>
2. Go to integrations and select `View Webhooks` or `Create Webhook`.
<img alt="Notifier Config" src="https://usercontent.catto.pictures/hirzi/e251f1e9-6b46-4051-be64-1945a6eaee33.png" width="300"/>
3. Create a new webhook and copy the Webhook URL
<img alt="Notifier Config" src="https://usercontent.catto.pictures/hirzi/e0af8410-527a-42e2-b284-48d7eb81456f.png" width="300"/>
4. Paste the Webhook URL on the webhook notifier configuration.
<img alt="Notifier Config" src="https://usercontent.catto.pictures/hirzi/b4ec26ad-e426-434e-b8c8-27ddc2916f5f.png" width="300"/>
> [!TIP]
> You can change the webhook icon and username on the webhook settings.
<img alt="Notifier Config" src="https://usercontent.catto.pictures/hirzi/2a4f7aba-9377-4722-bf19-3b7f0cc32772.png" width="300"/>
## 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)

View File

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

View File

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

265
handlers/application.js Normal file
View File

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

View File

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

28
handlers/configuration.js Normal file
View File

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

15
handlers/convertUnits.js Normal file
View File

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

View File

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

View File

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

15
handlers/getServers.js Normal file
View File

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

92
handlers/getStats.js Normal file
View File

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

15
handlers/getUsers.js Normal file
View File

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

View File

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

View File

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

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]);
}

137
handlers/setup.js Normal file
View File

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

View File

@@ -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")
}

33
handlers/webhook.js Normal file
View File

@@ -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"))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

46
setup.js Normal file
View File

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