From d6b4ecb4bcea913265a2309101e43ab4ce627ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=A7=81Ash=C3=BB=EA=A7=82?= <30575805+Ashu11-A@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:30:58 -0400 Subject: [PATCH] Base: Pterodactyl Login --- settings.exemple.json | 2 +- src/classes/{ctrlPanel.ts => CtrlPanel.ts} | 0 src/classes/Pterodactyl.ts | 443 ++++++++++++++++++ src/classes/pterodactyl.ts | 215 --------- src/crons/SystemStatus.ts | 2 +- .../account/ctrlPanel/createAccount.ts | 2 +- .../account/pterodactyl/createAccount.ts | 2 +- .../payments/cart/cartCollectorModal.ts | 68 ++- .../product/functions/buttonsCollector.ts | 2 +- .../product/functions/selectCollector.ts | 2 +- src/interfaces/Payments.ts | 16 + 11 files changed, 532 insertions(+), 222 deletions(-) rename src/classes/{ctrlPanel.ts => CtrlPanel.ts} (100%) create mode 100644 src/classes/Pterodactyl.ts delete mode 100644 src/classes/pterodactyl.ts diff --git a/settings.exemple.json b/settings.exemple.json index 1f1cd416..994771ca 100644 --- a/settings.exemple.json +++ b/settings.exemple.json @@ -14,7 +14,7 @@ } }, "Auth": { - "username": "exemple", + "email": "exemple", "password": "exemple", "uuid": "exemple" }, diff --git a/src/classes/ctrlPanel.ts b/src/classes/CtrlPanel.ts similarity index 100% rename from src/classes/ctrlPanel.ts rename to src/classes/CtrlPanel.ts diff --git a/src/classes/Pterodactyl.ts b/src/classes/Pterodactyl.ts new file mode 100644 index 00000000..18edf7b6 --- /dev/null +++ b/src/classes/Pterodactyl.ts @@ -0,0 +1,443 @@ +import { core, db } from '@/app' +import { numerosParaLetras, updateProgressAndEstimation } from '@/functions' +import { type PaymentServerPtero, type PaymentUserPtero, type EggObject, type NestObject, type NodeConfigObject, type NodeObject, type Server, type UserObject } from '@/interfaces' +import axios, { type AxiosError, type AxiosInstance } from 'axios' + +export class Pterodactyl { + private readonly url + private readonly token + private readonly tokenUser + constructor (options: { + url: string + token: string + tokenUser?: string + }) { + this.url = options.url + this.token = options.token + this.tokenUser = options.tokenUser + } + + /** + * @return PendingRequest + */ + private client (): AxiosInstance { + const { token, url } = this + return axios.create({ + baseURL: `${url}/api`, + maxRedirects: 5, + headers: { + Accept: 'Application/vnd.pterodactyl.v1+json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + } + + /** + * @return PendingRequest + */ + private clientAdmin (): AxiosInstance { + const { url, tokenUser } = this + return axios.create({ + baseURL: `${url}/api`, + method: 'POST', + maxRedirects: 5, + headers: { + Accept: 'Application/vnd.pterodactyl.v1+json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${tokenUser}` + } + }) + } + + private showLog (): void { + core.info(`Requicição para ${this.url}`) + } + + public async getNests (): Promise { + try { + return await this.client().get('application/nests?per_page=999') + .then(async (res) => { + if (res.status === 200) { + return res.data.data as NestObject[] + } + }) + } catch (err) { + console.log(err) + } + } + + public async getEggs (nestId: string | number): Promise { + try { + return await this.client().get(`application/nests/${nestId}/eggs`) + .then(async (res) => { + return res.data.data as EggObject[] + }) + } catch (err) { + console.log(err) + } + } + + /** + * Obter detalhes de um egg expecifico + * Metodo: GET + */ + public async getEgg (options: { + nest: number | string + egg: number | string + }): Promise | undefined> { + const { egg, nest } = options + try { + return await this.client().get(`application/nests/${nest}/eggs/${egg}`) + .then((res) => { + return res.data as EggObject + }) + } catch (err) { + console.log(err) + if (axios.isAxiosError(err)) { + return err + } + } + } + + public async user (options: { + userId?: string | number + data?: { + email: string + username: string + first_name: string + last_name: string + password?: string + } + type: 'create' | 'update' | 'delete' | 'list' + }): Promise> { + try { + const { type, data, userId } = options + + switch (type) { + case 'list': + return await this.client().get('application/users') + .then((res) => res.data as UserObject) + case 'create': + return await this.client().post('application/users', data) + .then(async (res) => { + return res.data as UserObject + }) + case 'update': + return await this.client().patch(`application/users/${userId}`, data) + .then(async (res) => { + return res.data as UserObject + }) + case 'delete': + return await this.client().delete(`application/users/${userId}`) + .then(async (res) => { + return res.status + }) + } + } catch (err: any | Error | AxiosError) { + console.log(err) + if (axios.isAxiosError(err)) { + return err + } + } + } + + public async getNodes (): Promise> { + try { + return await this.client().get('application/nodes?include=servers,location,allocations') + .then(async (res) => { + return res.data.data as NodeObject[] + }) + } catch (err) { + console.log(err) + if (axios.isAxiosError(err)) { + return err + } + } + } + + public async getConfigNode (options: { + id: string | number + }): Promise | undefined> { + try { + return await this.client().get(`application/nodes/${options.id}/configuration`) + .then(async (res) => { + return res.data as NodeConfigObject + }) + } catch (err) { + console.log(err) + if (axios.isAxiosError(err)) { + return err + } + } + } + + /** + * Solicitação para a criação de servidores Pterodactyl + * Metodo: Post + */ + public async createServer (options: { + name: string + user: number + egg: number + docker_image: string + startup: string + environment: { + BUNGEE_VERSION: string + SERVER_JARFILE: string + limits: { + memory: number + swap: 0 + disk: number + io: 500 + cpu: number + } + feature_limits: { + databases: 0 + backups: 0 + } + allocation: { + default: number + } + } + }): Promise | undefined> { + try { + return await this.client().post('application/servers', options) + .then(async (res) => { + return res.data as Server + }) + } catch (err) { + console.log(err) + if (axios.isAxiosError(err)) { + return err + } + } + } + + /** + * Pesquisar um E-mail específico + */ + public async searchEmail (options: { + email: string + guildId: string + }): Promise<{ status: boolean, userData: any[] | undefined }> { + const { email, guildId } = options + + let metadata = await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_users`).get('metadata') + + if (metadata?.lastPage === undefined) { + metadata = await this.updateDatabase({ guildId, type: 'users' }) + } + + core.info(`Procurando: ${email}`) + let foundUsers: any[] = [] + + async function scan (): Promise<{ + status: boolean + userData: any[] | undefined + }> { + let status: { status: boolean, userData: any[] | undefined } = { status: false, userData: undefined } + for (let page = 1; page <= metadata.lastPage; page++) { + const dataDB = await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_users`).get(String(page)) + + if (Array.isArray(dataDB)) { + foundUsers = dataDB.filter( + (user: { email: string }) => user.email.toLowerCase() === email.toLowerCase() + ) + + if (foundUsers.length > 0) { + core.info(`Pesquisando: ${page}/${metadata.lastPage} | Encontrei`) + status = { status: true, userData: foundUsers } + break + } else { + core.info(`Pesquisando: ${page}/${metadata.lastPage} |`) + } + } else { + core.error('dataDB não é um array iterável.') + status = { status: false, userData: undefined } + break + } + + if (page === metadata.last_page) { + status = { status: false, userData: undefined } + break + } + } + return status + } + return await scan() + } + + private async updateDatabase (options: { + guildId: string + type: 'users' | 'servers' + }): Promise< + { lastPage: number, perPage: number, total: number } | + undefined> { + const { guildId, type } = options + const { url, token } = this + const usersData: PaymentUserPtero[] = [] + const serversData: PaymentServerPtero[] = [] + const startTime = Date.now() + let clientCount = 0 + let teamCount = 0 + + async function fetchUsers (urlAPI: string): Promise<{ lastPage: number, perPage: number, total: number } | undefined> { + try { + const response = await axios.get(urlAPI, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + }) + + const data = response.data + const users = data.data as PaymentUserPtero[] + + for (const user of users) { + const { id, name, email, root_admin } = user + usersData.push({ + id, + name, + email, + root_admin + }) + if (user.root_admin) { + teamCount++ + } else { + clientCount++ + } + } + + if (data.current_page <= data.last_page) { + const dataBD = await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_users`).get(String(data.current_page)) + if (dataBD?.length <= 50 || usersData?.length > 0) { + let isDataChanged = false + + for (let i = 0; i < 50; i++) { + if (usersData?.[i] !== undefined && i >= 0 && i < usersData.length) { + if ( + (dataBD?.[i] === undefined) || + (JSON.stringify(usersData?.[i]) !== JSON.stringify(dataBD?.[i])) + ) { + // Se houver diferenças, marque como dados alterados + isDataChanged = true + break + } + } + } + if (isDataChanged) { + core.info(`Tabela: ${data.current_page}/${data.last_page} | Mesclando`) + await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_users`).set(`${data.current_page}`, usersData) + } else { + core.info(`Tabela: ${data.current_page}/${data.last_page} | Sincronizado`) + } + + if (data.current_page % 2 === 0) { + const { progress, estimatedTimeRemaining } = updateProgressAndEstimation({ + totalTables: data.last_page, + currentTable: data.current_page, + startTime + }) + core.log(`Tabelas: ${data.current_page}/${data.last_page}`, `Users: ${data.from} - ${data.to} / ${data.total}`, `${progress.toFixed(2)}% | Tempo Restante: ${estimatedTimeRemaining.toFixed(2)}s`) + } + } + + if (data.current_page === data.last_page) { + const { last_page: lastPage, per_page: perPage, total } = data + const metadata = { + lastPage, + perPage, + total, + clientCount, + teamCount + } + await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_users`).set('metadata', metadata) + return metadata + } else if (data.next_page_url !== null) { + usersData.length = 0 + return await fetchUsers(data.next_page_url) + } + } + } catch (err) { + console.log(err) + } + } + + async function fetchServers (urlAPI: string): Promise<{ lastPage: number, perPage: number, total: number } | undefined> { + try { + const response = await axios.get(urlAPI, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + }) + + const data = response.data + const servers = data.data + + for (const server of servers) { + const { user: userId, suspended, created_at: createAt, name, identifier, id } = server + serversData.push({ + userId, + name, + identifier, + suspended, + createAt, + id + }) + } + + if (data.current_page <= data.last_page) { + const dataBD = await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_servers`).get(String(data.current_page)) + if (dataBD?.length <= 50 || serversData?.length > 0) { + let isDataChanged = false + + for (let i = 0; i < 50; i++) { + if (serversData?.[i] !== undefined && i >= 0 && i < serversData.length) { + if ( + (dataBD?.[i] === undefined) || + (JSON.stringify(serversData?.[i]) !== JSON.stringify(dataBD?.[i])) + ) { + isDataChanged = true + break + } + } + } + if (isDataChanged) { + core.info(`Tabela: ${data.current_page}/${data.last_page} | Mesclando`) + await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_servers`).set(`${data.current_page}`, serversData) + } else { + core.info(`Tabela: ${data.current_page}/${data.last_page} | Sincronizado`) + } + } + + if (data.current_page === data.last_page) { + const { last_page: lastPage, per_page: perPage, total } = data + const metadata = { + lastPage, + perPage, + total, + sincDate: Number(new Date()) + } + console.log(metadata) + await db.ctrlPanel.table(`${numerosParaLetras(guildId)}_servers`).set('metadata', metadata) + return metadata + } else if (data.next_page_url !== null) { + serversData.length = 0 + return await fetchServers(data.next_page_url) + } + } + } catch (err) { + console.log(err) + } + } + + // Iniciar o processo sincronizar os dados externos com os atuais + if (type === 'users') { + return await fetchUsers(`${url}/api/application/users?page=1`) + } else if (type === 'servers') { + return await fetchServers(`${url}/api/application/servers?page=1`) + } + } +} diff --git a/src/classes/pterodactyl.ts b/src/classes/pterodactyl.ts deleted file mode 100644 index a5a8622a..00000000 --- a/src/classes/pterodactyl.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { core } from '@/app' -import { type EggObject, type NestObject, type NodeConfigObject, type NodeObject, type Server, type UserObject } from '@/interfaces' -import axios, { type AxiosError, type AxiosInstance } from 'axios' - -export class Pterodactyl { - private readonly url - private readonly token - private readonly tokenUser - constructor (options: { - url: string - token: string - tokenUser?: string - }) { - this.url = options.url - this.token = options.token - this.tokenUser = options.tokenUser - } - - /** - * @return PendingRequest - */ - private client (): AxiosInstance { - const { token, url } = this - return axios.create({ - baseURL: `${url}/api`, - maxRedirects: 5, - headers: { - Accept: 'Application/vnd.pterodactyl.v1+json', - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - } - }) - } - - /** - * @return PendingRequest - */ - private clientAdmin (): AxiosInstance { - const { url, tokenUser } = this - return axios.create({ - baseURL: `${url}/api`, - method: 'POST', - maxRedirects: 5, - headers: { - Accept: 'Application/vnd.pterodactyl.v1+json', - 'Content-Type': 'application/json', - Authorization: `Bearer ${tokenUser}` - } - }) - } - - private showLog (): void { - core.info(`Requicição para ${this.url}`) - } - - public async getNests (): Promise { - try { - return await this.client().get('application/nests?per_page=999') - .then(async (res) => { - if (res.status === 200) { - return res.data.data as NestObject[] - } - }) - } catch (err) { - console.log(err) - } - } - - public async getEggs (nestId: string | number): Promise { - try { - return await this.client().get(`application/nests/${nestId}/eggs`) - .then(async (res) => { - return res.data.data as EggObject[] - }) - } catch (err) { - console.log(err) - } - } - - /** - * Obter detalhes de um egg expecifico - * Metodo: GET - */ - public async getEgg (options: { - nest: number | string - egg: number | string - }): Promise | undefined> { - const { egg, nest } = options - try { - return await this.client().get(`application/nests/${nest}/eggs/${egg}`) - .then((res) => { - return res.data as EggObject - }) - } catch (err) { - console.log(err) - if (axios.isAxiosError(err)) { - return err - } - } - } - - public async user (options: { - userId?: string | number - data?: { - email: string - username: string - first_name: string - last_name: string - password?: string - } - type: 'create' | 'update' | 'delete' | 'list' - }): Promise> { - try { - const { type, data, userId } = options - - switch (type) { - case 'list': - return await this.client().get('application/users') - .then((res) => res.data as UserObject) - case 'create': - return await this.client().post('application/users', data) - .then(async (res) => { - return res.data as UserObject - }) - case 'update': - return await this.client().patch(`application/users/${userId}`, data) - .then(async (res) => { - return res.data as UserObject - }) - case 'delete': - return await this.client().delete(`application/users/${userId}`) - .then(async (res) => { - return res.status - }) - } - } catch (err: any | Error | AxiosError) { - console.log(err) - if (axios.isAxiosError(err)) { - return err - } - } - } - - public async getNodes (): Promise> { - try { - return await this.client().get('application/nodes?include=servers,location,allocations') - .then(async (res) => { - return res.data.data as NodeObject[] - }) - } catch (err) { - console.log(err) - if (axios.isAxiosError(err)) { - return err - } - } - } - - public async getConfigNode (options: { - id: string | number - }): Promise | undefined> { - try { - return await this.client().get(`application/nodes/${options.id}/configuration`) - .then(async (res) => { - return res.data as NodeConfigObject - }) - } catch (err) { - console.log(err) - if (axios.isAxiosError(err)) { - return err - } - } - } - - /** - * Solicitação para a criação de servidores Pterodactyl - * Metodo: Post - */ - public async createServer (options: { - name: string - user: number - egg: number - docker_image: string - startup: string - environment: { - BUNGEE_VERSION: string - SERVER_JARFILE: string - limits: { - memory: number - swap: 0 - disk: number - io: 500 - cpu: number - } - feature_limits: { - databases: 0 - backups: 0 - } - allocation: { - default: number - } - } - }): Promise | undefined> { - try { - return await this.client().post('application/servers', options) - .then(async (res) => { - return res.data as Server - }) - } catch (err) { - console.log(err) - if (axios.isAxiosError(err)) { - return err - } - } - } -} diff --git a/src/crons/SystemStatus.ts b/src/crons/SystemStatus.ts index c6e682b5..4234c011 100644 --- a/src/crons/SystemStatus.ts +++ b/src/crons/SystemStatus.ts @@ -1,6 +1,6 @@ import { client, db } from '@/app' import { Crons } from '@/classes/Crons' -import { Pterodactyl } from '@/classes/pterodactyl' +import { Pterodactyl } from '@/classes/Pterodactyl' import axios from 'axios' import { EmbedBuilder, type TextChannel } from 'discord.js' diff --git a/src/discord/components/account/ctrlPanel/createAccount.ts b/src/discord/components/account/ctrlPanel/createAccount.ts index c3e0abd0..082a32e5 100644 --- a/src/discord/components/account/ctrlPanel/createAccount.ts +++ b/src/discord/components/account/ctrlPanel/createAccount.ts @@ -3,7 +3,7 @@ import { gen } from '@/functions' import { EmbedBuilder, type CacheType, type ModalSubmitInteraction } from 'discord.js' import { sendDM } from '../functions/sendDM' import { validator } from '../functions/validator' -import { CtrlPanel } from '@/classes/ctrlPanel' +import { CtrlPanel } from '@/classes/CtrlPanel' import { showError } from '../functions/showError' import { type UserData } from '@/interfaces' diff --git a/src/discord/components/account/pterodactyl/createAccount.ts b/src/discord/components/account/pterodactyl/createAccount.ts index d111385d..d75570ab 100644 --- a/src/discord/components/account/pterodactyl/createAccount.ts +++ b/src/discord/components/account/pterodactyl/createAccount.ts @@ -3,7 +3,7 @@ import { gen } from '@/functions' import { type ModalSubmitInteraction, type CacheType, EmbedBuilder } from 'discord.js' import { sendDM } from '../functions/sendDM' import { validator } from '../functions/validator' -import { Pterodactyl } from '@/classes/pterodactyl' +import { Pterodactyl } from '@/classes/Pterodactyl' import { type UserObject } from '@/interfaces' import { showError } from '../functions/showError' diff --git a/src/discord/components/payments/cart/cartCollectorModal.ts b/src/discord/components/payments/cart/cartCollectorModal.ts index b27b2aa9..f61a8550 100644 --- a/src/discord/components/payments/cart/cartCollectorModal.ts +++ b/src/discord/components/payments/cart/cartCollectorModal.ts @@ -4,7 +4,8 @@ import { validarEmail } from '@/functions' import { EmbedBuilder, codeBlock, type CacheType, type ModalSubmitInteraction } from 'discord.js' import { getModalData } from './functions/getModalData' import { PaymentFunction } from './functions/cartCollectorFunctions' -import { CtrlPanel } from '@/classes/ctrlPanel' +import { CtrlPanel } from '@/classes/CtrlPanel' +import { Pterodactyl } from '@/classes/Pterodactyl' export default async function cartCollectorModal (options: { interaction: ModalSubmitInteraction @@ -87,6 +88,71 @@ export default async function cartCollectorModal (options: { } return } + case 'Pterodactyl': { + if (dataInfo.email === undefined) return + const [validador, messageInfo] = validarEmail(dataInfo.email) + if (validador) { + core.info(`Solicitação para o E-mail: ${dataInfo.email}`) + const { tokenPanel, url } = await db.payments.get(`${guildId}.config.pterodactyl`) + + if (tokenPanel === undefined || url === undefined) { + await interaction.reply({ + ephemeral, + embeds: [ + new EmbedBuilder({ + title: '☹️ | Desculpe-me, mas o dono do servidor não configurou essa opção...' + }).setColor('Red') + ] + }) + return + } + + const msg = await interaction.reply({ + embeds: [ + new EmbedBuilder({ + title: 'Aguarde, estou consultando os seus dados...', + description: '(Isso pode levar 1 minuto caso sua conta seja nova)' + }).setColor('Yellow') + ] + }) + + const PterodactylBuilder = new Pterodactyl({ url, token: tokenPanel }) + const { status, userData } = await PterodactylBuilder.searchEmail({ email: dataInfo.email, guildId }) + + console.log(status, userData) + + await msg.edit({ + embeds: [ + new EmbedBuilder({ + title: (status && userData !== undefined) ? `👋 Olá ${userData[0].name}` : 'Desculpe-me, mas o E-mail informado não foi encontrado...', + description: (status && userData !== undefined) ? codeBlock(`Sabia que seu id é ${userData[0].id}?`) : undefined + }) + ] + }) + + if (userData !== undefined) { + await db.payments.set(`${guildId}.process.${channelId}.user`, userData[0]) + + if (message !== null) { + const PaymentBuilder = new PaymentFunction({ interaction, key }) + + await db.payments.set(`${guildId}.process.${channelId}.typeRedeem`, key) + await db.payments.set(`${guildId}.process.${channelId}.properties.${key}`, true) + await db.payments.delete(`${guildId}.process.${channelId}.properties.Pterodactyl`) + await db.payments.delete(`${guildId}.process.${channelId}.properties.DM`) + await PaymentBuilder.NextOrBefore({ type: 'next', update: 'No' }) + + const cartData = await db.payments.get(`${guildId}.process.${channelId}`) + const cartBuilder = new UpdateCart({ interaction, cartData }) + await interaction.deleteReply() + await cartBuilder.embedAndButtons({ message }) + } + } + } else { + await interaction.reply({ ephemeral, content: messageInfo }) + } + return + } case 'Cupom': { if (dataInfo.code === undefined) return const codeVerify = await db.payments.get(`${guildId}.cupons.${dataInfo.code.toLowerCase()}`) diff --git a/src/discord/components/payments/product/functions/buttonsCollector.ts b/src/discord/components/payments/product/functions/buttonsCollector.ts index 0679cf3c..2a42cbdd 100644 --- a/src/discord/components/payments/product/functions/buttonsCollector.ts +++ b/src/discord/components/payments/product/functions/buttonsCollector.ts @@ -5,7 +5,7 @@ import { type ModalSubmitInteraction, type CacheType, type ButtonInteraction, ty import { checkProduct } from '../../functions/checkConfig' import { type productData } from '@/interfaces' import { UpdateProduct } from './updateProduct' -import { Pterodactyl } from '@/classes/pterodactyl' +import { Pterodactyl } from '@/classes/Pterodactyl' import { validator } from '@/discord/components/account/functions/validator' interface ProductButtonType { diff --git a/src/discord/components/payments/product/functions/selectCollector.ts b/src/discord/components/payments/product/functions/selectCollector.ts index ab42e89d..da88612b 100644 --- a/src/discord/components/payments/product/functions/selectCollector.ts +++ b/src/discord/components/payments/product/functions/selectCollector.ts @@ -3,7 +3,7 @@ import { type StringSelectMenuInteraction, type CacheType, ActionRowBuilder, typ import { ProductButtonCollector } from './buttonsCollector' import { TextChannel } from 'discord.js' import { UpdateProduct } from './updateProduct' -import { Pterodactyl } from '@/classes/pterodactyl' +import { Pterodactyl } from '@/classes/Pterodactyl' import { validator } from '@/discord/components/account/functions/validator' interface ProductSeletcType { diff --git a/src/interfaces/Payments.ts b/src/interfaces/Payments.ts index 2ef67741..b8fdf70d 100644 --- a/src/interfaces/Payments.ts +++ b/src/interfaces/Payments.ts @@ -85,6 +85,22 @@ export interface PaymentUserCTRL { role: string } +export interface PaymentUserPtero { + id: number + name: string + email: string + root_admin: boolean +} + +export interface PaymentServerPtero { + id: number + userId: number + identifier: string + name: string + suspended: boolean + createAt: number +} + export interface PaymentServerCTRL { userId: number pterodactylId: number