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 @@ -
- -PteroStats Banner - -## Bahasa / Language -[[Indonesia]](https://github.com/HirziDevs/PteroStats/blob/main/Indo.md) | [[Inggris]](https://github.com/HirziDevs/PteroStats/blob/main/README.md) - -
- -## Pengenalan -PteroStats adalah bot yang dirancang untuk memeriksa status panel pterodactyl dan dikirim ke server discord - -## Contoh -- Test Panel - - Example - -- [Calvs Cloud](https://discord.gg/ssCQjhrBJN) - - Calvs Cloud - -## Instalasi - 1. [Mendapatkan apikey dari pterodactyl](#mendapatkan-apikey-dari-pterodactyl) - 2. [Membuat Discord Bot](#membuat-discord-bot) - 3. [Menginvite Discord Bot](#menginvite-discord-bot) - 4. [Mendapatkan Channel ID](#mendapatkan-channel-id) - 5. [Memulai Bot](#memulai-bot) - - - [Mengunakan custom emoji](#mengunakan-custom-emoji) - - [Blacklist Nodes](#blacklist-nodes) - -### Mendapatkan apikey dari pterodactyl -1. Pergi ke `panel admin pterodactyl` dan pergi ke `Application API` - - Admin Panel - -2. Klik tombol `Create New` - - Application API Page - -3. Set semua permission ke `read` dan untuk description kamu bisa mengisi apa saja - - Create Application API - -4. Copy apikey-nya. - - Application API List - -5. Paste panel apikeynya dan panel urlnya di config - - Panel Config - -### Membuat Discord Bot -Kalian bisa cek [website ini](https://discordjs.guide/preparations/setting-up-a-bot-application.html) - -Paste bot tokennya di config - -Bot Config - -### Menginvite Discord Bot -Kalian bisa cek [website ini](https://discordjs.guide/preparations/adding-your-bot-to-servers.html) - -### Mendapatkan Channel ID -1. Aktifkan `Developer Mode` di settings discord kamu - - Discord User Settings - -2. Klik kanan teks channel dan pilih `Copy ID` - - Right Click Channel - -3. Paste id channelnya di config - - Channel Config - -### Memulai Bot -1. Pastikan kamu telah melakukan semua yang ada diatas -2. Jalankan command `npm install` di folder yang berisi file bot -3. Jalankan command `node index` dan kamu selesai! - -Jika kamu mendapat masalah bisa dm `Hirzi#8701` didiscord atau join [server support kami](https://discord.gg/zv6maQRah3) - -### Mengunakan custom emoji -1. ketik `\` di server yang ada custom emojinya - - Type \ on the chat - -2. Pilih custom emoji yang kamu mau - - Select Custom Emoji - -3. Copy textnya! - - Copy Emoji ID - -4. Paste id emojinya di config - - Status Config - -### Blacklist Nodes -1. Pilih node yang ada di node list admin page - - Nodes List - -2. Cek urlnya dan copy id nodenya - - Node Id - -3. Masukan ke blacklist di config - - Blacklist Config - -Kamu bisa memasukan lebih dari 1 node untuk di blacklist - -Blacklist Config - -## 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 @@
-PteroStats Banner +# PteroStats +PteroStats Banner
## 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 +PteroStats Image Preview - Example +PteroStats Console Preview -## 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) +PteroStats GIF Preview + +## 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. + Setup + + - [Getting Panel API Key](#getting-panel-api-key) + - [Getting a Channel ID](#getting-channel-id) + +6. Run `node index` if you want to start the app/bot again, and you're done! + + Console Logging + +### 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. + + 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. - Home +1. Go to your `Pterodactyl` or `Pelican` Panel and navigate to the `Account Page`. -2. Click on the `API Credentials` button + Home - Account Page +2. Click on the `API Credentials` button. -3. Fill the `Description` and click on the `Create` button + Account Page - Create Client API Key +3. Fill in the `Description` and click the `Create` button. + + Create Client API Key 4. Copy the API key. - API Key - -5. Paste the panel API key and panel url at the config - - Panel Config - -### Creating Discord Bot -Please refer to [this website](https://discordjs.guide/preparations/setting-up-a-bot-application.html) - -Paste the bot token at the config - -Bot Config - -### Inviting Discord Bot -Please refer to [this website](https://discordjs.guide/preparations/adding-your-bot-to-servers.html) + API Key ### Getting Channel ID -1. Enable Developer Feature at your discord settings +1. Enable Developer Mode in your Discord settings. Discord User Settings -2. Right Click text channel and select `Copy ID` +2. Right-click the text channel and select `Copy ID`. Right Click Channel -3. Paste the channel id at the config - - Channel Config - -### Starting bot -1. Make sure you have done the things above -2. Run `npm install` in the root directory of the bot files. -3. Run `node index` and you are done. - -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. Type \ on the chat -2. Select custom emoji you want +2. Select the custom emoji you want. Select Custom Emoji @@ -99,30 +104,67 @@ if you need help contact me on discord `@hirzidevs` or join [our discord support Copy Emoji ID -4. Paste the emoji id at the config +4. Paste the emoji ID into the config. - Status Config + Status Config ### Blacklist Nodes -1. Select node from node list on admin page +1. Select a node from the node list on the admin page. - Nodes List + Pterodactyl Nodes List -2. Check the url and copy the node id + Pelican Nodes List - Node Id +2. Check the URL and copy the node ID. -3. Paste the id to the blacklist on config + Pterodactyl Node ID - Blacklist Config + Pelican Node ID -You can add more than one node in the blacklist +3. Paste the ID into the blacklist in the config. -Blacklist Config + Blacklist 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) +Blacklist Config + +## Links +======= +### Notifier +Get a notification on Discord when your panel or specific nodes are currently down. + +Notifier Preview + + +#### Enabling Notifier +Open `config.yml` and set `enable` at the notifier configuration to `true` + +Notifier Config + +#### Getting Discord Webhook URL +1. Go to the channel settings of the channel you want to set for the notifier. + + Notifier Config + +2. Go to integrations and select `View Webhooks` or `Create Webhook`. + + Notifier Config + +3. Create a new webhook and copy the Webhook URL + + Notifier Config + +4. Paste the Webhook URL on the webhook notifier configuration. + + Notifier Config + + +> [!TIP] +> You can change the webhook icon and username on the webhook settings. + +Notifier Config ## 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.')); + } +});