diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 07bd06e..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "env": { - "node": true, - "es6": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2017, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "no-console": "off", - "no-unused-vars": "off", - "block-scoped-var": "error", - "curly": ["error", "multi-line"], - "no-empty-function": "off", - "no-labels": "error", - "no-lone-blocks": "error", - "no-useless-return": "off", - "semi": "error", - "eol-last": ["error", "always"], - "quotes": ["error", "single", { "allowTemplateLiterals": true, "avoidEscape": true }], - "object-curly-spacing": ["error", "always", { "arraysInObjects": true, "objectsInObjects": true }] - } -} diff --git a/.gitignore b/.gitignore index 67d954f..bc13ade 100644 --- a/.gitignore +++ b/.gitignore @@ -42,10 +42,6 @@ typings/ # Optional npm cache directory .npm -# eslint -.eslintcache -.eslintrc.json - # Optional REPL history .node_repl_history diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 7038806..28d296d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,7 @@ { "recommendations": [ "ms-azuretools.vscode-docker", - "dbaeumer.vscode-eslint", "gamunu.vscode-yarn", + "biomejs.biome", ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index b97a2de..ad83b87 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,9 +8,6 @@ "files.exclude": { "node_modules": true }, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, "npm.packageManager": "yarn", "files.eol": "\n", "editor.insertSpaces": true, diff --git a/Dockerfile b/Dockerfile index 218f1c3..2d0f5aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,28 @@ -FROM node:lts-alpine AS BUILD +FROM node:lts-alpine AS build WORKDIR /app RUN apk --no-cache add zip COPY . . -RUN yarn install --frozen-lockfile -RUN yarn env-gen -RUN yarn build -RUN yarn install --production -RUN zip -r app.zip ./node_modules ./build ./yarn.lock ./.env +RUN yarn install --frozen-lockfile \ + && yarn env-gen \ + && yarn build \ + && yarn install --production \ + && zip -r app.zip ./node_modules ./build ./yarn.lock ./.env ./entrypoint.sh # ------------------------------------------------------------ -FROM node:lts-alpine AS APP +FROM node:lts-alpine AS app WORKDIR /app RUN apk --no-cache add unzip -COPY --from=BUILD /app/app.zip . -RUN unzip app.zip && rm app.zip && mv ./build/* . && rm -rf ./build +COPY --from=build /app/app.zip . +RUN unzip app.zip \ + && rm app.zip \ + && mv ./build/* . \ + && rm -rf ./build \ + && chmod +x ./entrypoint.sh -CMD ["sh", "-c", "sleep 2 && node ./index.js"] +ENTRYPOINT ["./entrypoint.sh"] diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..a58e954 --- /dev/null +++ b/biome.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", + "organizeImports": { + "enabled": true + }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true, + "defaultBranch": "master" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "lineEnding": "lf" + } +} diff --git a/config.development.json b/config.development.json index 3533171..9377126 100644 --- a/config.development.json +++ b/config.development.json @@ -5,8 +5,8 @@ ], "minLevel": "debug", "includeTimestamp": true, - "prefix": "dab!", "botName": "Yoda Beta", + "botId": "703031563062870107", "communitymanagerRoleid": "1028257512383848478", "adminRoleid": "1028257512358674472", "delegatesRoleid": "1028257512358674468", @@ -143,8 +143,6 @@ "roleid": "1028257512312557591", "emote": "📱" }, - "ok_hand": "👌", - "warning": "⚠", "communicationServiceOptions": { "mailData": { "from": "mdpdevti@henallux.be", diff --git a/config.production.json b/config.production.json index b9fbcbb..ed82b8f 100644 --- a/config.production.json +++ b/config.production.json @@ -5,8 +5,8 @@ ], "minLevel": "info", "includeTimestamp": true, - "prefix": "da!", "botName": "Yoda", + "botId": "454768256364969985", "communitymanagerRoleid": "288659580064366592", "adminRoleid": "360850813914185738", "delegatesRoleid": "288659613732306944", @@ -151,8 +151,6 @@ "roleid": "364008970966269952", "emote": "📱" }, - "ok_hand": "👌", - "warning": "⚠", "communicationServiceOptions": { "mailData": { "from": "mdpdevti@henallux.be", diff --git a/docker-compose.yaml b/docker-compose.yaml index 78b6e6f..7c5cd91 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,7 +9,6 @@ x-restart-policy: &restart-policy restart: always -version: '3.8' services: database: <<: [*env-file, *network, *restart-policy] @@ -33,7 +32,7 @@ services: context: . dockerfile: ./Dockerfile volumes: - - bot_logs:/app/logs + - bot_logs:/var/log/datadrop/ depends_on: - database diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..bbf7c2a --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +echo "Waiting for PostgreSQL to be ready..." +until nc -z -v -w30 database 5432 +do + echo "Waiting for PostgreSQL..." + sleep 1 +done + +echo "PostgreSQL is up and running!" + +node ./index.js | tee -a /var/log/datadrop/console.log diff --git a/index.ts b/index.ts index ee3e9f8..f4179d8 100644 --- a/index.ts +++ b/index.ts @@ -1,29 +1,28 @@ -import * as dotenv from 'dotenv'; -import { GatewayIntentBits } from 'discord.js'; +import { GatewayIntentBits } from "discord.js"; +import * as dotenv from "dotenv"; -import { DatadropClient } from './src/datadrop.js'; -import { readConfig } from './src/config.js'; -import { Configuration } from './src/models/Configuration.js'; +import { readConfig } from "./src/config.js"; +import { DatadropClient } from "./src/datadrop.js"; +import type { Configuration } from "./src/models/Configuration.js"; -dotenv.config({ debug: Boolean(process.env.DEBUG) }); +dotenv.config({ debug: Boolean(process.env.DEBUG), encoding: "utf-8" }); let client: DatadropClient; readConfig() - .then( - async (config: Configuration) => { - client = new DatadropClient({ + .then(async (config: Configuration) => { + client = new DatadropClient( + { intents: [ + GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates, - GatewayIntentBits.Guilds, - GatewayIntentBits.MessageContent, - GatewayIntentBits.DirectMessages - ] - }, config); + GatewayIntentBits.GuildMessages, + GatewayIntentBits.DirectMessages, + ], + }, + config, + ); - await client.start(); - } - ) + await client.start(); + }) .catch(() => client?.stop()); diff --git a/package.json b/package.json index ddcdac1..6ae7ef5 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "datadrop", - "version": "1.11.1", + "version": "2.0.0", "type": "module", "main": "./build/index.js", "scripts": { "start": "docker-compose up --build", "stop": "docker-compose down", + "deploy:commands": "tsc && node ./scripts/deploy-commands.cjs", "build": "rm -rf build/ && tsc", - "lint": "eslint --fix --ext .ts .", + "lint": "biome check --write ./src index.ts ./scripts", "env-gen": "node ./scripts/envgen.cjs" }, "repository": { @@ -26,15 +27,14 @@ "@hunteroi/discord-temp-channels": "^3.3.0", "@hunteroi/discord-verification": "^1.5.0", "@sendgrid/mail": "8.1.3", + "discord-sync-commands": "^0.3.0", "discord.js": "^14.16.2", "dotenv": "^16.4.5", "ts-postgres": "1.3.0" }, "devDependencies": { + "@biomejs/biome": "^1.9.4", "@types/node": "^20.12.7", - "@typescript-eslint/eslint-plugin": "^7.6.0", - "@typescript-eslint/parser": "^7.6.0", - "eslint": "^9.0.0", "typescript": "^5.4.4" } } diff --git a/scripts/deploy-commands.cjs b/scripts/deploy-commands.cjs new file mode 100644 index 0000000..813b411 --- /dev/null +++ b/scripts/deploy-commands.cjs @@ -0,0 +1,120 @@ +const dotenv = require("dotenv"); +const { Client, Collection, REST, Routes } = require("discord.js"); +const path = require("node:path"); +const fsp = require("node:fs/promises"); +const synchronizeSlashCommands = require("discord-sync-commands"); +const { botId: botProdId } = require("../config.production.json"); +const { guildId, botId: botDevId } = require("../config.development.json"); + +async function deleteAllCommands(rest, applicationId, guildId) { + try { + if (guildId) { + console.log("Deleting all commands in the test guild..."); + console.log("Guild ID:", guildId); + await rest.put( + Routes.applicationGuildCommands(applicationId, guildId), + { body: [] }, + ); + console.log( + "Successfully deleted all application commands from the test guild.", + ); + } else { + console.log("Deleting all global commands..."); + await rest.put(Routes.applicationCommands(applicationId), { + body: [], + }); + console.log("Successfully deleted all application commands."); + } + } catch (error) { + console.error("Error deleting commands:", error); + } +} + +async function readFilesFrom(directory, callback) { + try { + const files = await fsp.readdir(directory); + for (const file of files) { + const filePath = path.join(directory, file); + const stats = await fsp.stat(filePath); + if (stats.isDirectory()) { + await readFilesFrom(filePath, callback); + } else if (stats.isFile() && file.endsWith(".js")) { + const props = await import(`file:///${filePath}`); + callback(file.replace(".js", ""), props.default); + } + } + } catch (error) { + console.error("Error reading files:", error); + } +} + +async function main(args) { + if (args.includes("--deleteAll")) { + const rest = new REST().setToken(process.env.DISCORD_TOKEN); + await deleteAllCommands( + rest, + applicationId, + args.includes("--guild") ? guildId : undefined, + ); + process.exit(0); + } + + if (args.includes("--guild")) { + console.log("Deploying commands to the test guild..."); + console.log("Guild ID:", guildId); + } + + const client = new Client({ intents: [] }); + client.once("ready", () => console.log("Ready!")); + client.once("error", console.error); + + const commands = new Collection(); + await readFilesFrom( + path.join(__dirname, "..", "build", "src", "commands"), + (commandName, command) => { + console.log(`Reading ${commandName}...`); + commands.set(commandName, command.data.toJSON()); + }, + ); + + const slashCommands = [...commands.values()]; + console.log("Total expected slash commands:", slashCommands.length); + + await client.login(process.env.DISCORD_TOKEN); + + await synchronizeSlashCommands(client, slashCommands, { + debug: true, + guildId: args.includes("--guild") ? guildId : undefined, + }); + + await client.destroy(); +} + +dotenv.config({ debug: Boolean(process.env.DEBUG), encoding: "utf-8" }); +if (!process.env.DISCORD_TOKEN) { + console.error( + "Error: Please provide a Discord token in the environment variable DISCORD_TOKEN.", + ); + process.exit(1); +} + +const args = process.argv.slice(2); +if (args.includes("--help")) { + console.log(` +Usage: yarn deploy:commands [options] +Options: + --help\t\tShow this help message + --guild\t\tDeploy commands to the test guild + --deleteAll\t\tDelete all deployed commands (pair with --guild to delete commands in the test guild) + --prod\t\tDeploy commands to the production bot (don't forget to use the prod token as well from .env) + `); + process.exit(0); +} + +let applicationId = botDevId; +if (args.includes("--prod")) { + console.log("Deploying commands to the production bot..."); + applicationId = botProdId; +} + +main(args).catch(console.error); diff --git a/scripts/envgen.cjs b/scripts/envgen.cjs index 1edc038..0fad551 100644 --- a/scripts/envgen.cjs +++ b/scripts/envgen.cjs @@ -1,5 +1,4 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const fs = require('fs/promises'); +const fs = require("node:fs/promises"); const path = `${__dirname}/..`; @@ -15,12 +14,20 @@ function extractEnvironmentName(envContent) { } async function asASingleVariable() { - const templatedEnv = await fs.readFile(`${path}/.env`, 'utf-8'); - const environment = (extractEnvironmentName(templatedEnv) || 'development').toLowerCase(); - const json = await fs.readFile(`${path}/config.${environment}.json`, 'utf-8'); + const templatedEnv = await fs.readFile(`${path}/.env`, "utf-8"); + const environment = ( + extractEnvironmentName(templatedEnv) || "development" + ).toLowerCase(); + const json = await fs.readFile( + `${path}/config.${environment}.json`, + "utf-8", + ); console.log(`Loading configuration for ${environment}`); - await fs.writeFile(`${path}/.env`, `CONFIG=${stringifyOnSingleLine(json)}\n${templatedEnv}`); + await fs.writeFile( + `${path}/.env`, + `CONFIG=${stringifyOnSingleLine(json)}\n${templatedEnv}`, + ); } asASingleVariable(); diff --git a/src/commands/admins/announce.ts b/src/commands/admins/announce.ts new file mode 100644 index 0000000..4dbeb43 --- /dev/null +++ b/src/commands/admins/announce.ts @@ -0,0 +1,120 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ChannelType, + type ChatInputCommandInteraction, + Colors, + EmbedBuilder, + InteractionContextType, + PermissionFlagsBits, + SlashCommandBuilder, + type TextChannel, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import type { Command } from "../../models/Command.js"; + +export default { + data: new SlashCommandBuilder() + .setName("announce") + .setDescription( + "Envoie une annonce dans le canal des annonces en mentionnant le rĂŽle des annonces.", + ) + .addStringOption((option) => + option + .setName("message") + .setDescription("Le contenu de l'annonce Ă  envoyer.") + .setRequired(true), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setContexts(InteractionContextType.Guild), + + async execute( + client: DatadropClient, + interaction: ChatInputCommandInteraction, + ) { + const content = interaction.options.getString("message", true); + const embed = new EmbedBuilder() + .setAuthor({ + name: "Annonce", + iconURL: "https://i.imgur.com/zcGyun6.png", + }) + .setColor(Colors.Orange) + .setDescription(content) + .setFooter({ + text: interaction.user.displayName, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ac-cancel-${interaction.user.id}`) + .setLabel("Non") + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ac-confirm-${interaction.user.id}`) + .setLabel("Oui") + .setStyle(ButtonStyle.Danger), + ); + + const response = await interaction.reply({ + content: + "Ceci est une preview de l'annonce. Voulez-vous l'envoyer?", + components: [row], + embeds: [embed], + ephemeral: true, + }); + + try { + const { announce } = client.config; + const confirmation = await response.awaitMessageComponent({ + filter: (i) => i.user.id === interaction.user.id, + time: 30_000, + }); + + if (confirmation.customId.startsWith("ac-confirm")) { + const annoncesChannel = await interaction.guild?.channels.fetch( + announce.channelid, + ); + if (!annoncesChannel) { + await confirmation.update({ + content: + "❌ **Oups!** - Le canal des annonces est introuvable.", + components: [], + }); + return; + } + + const annoncesRole = await interaction.guild?.roles.fetch( + announce.roleid, + ); + const announceMessage = await ( + annoncesChannel as TextChannel + ).send({ + content: annoncesRole?.toString(), + embeds: [embed], + }); + if (annoncesChannel.type === ChannelType.GuildAnnouncement) { + await announceMessage.crosspost(); + } + + await confirmation.update({ + content: "✅ Annonce envoyĂ©e!", + components: [], + }); + } else if (confirmation.customId.startsWith("ac-cancel")) { + await confirmation.update({ + content: "❌ Annonce annulĂ©e!", + components: [], + }); + } + } catch (e) { + await interaction.editReply({ + content: + "❌ **Oups!** - Aucune confirmation reçue, annulation...", + components: [], + }); + } + }, +} as Command; diff --git a/src/commands/announce.ts b/src/commands/announce.ts deleted file mode 100644 index 2831fe3..0000000 --- a/src/commands/announce.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ChannelType, Colors, EmbedBuilder, Message, TextChannel } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -export default { - name: 'announce', - aliases: ['annonce'], - description: - 'Envoie une annonce dans le canal des annonces en mentionnant le rĂŽle des annonces', - args: true, - usage: '', - guildOnly: true, - adminOnly: true, - - async execute(client: DatadropClient, message: Message, args: string[]) { - if (!message.inGuild()) return; - - const { announce } = client.config; - - const embed = new EmbedBuilder() - .setAuthor({ - name: 'Annonce', - iconURL: 'https://i.imgur.com/zcGyun6.png' - }) - .setColor(Colors.Orange) - .setDescription(args.join(' ')) - .setFooter({ - text: message.member!.user.tag, - iconURL: message.member!.user.displayAvatarURL() - }) - .setTimestamp(); - - await message.reply({ - content: "Ceci est une preview de l'annonce.Tapez 'yes' pour l'envoyer ou tout autre chose pour annuler.", - embeds: [embed] - }); - const collectedMessages = await message.channel.awaitMessages({ - filter: (m: Message) => m.author.id === message.author.id, - max: 1, - time: 30000 - }); - const confirmation = collectedMessages?.first(); - if (confirmation && confirmation.content.toLowerCase() === 'yes') { - const annoncesChannel = await message.guild.channels.fetch(announce.channelid); - if (!annoncesChannel) return; - - const annoncesRole = await message.guild.roles.fetch(announce.roleid); - const announceMessage = await (annoncesChannel as TextChannel).send({ - content: annoncesRole?.toString(), - embeds: [embed] - }); - if (annoncesChannel.type === ChannelType.GuildAnnouncement) { - await announceMessage.crosspost(); - } - - await message.channel.send('Annonce envoyĂ©e!'); - } else { - await message.channel.send('Annonce annulĂ©e!'); - } - }, -}; diff --git a/src/commands/email.ts b/src/commands/email.ts deleted file mode 100644 index fe8fbde..0000000 --- a/src/commands/email.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { EmbedBuilder, Message } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -const people = [ - { - name: 'Conseil Étudiant', - emails: ['cehenallux@gmail.com'], - }, - { - name: 'ComitĂ© IODA', - emails: ['ighenallux@gmail.com'], - }, -]; - -export default { - name: 'email', - aliases: ['emails'], - description: - "Affiche un message concernant l'envoi d'email Ă  un.e professeur.e ou aux organisations Ă©tudiantes.", - - async execute(client: DatadropClient, message: Message, args: string[]) { - const embed = new EmbedBuilder() - .setTitle(`Emails`) - .setColor('Random') - .setDescription( - `Si tu souhaites envoyer un email Ă  un.e professeur.e, Outlook possĂšde un systĂšme de recherche automatique dans l'annuaire de l'HĂ©nallux! Tape donc simplement son nom/son prĂ©nom et Outlook fera le reste!\n\nPour ce qui est des contacts Ă©tudiants, voici leurs emails!` - ); - embed.addFields( - people.map(p => ({ name: p.name, value: p.emails.join(', '), inline: true })) - ); - - if (message.channel.isSendable()) - await message.channel.send({ embeds: [embed] }); - } -}; diff --git a/src/commands/eval.ts b/src/commands/eval.ts deleted file mode 100644 index 6c2d967..0000000 --- a/src/commands/eval.ts +++ /dev/null @@ -1,36 +0,0 @@ -ï»żimport { Message, codeBlock } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; -import { clean } from '../helpers.js'; - -export default { - name: 'eval', - description: 'Évalue du code Javascript.', - ownerOnly: true, - args: true, - - async execute(client: DatadropClient, message: Message, args: string[]) { - const { config } = client; - - // double check sur l'identitĂ© juste pour la sĂ©curitĂ© - if (!config.ownerIds.includes(message.author.id)) return; - - let content = ''; - try { - const code = args.join(' '); - let evaled = eval(code); - - if (typeof (evaled) !== 'string') { - const util = await import('util'); - evaled = util.inspect(evaled); - } - - content = clean(evaled); - } catch (err) { - content = `// An error occured\n\n${clean(err)}`; - } finally { - if (message.channel.isSendable()) - message.channel.send(codeBlock('xl', content)); - } - } -}; diff --git a/src/commands/help.ts b/src/commands/help.ts deleted file mode 100644 index 245a8f1..0000000 --- a/src/commands/help.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Collection, ColorResolvable, EmbedBuilder, Message } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -export default { - name: 'help', - description: - "Liste toutes les commandes disponibles ou les informations d'une commande fournie en paramĂštres", - aliases: ['commandes'], - usage: '[commande]', - - execute: async (client: DatadropClient, message: Message, args: string[]) => { - const { prefix } = client.config; - const { commands } = client; - let embed: EmbedBuilder; - - if (!args.length) { - embed = listAvailableCommands(prefix, commands); - } else { - const name = args[0].toLowerCase(); - const command = commands.get(name) || commands.find((c) => c.aliases?.includes(name)); - - if (!command) { - if (message.channel.isSendable()) { - await message.channel.send("Ce n'est pas une commande valide."); - } - return; - } - - embed = buildCommandUsage(prefix, command); - } - - try { - if (message.channel.isSendable()) { - await message.channel.send({ embeds: [embed] }); - } - } catch (err) { - client.logger.error(`Erreur durant l'envoi du message d'aide pour ${message.author.username}:\n` + err); - } - } -}; - -function buildEmbed(title: string, color: ColorResolvable, description: string): EmbedBuilder { - return new EmbedBuilder().setTitle(title).setColor(color).setDescription(description); -} - -function listAvailableCommands(prefix: string | undefined, commands: Collection): EmbedBuilder { - const data: string[] = []; - - data.push(`Utilisez \`${prefix}${module.exports.name} [commande]\` pour lire le message d'aide sur une commande spĂ©cifique.\n`); - data.push('Commandes disponibles :'); - data.push(`- ${commands.map((command) => command.name).join('\n- ')}`); - - return buildEmbed('Liste des commandes', 'Random', data.join('\n')); -} - -function buildCommandUsage(prefix: string | undefined, command: any): EmbedBuilder { - const data: string[] = []; - - data.push(`**Nom:** ${command.name}`); - if (command.aliases) { - data.push(`**Alias:** ${command.aliases.join(', ')}`); - } - if (command.description) { - data.push(`**Description:** ${command.description}`); - } - if (command.usage) { - data.push(`**Usage:** \`${prefix}${command.name} ${command.usage}\``); - } - data.push(`**Cooldown:** ${command.cooldown || 3} seconde(s)`); - - return buildEmbed(`Aide pour '${command.name}'`, 'Random', data.join('\n')); -} diff --git a/src/commands/link.ts b/src/commands/link.ts deleted file mode 100644 index 2c13c93..0000000 --- a/src/commands/link.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Message } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -export default { - name: 'link', - aliases: ['lier', 'compte'], - description: 'Lie ton compte Discord avec ton adresse HĂ©nallux!', - - async execute(client: DatadropClient, message: Message, args: string[]) { - const user = message.author; - - const userFromDatabase = await client.database.read(user.id); - if (userFromDatabase?.activatedCode) { - await message.reply({ content: 'Tu as dĂ©jĂ  liĂ© ton compte HĂ©nallux avec ton compte Discord!' }); - return; - } - - const linkAccountButton = new ButtonBuilder() - .setLabel('Lier son compte') - .setEmoji('🔗') - .setStyle(ButtonStyle.Primary) - .setCustomId(`lae${user.id}`); - const buttonComponent = new ActionRowBuilder().addComponents(linkAccountButton); - - await message.reply({ - content: `Pour lier ton compte, rien de plus simple! Il te suffit de cliquer sur le bouton ci-dessous et remplir le formulaire! Tu recevras un code par email qu'il faudra envoyer ici ensuite!\n${client.config.warning} Nous conservons les informations soumises aprĂšs utilisation. Si tu soumets tes informations, tu acceptes que celles-ci nous soient transmises et que nous les conservions durant toute la durĂ©e de ta prĂ©sence sur le serveur!`, - components: [buttonComponent] - }); - }, -}; diff --git a/src/commands/others/link.ts b/src/commands/others/link.ts new file mode 100644 index 0000000..87cf6ad --- /dev/null +++ b/src/commands/others/link.ts @@ -0,0 +1,49 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction, + SlashCommandBuilder, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import type { Command } from "../../models/Command.js"; + +export default { + data: new SlashCommandBuilder() + .setName("link") + .setDescription("Lie ton compte Discord avec ton adresse HĂ©nallux!"), + + async execute( + client: DatadropClient, + interaction: ChatInputCommandInteraction, + ) { + const userFromDatabase = await client.database.read( + interaction.user.id, + ); + if (userFromDatabase?.activatedCode) { + await interaction.reply({ + content: + "❌ **Oups!** - Tu as dĂ©jĂ  liĂ© ton compte HĂ©nallux avec ton compte Discord!", + ephemeral: true, + }); + return; + } + + const linkAccountButton = new ButtonBuilder() + .setLabel("Lier son compte") + .setEmoji("🔗") + .setStyle(ButtonStyle.Primary) + .setCustomId(`lae${interaction.user.id}`); + const buttonComponent = + new ActionRowBuilder().addComponents( + linkAccountButton, + ); + + await interaction.reply({ + content: `Pour lier ton compte, rien de plus simple! Il te suffit de cliquer sur le bouton ci-dessous et remplir le formulaire! Tu recevras un code par email qu'il faudra envoyer ici ensuite!\n⚠ Nous conservons les informations soumises aprĂšs utilisation. Si tu soumets tes informations, tu acceptes que celles-ci nous soient transmises et que nous les conservions durant toute la durĂ©e de ta prĂ©sence sur le serveur!`, + components: [buttonComponent], + ephemeral: true, + }); + }, +} as Command; diff --git a/src/commands/others/pin.ts b/src/commands/others/pin.ts new file mode 100644 index 0000000..37b28be --- /dev/null +++ b/src/commands/others/pin.ts @@ -0,0 +1,79 @@ +import { + ApplicationCommandType, + ContextMenuCommandBuilder, + InteractionContextType, + type MessageContextMenuCommandInteraction, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import type { Command } from "../../models/Command.js"; + +export default { + data: new ContextMenuCommandBuilder() + .setName("(DĂ©s)Épingler le message") + .setType(ApplicationCommandType.Message) + .setContexts(InteractionContextType.Guild), + + async execute( + client: DatadropClient, + interaction: MessageContextMenuCommandInteraction, + ) { + if (!interaction.inGuild()) return; + + const member = await interaction.guild?.members.fetch( + interaction.user.id, + ); + if (!member) { + await interaction.reply({ + content: + "❌ **Oups!** - Impossible de rĂ©cupĂ©rer ton identifiant Discord!", + ephemeral: true, + }); + return; + } + + const { + communitymanagerRoleid, + adminRoleid, + delegatesRoleid, + professorRoleid, + } = client.config; + + if ( + ![ + communitymanagerRoleid, + adminRoleid, + delegatesRoleid, + professorRoleid, + ].some((r) => member.roles.cache.has(r)) + ) { + await interaction.reply({ + content: + "❌ **Oups!** - Tu n'es pas membre d'un des rĂŽles nĂ©cessaires et n'es donc pas Ă©ligible Ă  cette commande.", + ephemeral: true, + }); + client.logger.info( + `Le membre <${member.displayName}> (${member.id}) a tentĂ© d'Ă©pingler/dĂ©sĂ©pingler un message mais n'a pas les droits nĂ©cessaires.`, + ); + return; + } + + const referencedMessage = interaction.targetMessage; + if (referencedMessage.pinned) { + await referencedMessage.unpin(); + await interaction.reply({ + content: "✅ Message dĂ©sĂ©pinglĂ©!", + ephemeral: true, + }); + } else { + await referencedMessage.pin(); + await interaction.reply({ + content: "✅ Message Ă©pinglĂ©!", + ephemeral: true, + }); + } + client.logger.info( + `Le membre <${member.displayName}> (${member.id}) a Ă©pinglĂ©/dĂ©sĂ©pinglĂ© le message <${referencedMessage.id}>.`, + ); + }, +} as Command; diff --git a/src/commands/owner/eval.ts b/src/commands/owner/eval.ts new file mode 100644 index 0000000..ab808e2 --- /dev/null +++ b/src/commands/owner/eval.ts @@ -0,0 +1,58 @@ +ï»żimport { + type ChatInputCommandInteraction, + PermissionFlagsBits, + SlashCommandBuilder, + codeBlock, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import { clean } from "../../helpers.js"; +import type { Command } from "../../models/Command.js"; + +export default { + data: new SlashCommandBuilder() + .setName("eval") + .setDescription("Évalue du code Javascript.") + .addStringOption((option) => + option + .setName("code") + .setDescription("Le code Ă  Ă©valuer.") + .setRequired(true), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + ownerOnly: true, + + async execute( + client: DatadropClient, + interaction: ChatInputCommandInteraction, + ) { + // double check sur l'identitĂ© juste pour la sĂ©curitĂ© + const { ownerIds } = client.config; + if (!ownerIds.includes(interaction.user.id)) { + await interaction.reply({ + content: + "❌ **Oups!** - Vous n'ĂȘtes pas autorisĂ© Ă  utiliser cette commande.", + ephemeral: true, + }); + return; + } + + let content = ""; + try { + const code = interaction.options.getString("code", true); + // biome-ignore lint/security/noGlobalEval: normal use case for an eval command + let evaled = eval(code); + + if (typeof evaled !== "string") { + const util = await import("node:util"); + evaled = util.inspect(evaled); + } + + content = clean(evaled); + } catch (err) { + content = `// An error occured\n\n${clean(err)}`; + } finally { + await interaction.reply(codeBlock("xl", content)); + } + }, +} as Command; diff --git a/src/commands/owner/reload.ts b/src/commands/owner/reload.ts new file mode 100644 index 0000000..86a0d84 --- /dev/null +++ b/src/commands/owner/reload.ts @@ -0,0 +1,39 @@ +import { + type ChatInputCommandInteraction, + PermissionFlagsBits, + SlashCommandBuilder, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import type { Command } from "../../models/Command.js"; + +export default { + data: new SlashCommandBuilder() + .setName("reload") + .setDescription("Recharge la configuration du bot") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + ownerOnly: true, + + async execute( + client: DatadropClient, + interaction: ChatInputCommandInteraction, + ) { + // double check sur l'identitĂ© juste pour la sĂ©curitĂ© + const { ownerIds } = client.config; + if (!ownerIds.includes(interaction.user.id)) { + await interaction.reply({ + content: + "❌ **Oups!** - Vous n'ĂȘtes pas autorisĂ© Ă  utiliser cette commande.", + ephemeral: true, + }); + return; + } + + client.logger.info("Rechargement de la configuration en cours..."); + await client.reloadConfig(); + await interaction.reply({ + content: "Rechargement de la configuration en cours... 👌", + ephemeral: true, + }); + }, +} as Command; diff --git a/src/commands/owner/restart.ts b/src/commands/owner/restart.ts new file mode 100644 index 0000000..aaa8110 --- /dev/null +++ b/src/commands/owner/restart.ts @@ -0,0 +1,39 @@ +ï»żimport { + type ChatInputCommandInteraction, + PermissionFlagsBits, + SlashCommandBuilder, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import type { Command } from "../../models/Command.js"; + +export default { + data: new SlashCommandBuilder() + .setName("restart") + .setDescription("Relance le bot") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + ownerOnly: true, + + async execute( + client: DatadropClient, + interaction: ChatInputCommandInteraction, + ) { + // double check sur l'identitĂ© juste pour la sĂ©curitĂ© + const { ownerIds } = client.config; + if (!ownerIds.includes(interaction.user.id)) { + await interaction.reply({ + content: + "❌ **Oups!** - Vous n'ĂȘtes pas autorisĂ© Ă  utiliser cette commande.", + ephemeral: true, + }); + return; + } + + client.logger.info("ArrĂȘt en cours..."); + await interaction.reply({ + content: "ArrĂȘt en cours... 👌", + ephemeral: true, + }); + process.exit(); + }, +} as Command; diff --git a/src/commands/ping.ts b/src/commands/ping.ts deleted file mode 100644 index 72b1904..0000000 --- a/src/commands/ping.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Message } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -export default { - name: 'ping', - description: 'Pong!', - - async execute(client: DatadropClient, message: Message, args: string[]) { - const msg = await message.reply('Calcul en cours...'); - await msg.edit(`Pong: ${client.ws.ping} ms`); - }, -}; diff --git a/src/commands/pinmsg.ts b/src/commands/pinmsg.ts deleted file mode 100644 index 5e48695..0000000 --- a/src/commands/pinmsg.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Message, MessageReference, MessageResolvable } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -export default { - name: 'pinmsg', - description: "Ajoute ou retire le message liĂ© des Ă©pinglĂ©s du canal si l'utilisateur fait partie du rĂŽle 'Professeur(e)'.\n\nLes arguments possibles sont:\n ‱ `--verbose` ou `-v` pour avoir un retour texte pour la commande\n", - aliases: ['pin', 'Ă©pingler', 'unpin', 'unpinmsg', 'dĂ©sĂ©pingler'], - guildOnly: true, - usage: '[args]', - - async execute(client: DatadropClient, message: Message, args: string[]) { - if (!message.guild || !message.member || !message.reference) return; - - const { communitymanagerRoleid, adminRoleid, delegatesRoleid, professorRoleid } = client.config; - const verboseIsActive = args && args.length >= 1 && (args[0] === '--verbose' || args[0] === '-v'); - - const hasRequiredRoles = await isAuthorized(message, [communitymanagerRoleid, adminRoleid, delegatesRoleid, professorRoleid], client, verboseIsActive); - if (!hasRequiredRoles) return; - - const referencedMessage = await getMessage(message, client, verboseIsActive); - if (!referencedMessage) return; - - if (referencedMessage.pinned) { - await referencedMessage.unpin(); - await replyOnAction(message, '✅', 'Message dĂ©sĂ©pinglĂ©!', verboseIsActive); - } else { - await referencedMessage.pin(); - await replyOnAction(message, '✅', 'Message Ă©pinglĂ©!', verboseIsActive); - } - client.logger.info(`Le membre <${message.member.displayName}> (${message.member.id}) a Ă©pinglĂ©/dĂ©sĂ©pinglĂ© le message <${referencedMessage.id}>.`); - } -}; - -async function replyOnAction(message: Message, emoji: string, content: string, verboseIsActive?: boolean): Promise { - if (verboseIsActive && message.channel.isSendable()) { - await message.channel.send(`${emoji} ${content}`); - } else { - await message.react(emoji); - } -} - -async function isAuthorized(message: Message, requiredRoles: string[], client: DatadropClient, verboseIsActive?: boolean): Promise { - if (!requiredRoles.some(r => message.member!.roles.cache.has(r))) { - await replyOnAction(message, '❌', '**Oups!** - Tu n\'es pas membre d\'un des rĂŽles nĂ©cessaires et n\'es donc pas Ă©ligible Ă  cette commande.', verboseIsActive); - - client.logger.info(`Le membre <${message.member?.displayName}> (${message.member?.id}) a tentĂ© d'Ă©pingler/dĂ©sĂ©pingler un message mais n'a pas les droits nĂ©cessaires.`); - return false; - } - return true; -} - -async function getReference(message: Message, client: DatadropClient, verboseIsActive?: boolean): Promise { - const reference = message.reference; - if (!reference || reference.channelId != message.channel.id) { - await replyOnAction(message, '❌', '**Oups!** - Pas de rĂ©fĂ©rence. Peut-ĂȘtre avez-vous oubliĂ© de sĂ©lectionner le message Ă  (dĂ©s)Ă©pingler en y rĂ©pondant? (cfr )', verboseIsActive); - - client.logger.info(`Le membre <${message.member?.displayName}> (${message.member?.id}) a tentĂ© d'Ă©pingler/dĂ©sĂ©pingler un message sans le rĂ©fĂ©rencer.`); - return null; - } - return reference; -} - -async function getMessage(message: Message, client: DatadropClient, verboseIsActive?: boolean): Promise { - const reference = await getReference(message, client, verboseIsActive); - if (!reference) return null; - - const channel = message.channel; - const referencedMessage = await channel.messages.fetch(reference.messageId as MessageResolvable); - if (!referencedMessage) { - await replyOnAction(message, '❌', '**Oups!** - Message non trouvĂ©. Peut-ĂȘtre a-t-il Ă©tĂ© supprimĂ©?', verboseIsActive); - - client.logger.info(`Le membre <${message.member?.displayName}> (${message.member?.id}) a tentĂ© d'Ă©pingler/dĂ©sĂ©pingler un message non-trouvĂ©.`); - return null; - } - - return referencedMessage; -} diff --git a/src/commands/reload.ts b/src/commands/reload.ts deleted file mode 100644 index 405b2a2..0000000 --- a/src/commands/reload.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Message } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -export default { - name: 'reload', - description: 'Recharge la configuration du bot', - ownerOnly: true, - - async execute(client: DatadropClient, message: Message, args: string[]) { - const { ok_hand } = client.config; - client.logger.info('Recharge de la configuration...'); - await client.reloadConfig(); - await message.reply(ok_hand); - } -}; diff --git a/src/commands/restart.ts b/src/commands/restart.ts deleted file mode 100644 index aa78013..0000000 --- a/src/commands/restart.ts +++ /dev/null @@ -1,16 +0,0 @@ -ï»żimport { Message } from 'discord.js'; - -import { DatadropClient } from '../datadrop.js'; - -export default { - name: 'restart', - description: 'Relance le bot', - ownerOnly: true, - - async execute(client: DatadropClient, message: Message, args: string[]) { - const { ok_hand } = client.config; - client.logger.info('ArrĂȘt en cours...'); - await message.reply(ok_hand); - process.exit(); - } -}; diff --git a/src/commands/utility/email.ts b/src/commands/utility/email.ts new file mode 100644 index 0000000..cc75166 --- /dev/null +++ b/src/commands/utility/email.ts @@ -0,0 +1,48 @@ +import { + type ChatInputCommandInteraction, + EmbedBuilder, + SlashCommandBuilder, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import type { Command } from "../../models/Command.js"; + +const people = [ + { + name: "Conseil Étudiant", + emails: ["cehenallux@gmail.com"], + }, + { + name: "ComitĂ© IODA", + emails: ["comiteIODA2024@gmail.com"], + }, +]; + +export default { + data: new SlashCommandBuilder() + .setName("email") + .setDescription( + "Affiche un message concernant l'envoi d'email Ă  un.e professeur.e ou aux organisations Ă©tudiantes.", + ), + + async execute( + client: DatadropClient, + interaction: ChatInputCommandInteraction, + ) { + const embed = new EmbedBuilder() + .setTitle("Emails") + .setColor("Random") + .setDescription( + `Si tu souhaites envoyer un email Ă  un.e professeur.e, Outlook possĂšde un systĂšme de recherche automatique dans l'annuaire de l'HĂ©nallux.\nâžĄïž Tape donc simplement son nom/son prĂ©nom et Outlook fera le reste (en gĂ©nĂ©ral, l'email d'un.e professeur.e suit le format \`prenom.nom@henallux.be\`)!\n\nPour ce qui est des contacts Ă©tudiants, voici leurs emails:`, + ); + embed.addFields( + people.map((p) => ({ + name: p.name, + value: p.emails.join(", "), + inline: true, + })), + ); + + await interaction.reply({ embeds: [embed] }); + }, +} as Command; diff --git a/src/commands/utility/ping.ts b/src/commands/utility/ping.ts new file mode 100644 index 0000000..2ec41f7 --- /dev/null +++ b/src/commands/utility/ping.ts @@ -0,0 +1,19 @@ +import { + type ChatInputCommandInteraction, + SlashCommandBuilder, +} from "discord.js"; + +import type { DatadropClient } from "../../datadrop.js"; +import type { Command } from "../../models/Command.js"; + +export default { + data: new SlashCommandBuilder().setName("ping").setDescription("Pong!"), + + async execute( + client: DatadropClient, + interaction: ChatInputCommandInteraction, + ) { + const response = await interaction.reply("Calcul en cours..."); + await response.edit(`Pong: ${client.ws.ping} ms`); + }, +} as Command; diff --git a/src/config.ts b/src/config.ts index 9d1b35b..b0f9515 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,58 +1,61 @@ -import { Configuration } from './models/Configuration.js'; -import packageInfo from '../package.json' with { type: "json" }; +import packageInfo from "../package.json" with { type: "json" }; +import type { Configuration } from "./models/Configuration.js"; // should be Partial but codebase not ready yet const defaultConfig: Configuration = { - minLevel: 'info', + minLevel: "info", includeTimestamp: false, - guildId: '', + guildId: "", ownerIds: [], - prefix: '', - version: '', - botName: '', - communitymanagerRoleid: '', - adminRoleid: '', - delegatesRoleid: '', - professorRoleid: '', - verifiedRoleId: '', - informationsChannelid: '', - faqChannelid: '', - comiteeChannelid: '', - dynamicChannelPrefix: '', + version: "", + botName: "", + botId: "", + communitymanagerRoleid: "", + adminRoleid: "", + delegatesRoleid: "", + professorRoleid: "", + verifiedRoleId: "", + informationsChannelid: "", + faqChannelid: "", + comiteeChannelid: "", + dynamicChannelPrefix: "", dynamicChannelPrefixRegex: /.*/, staticTriggerChannelids: [], - rolesChannelid: '', - first: { channelid: '', roleid: '', emote: '', groups: [] }, - second: { channelid: '', roleid: '', emote: '', groups: [] }, - third: { channelid: '', roleid: '', emote: '', groups: [] }, - alumni: { roleid: '', emote: '' }, - tutor: { roleid: '', emote: '' }, - announce: { roleid: '', emote: '', channelid: '' }, - ok_hand: '', - warning: '', + rolesChannelid: "", + first: { channelid: "", roleid: "", emote: "", groups: [] }, + second: { channelid: "", roleid: "", emote: "", groups: [] }, + third: { channelid: "", roleid: "", emote: "", groups: [] }, + alumni: { roleid: "", emote: "" }, + tutor: { roleid: "", emote: "" }, + announce: { roleid: "", emote: "", channelid: "" }, communicationServiceOptions: { - apiKey: '', - mailData: { from: '', templateId: '' } - } + apiKey: "", + mailData: { from: "", templateId: "" }, + }, }; export async function readConfig(): Promise { try { - const environment = (process.env.NODE_ENV || 'development').toLowerCase(); + const environment = ( + process.env.NODE_ENV || "development" + ).toLowerCase(); - const json = JSON.parse(process.env.CONFIG ?? '{}'); + const json = JSON.parse(process.env.CONFIG ?? "{}"); for (const prop in json) { if (/regex/i.exec(prop)) { json[prop] = new RegExp(json[prop]); } } - const config = { ...json, version: `${environment}-v${packageInfo.version}` }; - config.communicationServiceOptions.apiKey = process.env.SENDGRID_API_KEY; + const config = { + ...json, + version: `${environment}-v${packageInfo.version}`, + }; + config.communicationServiceOptions.apiKey = + process.env.SENDGRID_API_KEY; return config; - } - catch (err: unknown) { + } catch (err: unknown) { console.error(err); return defaultConfig; } diff --git a/src/datadrop.ts b/src/datadrop.ts index 86188de..d660d8f 100644 --- a/src/datadrop.ts +++ b/src/datadrop.ts @@ -1,30 +1,61 @@ -import { Client, ClientOptions, Collection, ButtonInteraction, GuildMember, Message, GuildTextBasedChannel, VoiceChannel, Role, Snowflake, StringSelectMenuInteraction } from 'discord.js'; -import * as path from 'path'; - -import { LogEventLevel, DefaultLogger, ConsoleLogger } from '@hunteroi/advanced-logger'; -import { InteractionsSelfRoleManager, RoleToEmojiData, SelfRoleManagerEvents } from '@hunteroi/discord-selfrole'; -import { ChildChannelData, ParentChannelData, TempChannelsManager, TempChannelsManagerEvents } from '@hunteroi/discord-temp-channels'; -import { VerificationManager, VerificationManagerEvents } from '@hunteroi/discord-verification'; -import { SendGridService } from '@hunteroi/discord-verification/lib/services/SendGridService.js'; - -import PostgresDatabaseService from './services/PostgresDatabaseService.js'; -import { getErrorMessage, readFilesFrom } from './helpers.js'; -import { Configuration } from './models/Configuration.js'; -import { readConfig } from './config.js'; -import { User } from './models/User.js'; -import { IDatabaseService } from './models/IDatabaseService.js'; +import * as path from "node:path"; +import { + type ButtonInteraction, + Client, + type ClientEvents, + type ClientOptions, + Collection, + type GuildMember, + type GuildTextBasedChannel, + type Message, + type Role, + type Snowflake, + type StringSelectMenuInteraction, + type VoiceChannel, +} from "discord.js"; + +import { + ConsoleLogger, + type DefaultLogger, + LogEventLevel, +} from "@hunteroi/advanced-logger"; +import { + InteractionsSelfRoleManager, + type RoleToEmojiData, + SelfRoleManagerEvents, +} from "@hunteroi/discord-selfrole"; +import { + type ChildChannelData, + type ParentChannelData, + TempChannelsManager, + TempChannelsManagerEvents, +} from "@hunteroi/discord-temp-channels"; +import { + VerificationManager, + VerificationManagerEvents, +} from "@hunteroi/discord-verification"; +import { SendGridService } from "@hunteroi/discord-verification/lib/services/SendGridService.js"; + +import { readConfig } from "./config.js"; +import { getErrorMessage, readFilesFrom } from "./helpers.js"; +import type { Command } from "./models/Command.js"; +import type { Configuration } from "./models/Configuration.js"; +import type { Event } from "./models/Event.js"; +import type { IDatabaseService } from "./models/IDatabaseService.js"; +import type { User } from "./models/User.js"; +import PostgresDatabaseService from "./services/PostgresDatabaseService.js"; export class DatadropClient extends Client { #config: Configuration; readonly database: IDatabaseService; readonly logger: DefaultLogger; - readonly commands: Collection; + readonly commands: Collection; readonly selfRoleManager: InteractionsSelfRoleManager; readonly tempChannelsManager: TempChannelsManager; readonly verificationManager: VerificationManager; public readonly errorMessage = "Je n'ai pas su t'envoyer le code!"; - public readonly activeAccountMessage = 'ton compte est dĂ©jĂ  vĂ©rifiĂ©!'; + public readonly activeAccountMessage = "ton compte est dĂ©jĂ  vĂ©rifiĂ©!"; constructor(options: ClientOptions, config: Configuration) { super(options); @@ -39,22 +70,34 @@ export class DatadropClient extends Client { this.selfRoleManager = new InteractionsSelfRoleManager(this, { channelsMessagesFetchLimit: 10, - deleteAfterUnregistration: false + deleteAfterUnregistration: false, }); this.tempChannelsManager = new TempChannelsManager(this); this.database = new PostgresDatabaseService(this.logger); - const communicationService = new SendGridService(config.communicationServiceOptions); - this.verificationManager = new VerificationManager(this, this.database, communicationService, { - codeGenerationOptions: { length: 6 }, - maxNbCodeCalledBeforeResend: 3, - errorMessage: () => this.errorMessage, - pendingMessage: (user: User) => `Ton code de vĂ©rification vient de t'ĂȘtre envoyĂ©, ${user.username}`, - alreadyPendingMessage: (user: User) => `${user.username}, tu as dĂ©jĂ  un code en attente!`, - alreadyActiveMessage: (user: User) => `${user.username}, ${this.activeAccountMessage}`, - validCodeMessage: (user: User, code: string) => `Le code ${code} est valide. Bienvenue ${user.username}!`, - invalidCodeMessage: (_, code: string) => `Le code ${code} est invalide!` - }); + const communicationService = new SendGridService( + config.communicationServiceOptions, + ); + this.verificationManager = new VerificationManager( + this, + this.database, + communicationService, + { + codeGenerationOptions: { length: 6 }, + maxNbCodeCalledBeforeResend: 3, + errorMessage: () => this.errorMessage, + pendingMessage: (user: User) => + `Ton code de vĂ©rification vient de t'ĂȘtre envoyĂ©, ${user.username}`, + alreadyPendingMessage: (user: User) => + `${user.username}, tu as dĂ©jĂ  un code en attente!`, + alreadyActiveMessage: (user: User) => + `${user.username}, ${this.activeAccountMessage}`, + validCodeMessage: (user: User, code: string) => + `Le code ${code} est valide. Bienvenue ${user.username}!`, + invalidCodeMessage: (_, code: string) => + `Le code ${code} est invalide!`, + }, + ); } get config(): Configuration { @@ -66,116 +109,312 @@ export class DatadropClient extends Client { } #listenToVerificationEvents(): void { - this.verificationManager.on(VerificationManagerEvents.codeVerify, async (user: User, userid: Snowflake, code: string, isVerified: boolean) => { - this.logger.info(`L'utilisateur ${user.username} (${userid}) ${isVerified ? 'a Ă©tĂ© vĂ©rifiĂ© avec succĂšs' : 'a Ă©chouĂ© sa vĂ©rification'} avec le code ${code}.`); - - if (isVerified) { - const guild = await this.guilds.fetch(this.#config.guildId); - const member = await guild.members.fetch(userid); - await member.roles.add(this.#config.verifiedRoleId, `Compte HĂ©nallux vĂ©rifiĂ©! ${user.data['email']}`); - } - }); - this.verificationManager.on(VerificationManagerEvents.codeCreate, (code: string) => this.logger.debug(`Le code ${code} vient d'ĂȘtre crĂ©Ă©.`)); - this.verificationManager.on(VerificationManagerEvents.userCreate, (user: User) => this.logger.debug(`L'utilisateur ${user.username} (${user.userid}) vient d'ĂȘtre enregistrĂ©.`)); - this.verificationManager.on(VerificationManagerEvents.userAwait, (user: User) => this.logger.debug(`L'utilisateur ${user.username} (${user.userid}) attend d'ĂȘtre vĂ©rifiĂ©.`)); - this.verificationManager.on(VerificationManagerEvents.userActive, (user: User) => this.logger.debug(`L'utilisateur ${user.username} (${user.userid}) est dĂ©jĂ  actif!`)); - this.verificationManager.on(VerificationManagerEvents.error, (user: User, error: unknown) => this.logger.error(`Une erreur est survenue lors de l'envoi du code Ă  l'utilisateur ${user.username} (${user.userid}).\nErreur: ${getErrorMessage(error)}`)); + this.verificationManager.on( + VerificationManagerEvents.codeVerify, + async ( + user: User, + userid: Snowflake, + code: string, + isVerified: boolean, + ) => { + this.logger.info( + `L'utilisateur ${user.username} (${userid}) ${isVerified ? "a Ă©tĂ© vĂ©rifiĂ© avec succĂšs" : "a Ă©chouĂ© sa vĂ©rification"} avec le code ${code}.`, + ); + + if (isVerified) { + const guild = await this.guilds.fetch(this.#config.guildId); + const member = await guild.members.fetch(userid); + await member.roles.add( + this.#config.verifiedRoleId, + `Compte HĂ©nallux vĂ©rifiĂ©! ${user.data.email}`, + ); + } + }, + ); + this.verificationManager.on( + VerificationManagerEvents.codeCreate, + (code: string) => + this.logger.debug(`Le code ${code} vient d'ĂȘtre crĂ©Ă©.`), + ); + this.verificationManager.on( + VerificationManagerEvents.userCreate, + (user: User) => + this.logger.debug( + `L'utilisateur ${user.username} (${user.userid}) vient d'ĂȘtre enregistrĂ©.`, + ), + ); + this.verificationManager.on( + VerificationManagerEvents.userAwait, + (user: User) => + this.logger.debug( + `L'utilisateur ${user.username} (${user.userid}) attend d'ĂȘtre vĂ©rifiĂ©.`, + ), + ); + this.verificationManager.on( + VerificationManagerEvents.userActive, + (user: User) => + this.logger.debug( + `L'utilisateur ${user.username} (${user.userid}) est dĂ©jĂ  actif!`, + ), + ); + this.verificationManager.on( + VerificationManagerEvents.error, + (user: User, error: unknown) => + this.logger.error( + `Une erreur est survenue lors de l'envoi du code Ă  l'utilisateur ${user.username} (${user.userid}).\nErreur: ${getErrorMessage(error)}`, + ), + ); } #listenToTempChannelsEvents(): void { - this.tempChannelsManager.on(TempChannelsManagerEvents.channelRegister, async (parent: ParentChannelData) => { - const parentChannel = await this.channels.fetch(parent.channelId) as VoiceChannel; - this.logger.info(`Canal ${parentChannel.name} enregistrĂ© comme gĂ©nĂ©rateur de canaux temporaires!`); - }); - this.tempChannelsManager.on(TempChannelsManagerEvents.channelUnregister, async (parent: ParentChannelData) => { - const parentChannel = await this.channels.fetch(parent.channelId) as VoiceChannel; - this.logger.info(`Canal ${parentChannel.name} dĂ©senregistrĂ© comme gĂ©nĂ©rateur de canaux temporaires!`); - }); - this.tempChannelsManager.on(TempChannelsManagerEvents.childAdd, (child: ChildChannelData) => this.logger.info(`Le membre <${child.owner.displayName}> (${child.owner.id}) a lancĂ© la crĂ©ation d'un canal vocal dynamique`)); - this.tempChannelsManager.on(TempChannelsManagerEvents.childRemove, (child: ChildChannelData) => this.logger.info(`Plus aucun utilisateur dans <${child.voiceChannel.name}> (${child.voiceChannel.id}). Canal supprimĂ©.`)); - this.tempChannelsManager.on(TempChannelsManagerEvents.error, (error: unknown, message: string) => this.logger.error(`Une erreur est survenie lors de la gestion des canaux dynamiques.\nErreur: ${message}\n${getErrorMessage(error)}`)); + this.tempChannelsManager.on( + TempChannelsManagerEvents.channelRegister, + async (parent: ParentChannelData) => { + const parentChannel = (await this.channels.fetch( + parent.channelId, + )) as VoiceChannel; + this.logger.info( + `Canal ${parentChannel.name} enregistrĂ© comme gĂ©nĂ©rateur de canaux temporaires!`, + ); + }, + ); + this.tempChannelsManager.on( + TempChannelsManagerEvents.channelUnregister, + async (parent: ParentChannelData) => { + const parentChannel = (await this.channels.fetch( + parent.channelId, + )) as VoiceChannel; + this.logger.info( + `Canal ${parentChannel.name} dĂ©senregistrĂ© comme gĂ©nĂ©rateur de canaux temporaires!`, + ); + }, + ); + this.tempChannelsManager.on( + TempChannelsManagerEvents.childAdd, + (child: ChildChannelData) => + this.logger.info( + `Le membre <${child.owner.displayName}> (${child.owner.id}) a lancĂ© la crĂ©ation d'un canal vocal dynamique`, + ), + ); + this.tempChannelsManager.on( + TempChannelsManagerEvents.childRemove, + (child: ChildChannelData) => + this.logger.info( + `Plus aucun utilisateur dans <${child.voiceChannel.name}> (${child.voiceChannel.id}). Canal supprimĂ©.`, + ), + ); + this.tempChannelsManager.on( + TempChannelsManagerEvents.error, + (error: unknown, message: string) => + this.logger.error( + `Une erreur est survenie lors de la gestion des canaux dynamiques.\nErreur: ${message}\n${getErrorMessage(error)}`, + ), + ); } #listenToSelfRoleEvents(): void { - this.selfRoleManager.on(SelfRoleManagerEvents.interaction, async (rte: RoleToEmojiData, interaction: StringSelectMenuInteraction | ButtonInteraction) => { - await interaction.editReply("Ton interaction a Ă©tĂ© enregistrĂ©e."); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.maxRolesReach, async (member: GuildMember, interaction: StringSelectMenuInteraction | ButtonInteraction, nbRoles: number, maxRoles: number) => { - const channel = await member.guild.channels.fetch(interaction.message.channel.id); + this.selfRoleManager.on( + SelfRoleManagerEvents.interaction, + async ( + rte: RoleToEmojiData, + interaction: StringSelectMenuInteraction | ButtonInteraction, + ) => { + await interaction.editReply( + "Ton interaction a Ă©tĂ© enregistrĂ©e.", + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.maxRolesReach, + async ( + member: GuildMember, + interaction: StringSelectMenuInteraction | ButtonInteraction, + nbRoles: number, + maxRoles: number, + ) => { + const channel = await member.guild.channels.fetch( + interaction.message.channel.id, + ); - this.logger.info(`Le membre <${member.user.tag}> a atteint la limite de rĂŽles${(channel ? ` dans <${channel.name}>` : '')}! (${nbRoles}/${maxRoles})`); - await interaction.editReply(`Tu ne peux pas t'assigner plus de ${maxRoles} rĂŽle${(maxRoles > 1 ? 's' : '')} dans ce canal! Tu en as dĂ©jĂ  ${nbRoles} d'assignĂ©${(nbRoles > 1 ? 's' : '')}`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.messageRetrieve, (msg: Message) => { - const channel = msg.channel as GuildTextBasedChannel; - this.logger.info(`Message rĂ©cupĂ©rĂ© dans ${channel.parent!.name}-${channel.name} (${msg.channelId})`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.messageCreate, (msg: Message) => { - const channel = msg.channel as GuildTextBasedChannel; - this.logger.info(`Message crĂ©Ă© dans ${channel.parent!.name}-${channel.name} (${msg.channelId})`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.messageDelete, (msg: Message) => { - const channel = msg.channel as GuildTextBasedChannel; - this.logger.info(`Message supprimĂ© de ${channel.parent!.name}-${channel.name} (${msg.channelId})`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.roleAdd, async (role: Role, member: GuildMember, interaction: StringSelectMenuInteraction | ButtonInteraction) => { - this.logger.info(`Le rĂŽle ${role.name} (<${role.id}>) a Ă©tĂ© ajoutĂ© Ă  <${member.user.tag}>`); - await interaction.editReply(`Le rĂŽle ${role} t'a Ă©tĂ© ajoutĂ©.`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.roleRemove, async (role: Role, member: GuildMember, interaction?: StringSelectMenuInteraction | ButtonInteraction) => { - this.logger.info(`Le rĂŽle ${role.name} (<${role.id}>) a Ă©tĂ© retirĂ© de <${member.user.tag}>`); - if (interaction) await interaction.editReply(`Le rĂŽle ${role} t'a Ă©tĂ© retirĂ©.`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.requiredRolesMissing, async (member: GuildMember, interaction: StringSelectMenuInteraction | ButtonInteraction, role: Role, requiredRoles: string[]) => { - const requiredRolesMissing: Role[] = (await Promise.all(requiredRoles.map(requiredRole => member.guild.roles.fetch(requiredRole)))) - .map(role => role as Role) - .filter(requiredRole => !!requiredRole); - - const roleNames = requiredRolesMissing.map(role => `${role.name} (<${role.id}>)`).join(', '); - this.logger.info(`Le rĂŽle ${role.name} (<${role.id}>) n'a pas pu ĂȘtre donnĂ© Ă  <${member.user.tag}> parce que tous les rĂŽles requis ne sont pas assignĂ©s Ă  ce membre: ${roleNames}.`); - await interaction.editReply(`Tu ne peux pas t'assigner le rĂŽle ${role}! Tu dois d'abord avoir les rĂŽles suivants: ${requiredRolesMissing.join(', ')}.`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.maxRolesReach, async (member: GuildMember, interaction: StringSelectMenuInteraction | ButtonInteraction, currentRolesNumber: number, maxRolesNumber: number, role: Role) => { - this.logger.info(`Le rĂŽle ${role.name} (<${role.id}>) n'a pas pu ĂȘtre donnĂ© Ă  <${member.user.tag}> parce que ce membre a Ă©tĂ© la limite de rĂŽles: ${currentRolesNumber}/${maxRolesNumber}.`); - await interaction.editReply(`Tu ne peux pas t'assigner le rĂŽle ${role}! Tu as atteint la limite: ${currentRolesNumber}/${maxRolesNumber}.`); - }); - this.selfRoleManager.on(SelfRoleManagerEvents.error, (error: unknown, message: string) => { - this.logger.error(`Une erreur est survenue lors de la gestion des rĂŽles automatiques.\nErreur: ${message}\n${getErrorMessage(error)}`); - }); + let message = `Le membre <${member.user.tag}> a atteint la limite de rĂŽles`; + if (channel) message += ` dans <${channel.name}>`; + message += `! (${nbRoles}/${maxRoles})`; + this.logger.info(message); + await interaction.editReply( + `Tu ne peux pas t'assigner plus de ${maxRoles} rĂŽle${maxRoles > 1 ? "s" : ""} dans ce canal! Tu en as dĂ©jĂ  ${nbRoles} d'assignĂ©${nbRoles > 1 ? "s" : ""}`, + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.messageRetrieve, + (msg: Message) => { + const channel = msg.channel as GuildTextBasedChannel; + this.logger.info( + `Message rĂ©cupĂ©rĂ© dans ${channel.parent?.name}-${channel.name} (${msg.channelId})`, + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.messageCreate, + (msg: Message) => { + const channel = msg.channel as GuildTextBasedChannel; + this.logger.info( + `Message crĂ©Ă© dans ${channel.parent?.name}-${channel.name} (${msg.channelId})`, + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.messageDelete, + (msg: Message) => { + const channel = msg.channel as GuildTextBasedChannel; + this.logger.info( + `Message supprimĂ© de ${channel.parent?.name}-${channel.name} (${msg.channelId})`, + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.roleAdd, + async ( + role: Role, + member: GuildMember, + interaction: StringSelectMenuInteraction | ButtonInteraction, + ) => { + this.logger.info( + `Le rĂŽle ${role.name} (<${role.id}>) a Ă©tĂ© ajoutĂ© Ă  <${member.user.tag}>`, + ); + await interaction.editReply(`Le rĂŽle ${role} t'a Ă©tĂ© ajoutĂ©.`); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.roleRemove, + async ( + role: Role, + member: GuildMember, + interaction?: StringSelectMenuInteraction | ButtonInteraction, + ) => { + this.logger.info( + `Le rĂŽle ${role.name} (<${role.id}>) a Ă©tĂ© retirĂ© de <${member.user.tag}>`, + ); + if (interaction) + await interaction.editReply( + `Le rĂŽle ${role} t'a Ă©tĂ© retirĂ©.`, + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.requiredRolesMissing, + async ( + member: GuildMember, + interaction: StringSelectMenuInteraction | ButtonInteraction, + role: Role, + requiredRoles: string[], + ) => { + const requiredRolesMissing: Role[] = ( + await Promise.all( + requiredRoles.map((requiredRole) => + member.guild.roles.fetch(requiredRole), + ), + ) + ) + .map((role) => role as Role) + .filter((requiredRole) => !!requiredRole); + + const roleNames = requiredRolesMissing + .map((role) => `${role.name} (<${role.id}>)`) + .join(", "); + this.logger.info( + `Le rĂŽle ${role.name} (<${role.id}>) n'a pas pu ĂȘtre donnĂ© Ă  <${member.user.tag}> parce que tous les rĂŽles requis ne sont pas assignĂ©s Ă  ce membre: ${roleNames}.`, + ); + await interaction.editReply( + `Tu ne peux pas t'assigner le rĂŽle ${role}! Tu dois d'abord avoir les rĂŽles suivants: ${requiredRolesMissing.join(", ")}.`, + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.maxRolesReach, + async ( + member: GuildMember, + interaction: StringSelectMenuInteraction | ButtonInteraction, + currentRolesNumber: number, + maxRolesNumber: number, + role: Role, + ) => { + this.logger.info( + `Le rĂŽle ${role.name} (<${role.id}>) n'a pas pu ĂȘtre donnĂ© Ă  <${member.user.tag}> parce que ce membre a Ă©tĂ© la limite de rĂŽles: ${currentRolesNumber}/${maxRolesNumber}.`, + ); + await interaction.editReply( + `Tu ne peux pas t'assigner le rĂŽle ${role}! Tu as atteint la limite: ${currentRolesNumber}/${maxRolesNumber}.`, + ); + }, + ); + this.selfRoleManager.on( + SelfRoleManagerEvents.error, + (error: unknown, message: string) => { + this.logger.error( + `Une erreur est survenue lors de la gestion des rĂŽles automatiques.\nErreur: ${message}\n${getErrorMessage(error)}`, + ); + }, + ); } - #bindEvents(): void { - const eventDirectory = path.join(import.meta.dirname, 'events'); - this.logger.debug(`Chargement de ${eventDirectory}`); - readFilesFrom(eventDirectory, (eventName: string, props: any) => { - this.logger.info(`Event '${eventName}' chargĂ©`); - this.on(eventName, props.bind(null, this)); - }); + async #bindEvents(): Promise { + const eventDirectory = path.join(import.meta.dirname, "events"); + await readFilesFrom( + eventDirectory, + (eventFileName: string, event: Event) => { + this.logger.info( + `Event '${event.name}' ('${eventFileName}') chargĂ©`, + ); + + if (event.once) { + this.once( + event.name, + event.execute.bind(null, this) as ( + ...args: ClientEvents[keyof ClientEvents] + ) => void, + ); + } else { + this.on( + event.name, + event.execute.bind(null, this) as ( + ...args: ClientEvents[keyof ClientEvents] + ) => void, + ); + } + }, + this.logger, + ); } - #bindCommands(): void { - const commandDirectory = path.join(import.meta.dirname, 'commands'); - this.logger.debug(`Chargement de ${commandDirectory}`); - readFilesFrom(commandDirectory, (commandName: string, props: any) => { - this.logger.info(`Commande '${commandName}' chargĂ©e`); - this.commands.set(commandName, props); - }); + async #bindCommands(): Promise { + const commandDirectory = path.join(import.meta.dirname, "commands"); + await readFilesFrom( + commandDirectory, + (commandFileName: string, props: Command) => { + this.logger.info( + `Commande '${props.data.name}' ('${commandFileName}') chargĂ©e`, + ); + this.commands.set(props.data.name, props); + }, + this.logger, + ); } async start(): Promise { try { - this.#listenToSelfRoleEvents(); this.#listenToTempChannelsEvents(); this.#listenToVerificationEvents(); - this.#bindEvents(); + await this.#bindEvents(); + await this.#bindCommands(); - this.#bindCommands(); await this.database?.start(); this.login(); } catch (error) { - this.logger.error(`Une erreur est survenue lors du dĂ©marrage du bot.\nErreur: ${getErrorMessage(error)}`); + this.logger.error( + `Une erreur est survenue lors du dĂ©marrage du bot.\nErreur: ${getErrorMessage(error)}`, + ); throw error; } } diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 6f085cd..ed73f95 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -1,71 +1,110 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + Events, + type GuildMember, +} from "discord.js"; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, GuildMember } from 'discord.js'; +import type { DatadropClient } from "../datadrop.js"; +import type { AnnounceConfiguration } from "../models/Configuration.js"; +import type { Event } from "../models/Event.js"; -import { AnnounceConfiguration } from '../models/Configuration.js'; -import { DatadropClient } from '../datadrop.js'; +export default { + name: Events.GuildMemberAdd, + execute: guildMemberAdd, +} as Event; -export default async function guildMemberAdd(client: DatadropClient, member: GuildMember) { - if (member.user.bot) return; - client.logger.info(`L'utilisateur <${member.displayName} a rejoint le serveur.`); +async function guildMemberAdd(client: DatadropClient, member: GuildMember) { + if (member.user.bot) return; + client.logger.info( + `L'utilisateur <${member.displayName} a rejoint le serveur.`, + ); - const { announce, informationsChannelid, faqChannelid, rolesChannelid, comiteeChannelid } = client.config; - const annoncesRole = await member.guild.roles.fetch(announce.roleid); + const { + announce, + informationsChannelid, + faqChannelid, + rolesChannelid, + comiteeChannelid, + } = client.config; + const annoncesRole = await member.guild.roles.fetch(announce.roleid); - const userFromDatabase = await client.database.read(member.id); - if (userFromDatabase?.isDeleted) { - await client.database.undoDelete(member.id); - } + const userFromDatabase = await client.database.read(member.id); + if (userFromDatabase?.isDeleted) { + await client.database.undoDelete(member.id); + } - const embed = generateEmbed(informationsChannelid, faqChannelid, comiteeChannelid, rolesChannelid, announce); - const linkAccountButton = new ButtonBuilder() - .setLabel('Lier son compte') - .setEmoji('🔗') - .setStyle(ButtonStyle.Primary) - .setCustomId(`lae${member.id}`); - const buttonComponent = new ActionRowBuilder().addComponents(linkAccountButton); + const embed = generateEmbed( + informationsChannelid, + faqChannelid, + comiteeChannelid, + rolesChannelid, + announce, + ); + const linkAccountButton = new ButtonBuilder() + .setLabel("Lier son compte") + .setEmoji("🔗") + .setStyle(ButtonStyle.Primary) + .setCustomId(`lae${member.id}`); + const row = new ActionRowBuilder().addComponents( + linkAccountButton, + ); - try { - if (annoncesRole) { - await member.roles.add(annoncesRole); - client.logger.info(`Le rĂŽle <${annoncesRole.name}> a Ă©tĂ© ajoutĂ© Ă  <${member.user.tag}> Ă  l'entrĂ©e de la guilde`); - } + try { + if (annoncesRole) { + await member.roles.add(annoncesRole); + client.logger.info( + `Le rĂŽle <${annoncesRole.name}> a Ă©tĂ© ajoutĂ© Ă  <${member.user.tag}> Ă  l'entrĂ©e de la guilde`, + ); + } - await member.send({ embeds: [embed], components: [buttonComponent] }); - client.logger.info(`Un DM a Ă©tĂ© envoyĂ© Ă  <${member.user.tag}> Ă  son entrĂ©e dans la guilde`); - } catch (err: unknown) { - client.logger.error((err).message); - } -}; + await member.send({ embeds: [embed], components: [row] }); + client.logger.info( + `Un DM a Ă©tĂ© envoyĂ© Ă  <${member.user.tag}> Ă  son entrĂ©e dans la guilde`, + ); + } catch (err: unknown) { + client.logger.error((err).message); + } +} -function generateEmbed(informationsChannelid: string, faqChannelid: string, comiteeChannelid: string, rolesChannelid: string, announce: AnnounceConfiguration) { - const fields = [ - { - name: '1. Lie ton compte', - value: `Pour accĂ©der au serveur, tu dois lier ton compte Discord avec ton adresse HĂ©nallux. Pour se faire, rien de plus simple que de cliquer sur le bouton ci-dessous et remplir le formulaire! Tu recevras un code par email qu'il faudra envoyer ici!`, - }, - { - name: '2. Change ton pseudo', - value: `Sur Discord, tu peux changer ton pseudo sur chaque serveur (tu as donc un pseudo diffĂ©rent par serveur!). Pour cela, fais un clic-droit sur l'icĂŽne du serveur en question et sĂ©lectionne **Changer le pseudo**.`, - }, - { - name: '3. Lis les canaux importants', - value: `En arrivant, tu vas ĂȘtre un peu perdu. C'est normal, il y a beaucoup de choses et c'est pas forcĂ©ment simple Ă  suivre.\nOn te conseille d'abord de jeter un oeil aux diffĂ©rents canaux listĂ©s ci-dessous :\n- <#${informationsChannelid}>\n- <#${faqChannelid}>\n- <#${comiteeChannelid}>\n- <#${rolesChannelid}>\n- <#${announce.channelid}>`, - }, - ]; +function generateEmbed( + informationsChannelid: string, + faqChannelid: string, + comiteeChannelid: string, + rolesChannelid: string, + announce: AnnounceConfiguration, +) { + const fields = [ + { + name: "1. Lie ton compte", + value: `Pour accĂ©der au serveur, tu dois lier ton compte Discord avec ton adresse HĂ©nallux. Pour se faire, rien de plus simple que de cliquer sur le bouton ci-dessous et remplir le formulaire! Tu recevras un code par email qu'il faudra envoyer ici!`, + }, + { + name: "2. Change ton pseudo", + value: `Sur Discord, tu peux changer ton pseudo sur chaque serveur (tu as donc un pseudo diffĂ©rent par serveur!). Pour cela, fais un clic-droit sur l'icĂŽne du serveur en question et sĂ©lectionne **Changer le pseudo**.`, + }, + { + name: "3. Lis les canaux importants", + value: `En arrivant, tu vas ĂȘtre un peu perdu. C'est normal, il y a beaucoup de choses et c'est pas forcĂ©ment simple Ă  suivre.\nOn te conseille d'abord de jeter un oeil aux diffĂ©rents canaux listĂ©s ci-dessous :\n- <#${informationsChannelid}>\n- <#${faqChannelid}>\n- <#${comiteeChannelid}>\n- <#${rolesChannelid}>\n- <#${announce.channelid}>`, + }, + ]; - return new EmbedBuilder() - .setColor(0x117da3) - .setThumbnail( - 'https://cdn.discordapp.com/icons/288659194737983489/6d9aa353290265c6587ac75fd4247f71.png' - ) - .setTitle('Salut toi!') - .setDescription( - `Bienvenue sur le serveur Discord non-officiel de la section **Informatique Orientation _DĂ©veloppement d'Application_** de l'IESN. Ce serveur est une initiative Ă©tudiante et n'est donc pas une plateforme de communication officielle de la Haute-École Namur-LiĂšge-Luxembourg.\n\nPour bien commencer l'annĂ©e, on te recommande de suivre les quelques Ă©tapes suivantes :` - ) - .addFields(fields) - .setFooter({ - text: `Le ComitĂ© IODA`, - iconURL: 'https://cdn.discordapp.com/icons/491312065785364482/c9d724c34519c57d3cc1c28f79813f73.png' - }) - .setTimestamp(); + return new EmbedBuilder() + .setColor(0x117da3) + .setThumbnail( + "https://cdn.discordapp.com/icons/288659194737983489/6d9aa353290265c6587ac75fd4247f71.png", + ) + .setTitle("Salut toi!") + .setDescription( + `Bienvenue sur le serveur Discord non-officiel de la section **Informatique Orientation _DĂ©veloppement d'Application_** de l'IESN. Ce serveur est une initiative Ă©tudiante et n'est donc pas une plateforme de communication officielle de la Haute-École Namur-LiĂšge-Luxembourg.\n\nPour bien commencer l'annĂ©e, on te recommande de suivre les quelques Ă©tapes suivantes :`, + ) + .addFields(fields) + .setFooter({ + text: "Le ComitĂ© IODA", + iconURL: + "https://cdn.discordapp.com/icons/491312065785364482/c9d724c34519c57d3cc1c28f79813f73.png", + }) + .setTimestamp(); } diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index af34769..bddf2e4 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -1,15 +1,23 @@ -import { GuildMember } from 'discord.js'; +import { Events, type GuildMember } from "discord.js"; -import { DatadropClient } from '../datadrop.js'; +import type { DatadropClient } from "../datadrop.js"; +import type { Event } from "../models/Event.js"; -export default async function guildMemberRemove(client: DatadropClient, member: GuildMember) { - if (member.user.bot) return; - if (member.guild.id !== client.config.guildId) return; - client.logger.info(`L'utilisateur <${member.displayName} a quittĂ© le serveur.`); +export default { + name: Events.GuildMemberRemove, + execute: guildMemberRemove, +} as Event; - try { - await client.database.delete(member.id); - } catch (err: unknown) { - client.logger.error((err).message); - } -}; +async function guildMemberRemove(client: DatadropClient, member: GuildMember) { + if (member.user.bot) return; + if (member.guild.id !== client.config.guildId) return; + client.logger.info( + `L'utilisateur <${member.displayName} a quittĂ© le serveur.`, + ); + + try { + await client.database.delete(member.id); + } catch (err: unknown) { + client.logger.error((err).message); + } +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 1c24af2..97caa8c 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,49 +1,124 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, Interaction, italic, ModalBuilder, ModalSubmitInteraction, RepliableInteraction, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { + ActionRowBuilder, + ButtonBuilder, + type ButtonInteraction, + ButtonStyle, + type ChatInputCommandInteraction, + Events, + type Interaction, + type MessageContextMenuCommandInteraction, + ModalBuilder, + type ModalSubmitInteraction, + type RepliableInteraction, + TextInputBuilder, + TextInputStyle, + italic, +} from "discord.js"; -import { DatadropClient } from 'src/datadrop.js'; +import type { DatadropClient } from "../datadrop.js"; +import type { Event } from "../models/Event.js"; +import { CommandHandler } from "../services/CommandHandler.js"; -export default async function interactionCreate(client: DatadropClient, interaction: Interaction) { - if (isVerificationButton(interaction)) { - if (await isAlreadyVerified(client, interaction)) return; +export default { + name: Events.InteractionCreate, + execute: interactionCreate, +} as Event; - await showVerificationModal(interaction); +async function interactionCreate( + client: DatadropClient, + interaction: Interaction, +) { + if ( + interaction.isChatInputCommand() || + interaction.isMessageContextMenuCommand() + ) { + await handleCommandInteraction(client, interaction); + } else if (isVerificationButton(interaction)) { + await handleVerificationButton(client, interaction); + } else if (isVerificationModalSubmission(interaction)) { + await handleVerificationModalSubmission(client, interaction); + } else if ( + isUnhandledInteraction(interaction) && + interaction.isRepliable() + ) { + await interaction.reply({ + content: "Ce message ne t'Ă©tait assurĂ©ment pas destinĂ©!", + ephemeral: true, + }); } - else if (isVerificationModalSubmission(interaction)) { - await interaction.deferReply({ ephemeral: true }); +} - if (await isAlreadyVerified(client, interaction)) return; +async function handleCommandInteraction( + client: DatadropClient, + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction, +) { + const commandHandler = new CommandHandler(client); + if (commandHandler.shouldExecute(interaction)) { + await commandHandler.execute(interaction); + } +} + +async function handleVerificationButton( + client: DatadropClient, + interaction: ButtonInteraction, +) { + if (await isAlreadyVerified(client, interaction)) return; + await showVerificationModal(interaction); +} - let content: string; - switch (interaction.customId) { - case `lacm${interaction.user.id}`: { +async function handleVerificationModalSubmission( + client: DatadropClient, + interaction: ModalSubmitInteraction, +) { + await interaction.deferReply({ ephemeral: true }); + + if (await isAlreadyVerified(client, interaction)) return; + + let content: string; + switch (interaction.customId) { + case `lacm${interaction.user.id}`: { + if (emailIsValid(interaction)) { content = await getEmailAndSendCode(interaction, client); - await showVerificationButton(interaction, `${content}\n${italic("D'ailleurs, l'email peut potentiellement se retrouver dans tes spams!")}`, client); - break; - } - case `lav${interaction.user.id}`: { - content = await verifyCode(interaction, client); - await interaction.editReply({ content }); - break; + await showVerificationButton( + interaction, + `${content}\n${italic("D'ailleurs, l'email peut potentiellement se retrouver dans tes spams!")}`, + client, + ); + } else { + await interaction.editReply({ + content: + "L'adresse email fournie n'est pas valide!\nSi tu es un.e Ă©tudiant.e, elle doit correspondre Ă  etuXXXXX@henallux.be.\nSi tu es un.e professeur.e, elle doit correspondre Ă  mdpXXXXX@henallux.be.", + }); } + break; + } + case `lav${interaction.user.id}`: { + content = await verifyCode(interaction, client); + await interaction.editReply({ content }); + break; } - } - else if (isUnhandledInteraction(interaction) && interaction.isRepliable()) { - await interaction.reply({ ephemeral: true, content: "Ce message ne t'Ă©tait assurĂ©ment pas destinĂ©!" }); } } -async function isAlreadyVerified(client: DatadropClient, interaction: Interaction) { +async function isAlreadyVerified( + client: DatadropClient, + interaction: Interaction, +) { const userFromDatabase = await client.database.read(interaction.user.id); if (userFromDatabase?.activatedCode) { if (interaction.isRepliable()) { const replyOptions = { - content: 'Tu as dĂ©jĂ  liĂ© ton compte HĂ©nallux avec ton compte Discord!', - ephemeral: true + content: + "Tu as dĂ©jĂ  liĂ© ton compte HĂ©nallux avec ton compte Discord!", + ephemeral: true, }; - if (interaction.replied) { + if (interaction.deferred) { await interaction.editReply(replyOptions); - } - else { + } else if (interaction.replied) { + await interaction.followUp(replyOptions); + } else { await interaction.reply(replyOptions); } } @@ -53,70 +128,113 @@ async function isAlreadyVerified(client: DatadropClient, interaction: Interactio return false; } -function isVerificationButton(interaction: Interaction): interaction is ButtonInteraction { - return interaction.isButton() && interaction.customId.startsWith('la') && interaction.customId.includes(interaction.user.id); +function isVerificationButton( + interaction: Interaction, +): interaction is ButtonInteraction { + return ( + interaction.isButton() && + interaction.customId.startsWith("la") && + interaction.customId.includes(interaction.user.id) + ); } async function showVerificationModal(interaction: ButtonInteraction) { - const modal = new ModalBuilder().setTitle('Lier son compte'); + const modal = new ModalBuilder().setTitle("Lier son compte"); const input = new TextInputBuilder(); switch (interaction.customId) { case `lae${interaction.user.id}`: modal.setCustomId(`lacm${interaction.user.id}`); - input.setLabel('Email HĂ©nallux') - .setPlaceholder('********@henallux.be') + input + .setLabel("Email HĂ©nallux") + .setPlaceholder("********@henallux.be") .setRequired(true) .setMinLength(20) .setStyle(TextInputStyle.Short) - .setCustomId('email'); + .setCustomId("email"); break; + case `lacb${interaction.user.id}`: modal.setCustomId(`lav${interaction.user.id}`); - input.setLabel('Code de vĂ©rification') - .setPlaceholder('******') + input + .setLabel("Code de vĂ©rification") + .setPlaceholder("******") .setRequired(true) .setMinLength(6) .setMaxLength(6) .setStyle(TextInputStyle.Short) - .setCustomId('code'); + .setCustomId("code"); break; } - const inputComponent = new ActionRowBuilder().addComponents(input); + const inputComponent = + new ActionRowBuilder().addComponents(input); modal.addComponents(inputComponent); await interaction.showModal(modal); } -function isVerificationModalSubmission(interaction: Interaction): interaction is ModalSubmitInteraction { - return interaction.isModalSubmit() && interaction.customId.includes(interaction.user.id); +function isVerificationModalSubmission( + interaction: Interaction, +): interaction is ModalSubmitInteraction { + return ( + interaction.isModalSubmit() && + interaction.customId.includes(interaction.user.id) + ); } -async function verifyCode(interaction: ModalSubmitInteraction, client: DatadropClient) { - const code = interaction.fields.getTextInputValue('code'); - return await client.verificationManager.verifyCode(interaction.user.id, code); +async function verifyCode( + interaction: ModalSubmitInteraction, + client: DatadropClient, +) { + const code = interaction.fields.getTextInputValue("code"); + return await client.verificationManager.verifyCode( + interaction.user.id, + code, + ); } -async function getEmailAndSendCode(interaction: ModalSubmitInteraction, client: DatadropClient) { +function emailIsValid(interaction: ModalSubmitInteraction): boolean { const regex = /(etu\d{5,}|mdp[a-z]{5,})@henallux\.be/; - const email = interaction.fields.getTextInputValue('email'); - if (!regex.test(email)) { - return "L'adresse email fournie n'est pas valide!\nSi tu es un.e Ă©tudiant.e, elle doit correspondre Ă  etuXXXXX@henallux.be.\nSi tu es un.e professeur.e, elle doit correspondre Ă  mdpXXXXX@henallux.be."; - } + const email = interaction.fields.getTextInputValue("email"); + return regex.test(email); +} - return await client.verificationManager.sendCode(interaction.user.id, { to: email, guildId: interaction.guildId }); +async function getEmailAndSendCode( + interaction: ModalSubmitInteraction, + client: DatadropClient, +) { + const email = interaction.fields.getTextInputValue("email"); + return await client.verificationManager.sendCode(interaction.user.id, { + to: email, + guildId: interaction.guildId, + }); } -async function showVerificationButton(interaction: RepliableInteraction, content: string, client: DatadropClient) { +async function showVerificationButton( + interaction: RepliableInteraction, + content: string, + client: DatadropClient, +) { const linkAccountButton = new ButtonBuilder() - .setLabel('VĂ©rifier son code') - .setEmoji('🎰') + .setLabel("VĂ©rifier son code") + .setEmoji("🎰") .setStyle(ButtonStyle.Primary) .setCustomId(`lacb${interaction.user.id}`) - .setDisabled(content.includes(client.errorMessage) || content.includes(client.activeAccountMessage)); - const buttonComponent = new ActionRowBuilder().addComponents(linkAccountButton); + .setDisabled( + content.includes(client.errorMessage) || + content.includes(client.activeAccountMessage), + ); + const buttonComponent = new ActionRowBuilder().addComponents( + linkAccountButton, + ); await interaction.editReply({ content, components: [buttonComponent] }); } function isUnhandledInteraction(interaction: Interaction) { - return (interaction.isButton() || interaction.isModalSubmit() || interaction.isStringSelectMenu()) - && !interaction.customId.startsWith('sr-') && !interaction.customId.includes(interaction.user.id) + return ( + (interaction.isButton() || + interaction.isModalSubmit() || + interaction.isStringSelectMenu()) && + !interaction.customId.startsWith("sr-") && + !interaction.customId.startsWith("ac-") && + !interaction.customId.includes(interaction.user.id) + ); } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts deleted file mode 100644 index acc791c..0000000 --- a/src/events/messageCreate.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { ChannelType, Message } from 'discord.js'; -import { DatadropClient } from '../datadrop.js'; - -const escapeRegex = (str: string | null | undefined) => str?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - -export default async function messageCreate(client: DatadropClient, message: Message) { - if (message.author.bot) return; - - const lowerCasedContent = message.content.toLowerCase(); - const prefixRegex = new RegExp(`^(<@!?${client.user!.id}>|${escapeRegex(client.config.prefix)})\\s*`); - if (!isCommand(lowerCasedContent, prefixRegex)) return; - - const commandName = getCommandNameFromMessage(message.content, prefixRegex); - const command = getCommand(client, commandName); - if (!command) return; - - logCommandUsage(client, message, command); - - if (!isCommandAllowed(client, message, command)) return; - - try { - executeCommand(client, message, command); - } catch (err) { - handleCommandError(client, message); - } -} - -function isCommand(content: string, prefixRegex: RegExp): boolean { - return prefixRegex.test(content.split(' ').join('')); -} - -function getCommandNameFromMessage(content: string, prefixRegex: RegExp): string { - const matches = prefixRegex.exec(content); - if (!matches) return ''; - const [, matchedPrefix] = matches; - const args = content.slice(matchedPrefix.length).trim().split(/ +/g) ?? []; - if (!args || args.length === 0) return ''; - return args.shift()!.toLowerCase(); -} - -function getCommand(client: DatadropClient, commandName: string) { - return client.commands.get(commandName) || client.commands.find((cmd) => cmd.aliases?.includes(commandName)); -} - -function logCommandUsage(client: DatadropClient, message: Message, command: any) { - const channelInfo = message.channel.type === ChannelType.GuildText ? `dans #${message.channel.name} (${message.channel.id})` : 'en DM'; - client.logger.info(`${message.author.tag} (${message.author.id}) a utilisĂ© '${command.name}' ${channelInfo}`); -} - -function isCommandAllowed(client: DatadropClient, message: Message, command: any): boolean { - const { ownerIds, communitymanagerRoleid, adminRoleid } = client.config; - - const isAuthorized = ownerIds.includes(message.author.id) || message.member!.roles.cache.get(communitymanagerRoleid) || message.member!.roles.cache.get(adminRoleid); - if ((command.adminOnly || command.ownerOnly) && !isAuthorized) return false; - - if (command.guildOnly && message.channel.type !== ChannelType.GuildText) { - message.reply("Je ne peux pas exĂ©cuter cette commande en dehors d'une guilde!"); - return false; - } - - if (command.args && !message.content.includes(' ')) { - const reply = `Vous n'avez pas donnĂ© d'arguments, ${message.author}!`; - if (command.usage) { - message.reply(`${reply}\nL'utilisation correcte de cette commande est : \`${message.content} ${command.usage}\``); - } else { - message.reply(reply); - } - return false; - } - - return true; -} - -function executeCommand(client: DatadropClient, message: Message, command: any) { - command.execute(client, message, message.content.split(' ').slice(1)); -} - -function handleCommandError(client: DatadropClient, message: Message) { - client.logger.error('An error occurred while executing the command'); - message.reply(":x: **Oups!** - Une erreur est apparue en essayant cette commande. Reporte-le Ă  un membre du Staff s'il te plaĂźt!"); -} diff --git a/src/events/ready.ts b/src/events/ready.ts index a73bfac..034e605 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,79 +1,106 @@ -import { Role, roleMention, bold, Snowflake, ButtonStyle } from 'discord.js'; +import { ButtonStyle, Events, Role, bold, roleMention } from "discord.js"; -import { RoleToEmojiData } from '@hunteroi/discord-selfrole'; +import type { RoleToEmojiData } from "@hunteroi/discord-selfrole"; -import { DatadropClient } from '../datadrop.js'; -import { Configuration } from '../models/Configuration.js'; +import type { DatadropClient } from "../datadrop.js"; +import type { Configuration } from "../models/Configuration.js"; +import type { Event } from "../models/Event.js"; -export default async function ready(client: DatadropClient) { - const { config } = client; - await registerRolesChannels(client, config); - await registerDynamicChannels(client, config); +export default { + name: Events.ClientReady, + once: true, + execute: ready, +} as Event; - await client.user?.setUsername(config.botName); - if (config.version) client.user?.setActivity({ name: config.version }); +async function ready(client: DatadropClient) { + const { config } = client; + await registerRolesChannels(client, config); + await registerDynamicChannels(client, config); - client.logger.info(`ConnectĂ© en tant que ${client.user?.tag}, version ${config.version}!`); -}; + await client.user?.setUsername(config.botName); + if (config.version) client.user?.setActivity({ name: config.version }); -async function registerRolesChannels(client: DatadropClient, config: Configuration): Promise { - const { rolesChannelid, first, second, third, alumni, tutor, announce } = config; - const message = { - options: { - sendAsEmbed: true, - format: (rte: RoleToEmojiData) => { - const initialFormatedRte = `${rte.emoji} - ${rte.role instanceof Role ? rte.role : roleMention(rte.role)}`; - return rte.smallNote ? `${initialFormatedRte} (${rte.smallNote})` : initialFormatedRte; - }, - descriptionPrefix: bold( - 'Utilisez les boutons suivants pour vous attribuer/retirer le rĂŽle souhaitĂ©!' - ) - } - }; + client.logger.info( + `ConnectĂ© en tant que ${client.user?.tag}, version ${config.version}!`, + ); +} - await Promise.all([ - client.selfRoleManager.registerChannel(rolesChannelid, { - rolesToEmojis: [ - ...([first, second, third, alumni, tutor, announce].map(cfg => ({ role: cfg.roleid, emoji: cfg.emote }))), - ], - message: { - ...message, +async function registerRolesChannels( + client: DatadropClient, + config: Configuration, +): Promise { + const { rolesChannelid, first, second, third, alumni, tutor, announce } = + config; + const message = { options: { - ...message.options, - descriptionSuffix: - '\nLes Professeurs, les DĂ©lĂ©guĂ©s et les membres du ComitĂ© IODA doivent notifier un Admin/Community Manager pour avoir leur rĂŽle.' - } - } - }), - ...([first, second, third].map(({ roleid, channelid, groups }) => - client.selfRoleManager.registerChannel(channelid, { - rolesToEmojis: groups.map((group) => ({ - role: group.roleid, - emoji: group.emote, - requiredRoles: [roleid] - })), - message, - maxRolesAssigned: 1, - selectMenu: { - placeholder: 'SĂ©lectionnez votre groupe', - resetButton: { - label: 'Retirer le rĂŽle', - emoji: 'đŸ—‘ïž', - style: ButtonStyle.Danger - } - } - }))), - ]); + sendAsEmbed: true, + format: (rte: RoleToEmojiData) => { + const initialFormatedRte = `${rte.emoji} - ${rte.role instanceof Role ? rte.role : roleMention(rte.role)}`; + return rte.smallNote + ? `${initialFormatedRte} (${rte.smallNote})` + : initialFormatedRte; + }, + descriptionPrefix: bold( + "Utilisez les boutons suivants pour vous attribuer/retirer le rĂŽle souhaitĂ©!", + ), + }, + }; + + await Promise.all([ + client.selfRoleManager.registerChannel(rolesChannelid, { + rolesToEmojis: [ + ...[first, second, third, alumni, tutor, announce].map( + (cfg) => ({ role: cfg.roleid, emoji: cfg.emote }), + ), + ], + message: { + ...message, + options: { + ...message.options, + descriptionSuffix: + "\nLes Professeurs, les DĂ©lĂ©guĂ©s et les membres du ComitĂ© IODA doivent notifier un Admin/Community Manager pour avoir leur rĂŽle.", + }, + }, + }), + ...[first, second, third].map(({ roleid, channelid, groups }) => + client.selfRoleManager.registerChannel(channelid, { + rolesToEmojis: groups.map((group) => ({ + role: group.roleid, + emoji: group.emote, + requiredRoles: [roleid], + })), + message, + maxRolesAssigned: 1, + selectMenu: { + placeholder: "SĂ©lectionnez votre groupe", + resetButton: { + label: "Retirer le rĂŽle", + emoji: "đŸ—‘ïž", + style: ButtonStyle.Danger, + }, + }, + }), + ), + ]); } -async function registerDynamicChannels(client: DatadropClient, config: Configuration): Promise { - const { dynamicChannelPrefix, dynamicChannelPrefixRegex, staticTriggerChannelids } = config; - staticTriggerChannelids.forEach((id: Snowflake) => client.tempChannelsManager.registerChannel(id, { - childAutoDeleteIfEmpty: true, - childAutoDeleteIfParentGetsUnregistered: true, - childAutoDeleteIfOwnerLeaves: false, - childVoiceFormat: (str) => `${dynamicChannelPrefix} ${str}`, - childVoiceFormatRegex: dynamicChannelPrefixRegex, - childCanBeRenamed: true - })); +async function registerDynamicChannels( + client: DatadropClient, + config: Configuration, +): Promise { + const { + dynamicChannelPrefix, + dynamicChannelPrefixRegex, + staticTriggerChannelids, + } = config; + for (const id of staticTriggerChannelids) { + client.tempChannelsManager.registerChannel(id, { + childAutoDeleteIfEmpty: true, + childAutoDeleteIfParentGetsUnregistered: true, + childAutoDeleteIfOwnerLeaves: false, + childVoiceFormat: (str) => `${dynamicChannelPrefix} ${str}`, + childVoiceFormatRegex: dynamicChannelPrefixRegex, + childCanBeRenamed: true, + }); + } } diff --git a/src/helpers.ts b/src/helpers.ts index b48999a..d9d331e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,20 +1,43 @@ -import * as fs from 'fs'; +import * as fsp from "node:fs/promises"; +import * as path from "node:path"; +import { ConsoleLogger, type DefaultLogger } from "@hunteroi/advanced-logger"; -export function readFilesFrom(path: string, callback: (name: string, props: any) => void): void { - fs.readdir(path, async (err, files) => { - if (err) return console.error; +const console = new ConsoleLogger(); + +export async function readFilesFrom( + directory: string, + callback: (name: string, props: T) => void, + logger: DefaultLogger = console, +): Promise { + try { + logger.debug(`Lecture du rĂ©pertoire ${directory}`); + + const files = await fsp.readdir(directory); for (const file of files) { - if (!file.endsWith('.js')) return; - const props = await import(`${path}/${file}`); - const fileName = file.split('.')[0]; - callback(fileName, props.default); + const filePath = path.join(directory, file); + const stats = await fsp.stat(filePath); + + if (stats.isDirectory()) { + await readFilesFrom(filePath, callback, logger); + continue; + } + + if (stats.isFile() && !file.endsWith(".js")) continue; + + logger.debug(`Lecture du fichier ${filePath}`); + + const props = await import(filePath); + callback(file.replace(".js", ""), props.default as T); } - }); + } catch (err) { + logger.error(getErrorMessage(err)); + } } +// biome-ignore lint/suspicious/noExplicitAny: evaluated code is of type "any" export function clean(text: any): string { - if (typeof (text) === 'string') { - return text.replace(/@/g, '@​'); + if (typeof text === "string") { + return text.replace(/@/g, "@​"); } return text; } @@ -23,15 +46,15 @@ export function clean(text: any): string { // source: https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript type ErrorWithMessage = { - message: string -} + message: string; +}; function isErrorWithMessage(error: unknown): error is ErrorWithMessage { return ( - typeof error === 'object' && + typeof error === "object" && error !== null && - 'message' in error && - typeof (error as Record).message === 'string' + "message" in error && + typeof (error as Record).message === "string" ); } diff --git a/src/models/Command.ts b/src/models/Command.ts new file mode 100644 index 0000000..40a6ee0 --- /dev/null +++ b/src/models/Command.ts @@ -0,0 +1,19 @@ +import type { + ChatInputCommandInteraction, + ContextMenuCommandBuilder, + MessageContextMenuCommandInteraction, + SlashCommandBuilder, +} from "discord.js"; + +import type { DatadropClient } from "../datadrop.js"; + +export interface Command { + data: SlashCommandBuilder | ContextMenuCommandBuilder; + ownerOnly?: boolean; + execute( + client: DatadropClient, + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction, + ): Promise; +} diff --git a/src/models/Configuration.ts b/src/models/Configuration.ts index 93066af..5c70eb2 100644 --- a/src/models/Configuration.ts +++ b/src/models/Configuration.ts @@ -1,6 +1,6 @@ -import { Snowflake } from 'discord.js'; +import type { Snowflake } from "discord.js"; -import { SendGridOptions } from '@hunteroi/discord-verification/lib/services/SendGridService.js'; +import type { SendGridOptions } from "@hunteroi/discord-verification/lib/services/SendGridService.js"; export interface SpecialRoleConfiguration { roleid: Snowflake; @@ -31,9 +31,9 @@ export interface Configuration { guildId: Snowflake; ownerIds: Snowflake[]; - prefix: string | undefined; version: string | undefined; botName: string; + botId: Snowflake; communitymanagerRoleid: Snowflake; adminRoleid: Snowflake; @@ -59,8 +59,5 @@ export interface Configuration { announce: AnnounceConfiguration; - ok_hand: string; - warning: string; - communicationServiceOptions: SendGridOptions; } diff --git a/src/models/Event.ts b/src/models/Event.ts new file mode 100644 index 0000000..a9511a8 --- /dev/null +++ b/src/models/Event.ts @@ -0,0 +1,11 @@ +import type { ClientEvents } from "discord.js"; +import type { DatadropClient } from "../datadrop.js"; + +export interface Event { + name: keyof ClientEvents; + once?: boolean; + execute( + client: DatadropClient, + ...args: ClientEvents[this["name"]] + ): Promise; +} diff --git a/src/models/IDatabaseService.ts b/src/models/IDatabaseService.ts index 02bec41..1bba740 100644 --- a/src/models/IDatabaseService.ts +++ b/src/models/IDatabaseService.ts @@ -1,29 +1,29 @@ -import { Snowflake } from "discord.js"; +import type { Snowflake } from "discord.js"; -import { IStoringSystem } from '@hunteroi/discord-verification'; +import type { IStoringSystem } from "@hunteroi/discord-verification"; -import { User } from './User.js'; +import type { User } from "./User.js"; export type IDatabaseService = { /** * Opens the connection with the database. */ - start: () => Promise, + start: () => Promise; /** * Closes the connection with the database. */ - stop: () => Promise, + stop: () => Promise; /** * Soft-deletes the user whose id is userid from the database (sets a flag to true). * @param userid */ - delete: (userid: Snowflake) => Promise, + delete: (userid: Snowflake) => Promise; /** * Undo the soft-delete of the user whose id is userid (sets a flag to false). * */ - undoDelete: (userid: Snowflake) => Promise, + undoDelete: (userid: Snowflake) => Promise; } & IStoringSystem; diff --git a/src/models/User.ts b/src/models/User.ts index 38742b9..a64e18e 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,4 +1,4 @@ -import { IUser } from '@hunteroi/discord-verification'; +import type { IUser } from "@hunteroi/discord-verification"; export interface User extends IUser { createdAt: Date; diff --git a/src/services/CommandHandler.ts b/src/services/CommandHandler.ts new file mode 100644 index 0000000..1332f96 --- /dev/null +++ b/src/services/CommandHandler.ts @@ -0,0 +1,137 @@ +import { + ChannelType, + type ChatInputCommandInteraction, + type MessageContextMenuCommandInteraction, +} from "discord.js"; + +import type { DatadropClient } from "../datadrop.js"; +import type { Command } from "../models/Command.js"; + +type AuthorizationResponse = { + error?: string; +}; + +export class CommandHandler { + readonly #client: DatadropClient; + + constructor(client: DatadropClient) { + this.#client = client; + } + + shouldExecute( + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction, + ): boolean { + return true; + } + + async execute( + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction, + ): Promise { + const command = this.#client.commands.get(interaction.commandName); + if (!command) { + await interaction.reply({ + content: "❌ **Oups!** - Cette commande n'existe pas.", + ephemeral: true, + }); + return; + } + + const authorizationResponse = await this.#checkAuthorization( + interaction, + command, + ); + this.#logUsage(interaction, command, !authorizationResponse.error); + + if (authorizationResponse.error) { + await interaction.reply({ + content: authorizationResponse.error, + ephemeral: true, + }); + return; + } + + try { + await command.execute(this.#client, interaction); + } catch (err) { + this.#client.logger.error( + `Une erreur est survenue lors de l'exĂ©cution de la commande <${command.data.name}>: ${JSON.stringify(err)}`, + ); + const replyOptions = { + content: + "❌ **Oups!** - Une erreur est survenue en essayant cette commande. Reporte-le Ă  un membre du Staff s'il te plaĂźt!", + ephemeral: true, + }; + if (interaction.deferred) { + await interaction.editReply(replyOptions.content); + } else if (interaction.replied) { + await interaction.followUp(replyOptions); + } else { + await interaction.reply(replyOptions); + } + } + } + + /** + * Side-effect function to log the usage of the command. + * @param message The message + * @param command The command + * @param isAuthorized Whether the user responsible of the message is authorized to use the command or not. + */ + #logUsage( + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction, + command: Command, + isAuthorized: boolean, + ): void { + let channelInfo: string; + const channel = interaction.channel; + if (!channel) { + channelInfo = "dans un canal inconnu"; + } else { + channelInfo = + channel.type === ChannelType.GuildText + ? `dans #${channel.name} (${channel.id})` + : "en DM"; + } + const author = interaction.user; + this.#client.logger.info( + `${author.tag} (${author.id}) a utilisĂ© '${command.data.name}' ${channelInfo} ${isAuthorized ? "avec" : "sans"} autorisation`, + ); + } + + async #checkAuthorization( + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction, + command: Command, + ): Promise { + const { ownerIds, communitymanagerRoleid, adminRoleid } = + this.#client.config; + + const member = await interaction.guild?.members.fetch( + interaction.user.id, + ); + if (!member) { + return { + error: "❌ **Oups!** - Je n'ai pas pu rĂ©cupĂ©rer vos informations de membre. RĂ©essayez plus tard.", + }; + } + + const canBypassAuthorization = + ownerIds.includes(interaction.user.id) || + member?.roles.cache.has(communitymanagerRoleid) || + member?.roles.cache.has(adminRoleid); + if (command.ownerOnly && !canBypassAuthorization) { + return { + error: "❌ **Oups!** - Cette commande est rĂ©servĂ©e Ă  un nombre limitĂ© de personnes dont vous ne faites pas partie.", + }; + } + + return {}; + } +} diff --git a/src/services/PostgresDatabaseService.ts b/src/services/PostgresDatabaseService.ts index 46fca8b..cee8ada 100644 --- a/src/services/PostgresDatabaseService.ts +++ b/src/services/PostgresDatabaseService.ts @@ -1,14 +1,19 @@ -import { Snowflake } from 'discord.js'; -import { Client, DatabaseError, PreparedStatement, Value } from 'ts-postgres'; +import type { Snowflake } from "discord.js"; +import { + Client, + type DatabaseError, + type PreparedStatement, + type Value, +} from "ts-postgres"; -import { ConsoleLogger } from '@hunteroi/advanced-logger'; +import type { ConsoleLogger } from "@hunteroi/advanced-logger"; -import { User } from '../models/User.js'; -import { IDatabaseService } from '../models/IDatabaseService.js'; +import type { IDatabaseService } from "../models/IDatabaseService.js"; +import type { User } from "../models/User.js"; export default class PostgresDatabaseService implements IDatabaseService { - #logger: ConsoleLogger; - #database: Client; + readonly #logger: ConsoleLogger; + readonly #database: Client; /** * Creates an instance of DatabaseService. @@ -22,7 +27,7 @@ export default class PostgresDatabaseService implements IDatabaseService { password: process.env.POSTGRES_PASSWORD, host: process.env.DATABASE_HOST, port: Number(process.env.DATABASE_PORT), - database: process.env.POSTGRES_DB + database: process.env.POSTGRES_DB, }); this.#listenToDatabaseEvents(); } @@ -60,11 +65,16 @@ export default class PostgresDatabaseService implements IDatabaseService { END; $set_updatedAt$ LANGUAGE plpgsql; `); - await this.#database.query(`CREATE OR REPLACE TRIGGER users_update BEFORE UPDATE ON Users FOR EACH ROW EXECUTE PROCEDURE set_updatedAt();`); + await this.#database.query( + "CREATE OR REPLACE TRIGGER users_update BEFORE UPDATE ON Users FOR EACH ROW EXECUTE PROCEDURE set_updatedAt();", + ); - const jobs = await this.#database.query('SELECT * FROM cron.job'); + const jobs = await this.#database.query("SELECT * FROM cron.job"); if (jobs.rows.length === 0) { - await this.#database.query(`SELECT cron.schedule($1, $2);`, ['0 0 * * *', "DELETE FROM Users WHERE isDeleted IS NOT NULL AND isDeleted < NOW() - INTERVAL '6 months'"]); + await this.#database.query("SELECT cron.schedule($1, $2);", [ + "0 0 * * *", + "DELETE FROM Users WHERE isDeleted IS NOT NULL AND isDeleted < NOW() - INTERVAL '6 months'", + ]); } await this.#runMigrations(); @@ -81,49 +91,86 @@ export default class PostgresDatabaseService implements IDatabaseService { * @inherited */ public async read(userid: Snowflake): Promise { - this.#logger.verbose(`Lecture de l'utilisateur sur base de l'identifiant ${userid}`); - const statement = await this.#database.prepare('SELECT * FROM Users WHERE userId = $1;'); + this.#logger.verbose( + `Lecture de l'utilisateur sur base de l'identifiant ${userid}`, + ); + const statement = await this.#database.prepare( + "SELECT * FROM Users WHERE userId = $1;", + ); return await this.#executeStatement(statement, [userid]); } /** * @inherited */ - public async readBy(argument: Map | ((user: User, index: string | number) => boolean)): Promise { - if (!(argument instanceof Map)) throw new Error('Method not implemented.'); + public async readBy( + argument: // biome-ignore lint/suspicious/noExplicitAny: DB values can be of any type + Map | ((user: User, index: string | number) => boolean), + ): Promise { + if (!(argument instanceof Map)) + throw new Error("Method not implemented."); - this.#logger.verbose(`Lecture de l'utilisateur sur base des filtres ${JSON.stringify(argument)}`); - let sqlQuery = 'SELECT * FROM Users'; + this.#logger.verbose( + `Lecture de l'utilisateur sur base des filtres ${JSON.stringify(argument)}`, + ); + let sqlQuery = "SELECT * FROM Users"; let nbArguments = 1; while (nbArguments <= argument.size * 2) { - if (!sqlQuery.includes('WHERE')) sqlQuery += ' WHERE '; - else sqlQuery += ' AND '; + if (!sqlQuery.includes("WHERE")) sqlQuery += " WHERE "; + else sqlQuery += " AND "; sqlQuery += `$${nbArguments} = $${++nbArguments}`; nbArguments++; } const statement = await this.#database.prepare(sqlQuery); - return await this.#executeStatement(statement, [...argument.entries()].flat()); + return await this.#executeStatement( + statement, + [...argument.entries()].flat(), + ); } /** * @inherited */ public async write(user: User): Promise { - this.#logger.verbose(`Écriture de l'utilisateur ${JSON.stringify(user)}`); + this.#logger.verbose( + `Écriture de l'utilisateur ${JSON.stringify(user)}`, + ); const userid = user.userid; const data = user.data; const offset = 3; const userEntries = Object.entries(user); - const insertColumns = ['status', 'code', 'nbCodeCalled', 'nbVerifyCalled', 'username'].sort(this.#ascendingSort); - const insertValues = userEntries.filter(([prop]) => insertColumns.includes(prop)).sort(([prop1], [prop2]) => this.#ascendingSort(prop1, prop2)); - const updateColumns = ['status', 'code', 'nbCodeCalled', 'nbVerifyCalled', 'activatedCode', 'activationTimestamp'].sort(this.#ascendingSort); - const updateValues = userEntries.filter(([prop]) => updateColumns.includes(prop)).sort(([prop1], [prop2]) => this.#ascendingSort(prop1, prop2)); + const insertColumns = [ + "status", + "code", + "nbCodeCalled", + "nbVerifyCalled", + "username", + ].sort(this.#ascendingSort); + const insertValues = userEntries + .filter(([prop]) => insertColumns.includes(prop)) + .sort(([prop1], [prop2]) => this.#ascendingSort(prop1, prop2)); + const updateColumns = [ + "status", + "code", + "nbCodeCalled", + "nbVerifyCalled", + "activatedCode", + "activationTimestamp", + ].sort(this.#ascendingSort); + const updateValues = userEntries + .filter(([prop]) => updateColumns.includes(prop)) + .sort(([prop1], [prop2]) => this.#ascendingSort(prop1, prop2)); const sqlQuery = `INSERT INTO Users (userId, data, createdAt, ${this.#listParameters(insertColumns)}) VALUES ($1, $2, NOW(), ${this.#listValues(insertValues, offset)}) ON CONFLICT (userId) DO UPDATE SET ${this.#listParametersWithValues(updateValues, offset + insertValues.length)} WHERE EXCLUDED.userId = $1;`; - const values = [userid, JSON.stringify(data), ...this.#deconstructValues(insertValues), ...this.#deconstructValues(updateValues)]; + const values = [ + userid, + JSON.stringify(data), + ...this.#deconstructValues(insertValues), + ...this.#deconstructValues(updateValues), + ]; const statement = await this.#database.prepare(sqlQuery); await this.#executeStatement(statement, values, false); @@ -133,8 +180,12 @@ export default class PostgresDatabaseService implements IDatabaseService { * @inherited */ public async delete(userid: Snowflake): Promise { - this.#logger.verbose(`Suppresion de l'utilisateur sur base de l'identifiant ${userid}`); - const statement = await this.#database.prepare('UPDATE Users SET isDeleted = NOW() WHERE userId = $1;'); + this.#logger.verbose( + `Suppresion de l'utilisateur sur base de l'identifiant ${userid}`, + ); + const statement = await this.#database.prepare( + "UPDATE Users SET isDeleted = NOW() WHERE userId = $1;", + ); await this.#executeStatement(statement, [userid], false); } @@ -142,31 +193,51 @@ export default class PostgresDatabaseService implements IDatabaseService { * @inherited */ public async undoDelete(userid: Snowflake): Promise { - this.#logger.verbose(`RĂ©version de la suppresion de l'utilisateur sur base de l'identifiant ${userid}`); - const statement = await this.#database.prepare('UPDATE Users SET isDeleted = NULL WHERE userId = $1;'); + this.#logger.verbose( + `RĂ©version de la suppresion de l'utilisateur sur base de l'identifiant ${userid}`, + ); + const statement = await this.#database.prepare( + "UPDATE Users SET isDeleted = NULL WHERE userId = $1;", + ); await this.#executeStatement(statement, [userid], false); } //#region private async #runMigrations(): Promise { - await this.#runMigration('User soft delete', async () => { - await this.#database.query('ALTER TABLE Users ADD isDeleted timestamp;'); + await this.#runMigration("User soft delete", async () => { + await this.#database.query( + "ALTER TABLE Users ADD IF NOT EXISTS isDeleted timestamp;", + ); }); } async #runMigration(name: string, callback: () => Promise) { - const result = await this.#database.query('SELECT * FROM Migrations WHERE name = $1', [name]); + const result = await this.#database.query( + "SELECT * FROM Migrations WHERE name = $1", + [name], + ); const migration = [...result].pop(); if (!migration) { - await this.#database.query('INSERT INTO Migrations (name) VALUES($1);', [name]); + await this.#database.query( + "INSERT INTO Migrations (name) VALUES($1);", + [name], + ); await callback(); } } #listenToDatabaseEvents() { - this.#database.on('connect', () => this.#logger.info('Connexion Ă©tablie avec la base de donnĂ©es!')); - this.#database.on('end', () => this.#logger.info('Connexion fermĂ©e avec la base de donnĂ©es!')); - this.#database.on('error', (error: DatabaseError) => this.#logger.error(`Une erreur est survenue lors de l'utilisation de la base de donnĂ©es: \n${error.message}`)); + this.#database.on("connect", () => + this.#logger.info("Connexion Ă©tablie avec la base de donnĂ©es!"), + ); + this.#database.on("end", () => + this.#logger.info("Connexion fermĂ©e avec la base de donnĂ©es!"), + ); + this.#database.on("error", (error: DatabaseError) => + this.#logger.error( + `Une erreur est survenue lors de l'utilisation de la base de donnĂ©es: \n${error.message}`, + ), + ); } #ascendingSort(string1: string, string2: string): number { @@ -176,23 +247,36 @@ export default class PostgresDatabaseService implements IDatabaseService { } #listParameters(parameters: string[]): string { - return parameters.join(', '); + return parameters.join(", "); } + // biome-ignore lint/suspicious/noExplicitAny: DB values can be of any type #listValues(values: [string, any][], offset: number): string { - return values.map((_, index) => '$' + (index + offset)).join(', '); + return values.map((_, index) => `$${index + offset}`).join(", "); } + // biome-ignore lint/suspicious/noExplicitAny: DB values can be of any type #listParametersWithValues(values: [string, any][], offset: number) { - return values.map(([prop], index) => prop + ' = $' + (index + offset)).join(', '); + return values + .map(([prop], index) => `${prop} = $${index + offset}`) + .join(", "); } + // biome-ignore lint/suspicious/noExplicitAny: DB values can be of any type #deconstructValues(values: [string, any][]): Value[] { - return values.flatMap(([, v]) => v instanceof Object && typeof v !== 'bigint' ? JSON.stringify(v) : (v ?? null)); + return values.flatMap(([, v]) => + v instanceof Object && typeof v !== "bigint" + ? JSON.stringify(v) + : (v ?? null), + ); } - async #executeStatement(statement: PreparedStatement, values: Value[] = [], isSelect = true): Promise { - const notFoundMessage = 'User not found'; + async #executeStatement( + statement: PreparedStatement, + values: Value[] = [], + isSelect = true, + ): Promise { + const notFoundMessage = "User not found"; try { const entities = await statement.execute(values); if (!isSelect) return null; @@ -200,26 +284,30 @@ export default class PostgresDatabaseService implements IDatabaseService { const entity = [...entities].pop(); if (!entity) throw new Error(notFoundMessage); - const asDate = (value: string | undefined | null): Date | null => value ? new Date(value) : null; - const asInteger = (value: string | undefined | null): number | null => value ? parseInt(value) : null; + const asDate = (value: string | undefined | null): Date | null => + value ? new Date(value) : null; + const asInteger = ( + value: string | undefined | null, + ): number | null => (value ? Number.parseInt(value) : null); const user = { - userid: entity.get('userid')?.valueOf(), - data: JSON.parse(entity.get('data')?.toString() ?? '{}'), - username: entity.get('username')?.valueOf(), - createdAt: asDate(entity.get('createdat')?.valueOf() as string), - updatedAt: asDate(entity.get('updatedat')?.valueOf() as string), - status: entity.get('status')?.valueOf(), - code: entity.get('code')?.valueOf(), - activatedCode: entity.get('activatedcode')?.valueOf(), - activationTimestamp: asInteger(entity.get('activationtimestamp')?.valueOf() as string), - nbCodeCalled: entity.get('nbcodecalled')?.valueOf(), - nbVerifyCalled: entity.get('nbverifycalled')?.valueOf(), - isDeleted: asDate(entity.get('isdeleted')?.valueOf() as string), + userid: entity.get("userid")?.valueOf(), + data: JSON.parse(entity.get("data")?.toString() ?? "{}"), + username: entity.get("username")?.valueOf(), + createdAt: asDate(entity.get("createdat")?.valueOf() as string), + updatedAt: asDate(entity.get("updatedat")?.valueOf() as string), + status: entity.get("status")?.valueOf(), + code: entity.get("code")?.valueOf(), + activatedCode: entity.get("activatedcode")?.valueOf(), + activationTimestamp: asInteger( + entity.get("activationtimestamp")?.valueOf() as string, + ), + nbCodeCalled: entity.get("nbcodecalled")?.valueOf(), + nbVerifyCalled: entity.get("nbverifycalled")?.valueOf(), + isDeleted: asDate(entity.get("isdeleted")?.valueOf() as string), } as User; return user; - } - catch (error: unknown) { + } catch (error: unknown) { const err = error; if (err.message === notFoundMessage) { this.#logger.verbose(err.message); @@ -227,8 +315,7 @@ export default class PostgresDatabaseService implements IDatabaseService { this.#logger.error(err.message); } return null; - } - finally { + } finally { await statement.close(); } } diff --git a/yarn.lock b/yarn.lock index bba3d14..80aecab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,71 @@ # yarn lockfile v1 +"@biomejs/biome@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.9.4.tgz#89766281cbc3a0aae865a7ff13d6aaffea2842bf" + integrity sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog== + optionalDependencies: + "@biomejs/cli-darwin-arm64" "1.9.4" + "@biomejs/cli-darwin-x64" "1.9.4" + "@biomejs/cli-linux-arm64" "1.9.4" + "@biomejs/cli-linux-arm64-musl" "1.9.4" + "@biomejs/cli-linux-x64" "1.9.4" + "@biomejs/cli-linux-x64-musl" "1.9.4" + "@biomejs/cli-win32-arm64" "1.9.4" + "@biomejs/cli-win32-x64" "1.9.4" + +"@biomejs/cli-darwin-arm64@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz#dfa376d23a54a2d8f17133c92f23c1bf2e62509f" + integrity sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw== + +"@biomejs/cli-darwin-x64@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz#eafc2ce3849d385fc02238aad1ca4a73395a64d9" + integrity sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg== + +"@biomejs/cli-linux-arm64-musl@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz#d780c3e01758fc90f3268357e3f19163d1f84fca" + integrity sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA== + +"@biomejs/cli-linux-arm64@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz#8ed1dd0e89419a4b66a47f95aefb8c46ae6041c9" + integrity sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g== + +"@biomejs/cli-linux-x64-musl@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz#f36982b966bd671a36671e1de4417963d7db15fb" + integrity sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg== + +"@biomejs/cli-linux-x64@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz#a0a7f56680c76b8034ddc149dbf398bdd3a462e8" + integrity sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg== + +"@biomejs/cli-win32-arm64@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz#e2ef4e0084e76b7e26f0fc887c5ef1265ea56200" + integrity sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg== + +"@biomejs/cli-win32-x64@1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz#4c7afa90e3970213599b4095e62f87e5972b2340" + integrity sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA== + +"@discordjs/builders@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-0.5.0.tgz#646cbea9cc67f68639e6fb70ed1278b26dacdb14" + integrity sha512-HP5y4Rqw68o61Qv4qM5tVmDbWi4mdTFftqIOGRo33SNPpLJ1Ga3KEIR2ibKofkmsoQhEpLmopD1AZDs3cKpHuw== + dependencies: + "@sindresorhus/is" "^4.0.1" + discord-api-types "^0.22.0" + ow "^0.27.0" + ts-mixer "^6.0.0" + tslib "^2.3.0" + "@discordjs/builders@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.9.0.tgz#71fa6de91132bd1deaff2a9daea7aa5d5c9f124a" @@ -20,11 +85,25 @@ resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.3.tgz#5a1250159ebfff9efa4f963cfa7e97f1b291be18" integrity sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ== +"@discordjs/collection@^0.2.1": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.2.4.tgz#c8ff2250430dcec7324dd4aafd1ccbcbdfa9ac14" + integrity sha512-PVrEJH+V6Ob0OwfagYQ/57kwt/HNEJxt5jqY4P+S3st9y29t9iokdnGMQoJXG5VEMAQIPbzu9Snw1F6yE8PdLA== + "@discordjs/collection@^2.1.0", "@discordjs/collection@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-2.1.1.tgz#901917bc538c12b9c3613036d317847baee08cae" integrity sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg== +"@discordjs/form-data@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@discordjs/form-data/-/form-data-3.0.1.tgz#5c9e6be992e2e57d0dfa0e39979a850225fb4697" + integrity sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + "@discordjs/formatters@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@discordjs/formatters/-/formatters-0.5.0.tgz#2d284c4271bc41984339936df1d0164e470f3b7a" @@ -67,69 +146,6 @@ tslib "^2.6.2" ws "^8.16.0" -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" - integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== - -"@eslint/config-array@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d" - integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw== - dependencies: - "@eslint/object-schema" "^2.1.4" - debug "^4.3.1" - minimatch "^3.1.2" - -"@eslint/eslintrc@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" - integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^10.0.1" - globals "^14.0.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/js@9.10.0": - version "9.10.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.10.0.tgz#eaa3cb0baec497970bb29e43a153d0d5650143c6" - integrity sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g== - -"@eslint/object-schema@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" - integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== - -"@eslint/plugin-kit@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz#809b95a0227ee79c3195adfb562eb94352e77974" - integrity sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ== - dependencies: - levn "^0.4.1" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/retry@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" - integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== - "@hunteroi/advanced-logger@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@hunteroi/advanced-logger/-/advanced-logger-0.2.0.tgz#750064e84e2eb04e3e838c789815d41547572876" @@ -153,28 +169,7 @@ resolved "https://registry.yarnpkg.com/@hunteroi/discord-verification/-/discord-verification-1.5.0.tgz#4278904eba608aa8db70c19ec65994f553630a61" integrity sha512-BSn+gLc9XpC1xvU+qw5WXo5utEjwqbM8Q6lQTPiZmX3F3u+PuRt+Sl2JAQnztYzneCxUy4IORneZE7bevSKfuA== -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@sapphire/async-queue@^1.5.2", "@sapphire/async-queue@^1.5.3": +"@sapphire/async-queue@^1.1.4", "@sapphire/async-queue@^1.5.2", "@sapphire/async-queue@^1.5.3": version "1.5.3" resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.3.tgz#03cd2a2f3665068f314736bdc56eee2025352422" integrity sha512-x7zadcfJGxFka1Q3f8gCts1F0xMwCKbZweM85xECGI0hBTeIZJGGCrHgLggihBoprlQ/hBmDR5LKfIPqnmHM3w== @@ -215,6 +210,11 @@ "@sendgrid/client" "^8.1.3" "@sendgrid/helpers" "^8.0.0" +"@sindresorhus/is@^4.0.1": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + "@types/node@*": version "22.5.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" @@ -229,6 +229,13 @@ dependencies: undici-types "~6.19.2" +"@types/ws@^7.4.7": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + "@types/ws@^8.5.10": version "8.5.12" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" @@ -236,117 +243,11 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^7.6.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz#b16d3cf3ee76bf572fdf511e79c248bdec619ea3" - integrity sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.18.0" - "@typescript-eslint/type-utils" "7.18.0" - "@typescript-eslint/utils" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" - graphemer "^1.4.0" - ignore "^5.3.1" - natural-compare "^1.4.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/parser@^7.6.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.18.0.tgz#83928d0f1b7f4afa974098c64b5ce6f9051f96a0" - integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg== - dependencies: - "@typescript-eslint/scope-manager" "7.18.0" - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/typescript-estree" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz#c928e7a9fc2c0b3ed92ab3112c614d6bd9951c83" - integrity sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA== - dependencies: - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" - -"@typescript-eslint/type-utils@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz#2165ffaee00b1fbbdd2d40aa85232dab6998f53b" - integrity sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA== - dependencies: - "@typescript-eslint/typescript-estree" "7.18.0" - "@typescript-eslint/utils" "7.18.0" - debug "^4.3.4" - ts-api-utils "^1.3.0" - -"@typescript-eslint/types@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9" - integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ== - -"@typescript-eslint/typescript-estree@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz#b5868d486c51ce8f312309ba79bdb9f331b37931" - integrity sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA== - dependencies: - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/utils@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.18.0.tgz#bca01cde77f95fc6a8d5b0dbcbfb3d6ca4be451f" - integrity sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "7.18.0" - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/typescript-estree" "7.18.0" - -"@typescript-eslint/visitor-keys@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7" - integrity sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg== - dependencies: - "@typescript-eslint/types" "7.18.0" - eslint-visitor-keys "^3.4.3" - "@vladfrangu/async_event_emitter@^2.2.4", "@vladfrangu/async_event_emitter@^2.4.6": version "2.4.6" resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz#508b6c45b03f917112a9008180b308ba0e4d1805" integrity sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA== -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^8.12.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== - -ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -354,16 +255,6 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -378,34 +269,7 @@ axios@^1.6.8: form-data "^4.0.0" proxy-from-env "^1.1.0" -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -callsites@^3.0.0: +callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== @@ -418,14 +282,6 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -445,32 +301,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== - dependencies: - ms "^2.1.3" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -481,13 +311,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - discord-api-types@0.37.83: version "0.37.83" resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.83.tgz#a22a799729ceded8176ea747157837ddf4708b1f" @@ -498,6 +321,18 @@ discord-api-types@0.37.97: resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.97.tgz#d658573f726ad179261d538dbad4e7e8eca48d11" integrity sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA== +discord-api-types@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.22.0.tgz#34dc57fe8e016e5eaac5e393646cd42a7e1ccc2a" + integrity sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg== + +discord-sync-commands@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/discord-sync-commands/-/discord-sync-commands-0.3.0.tgz#471d711a7c6cbcccb7ab29e29bb53d68d02d0dfa" + integrity sha512-JPKJTqzUvAtqTFcHp8GgEZfgpkOaeUIHpLVcnP88lK8gMMxUfz1X9+DsVgbAbsWSF7uer0PpBowHyWomzTJH/g== + dependencies: + discord.js discordjs/discord.js#refs/pull/6414/head + discord.js@^14.16.2: version "14.16.2" resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.16.2.tgz#c878977b5a377cf41eaed0b1901115f8faec9852" @@ -516,175 +351,36 @@ discord.js@^14.16.2: tslib "^2.6.3" undici "6.19.8" -dotenv@^16.4.5: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.2.tgz#5cbb33d4384c9136083a71190d548158fe128f94" - integrity sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA== +discord.js@discordjs/discord.js#refs/pull/6414/head: + version "13.2.0-dev" + resolved "https://codeload.github.com/discordjs/discord.js/tar.gz/b2ef10fdd742aa08da3a33b2cf35f0f2350e915a" dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + "@discordjs/builders" "^0.5.0" + "@discordjs/collection" "^0.2.1" + "@discordjs/form-data" "^3.0.1" + "@sapphire/async-queue" "^1.1.4" + "@types/ws" "^7.4.7" + discord-api-types "^0.22.0" + node-fetch "^2.6.1" + ws "^7.5.1" -eslint-visitor-keys@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" - integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== - -eslint@^9.0.0: - version "9.10.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.10.0.tgz#0bd74d7fe4db77565d0e7f57c7df6d2b04756806" - integrity sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.11.0" - "@eslint/config-array" "^0.18.0" - "@eslint/eslintrc" "^3.1.0" - "@eslint/js" "9.10.0" - "@eslint/plugin-kit" "^0.1.0" - "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.3.0" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - escape-string-regexp "^4.0.0" - eslint-scope "^8.0.2" - eslint-visitor-keys "^4.0.0" - espree "^10.1.0" - esquery "^1.5.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^8.0.0" - find-up "^5.0.0" - glob-parent "^6.0.2" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - json-stable-stringify-without-jsonify "^1.0.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -espree@^10.0.1, espree@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-10.1.0.tgz#8788dae611574c0f070691f522e4116c5a11fc56" - integrity sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA== - dependencies: - acorn "^8.12.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.0.0" - -esquery@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" - integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== dependencies: - estraverse "^5.2.0" + is-obj "^2.0.0" -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== -fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +fast-deep-equal@3.1.3, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -file-entry-cache@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" - integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== - dependencies: - flat-cache "^4.0.0" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" - integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.4" - -flatted@^3.2.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" - integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== - follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" @@ -699,140 +395,20 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -globals@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" - integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -ignore@^5.2.0, ignore@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -isexe@^2.0.0: +is-obj@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -keyv@^4.5.4: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== lodash.snakecase@4.1.1: version "4.1.1" @@ -849,19 +425,6 @@ magic-bytes.js@^1.10.0: resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz#c41cf4bc2f802992b05e64962411c9dd44fdef92" integrity sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ== -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -874,159 +437,35 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - moment@^2.24.0: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.5" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== +node-fetch@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: - p-limit "^3.0.2" + whatwg-url "^5.0.0" -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== +ow@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/ow/-/ow-0.27.0.tgz#d44da088e8184fa11de64b5813206f9f86ab68d0" + integrity sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ== dependencies: - callsites "^3.0.0" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + "@sindresorhus/is" "^4.0.1" + callsites "^3.1.0" + dot-prop "^6.0.1" + lodash.isequal "^4.5.0" + type-fest "^1.2.1" + vali-date "^1.0.0" proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -semver@^7.6.0: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -1034,24 +473,12 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -ts-api-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== - -ts-mixer@^6.0.4: +ts-mixer@^6.0.0, ts-mixer@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.4.tgz#1da39ceabc09d947a82140d9f09db0f84919ca28" integrity sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA== @@ -1068,17 +495,20 @@ ts-typed-events@^3.0.0: resolved "https://registry.yarnpkg.com/ts-typed-events/-/ts-typed-events-3.0.0.tgz#2f9d96ff962edfc936402c859370337373880faa" integrity sha512-+2FZ0XPX+UPR7PO8ZQjuvnuDMYRhzrDaCRaNHaBG1xSL//0oPa3XMU5yxgDTzW67VzkE33fQpx1YxWBdkaF7Zw== +tslib@^2.3.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + tslib@^2.6.2, tslib@^2.6.3: version "2.7.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" +type-fest@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== typescript@^5.4.4: version "5.6.2" @@ -1095,31 +525,30 @@ undici@6.19.8: resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" +vali-date@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6" + integrity sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: - isexe "^2.0.0" + tr46 "~0.0.3" + webidl-conversions "^3.0.0" -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +ws@^7.5.1: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.16.0: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==