diff --git a/.gitignore b/.gitignore index ecd64c4..2ef37d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ config-dev.yml package-lock.json -cache.json \ No newline at end of file +cache.json +.env \ No newline at end of file diff --git a/config.yml b/config.yml index b9f2ddf..81dd81d 100644 --- a/config.yml +++ b/config.yml @@ -1,8 +1,8 @@ # PteroStats config # If you need help, join our discord server here: https://discord.gg/zv6maQRah3 +version: 4 # Do not change this unless you know what you are doing! # Bot Configuration -token: "Put Bot Token Here" presence: enable: true text: "Hosting Panel" @@ -10,15 +10,9 @@ presence: status: "online" # can be 'online', 'idle', 'dnd', or 'invisible' # Discord Channel and Refresh Time Configuration -channel: "Put Channel ID Here" refresh: 10 # How much time the bot will refresh the stats timeout: 5 # How much time to wait for a node to respond to the bot (if you change this, it will add more time to refresh the stats) -# Panel Configuration -panel: - url: "Put Panel URL Here" - key: "Put Panel APIKEY Here" - # Message and Embed Configuration # set the option as '' if you want to disable it message: @@ -28,10 +22,10 @@ message: embed: title_nodes: "Nodes Stats" title_panel: "Panel Stats" - author: "PteroStats" + author: "Hosting Panel" color: "5865F2" description: "Next update {{time}}" # You can use {{time}} to make "in X seconds" time format - footer: "By Hirzi#8701" + footer: "By @HirziDevs" timestamp: true thumbnail: "" image: "" @@ -42,6 +36,8 @@ button: row1: - label: "Panel" url: "https://panel.example.com" +# - label: "Dashboard" +# url: "https://dash.example.com" # Status Message Configuration # How to use custom emoji: https://github.com/HirziDevs/PteroStats#using-custom-emoji diff --git a/handlers/config.js b/handlers/config.js index b444ac2..d781a78 100644 --- a/handlers/config.js +++ b/handlers/config.js @@ -3,6 +3,8 @@ 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...")) @@ -10,11 +12,18 @@ if (fs.existsSync("config-dev.yml")) { } try { - const testURL = new URL(config.panel.url); + 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 !== 4) { + console.error('Config Error | Invalid config version! The config has been updated, please get the new config from https://github.com/HirziDevs/PteroStats/blob/dev/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 index 287f257..d75d46c 100644 --- a/handlers/convertUnits.js +++ b/handlers/convertUnits.js @@ -12,7 +12,7 @@ module.exports = function convertUnits(value1, value2, unit) { return `${!percentage ? 0 : percentage}%`; } else if (units.includes(unit)) { const formatValue = (value) => Number.isInteger(value) ? value.toLocaleString() : value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); - + return `${formatValue(value1 * conversionFactors[unit])} ${unit}/${(value2 === 0 || value2 < 1) ? "Unlimited" : formatValue(value2 * conversionFactors[unit]) + " " + unit}`; } else { return `${value1.toLocaleString()} MB/${value2.toLocaleString()} MB`; diff --git a/handlers/getNodeConfiguration.js b/handlers/getNodeConfiguration.js index 00f2cdc..ebbb4b7 100644 --- a/handlers/getNodeConfiguration.js +++ b/handlers/getNodeConfiguration.js @@ -1,11 +1,11 @@ const config = require("./config.js"); module.exports = async function getNodeConfiguration(id) { - return fetch(`${new URL(config.panel.url).origin}/api/application/nodes/${id}/configuration`, { + return fetch(`${new URL(process.env?.PanelURL).origin}/api/application/nodes/${id}/configuration`, { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.json()) diff --git a/handlers/getNodesDetails.js b/handlers/getNodesDetails.js index 0966acd..a4f75e5 100644 --- a/handlers/getNodesDetails.js +++ b/handlers/getNodesDetails.js @@ -3,11 +3,11 @@ const config = require("./config.js"); const axios = require("axios"); module.exports = async function getAllNodes() { - return axios(`${new URL(config.panel.url).origin}/api/application/nodes?include=servers,location,allocations`, { + return axios(`${new URL(process.env?.PanelURL).origin}/api/application/nodes?include=servers,location,allocations`, { method: "GET", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.data.data.filter((node) => !config.nodes_settings.blacklist.includes(node.attributes.id))) diff --git a/handlers/getServers.js b/handlers/getServers.js index 18bcbc0..5e96b77 100644 --- a/handlers/getServers.js +++ b/handlers/getServers.js @@ -2,12 +2,12 @@ const config = require("./config.js"); const cliColor = require("cli-color"); module.exports = async function getServers() { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel servers...")) - return fetch(`${new URL(config.panel.url).origin}/api/application/servers`, { + 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 ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.json()) diff --git a/handlers/getStats.js b/handlers/getStats.js index 24e73e5..47d6549 100644 --- a/handlers/getStats.js +++ b/handlers/getStats.js @@ -9,18 +9,18 @@ const promiseTimeout = require("./promiseTimeout.js"); const cliColor = require("cli-color"); module.exports = async function getStats() { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel nodes...")) + 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(`Getting "${node.attributes.name}" configuration...`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Fetching "${node.attributes.name}" configuration...`)) const nodeConfig = await getNodeConfiguration(node.attributes.id); - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Getting "${node.attributes.name}" wings status...`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow(`Checking "${node.attributes.name}" wings status...`)) const nodeStatus = await promiseTimeout(getWingsStatus(node, nodeConfig.token), config.timeout * 1000); if (!nodeStatus) - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node "${node.attributes.name}" is offline`)) + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright(`Node "${node.attributes.name}" is currently offline`)) return { attributes: { name: node.attributes.name, diff --git a/handlers/getUsers.js b/handlers/getUsers.js index 55d72be..19e403d 100644 --- a/handlers/getUsers.js +++ b/handlers/getUsers.js @@ -2,12 +2,12 @@ const config = require("./config.js"); const cliColor = require("cli-color"); module.exports = async function getUsers() { - console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.yellow("Getting panel users...")) - return fetch(`${new URL(config.panel.url).origin}/api/application/users`, { + 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 ${config.panel.key}` + "Authorization": `Bearer ${process.env?.PanelKEY}` }, }) .then((res) => res.json()) diff --git a/handlers/installer.js b/handlers/installer.js new file mode 100644 index 0000000..4d2a6c0 --- /dev/null +++ b/handlers/installer.js @@ -0,0 +1,77 @@ +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); +const fs = require("fs") +const cliColor = require("cli-color") + +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 answers = []; + +const isValidURL = (url) => { + try { + new URL(url); + return true; + } catch (error) { + return false; + } +}; + +module.exports = function Installer() { + console.log("Welcome! Please fill this question to start PteroStats."); + + const askQuestion = (index) => { + if (index < questions.length) { + console.log(questions[index]); + + readline.question('> ', answer => { + let isValid = true; + + if (index === 1 && !isValidURL(answer)) { + console.log(cliColor.redBright("Invalid URL. Please enter a valid URL.")); + isValid = false; + } else if (index === 2 && !/^(plcn_|ptlc_|peli_|ptla_)/.test(answer)) { + console.log(cliColor.redBright("Invalid API key. It must start with 'plcn_' or 'ptlc_'.")); + isValid = false; + } else if (index === 4 && !/^\d+$/.test(answer)) { + console.log(cliColor.redBright("Invalid Channel ID. It must be a number.")); + isValid = false; + } + + if (index === 2 && /^(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 { + console.log(" \n====================================================") + console.log("Thank you! Here are the details you've provided:"); + console.log(`Panel Name : ${cliColor.cyanBright(answers[0])}`); + console.log(`Panel URL : ${cliColor.cyanBright(answers[1])}`); + console.log(`Panel API Key : ${cliColor.cyanBright(answers[2])}`); + console.log(`Discord Bot Token : ${cliColor.cyanBright(answers[3])}`); + console.log(`Discord Channel ID : ${cliColor.cyanBright(answers[4])}`); + console.log("====================================================\n \n") + + fs.writeFileSync(".env", `PanelURL=${answers[1]}\nPanelKEY=${answers[2]}\nDiscordBotToken=${answers[3]}\nDiscordChannel=${answers[4]}`, "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(cliColor.cyanBright("Please restart the bot to continue.")) + readline.close(); + } + }; + + askQuestion(0); +} \ No newline at end of file diff --git a/index.js b/index.js index e123485..4b356a1 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,30 @@ -const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType } = require("discord.js"); -const config = require("./handlers/config.js"); -const convertUnits = require("./handlers/convertUnits.js"); -const getStats = require("./handlers/getStats.js"); +const { Client, GatewayIntentBits, EmbedBuilder, time, ActivityType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const cachePath = require('node:path').join(__dirname, "./cache.json"); const fs = require("node:fs"); const cliColor = require("cli-color"); +console.log(` + _${cliColor.blueBright.bold("Pterodactyl & Pelican")}___ ______ ______ + /\\ ___\\ /\\__ _\\ /\\ __ \\ /\\__ _\\ /\\ ___\\ + \\ \\___ \\ \\/_ \\ \\/ \\ \\ \\_\\ \\ \\/_/\\ \\/ \\ \\___ \\ + \\/\\_____\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\/\\_____\\ + \\/_____/ \\/_/ \\/_/\\/_/ \\/_/ \\/_____/${cliColor.yellowBright.bold("4.0.0-dev")}`); + +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 \nPteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server. \n \n" +); + +if (!fs.existsSync(".env")) return require("./handlers/installer.js")() + +require("dotenv").config() +const config = require("./handlers/config.js"); +const convertUnits = require("./handlers/convertUnits.js"); +const getStats = require("./handlers/getStats.js"); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright("Starting bot...")); console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.redBright("You are using development build! some features may not work.")); @@ -141,7 +160,7 @@ async function createMessage({ cache, panel, nodes, servers, users }) { if (!cache && !panel) { embeds[embeds.length - 1].setDescription( - embeds[embeds.length - 1].data.description ? embeds[embeds.length - 1].data.description + "\n\nThere are no nodes to display!" : "There are no nodes to display!" + 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!" ); } @@ -159,10 +178,13 @@ async function createMessage({ cache, panel, nodes, servers, users }) { ) ) } + + 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.greenBright("Panel stats have been posted to the channel!")); + console.log(cliColor.cyanBright("[PteroStats] ") + cliColor.greenBright(`Panel stats successfuly posted to the "${channel.name}" channel!`)); setTimeout(() => startGetStatus(), config.refresh * 1000); @@ -192,7 +214,7 @@ function DiscordErrorHandler(error) { } try { - client.login(config.token); + 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(); diff --git a/package.json b/package.json index 988b7ad..f974cf6 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "pterostats", - "version": "v4.0.0", - "description": "PteroStats is a bot designed to check Pterodactyl Panel and Nodes status and post it to your discord server", + "version": "4.0.0", + "description": "PteroStats is a Discord Bot that designed to check Pterodactyl or Pelican Panel stats and post it to your Discord server.", "license": "MIT", "repository": "HirziDevs/PteroStats", - "homepage": "https://pterostats.hirzidevs.xyz", + "homepage": "https://pterostats.znproject.my.id", "bugs": { "email": "hirzidevs@gmail.com", "url": "https://github.com/HirziDevs/PteroStats/issues" @@ -13,12 +13,13 @@ "start": "node index.js" }, "dependencies": { - "axios": "^1.7.2", + "axios": "^1.7.7", "cli-color": "^2.0.4", - "discord.js": "^14.15.3", + "discord.js": "^14.16.1", + "dotenv": "^16.4.5", "js-yaml": "^4.1.0" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" } } \ No newline at end of file