From cf894afeca9369102746b4b76fe9d44f1c8d8176 Mon Sep 17 00:00:00 2001 From: Ashu11-A Date: Mon, 19 Feb 2024 21:29:45 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AB=20New=20Cron=20System?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 22 ++- package.json | 3 +- src/classes/Crons.ts | 168 ++++++++++++++++++ .../statusPtero => crons}/SystemStatus.ts | 28 +-- src/crons/authUpdate.ts | 34 ++++ src/crons/statusPresence.ts | 67 +++++++ src/discord/commands/configs/config.ts | 2 +- src/discord/events/ready/index.ts | 9 +- .../events/ready/statusPresence/index.ts | 60 ------- src/structural/Crons.ts | 37 ++++ 10 files changed, 346 insertions(+), 84 deletions(-) create mode 100644 src/classes/Crons.ts rename src/{discord/events/ready/statusPtero => crons}/SystemStatus.ts (92%) create mode 100644 src/crons/authUpdate.ts create mode 100644 src/crons/statusPresence.ts delete mode 100644 src/discord/events/ready/statusPresence/index.ts create mode 100644 src/structural/Crons.ts diff --git a/package-lock.json b/package-lock.json index b425ec5b..fd338324 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "chalk": "^4.1.2", "colors": "^1.4.0", "cors": "^2.8.5", - "discord.js": "^14.13.0", + "cron-parser": "^4.9.0", + "discord.js": "^14.14.1", "dotenv": "^16.3.1", "eslint": "^8.46.0", "express": "^4.18.2", @@ -2548,6 +2549,17 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5187,6 +5199,14 @@ "node": "14 || >=16.14" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-bytes.js": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.8.0.tgz", diff --git a/package.json b/package.json index e4d40753..9bafd088 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "chalk": "^4.1.2", "colors": "^1.4.0", "cors": "^2.8.5", - "discord.js": "^14.13.0", + "cron-parser": "^4.9.0", + "discord.js": "^14.14.1", "dotenv": "^16.3.1", "eslint": "^8.46.0", "express": "^4.18.2", diff --git a/src/classes/Crons.ts b/src/classes/Crons.ts new file mode 100644 index 00000000..1d1d0339 --- /dev/null +++ b/src/classes/Crons.ts @@ -0,0 +1,168 @@ +import colors from 'colors' +import { EventEmitter } from 'events' +import { randomUUID } from 'crypto' +import cronParser, { type CronExpression } from 'cron-parser' +import { core } from '@/app' + +/** + * Configuration object for defining recurring cron jobs. + */ +export interface CronsConfigurations { + /** + * Identifier for the Cron job. + */ + name: string + /** + * Cron argument, e.g., "* * * * * *" for every second. + */ + cron: string + /** + * Function to be executed when the cron job is triggered. + */ + // eslint-disable-next-line @typescript-eslint/method-signature-style + exec(cron: CronsConfigurations, interval: CronExpression): any + /** + * Metadata for the cron job, for specific information. + */ + metadata?: Metadata + /** + * Indicates whether the cron job should run only once. + */ + once?: boolean +} + +/** + * Configuration object for defining a unique cron job to run only once. + */ +export interface UniqueCron { + /** + * Identifier for the unique cron job. + */ + name: string + /** + * Cron argument, e.g., "* * * * * *" for every second. + */ + cron: string + /** + * Function to be executed when the unique cron job is triggered. + */ + // eslint-disable-next-line @typescript-eslint/method-signature-style + exec(cron: UniqueCron): any + /** + * Metadata for the unique cron job, for specific information. + */ + metadata?: MetaArgs +} + +/** + * Extended configuration interface for cron jobs, including a UUID. + */ +export interface CronsConfigurationsSystem extends CronsConfigurations { + uuid: string +} + +/** + * Class representing a collection of cron jobs. + */ +export class Crons { + /** + * Array containing all defined cron jobs. + */ + public static all: Array['data']['metadata']>> = [] + /** + * EventEmitter used for managing cron job events. + */ + public static set = new EventEmitter() + public static timeouts = new Map() + + /** + * Starts the specified cron job. + * @param cron - Configuration for the cron job. + */ + public static start (cron: CronsConfigurationsSystem['data']['metadata']>): void { + const interval = cronParser.parseExpression(cron.cron) + const nextScheduledTime = interval.next().getTime() + const currentTime = Date.now() + const delay = nextScheduledTime - currentTime + + // Updates the interval and schedules the next execution + this.timeouts.set(cron.uuid, setTimeout(() => { + Crons.set.emit(cron.uuid, cron, interval) + if (!(cron.once ?? false)) Crons.start(cron) + }, delay)) + } + + /** + * Configures unique cron jobs that run only once. + * @return Returns the setTimeout ID, which can be used for cancellation. + */ + public static once(cron: UniqueCron): NodeJS.Timeout { + const interval = cronParser.parseExpression(cron.cron) + const nextScheduledTime = interval.next().getTime() + const currentTime = Date.now() + const delay = nextScheduledTime - currentTime + + // Schedules the unique cron job and logs success + console.log(`| ${colors.green('Unique Cron')} - ${colors.blue(cron.name)} added successfully.`) + + return setTimeout(() => { + cron.exec(cron) + }, delay) + } + + /** + * Updates or adds a cron job. + * @param cron - Configuration for the cron job. + */ + public static post (cron: CronsConfigurationsSystem['data']['metadata']>): void { + if ((cron?.uuid).length > 0) { + const index = Crons.all.findIndex(c => c.uuid === cron.uuid) + if (index !== -1) { + if (cron === Crons.all[index]) return + Crons.all[index] = cron + console.log(`${colors.green('Crons')} - ${colors.blue(cron.name)} | updated successfully.`) + } + } else { + // Generates a new UUID if not provided + const newcron = { + ...cron, + uuid: randomUUID().replaceAll('-', '') + } + newcron.once = cron.once ?? false + Crons.all.push(newcron) + console.log(`${colors.green('Crons')} - ${colors.blue(cron.name)} | added successfully.`) + } + } + + /** + * Generates a cron expression for a specific date. + * @param date - Expiration date. + * @returns Returns the cron expression. + */ + public static date (date: Date): string { + const seconds = date.getSeconds() + const minutes = date.getMinutes() + const hours = date.getHours() + const dayOfMonth = date.getDate() + let month = date.getMonth() + 1 // Months start from zero in JavaScript + if (month === 13) { + month = 12 + } + return `${seconds} ${minutes} ${hours} ${dayOfMonth} ${month} *` + } + + /** + * Constructor for the Crons class. + * @param data - Configuration for the cron job. + */ + constructor (public data: CronsConfigurations) { + core.log(`${colors.green('Schedules')} - ${colors.blue(data.name)} | configured successfully.`) + + const cron = { + ...data, + uuid: randomUUID().replaceAll('-', '') + } + cron.once = data.once ?? false + Crons.all.push(cron as CronsConfigurationsSystem['data']['metadata']>) + } +} diff --git a/src/discord/events/ready/statusPtero/SystemStatus.ts b/src/crons/SystemStatus.ts similarity index 92% rename from src/discord/events/ready/statusPtero/SystemStatus.ts rename to src/crons/SystemStatus.ts index c4d07e14..c6e682b5 100644 --- a/src/discord/events/ready/statusPtero/SystemStatus.ts +++ b/src/crons/SystemStatus.ts @@ -1,8 +1,8 @@ -import { EmbedBuilder, type TextChannel } from 'discord.js' -import axios from 'axios' -import { setIntervalAsync } from 'set-interval-async' import { client, db } from '@/app' +import { Crons } from '@/classes/Crons' import { Pterodactyl } from '@/classes/pterodactyl' +import axios from 'axios' +import { EmbedBuilder, type TextChannel } from 'discord.js' interface NodeData { id: number @@ -49,6 +49,7 @@ export async function genEmbeds (options: { // Obtém as informações de estatísticas dos nós async function getNodeStats (): Promise { + if (token === undefined || url === undefined) return const pteroConect = new Pterodactyl({ token, url }) const nodesList = await pteroConect.getNodes() if (nodesList === undefined || axios.isAxiosError(nodesList)) return @@ -150,20 +151,21 @@ export async function genEmbeds (options: { return embeds } -export default async function statusPtero (): Promise { - const guilds = client.guilds.cache - - for (const [, guild] of guilds.entries()) { - const timeout = (await db.system.get(`${guild.id}.pterodactyl.timeout`)) ?? 15000 +new Crons({ + name: 'statusPtero', + cron: '*/15 * * * * *', + once: false, + async exec (cron, interval) { + if (interval === undefined) return + const guilds = client.guilds.cache - // Set an interval - setIntervalAsync(async () => { + for (const [, guild] of guilds.entries()) { const enabled = await db.system.get(`${guild.id}.status.PteroStatus`) if (enabled === false) return try { const embeds: EmbedBuilder[] = [] const now = new Date() - const futureTime = new Date(now.getTime() + parseInt(timeout)) + const futureTime = new Date(now.getTime() + 15000) const futureTimeString = `` embeds.push( @@ -195,6 +197,6 @@ export default async function statusPtero (): Promise { } catch (err) { console.log(err) } - }, parseInt(timeout)) + } } -} +}) diff --git a/src/crons/authUpdate.ts b/src/crons/authUpdate.ts new file mode 100644 index 00000000..13c2f7c6 --- /dev/null +++ b/src/crons/authUpdate.ts @@ -0,0 +1,34 @@ +import { core } from '@/app' +import { Crons } from '@/classes/Crons' +import { PaymentBot } from '@/classes/PaymentBot' +import getSettings from '@/functions/getSettings' +import internalDB from '@/settings/settings.json' + +new Crons({ + name: 'Auth - Start', + cron: '* * * * * *', + once: true, + async exec (cron, interval) { + if (interval === undefined) return + await authUpdate() + } +}) + +new Crons({ + name: 'Auth', + cron: '0 */1 * ? * *', + once: false, + async exec (cron, interval) { + if (interval === undefined) return + await authUpdate() + } +}) + +async function authUpdate (): Promise { + const { Auth } = getSettings() + if (Auth?.email === undefined || Auth.password === undefined || Auth.uuid === undefined) { core.warn('Sistema de autenticação não configurado'); return } + const PaymentAuth = new PaymentBot({ url: internalDB.API }) + + await PaymentAuth.login({ email: Auth.email, password: Auth.password }) + await PaymentAuth.validate({ uuid: Auth.uuid }) +} diff --git a/src/crons/statusPresence.ts b/src/crons/statusPresence.ts new file mode 100644 index 00000000..4e07d310 --- /dev/null +++ b/src/crons/statusPresence.ts @@ -0,0 +1,67 @@ +import { ActivityType, type PresenceStatusData } from 'discord.js' +import { client, db } from '@/app' +import axios from 'axios' +import { Crons } from '@/classes/Crons' + +new Crons({ + name: 'statusPresence', + cron: '*/30 * * * * *', + once: false, + async exec (cron, interval) { + if (interval === undefined) return + const guilds = client.guilds.cache + + for (const guild of guilds.values()) { + const enabled = await db.system.get(`${guild.id}.status.systemStatus`) + if (enabled !== undefined && enabled === false) return + + const type = (await db.system.get(`${guild.id}.status.systemStatusType`)) as PresenceStatusData + const typeStatus = await db.system.get(`${guild.id}.status.systemStatusMinecraft`) + + if (typeStatus !== undefined && typeStatus === true) { + const ip = await db.guilds.get(`${guild.id}.minecraft.ip`) + await mineStatus(ip, type) + } else if (typeStatus === undefined || typeStatus === false) { + const messages = await db.messages.get(`${guild.id}.system.status.messages`) + + if (messages?.[0] !== undefined) { + let currentMessage = await db.messages.get(`${guild.id}.system.status.currentMessage`) + + if (currentMessage >= messages?.length || currentMessage === undefined) { + currentMessage = 0 + await db.messages.set(`${guild.id}.system.status.currentMessage`, 0) + } + + const newStatus = messages[currentMessage] + client?.user?.setPresence({ + activities: [{ name: newStatus, type: ActivityType.Playing }], + status: type + }) + await db.messages.add(`${guild.id}.system.status.currentMessage`, 1) + } + } + } + } +}) + +async function mineStatus (ip: string, type: PresenceStatusData): Promise { + try { + const res = await axios.get(`https://api.mcsrvstat.us/3/${ip}`) + const formatRes = `${ip} | Status: ${ + res.data.online === true + ? `Online | Players: ${res.data.players.online ?? 0}/${res.data.players.max ?? 0}` + : 'Offline' + }` + + client?.user?.setPresence({ + activities: [{ name: formatRes, type: ActivityType.Playing }], + status: `${type ?? 'online'}` + }) + } catch (err) { + console.error(err) + client?.user?.setPresence({ + activities: [{ name: 'API Error', type: ActivityType.Playing }], + status: 'idle' + }) + } +} diff --git a/src/discord/commands/configs/config.ts b/src/discord/commands/configs/config.ts index ccc40783..2b77f345 100644 --- a/src/discord/commands/configs/config.ts +++ b/src/discord/commands/configs/config.ts @@ -1,9 +1,9 @@ import { db } from '@/app' +import { genEmbeds } from '@/crons/SystemStatus' import { Command } from '@/discord/base' import { setSystem } from '@/discord/commands/configs/utils/setSystem' import { MpModalconfig } from '@/discord/components/config/modals/mpModal' import { sendEmbed } from '@/discord/components/payments' -import { genEmbeds } from '@/discord/events/ready/statusPtero/SystemStatus' import { Database, validarURL } from '@/functions' import { CustomButtonBuilder, Discord } from '@/functions/Discord' import { diff --git a/src/discord/events/ready/index.ts b/src/discord/events/ready/index.ts index 51bff791..2a4edcc7 100644 --- a/src/discord/events/ready/index.ts +++ b/src/discord/events/ready/index.ts @@ -1,19 +1,12 @@ -import { setIntervalAsync } from 'set-interval-async/fixed' import { Event } from '@/discord/base' -import statusPresence from './statusPresence' import moduleExpress from '@/express/express' -import statusPtero from './statusPtero/SystemStatus' +import { StructuralCrons } from '@/structural/Crons' // import { telegramNotify } from './telegram' export default new Event({ name: 'ready', async run () { - await statusPresence() await moduleExpress() - await statusPtero() // await telegramNotify() - setIntervalAsync(async () => { - await statusPresence() - }, 15000) } }) diff --git a/src/discord/events/ready/statusPresence/index.ts b/src/discord/events/ready/statusPresence/index.ts deleted file mode 100644 index 97d4cfd2..00000000 --- a/src/discord/events/ready/statusPresence/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ActivityType, type PresenceStatusData } from 'discord.js' -import { client, db } from '@/app' -import axios from 'axios' - -export default async function statusPresence (): Promise { - const guilds = client.guilds.cache - - for (const guild of guilds.values()) { - const enabled = await db.system.get(`${guild.id}.status.systemStatus`) - if (enabled !== undefined && enabled === false) return - - const type = (await db.system.get(`${guild.id}.status.systemStatusType`)) as PresenceStatusData - const typeStatus = await db.system.get(`${guild.id}.status.systemStatusMinecraft`) - - if (typeStatus !== undefined && typeStatus === true) { - const ip = await db.guilds.get(`${guild.id}.minecraft.ip`) - await mineStatus(ip, type) - } else if (typeStatus === undefined || typeStatus === false) { - const messages = await db.messages.get(`${guild.id}.system.status.messages`) - - if (messages?.[0] !== undefined) { - let currentMessage = await db.messages.get(`${guild.id}.system.status.currentMessage`) - - if (currentMessage >= messages?.length || currentMessage === undefined) { - currentMessage = 0 - await db.messages.set(`${guild.id}.system.status.currentMessage`, 0) - } - - const newStatus = messages[currentMessage] - client?.user?.setPresence({ - activities: [{ name: newStatus, type: ActivityType.Playing }], - status: type - }) - await db.messages.add(`${guild.id}.system.status.currentMessage`, 1) - } - } - } -} - -async function mineStatus (ip: string, type: PresenceStatusData): Promise { - try { - const res = await axios.get(`https://api.mcsrvstat.us/3/${ip}`) - const formatRes = `${ip} | Status: ${ - res.data.online === true - ? `Online | Players: ${res.data.players.online ?? 0}/${res.data.players.max ?? 0}` - : 'Offline' - }` - - client?.user?.setPresence({ - activities: [{ name: formatRes, type: ActivityType.Playing }], - status: `${type ?? 'online'}` - }) - } catch (err) { - console.error(err) - client?.user?.setPresence({ - activities: [{ name: 'API Error', type: ActivityType.Playing }], - status: 'idle' - }) - } -} diff --git a/src/structural/Crons.ts b/src/structural/Crons.ts new file mode 100644 index 00000000..c24467c1 --- /dev/null +++ b/src/structural/Crons.ts @@ -0,0 +1,37 @@ +import { Crons } from '@/classes/Crons' +import { glob } from 'glob' +import path from 'path' + +/** + * Configuration Crons for panel + */ +export async function StructuralCrons (): Promise { + const CoreDIR = path.join(__dirname, '../') + console.log(CoreDIR) + const paths = await glob(['crons/**/*.{ts,js}'], { cwd: CoreDIR }) + + /** + * Organize Crons filter + */ + const customSort = (a: string, b: string): number => { + const partsA = a.split('/') + const partsB = b.split('/') + for (let i = 0; i < Math.min(partsA.length, partsB.length); i++) { + if (partsA[i] !== partsB[i]) { + return partsA[i].localeCompare(partsB[i]) + } + } + return partsA.length - partsB.length + } + + const sortedPaths = paths.sort(customSort) + + for (const pather of sortedPaths) { + await import(`${path.join(__dirname, '..', pather)}`) + } + + for (const isolated of Crons.all) { + Crons.set.on(isolated.uuid, isolated.exec)// create Cron Event + Crons.start(isolated) // Run Cron events + } +}