From 528fff3a5ba879901066c6d897138ea5125d29e1 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Tue, 12 May 2026 15:42:21 +0200 Subject: [PATCH] refactor: improve code quality --- .editorconfig | 9 + .gitea/workflows/docker-build.yml | 18 +- apps/discord-bot/Dockerfile | 3 + apps/discord-bot/package.json | 42 +- apps/discord-bot/src/commands/gemmes.ts | 98 ++- apps/discord-bot/src/commands/icone.ts | 84 ++- apps/discord-bot/src/commands/index.ts | 29 +- apps/discord-bot/src/commands/ping.ts | 19 +- apps/discord-bot/src/commands/quete.ts | 37 +- apps/discord-bot/src/commands/result.ts | 21 +- apps/discord-bot/src/commands/tejtrack.ts | 61 -- apps/discord-bot/src/commands/track.ts | 59 -- apps/discord-bot/src/commands/tracking.ts | 67 ++ apps/discord-bot/src/discord.ts | 24 + apps/discord-bot/src/economy.ts | 23 + apps/discord-bot/src/env.ts | 71 +-- apps/discord-bot/src/index.ts | 63 +- apps/discord-bot/src/modes/bot.ts | 98 +-- apps/discord-bot/src/modes/user.ts | 54 +- apps/discord-bot/src/quests.ts | 169 +++++ apps/discord-bot/src/services/account.ts | 32 - apps/discord-bot/src/services/tracking.ts | 74 --- apps/discord-bot/src/services/wov.ts | 139 ---- apps/discord-bot/src/tracking.ts | 82 +++ apps/discord-bot/src/utils/cli.ts | 32 - apps/discord-bot/src/utils/discord.ts | 118 ---- apps/discord-bot/src/utils/quest.ts | 127 ---- apps/discord-bot/src/wov.ts | 89 +++ apps/discord-bot/tsconfig.build.json | 24 +- apps/discord-bot/tsconfig.json | 47 +- eslint.config.mjs | 25 + package.json | 7 +- packages/database/drizzle.config.ts | 12 +- packages/database/package.json | 67 +- packages/database/tsconfig.build.json | 24 +- packages/database/tsconfig.json | 47 +- packages/utils/package.json | 53 +- packages/utils/src/env.ts | 20 +- packages/utils/src/logger.ts | 137 ++-- packages/utils/tsconfig.build.json | 24 +- packages/utils/tsconfig.json | 48 +- pnpm-lock.yaml | 734 +++++++++++++++++++++- 42 files changed, 1756 insertions(+), 1255 deletions(-) create mode 100644 .editorconfig delete mode 100644 apps/discord-bot/src/commands/tejtrack.ts delete mode 100644 apps/discord-bot/src/commands/track.ts create mode 100644 apps/discord-bot/src/commands/tracking.ts create mode 100644 apps/discord-bot/src/discord.ts create mode 100644 apps/discord-bot/src/economy.ts create mode 100644 apps/discord-bot/src/quests.ts delete mode 100644 apps/discord-bot/src/services/account.ts delete mode 100644 apps/discord-bot/src/services/tracking.ts delete mode 100644 apps/discord-bot/src/services/wov.ts create mode 100644 apps/discord-bot/src/tracking.ts delete mode 100644 apps/discord-bot/src/utils/cli.ts delete mode 100644 apps/discord-bot/src/utils/discord.ts delete mode 100644 apps/discord-bot/src/utils/quest.ts create mode 100644 apps/discord-bot/src/wov.ts create mode 100644 eslint.config.mjs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cbd1548 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[**/*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 510cf98..ac6600d 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -6,8 +6,24 @@ on: - main jobs: + typecheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + run_install: true + + - name: Typecheck and Lint + run: pnpm check + build: runs-on: ubuntu-latest + needs: typecheck steps: - name: Checkout code @@ -25,5 +41,5 @@ jobs: - name: Build and push Docker image run: | - docker build -t git.pihkaal.me/pihkaal/lbf-bot:latest -f apps/discord-bot/Dockerfile . + docker build --build-arg COMMIT_SHA=$(git rev-parse --short HEAD) -t git.pihkaal.me/pihkaal/lbf-bot:latest -f apps/discord-bot/Dockerfile . docker push git.pihkaal.me/pihkaal/lbf-bot:latest diff --git a/apps/discord-bot/Dockerfile b/apps/discord-bot/Dockerfile index c7e1e41..3867d9d 100644 --- a/apps/discord-bot/Dockerfile +++ b/apps/discord-bot/Dockerfile @@ -15,4 +15,7 @@ RUN pnpm --filter @lbf-bot/utils run build RUN pnpm --filter @lbf-bot/database run build RUN pnpm --filter @lbf/discord-bot run build +ARG COMMIT_SHA=dev +ENV COMMIT_SHA=$COMMIT_SHA + CMD ["node", "apps/discord-bot/dist/index.js"] diff --git a/apps/discord-bot/package.json b/apps/discord-bot/package.json index 4a5e6ff..c98e777 100644 --- a/apps/discord-bot/package.json +++ b/apps/discord-bot/package.json @@ -1,23 +1,23 @@ { - "name": "@lbf/discord-bot", - "type": "module", - "scripts": { - "dev": "tsx src/index.ts", - "start": "node dist/index.js", - "build": "rm -rf dist && tsc --project tsconfig.build.json && tsc-alias --project tsconfig.build.json", - "dev:user": "tsx src/index.ts -- --user" - }, - "devDependencies": { - "@types/node": "^22.10.2", - "tsc-alias": "^1.8.16", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - }, - "dependencies": { - "@lbf-bot/utils": "workspace:*", - "@lbf-bot/database": "workspace:*", - "discord.js": "^14.21.0", - "dotenv": "^17.2.3", - "zod": "^3.24.4" - } + "name": "@lbf/discord-bot", + "type": "module", + "scripts": { + "dev": "COMMIT_SHA=$(git rev-parse --short HEAD)-dev tsx src/index.ts", + "start": "node dist/index.js", + "build": "rm -rf dist && tsc --project tsconfig.build.json && tsc-alias --project tsconfig.build.json", + "dev:user": "COMMIT_SHA=$(git rev-parse --short HEAD)-dev tsx src/index.ts -- --user", + "check": "tsc --noEmit && eslint src/" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsc-alias": "^1.8.16", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "dependencies": { + "@lbf-bot/utils": "workspace:*", + "@lbf-bot/database": "workspace:*", + "discord.js": "^14.21.0", + "zod": "4.1.11" + } } diff --git a/apps/discord-bot/src/commands/gemmes.ts b/apps/discord-bot/src/commands/gemmes.ts index b1a3602..cf6171e 100644 --- a/apps/discord-bot/src/commands/gemmes.ts +++ b/apps/discord-bot/src/commands/gemmes.ts @@ -1,67 +1,51 @@ import { EmbedBuilder } from "discord.js"; import { env } from "~/env"; -import type { Command } from "~/commands"; -import { getAccountBalance, setAccountBalance } from "~/services/account"; -import { getClanMembers } from "~/services/wov"; -import { replyError } from "~/utils/discord"; +import { getClanMembers } from "~/wov"; +import { getAccountBalance, setAccountBalance } from "~/economy"; +import { replyError } from "~/discord"; +import type { Command } from "./index"; +import { noMention } from "~/discord"; -export const gemmesCommand: Command = async (message, args) => { - // retrieve player name - // NOTE: discord members have display name formatted like "🕾 | InGamePseudo" - const displayName = message.member?.displayName || message.author.username; - const playerName = args[0] || displayName.replace("🕾 |", "").trim(); +export const gemmesCommand: Command = { + help: "Affiche ou modifie le solde de gemmes d'un membre", + handler: async (message, args) => { + // discord members have display name formatted like "🕾 | InGamePseudo" + const displayName = message.member?.displayName || message.author.username; + const playerName = args[0] || displayName.replace("🕾 |", "").trim(); - // get clan member - const clanMembers = await getClanMembers(); - const clanMember = clanMembers.find((x) => x.username === playerName); - if (!clanMember) { - await replyError( - message, - `\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`, - ); - return; - } + const clanMembers = await getClanMembers(); + const clanMember = clanMembers.find((x) => x.username === playerName); + if (!clanMember) { + await replyError(message, `\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`); + return; + } - // handle balance modification (staff only) - if (args.length === 2) { - if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) { - await replyError( - message, - "Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff", - ); - return; - } + if (args.length === 2) { + if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) { + await replyError(message, "Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff"); + return; + } - const op = args[1][0]; - const amount = Number(args[1].substring(1)); - if ((op !== "+" && op !== "-") || args[1].length === 1 || isNaN(amount)) { - await replyError( - message, - "Format: `@LBF gemmes <+GEMMES | -GEMMES>`\nExemple: `@LBF gemmes Yuno -10000`\n**Attention les majuscules sont importantes**", - ); - return; - } + const op = args[1][0]; + const amount = Number(args[1].substring(1)); + if ((op !== "+" && op !== "-") || args[1].length === 1 || isNaN(amount)) { + await replyError(message, "Usage: `@LBF gemmes <+GEMMES | -GEMMES>`\nExemple: `@LBF gemmes Yuno -10000`\n**Attention les majuscules sont importantes**"); + return; + } - const balance = await getAccountBalance(clanMember.playerId); - const delta = amount * (op === "+" ? 1 : -1); - await setAccountBalance(clanMember.playerId, Math.max(0, balance + delta)); - } + const balance = await getAccountBalance(clanMember.playerId); + const delta = amount * (op === "+" ? 1 : -1); + await setAccountBalance(clanMember.playerId, Math.max(0, balance + delta)); + } - // display balance - const balance = await getAccountBalance(clanMember.playerId); - await message.reply({ - embeds: [ - new EmbedBuilder() - .setDescription( - // TODO: mention here instead of in the env - `### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour Ă©changer contre skin/carte etc`, - ) - .setColor(0x4289c1), - ], - options: { - allowedMentions: { - repliedUser: false, - }, + const balance = await getAccountBalance(clanMember.playerId); + await message.reply({ + options: noMention, + embeds: [ + new EmbedBuilder() + .setDescription(`### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour Ă©changer contre skin/carte etc`) + .setColor(0x4289c1) + ], + }); }, - }); }; diff --git a/apps/discord-bot/src/commands/icone.ts b/apps/discord-bot/src/commands/icone.ts index dd86335..e078db3 100644 --- a/apps/discord-bot/src/commands/icone.ts +++ b/apps/discord-bot/src/commands/icone.ts @@ -1,57 +1,43 @@ import { EmbedBuilder } from "discord.js"; -import type { Command } from "~/commands"; -import { searchPlayer, getClanInfos } from "~/services/wov"; -import { replyError } from "~/utils/discord"; +import { searchPlayer, getClanInfo } from "~/wov"; +import { replyError } from "~/discord"; +import type { Command } from "./index"; +import { noMention } from "~/discord"; -export const iconeCommand: Command = async (message, args) => { - const playerName = args[0]; - if (!playerName) { - await replyError( - message, - "Usage:`@LBF icone NOM_JOUEUR`, exemple: `@LBF icone Yuno`.\n**Attention les majuscules sont importantes**", - ); - return; - } +export const iconeCommand: Command = { + help: "Affiche l'icone et le nom du clan d'un joueur", + handler: async (message, args) => { + const playerName = args[0]; + if (!playerName) { + await replyError(message, "Usage:`@LBF icone NOM_JOUEUR`, exemple: `@LBF icone Yuno`.\n**Attention les majuscules sont importantes**"); + return; + } - const player = await searchPlayer(playerName); - if (!player) { - await replyError( - message, - "Joueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**", - ); - return; - } + const player = await searchPlayer(playerName); + if (!player) { + await replyError(message, "Joueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**"); + return; + } - if (!player.clanId) { - await replyError( - message, - "Cette personne __n'a pas de clan__ ou __a cachĂ© son clan__.\n**Attention les majuscules sont importantes**", - ); - return; - } + if (!player.clanId) { + await replyError(message, "Cette personne __n'a pas de clan__ ou __a cachĂ© son clan__.\n**Attention les majuscules sont importantes**"); + return; + } - const clan = await getClanInfos(player.clanId); - if (!clan) { - await replyError( - message, - "Impossible de rĂ©cupĂ©rer les informations du clan.", - ); - return; - } + const clan = await getClanInfo(player.clanId); + if (!clan) { + await replyError(message, "Impossible de rĂ©cupĂ©rer les informations du clan."); + return; + } - await message.reply({ - content: clan.tag, - embeds: [ - new EmbedBuilder() - .setDescription( - `### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``, - ) - .setColor(65280), - ], - options: { - allowedMentions: { - repliedUser: false, - }, + await message.reply({ + options: noMention, + content: clan.tag, + embeds: [ + new EmbedBuilder() + .setDescription(`### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``) + .setColor(65280) + ], + }); }, - }); }; diff --git a/apps/discord-bot/src/commands/index.ts b/apps/discord-bot/src/commands/index.ts index d4c8fc5..94d655d 100644 --- a/apps/discord-bot/src/commands/index.ts +++ b/apps/discord-bot/src/commands/index.ts @@ -1,23 +1,26 @@ import type { Message, OmitPartialGroupDMChannel } from "discord.js"; import { pingCommand } from "./ping"; -import { trackCommand } from "./track"; -import { tejtrackCommand } from "./tejtrack"; +import { trackCommand } from "./tracking"; import { iconeCommand } from "./icone"; import { gemmesCommand } from "./gemmes"; import { resultCommand } from "./result"; import { queteCommand } from "./quete"; -export type Command = ( - message: OmitPartialGroupDMChannel>, - args: Array, +export type CommandHandler = ( + message: OmitPartialGroupDMChannel>, + args: Array, ) => Promise | void; -export const commands: Record = { - ping: pingCommand, - track: trackCommand, - tejtrack: tejtrackCommand, - icone: iconeCommand, - gemmes: gemmesCommand, - result: resultCommand, - quete: queteCommand, +export type Command = { + help: string; + handler: CommandHandler; +}; + +export const commands: Record = { + ping: pingCommand, + track: trackCommand, + icone: iconeCommand, + gemmes: gemmesCommand, + quete: queteCommand, + result: resultCommand, }; diff --git a/apps/discord-bot/src/commands/ping.ts b/apps/discord-bot/src/commands/ping.ts index 74c97d5..c554288 100644 --- a/apps/discord-bot/src/commands/ping.ts +++ b/apps/discord-bot/src/commands/ping.ts @@ -1,12 +1,13 @@ -import type { Command } from "~/commands"; +import { noMention } from "~/discord"; +import { env } from "~/env"; +import type { Command } from "./index"; -export const pingCommand: Command = async (message, args) => { - await message.reply({ - content: "đŸ«” Pong", - options: { - allowedMentions: { - repliedUser: false, - }, +export const pingCommand: Command = { + help: "Pong", + handler: async (message) => { + await message.reply({ + options: noMention, + content: `đŸ«” Pong \`${env.COMMIT_SHA}\``, + }); }, - }); }; diff --git a/apps/discord-bot/src/commands/quete.ts b/apps/discord-bot/src/commands/quete.ts index 30a484f..b93e31e 100644 --- a/apps/discord-bot/src/commands/quete.ts +++ b/apps/discord-bot/src/commands/quete.ts @@ -1,20 +1,27 @@ import { EmbedBuilder } from "discord.js"; -import type { Command } from "~/commands"; -import { getActiveQuest, getLatestQuest } from "~/services/wov"; +import { getActiveQuest, getLatestQuest } from "~/wov"; +import { replyError } from "~/discord"; +import type { Command } from "./index"; +import { noMention } from "~/discord"; -export const queteCommand: Command = async (message) => { - const quest = (await getActiveQuest()) ?? (await getLatestQuest()); - const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); +export const queteCommand: Command = { + help: "Affiche la quĂȘte en cours ou la derniĂšre quĂȘte", + handler: async (message) => { + const quest = (await getActiveQuest()) ?? (await getLatestQuest()); + if (!quest) { + await replyError(message, "Impossible de rĂ©cupĂ©rer la quĂȘte."); + return; + } + const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); - await message.channel.send({ - allowedMentions: { - repliedUser: false + await message.channel.send({ + options: noMention, + embeds: [ + new EmbedBuilder() + .setTitle("QuĂȘte actuelle") + .setImage(quest.quest.promoImageUrl) + .setColor(color) + ], + }); }, - embeds: [ - new EmbedBuilder() - .setTitle("QuĂȘte actuelle") - .setImage(quest.quest.promoImageUrl) - .setColor(color), - ], - }); }; diff --git a/apps/discord-bot/src/commands/result.ts b/apps/discord-bot/src/commands/result.ts index 6ae419e..7f82a4e 100644 --- a/apps/discord-bot/src/commands/result.ts +++ b/apps/discord-bot/src/commands/result.ts @@ -1,9 +1,16 @@ -import type { Command } from "~/commands"; -import { getLatestQuest } from "~/services/wov"; -import { askForGrinders } from "~/utils/quest"; +import { getLatestQuest } from "~/wov"; +import { askForGrinders } from "~/quests"; +import { replyError } from "~/discord"; +import type { Command } from "./index"; -export const resultCommand: Command = async (message, args) => { - const client = message.client; - const quest = await getLatestQuest(); - await askForGrinders(quest, client); +export const resultCommand: Command = { + help: "DĂ©clenche manuellement la publication des rĂ©sultats de la derniĂšre quĂȘte", + handler: async (message) => { + const quest = await getLatestQuest(); + if (!quest) { + await replyError(message, "Impossible de rĂ©cupĂ©rer la derniĂšre quĂȘte."); + return; + } + await askForGrinders(quest, message.client); + }, }; diff --git a/apps/discord-bot/src/commands/tejtrack.ts b/apps/discord-bot/src/commands/tejtrack.ts deleted file mode 100644 index a4d363e..0000000 --- a/apps/discord-bot/src/commands/tejtrack.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Command } from "~/commands"; -import { isWovPlayerTracked, untrackWovPlayer } from "~/services/tracking"; -import { searchPlayer } from "~/services/wov"; -import { replyError, createInfoEmbed, replySuccess } from "~/utils/discord"; -import { env } from "~/env"; -import { createLogger } from "@lbf-bot/utils"; - -const trackingLogger = createLogger({ prefix: "tracking" }); - -export const tejtrackCommand: Command = async (message, args) => { - // check staff permission - if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) { - await replyError(message, "Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff"); - return; - } - - const playerName = args[0]; - if (!playerName) { - await replyError( - message, - "Usage:`@LBF untrack NOM_JOUEUR`, exemple: `@LBF untrack Yuno`.\n**Attention les majuscules sont importantes**", - ); - return; - } - - const player = await searchPlayer(playerName); - if (!player) { - await replyError( - message, - "Cette personne n'existe pas.\n**Attention les majuscules sont importantes**", - ); - return; - } - - if (!(await isWovPlayerTracked(player.id))) { - await replyError( - message, - `Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`, - ); - return; - } - - await untrackWovPlayer(player.id); - - await replySuccess( - message, - `Tracker enlevĂ© pour \`${playerName}\` [\`${player.id}\`]`, - ); - - const chan = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); - if (!chan?.isSendable()) { - return trackingLogger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'"); - } - - await chan.send( - createInfoEmbed( - `### [REMOVED] \`${playerName}\` [\`${player.id}\`]`, - 0xea0000, - ), - ); -}; diff --git a/apps/discord-bot/src/commands/track.ts b/apps/discord-bot/src/commands/track.ts deleted file mode 100644 index ef579e9..0000000 --- a/apps/discord-bot/src/commands/track.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Command } from "~/commands"; -import { trackWovPlayer, isWovPlayerTracked } from "~/services/tracking"; -import { searchPlayer } from "~/services/wov"; -import { replyError, createInfoEmbed, replySuccess } from "~/utils/discord"; -import { env } from "~/env"; -import { createLogger } from "@lbf-bot/utils"; - -const trackingLogger = createLogger({ prefix: "tracking" }); - -export const trackCommand: Command = async (message, args) => { - // check staff permission - if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) { - await replyError(message, "Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff"); - return; - } - - const playerName = args[0]; - if (!playerName) { - await replyError( - message, - "Usage:`@LBF track NOM_JOUEUR`, exemple: `@LBF track Yuno`.\n**Attention les majuscules sont importantes**", - ); - return; - } - - const player = await searchPlayer(playerName); - if (!player) { - await replyError( - message, - "Cette personne n'existe pas.\n**Attention les majuscules sont importantes**", - ); - return; - } - - const alreadyTracked = await isWovPlayerTracked(player.id); - if (alreadyTracked) { - await replyError( - message, - `Tracker dĂ©jĂ  enregistrĂ© pour \`${playerName}\` [\`${player.id}\`]`, - ); - return; - } - - await trackWovPlayer(player.id); - - await replySuccess( - message, - `Tracker enregistrĂ© pour \`${playerName}\` [\`${player.id}\`]`, - ); - - const chan = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); - if (!chan?.isSendable()) { - return trackingLogger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'"); - } - - await chan.send( - createInfoEmbed(`### [NEW] \`${playerName}\` [\`${player.id}\`]`), - ); -}; diff --git a/apps/discord-bot/src/commands/tracking.ts b/apps/discord-bot/src/commands/tracking.ts new file mode 100644 index 0000000..fd16a1d --- /dev/null +++ b/apps/discord-bot/src/commands/tracking.ts @@ -0,0 +1,67 @@ +import { env } from "~/env"; +import { searchPlayer } from "~/wov"; +import { createInfoEmbed, replyError, replySuccess } from "~/discord"; +import { isTracked, trackPlayer, untrackPlayer } from "~/tracking"; +import type { Command } from "./index"; + +export const trackCommand: Command = { + help: "GĂšre le tracking d'un joueur", + handler: async (message, args) => { + if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) { + await replyError(message, "Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff"); + return; + } + + const [subcommand, playerName] = args; + + if (!playerName) { + await replyError(message, "Usage: `@LBF track add/rm NOM_JOUEUR`. \`add\` = ajoute, \`rm\` = retirer\n**Attention les majuscules sont importantes**"); + return; + } + + const player = await searchPlayer(playerName); + if (!player) { + await replyError(message, "Cette personne n'existe pas.\n**Attention les majuscules sont importantes**"); + return; + } + + + const trackingChannel = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); + if (!trackingChannel?.isSendable()) { + await replyError(message, `Impossible d'envoyer un message dans <#${env.DISCORD_TRACKING_CHANNEL}>`); + return; + } + + switch (subcommand) { + case "add": { + if (await isTracked(player.id)) { + await replyError(message, `Tracker dĂ©jĂ  enregistrĂ© pour \`${playerName}\` [\`${player.id}\`]`); + return; + } + + await trackPlayer(player.id); + await replySuccess(message, `Tracker enregistrĂ© pour \`${playerName}\` [\`${player.id}\`]`); + await trackingChannel.send(createInfoEmbed(`### [NEW] \`${playerName}\` [\`${player.id}\`]`)); + + break; + } + + case "rm": + case "remove": { + if (!(await isTracked(player.id))) { + await replyError(message, `Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`); + return; + } + + await untrackPlayer(player.id); + await replySuccess(message, `Tracker enlevĂ© pour \`${playerName}\` [\`${player.id}\`]`); + await trackingChannel.send(createInfoEmbed(`### [REMOVED] \`${playerName}\` [\`${player.id}\`]`, 0xea0000)); + break; + } + + default: { + await replyError(message, `Sous-commande inconnue \`${subcommand}\`. Usage: \`@LBF track add/rm NOM_JOUEUR\`. \`add\` = ajoute, \`rm\` = retirer`); + } + } + }, +}; diff --git a/apps/discord-bot/src/discord.ts b/apps/discord-bot/src/discord.ts new file mode 100644 index 0000000..5335ce7 --- /dev/null +++ b/apps/discord-bot/src/discord.ts @@ -0,0 +1,24 @@ +import type { MessageCreateOptions, Message } from "discord.js"; + +export const noMention = { allowedMentions: { repliedUser: false } } as const; + +export const createErrorEmbed = (message: string, color = 0xea0000): MessageCreateOptions => ({ + embeds: [{ description: `### Erreur\n\n\n${message}`, color }], +}); + +export const createSuccessEmbed = (message: string, color = 0x00ea00): MessageCreateOptions => ({ + embeds: [{ description: `### ${message}`, color }], +}); + +export const createInfoEmbed = (message: string, color = 0x89cff0): MessageCreateOptions => ({ + embeds: [{ description: message, color }], +}); + +export const replyError = (message: Message, text: string, color?: number) => + message.reply(createErrorEmbed(text, color)); + +export const replySuccess = (message: Message, text: string, color?: number) => + message.reply(createSuccessEmbed(text, color)); + +export const replyInfo = (message: Message, text: string, color?: number) => + message.reply(createInfoEmbed(text, color)); diff --git a/apps/discord-bot/src/economy.ts b/apps/discord-bot/src/economy.ts new file mode 100644 index 0000000..b4ec61d --- /dev/null +++ b/apps/discord-bot/src/economy.ts @@ -0,0 +1,23 @@ +import { db, tables, eq } from "@lbf-bot/database"; + +export const getAccountBalance = async (playerId: string): Promise => { + const account = await db.query.accounts.findFirst({ + where: eq(tables.accounts.playerId, playerId), + }); + + if (!account) { + await setAccountBalance(playerId, 0); + return 0; + } + return account.balance; +}; + +export const setAccountBalance = async (playerId: string, balance: number): Promise => { + await db + .insert(tables.accounts) + .values({ playerId, balance }) + .onConflictDoUpdate({ + target: tables.accounts.playerId, + set: { balance, updatedAt: new Date() }, + }); +}; diff --git a/apps/discord-bot/src/env.ts b/apps/discord-bot/src/env.ts index 36e5de6..90cb15d 100644 --- a/apps/discord-bot/src/env.ts +++ b/apps/discord-bot/src/env.ts @@ -1,46 +1,35 @@ import { z } from "zod"; -import "dotenv/config"; -import { logger } from "@lbf-bot/utils"; +import { parseEnv, logger } from "@lbf-bot/utils"; // TODO: use parseEnv from utils -const schema = z.object({ - DISCORD_BOT_TOKEN: z.string(), - DISCORD_MENTION: z.string(), - DISCORD_REWARDS_GIVER: z.string(), - DISCORD_REWARDS_CHANNEL: z.string(), - // TODO: remove and compose from staff role id - DISCORD_ADMIN_MENTION: z.string(), - // TODO: rename to reward ask channel or smth - DISCORD_ADMIN_CHANNEL: z.string(), - DISCORD_TRACKING_CHANNEL: z.string(), - DISCORD_STAFF_ROLE_ID: z.string(), - WOV_API_KEY: z.string(), - WOV_CLAN_ID: z.string(), - WOV_FETCH_INTERVAL: z.coerce.number(), - WOV_TRACKING_INTERVAL: z.coerce.number(), - QUEST_REWARDS: z - .string() - .transform((x) => x.split(",").map((x) => x.trim())) - .optional(), - QUEST_REWARDS_ARE_GEMS: z - .string() - .transform((val) => val.toLowerCase() === "true") - .pipe(z.boolean()), - QUEST_EXCLUDE: z - .string() - .transform((x) => x.split(",").map((x) => x.trim())) - .optional() - .default(""), +export const env = parseEnv({ + DISCORD_BOT_TOKEN: z.string(), + DISCORD_MENTION: z.string(), + DISCORD_REWARDS_GIVER: z.string(), + DISCORD_REWARDS_CHANNEL: z.string(), + // TODO: remove and compose from staff role id + DISCORD_ADMIN_MENTION: z.string(), + // TODO: rename to reward ask channel or smth + DISCORD_ADMIN_CHANNEL: z.string(), + DISCORD_TRACKING_CHANNEL: z.string(), + DISCORD_STAFF_ROLE_ID: z.string(), + COMMIT_SHA: z.string().default("dev"), + WOV_API_KEY: z.string(), + WOV_CLAN_ID: z.string(), + WOV_FETCH_INTERVAL: z.coerce.number(), + WOV_TRACKING_INTERVAL: z.coerce.number(), + QUEST_REWARDS: z + .string() + .transform((x) => x.split(",").map((x) => x.trim())) + .optional(), + QUEST_REWARDS_ARE_GEMS: z + .string() + .transform((val) => val.toLowerCase() === "true") + .pipe(z.boolean()), + QUEST_EXCLUDE: z + .string() + .transform((x) => x.split(",").map((x) => x.trim())) + .optional() + .default([]), }); - -const result = schema.safeParse(process.env); -if (!result.success) { - logger.fatal( - `❌ Invalid environments variables:\n${result.error.errors - .map((x) => `- ${x.path.join(".")}: ${x.message}`) - .join("\n")}`, - ); -} - -export const env = result.data; diff --git a/apps/discord-bot/src/index.ts b/apps/discord-bot/src/index.ts index 1d733af..2d8417f 100644 --- a/apps/discord-bot/src/index.ts +++ b/apps/discord-bot/src/index.ts @@ -1,43 +1,58 @@ +import { logger } from "@lbf-bot/utils"; +import { runMigrations } from "@lbf-bot/database"; import { env } from "~/env"; import { Client, GatewayIntentBits, Partials } from "discord.js"; import { setupBotMode } from "~/modes/bot"; import { setupUserMode } from "~/modes/user"; -import { parseArgs } from "~/utils/cli"; -import { runMigrations } from "@lbf-bot/database"; -import { logger } from "@lbf-bot/utils"; logger.info("Running database migrations..."); await runMigrations(); -const mode = parseArgs(process.argv.slice(2)); +type Mode = { type: "bot" } | { type: "user"; channelId: string }; +let mode: Mode = { type: "bot" }; +const args = process.argv.slice(2); -logger.info(`Mode: ${mode.type}`); +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + switch (arg) { + case "-u": + case "--as-user": { + const channelId = args[(i += 1)]; + if (!channelId) { + console.error(`ERROR: ${arg} requires a channel ID`); + process.exit(1); + } + mode = { type: "user", channelId }; + break; + } + + default: { + console.error(`ERROR: Unrecognized argument: '${arg}'`); + process.exit(1); + } + } +} const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.DirectMessages, - ], - partials: [Partials.Message, Partials.Channel], + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.DirectMessages, + ], + partials: [Partials.Message, Partials.Channel], }); -switch (mode.type) { - case "user": { +logger.info(`Starting as ${mode.type}`); + +if (mode.type === "user") { setupUserMode(client, mode.channelId); - break; - } - - case "bot": { +} else if (mode.type === "bot") { setupBotMode(client); - break; - } - - default: { - // @ts-ignore +} else { + // @ts-expect-error -- exhaustive default for unimplemented mode types logger.fatal(`ERROR: Not implemented: '${mode.type}'`); - } } await client.login(env.DISCORD_BOT_TOKEN); diff --git a/apps/discord-bot/src/modes/bot.ts b/apps/discord-bot/src/modes/bot.ts index bc4e402..87a2da6 100644 --- a/apps/discord-bot/src/modes/bot.ts +++ b/apps/discord-bot/src/modes/bot.ts @@ -1,97 +1,45 @@ -import type { Client } from "discord.js"; -import { createLogger, logger } from "@lbf-bot/utils"; +import type { Client, OmitPartialGroupDMChannel, Message } from "discord.js"; +import { logger } from "@lbf-bot/utils"; import { env } from "~/env"; -import { - listTrackedPlayers, - getTrackedPlayerUsernames, - addUsernameToHistory, -} from "~/services/tracking"; -import { checkForNewQuest, getPlayer } from "~/services/wov"; -import { createInfoEmbed } from "~/utils/discord"; -import { askForGrinders } from "~/utils/quest"; +import { questCheckCron } from "~/quests"; +import { trackingCron } from "~/tracking"; import { commands } from "~/commands"; -const questsLogger = createLogger({ prefix: "quests" }); -const trackingLogger = createLogger({ prefix: "tracking" }); - -const questCheckCron = async (client: Client) => { - questsLogger.info("Checking for new quest"); - const quest = await checkForNewQuest(); - if (quest) { - questsLogger.info(`New quest found: '${quest.quest.id}'`); - await askForGrinders(quest, client); - } else { - questsLogger.info("No new quest found"); - } -}; - -const trackingCron = async (client: Client) => { - trackingLogger.info("Checking for tracked players"); - const trackedPlayers = await listTrackedPlayers(); - trackingLogger.info(`${trackedPlayers.length} players to check`); - for (const playerId of trackedPlayers) { - const player = await getPlayer(playerId); - if (!player) continue; - - const usernames = await getTrackedPlayerUsernames(playerId); - if (usernames.includes(player.username)) continue; - - await addUsernameToHistory(playerId, player.username); - - const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); - if (!chan?.isSendable()) { - return logger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'"); - } - const lastUsername = usernames[usernames.length - 1]; - - await chan.send( - createInfoEmbed( - `### [UPDATE] \`${lastUsername}\` -> \`${player.username}\` [\`${playerId}\`]\n\n**Nouveau pseudo:** \`${player.username}\`\n**Anciens pseudos:**\n${usernames.map((x) => `- \`${x}\``).join("\n")}`, - 0x00ea00, - ), - ); - - trackingLogger.info( - `Username changed: ${lastUsername} -> ${player.username} [${playerId}]`, - ); - } -}; - -export const setupBotMode = (client: Client) => { - client.on("clientReady", async (client) => { +const onReady = async (client: Client) => { logger.info(`Client ready`); logger.info(`Connected as @${client.user.username}`); await questCheckCron(client); - setInterval(() => questCheckCron(client), env.WOV_FETCH_INTERVAL); + setInterval(() => void questCheckCron(client), env.WOV_FETCH_INTERVAL); await trackingCron(client); - setInterval(() => trackingCron(client), env.WOV_TRACKING_INTERVAL); - }); + setInterval(() => void trackingCron(client), env.WOV_TRACKING_INTERVAL); +}; - client.on("messageCreate", async (message) => { +const onMessage = async (message: OmitPartialGroupDMChannel, client: Client) => { if (message.author.bot) return; if (message.content.startsWith(`<@${client.user!.id}>`)) { - const [command, ...args] = message.content + const [commandName, ...args] = message.content .replace(`<@${client.user!.id}>`, "") .trim() .split(" "); - const commandHandler = commands[command]; - if (commandHandler) { - const child = logger.child( - `cmd:${command}${args.length > 0 ? " " : ""}${args.join(" ")}`, - ); + const command = commands[commandName]; + if (!command) return; + + const child = logger.child(`cmd:${commandName}${args.length > 0 ? " " : ""}${args.join(" ")}`); try { - const start = Date.now(); - await commandHandler(message, args); - const end = Date.now(); - child.info(`Done in ${(end - start).toFixed(2)}ms`); + const start = Date.now(); + await command.handler(message, args); + child.info(`Done in ${(Date.now() - start).toFixed(2)}ms`); } catch (error: unknown) { - child.error("Failed:", error); + child.error("Failed:", error); } - } } - }); +}; + +export const setupBotMode = (client: Client) => { + client.on("clientReady", (client) => { void onReady(client); }); + client.on("messageCreate", (message) => { void onMessage(message, client); }); }; diff --git a/apps/discord-bot/src/modes/user.ts b/apps/discord-bot/src/modes/user.ts index c10571a..6bcf954 100644 --- a/apps/discord-bot/src/modes/user.ts +++ b/apps/discord-bot/src/modes/user.ts @@ -1,36 +1,36 @@ import { logger } from "@lbf-bot/utils"; -import type { Client, TextChannel } from "discord.js"; +import type { Client } from "discord.js"; import { ChannelType } from "discord.js"; import * as readline from "node:readline"; export const setupUserMode = (client: Client, channelId: string) => { - client.on("clientReady", (client) => { - logger.info(`Client ready`); - logger.info(`Connected as @${client.user.username}`); + client.on("clientReady", (client) => { + logger.info(`Client ready`); + logger.info(`Connected as @${client.user.username}`); - const chan = client.channels.cache.get(channelId); - if (chan?.type !== ChannelType.GuildText) { - console.error("ERROR: invalid channel"); - process.exit(1); - } + const chan = client.channels.cache.get(channelId); + if (chan?.type !== ChannelType.GuildText) { + console.error("ERROR: invalid channel"); + process.exit(1); + } - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - prompt: `${chan.name} ~ `, + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: `${chan.name} ~ `, + }); + + rl.prompt(); + + rl.on("line", (line) => { + if (line.trim().length > 0) { + void chan.send(line); + } + rl.prompt(); + }); + + rl.on("close", () => { + process.exit(0); + }); }); - - rl.prompt(); - - rl.on("line", async (line) => { - if (line.trim().length > 0) { - await (chan as TextChannel).send(line); - } - rl.prompt(); - }); - - rl.on("close", () => { - process.exit(0); - }); - }); }; diff --git a/apps/discord-bot/src/quests.ts b/apps/discord-bot/src/quests.ts new file mode 100644 index 0000000..e990623 --- /dev/null +++ b/apps/discord-bot/src/quests.ts @@ -0,0 +1,169 @@ +import { ChannelType, EmbedBuilder, type Client, type Message, type MessageCreateOptions } from "discord.js"; +import { createLogger } from "@lbf-bot/utils"; +import { env } from "~/env"; +import { checkForNewQuest, type QuestResult } from "~/wov"; +import { getAccountBalance, setAccountBalance } from "~/economy"; + +const logger = createLogger({ prefix: "quests" }); + +// --- rewards --- + +export const makeResultEmbed = async (result: QuestResult, exclude: Array): Promise => { + const imageUrl = result.quest.promoImageUrl; + const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16); + const participants = result.participants.toSorted((a, b) => b.xp - a.xp); + + let rewardsEmbed: EmbedBuilder | undefined; + if (env.QUEST_REWARDS) { + const rewarded = participants + .map((x) => ({ id: x.playerId, username: x.username })) + .filter((x) => !exclude.includes(x.username)); + const medals = ["đŸ„‡", "đŸ„ˆ", "đŸ„‰"].concat(new Array(rewarded.length).fill("🏅")); + + const rewards = rewarded + .slice(0, Math.min(rewarded.length, env.QUEST_REWARDS.length)) + .map((x, i) => `- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} ${env.QUEST_REWARDS_ARE_GEMS ? "gemmes" : ""}`); + + if (env.QUEST_REWARDS_ARE_GEMS) { + const arr = rewarded.slice(0, Math.min(rewarded.length, env.QUEST_REWARDS.length)); + for (let i = 0; i < arr.length; i++) { + const balance = await getAccountBalance(arr[i].id); + await setAccountBalance(arr[i].id, balance + parseInt(env.QUEST_REWARDS[i])); + } + } + + rewardsEmbed = new EmbedBuilder() + .setTitle("RĂ©compenses") + .setDescription(`${rewards.join("\n")}\n\n-# \`@LBF gemmes\` pour voir votre nombre de gemmes. Puis avec ${env.DISCORD_REWARDS_GIVER} pour Ă©changer contre des cadeaux !`) + .setColor(color); + } + + return { + content: `-# ||${env.DISCORD_MENTION}||`, + embeds: [ + new EmbedBuilder() + .setDescription(`# RĂ©sultats de quĂȘte\n\nMerci Ă  toutes et Ă  tous d'avoir participĂ© đŸ«Ą`) + .setColor(color) + .setImage(imageUrl), + ...(rewardsEmbed ? [rewardsEmbed] : []), + new EmbedBuilder() + .setTitle("Classement") + .setColor(color) + .setDescription( + participants + .filter((x) => !exclude.includes(x.username)) + .filter((_, i) => i < 8) + .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) + .join("\n"), + ), + ], + }; +}; + +export const askForGrinders = async (quest: QuestResult, client: Client) => { + const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); + if (!adminChannel || adminChannel.type !== ChannelType.GuildText) { + return logger.fatal("Invalid 'DISCORD_ADMIN_CHANNEL'"); + } + + let exclude: string[] = []; + if (env.QUEST_REWARDS) { + const top10 = quest.participants + .filter((x) => !env.QUEST_EXCLUDE.includes(x.username)) + .sort((a, b) => b.xp - a.xp) + .slice(0, 10) + .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) + .join("\n"); + + const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); + + await adminChannel.send({ + content: `-# ||${env.DISCORD_ADMIN_MENTION}||`, + embeds: [ + new EmbedBuilder() + .setTitle("QuĂȘte terminĂ©e !") + .setColor(color), + new EmbedBuilder() + .setTitle("Top 10 XP") + .setDescription(top10) + .setColor(color), + new EmbedBuilder() + .setTitle("Qui a grind ?") + .setDescription("Merci d'entrer les pseudos des joueurs qui ont grind.\n\nFormat:```@LBF onion,Yuno,...```\n**Attention les majuscules comptent**\nPour entrer la liste des joueurs, il faut __mentionner le bot__, si personne n'a grind, `@LBF tg`") + .setColor(color), + ], + }); + + const filter = (msg: Message) => + msg.channel.id === adminChannel.id && + !msg.author.bot && + msg.content.startsWith(`<@${client.user!.id}>`); + + let confirmed = false; + let answer: string | null = null; + while (!confirmed) { + const collected = await adminChannel.awaitMessages({ filter, max: 1 }); + answer = collected.first()?.content || null; + if (!answer) continue; + + answer = answer.replace(`<@${client.user!.id}>`, "").trim(); + if (answer.toLowerCase() === "tg") { + answer = ""; + break; + } + + const players = answer.split(",").map((x) => x.trim()).filter(Boolean); + await adminChannel.send({ + content: `Est-ce correct ? (oui/non)`, + embeds: [ + new EmbedBuilder() + .setTitle("Joueurs entrĂ©s") + .setDescription(players.length ? players.map((name) => `- ${name}`).join("\n") : "*Aucun joueur entrĂ©*") + .setColor(color), + ], + }); + + const confirmFilter = (msg: Message) => + msg.channel.id === adminChannel.id && + !msg.author.bot && + ["oui", "non", "yes", "no"].includes(msg.content.toLowerCase()); + + const confirmCollected = await adminChannel.awaitMessages({ filter: confirmFilter, max: 1 }); + const confirmation = confirmCollected.first()?.content.toLowerCase(); + if (confirmation === "oui" || confirmation === "yes") { + confirmed = true; + await adminChannel.send({ content: "Ok" }); + } else { + await adminChannel.send({ content: "D'accord, veuillez rĂ©essayer. Qui a grind ?" }); + } + } + + if (answer === null) { + return logger.fatal("Answer was 'null', this should be unreachable"); + } + + exclude = answer.split(",").map((x) => x.trim()).filter(Boolean); + } + + const embed = await makeResultEmbed(quest, [...env.QUEST_EXCLUDE, ...exclude]); + const rewardChannel = await client.channels.fetch(env.DISCORD_REWARDS_CHANNEL); + if (!rewardChannel || rewardChannel.type !== ChannelType.GuildText) { + return logger.fatal("Invalid 'DISCORD_REWARDS_CHANNEL'"); + } + + await rewardChannel.send(embed); + + if (env.QUEST_EXCLUDE) await adminChannel.send("EnvoyĂ© !"); + logger.info(`Results posted at: ${new Date().toISOString()}`); +}; + +export const questCheckCron = async (client: Client) => { + logger.info("Checking for new quest"); + const quest = await checkForNewQuest(); + if (quest) { + logger.info(`New quest found: '${quest.quest.id}'`); + await askForGrinders(quest, client); + } else { + logger.info("No new quest found"); + } +}; diff --git a/apps/discord-bot/src/services/account.ts b/apps/discord-bot/src/services/account.ts deleted file mode 100644 index 6c43239..0000000 --- a/apps/discord-bot/src/services/account.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { db, tables, eq } from "@lbf-bot/database"; - -export const getAccountBalance = async (playerId: string): Promise => { - const account = await db.query.accounts.findFirst({ - where: eq(tables.accounts.playerId, playerId), - }); - - if (account) return account.balance; - - await db.insert(tables.accounts).values({ - playerId, - balance: 0, - }); - - return 0; -}; - -export const setAccountBalance = async ( - playerId: string, - balance: number, -): Promise => { - await db - .insert(tables.accounts) - .values({ - playerId, - balance, - }) - .onConflictDoUpdate({ - target: tables.accounts.playerId, - set: { balance, updatedAt: new Date() }, - }); -}; diff --git a/apps/discord-bot/src/services/tracking.ts b/apps/discord-bot/src/services/tracking.ts deleted file mode 100644 index ef4437b..0000000 --- a/apps/discord-bot/src/services/tracking.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { getPlayer } from "~/services/wov"; -import { db, tables, eq } from "@lbf-bot/database"; - -export async function listTrackedPlayers(): Promise { - const players = await db.query.trackedPlayers.findMany({ - columns: { - playerId: true, - }, - }); - - return players.map((p) => p.playerId); -} - -export async function isWovPlayerTracked(playerId: string): Promise { - const player = await db.query.trackedPlayers.findFirst({ - where: eq(tables.trackedPlayers.playerId, playerId), - }); - - return player !== undefined; -} - -export async function untrackWovPlayer(playerId: string): Promise { - await db - .delete(tables.trackedPlayers) - .where(eq(tables.trackedPlayers.playerId, playerId)); -} - -export async function trackWovPlayer(playerId: string): Promise { - const alreadyTracked = await isWovPlayerTracked(playerId); - if (alreadyTracked) return; - - const player = await getPlayer(playerId); - if (!player) return; - - await db.insert(tables.trackedPlayers).values({ - playerId, - }); - - await db.insert(tables.usernameHistory).values({ - playerId, - username: player.username, - }); -} - -export async function getTrackedPlayerUsernames( - playerId: string, -): Promise { - const tracked = await db.query.trackedPlayers.findFirst({ - where: eq(tables.trackedPlayers.playerId, playerId), - with: { - usernameHistory: { - orderBy: (history, { asc }) => [asc(history.firstSeenAt)], - }, - }, - }); - - if (!tracked) return []; - return tracked.usernameHistory.map((h) => h.username); -} - -export async function addUsernameToHistory( - playerId: string, - username: string, -): Promise { - await db.insert(tables.usernameHistory).values({ - playerId, - username, - }); - - await db - .update(tables.trackedPlayers) - .set({ updatedAt: new Date() }) - .where(eq(tables.trackedPlayers.playerId, playerId)); -} diff --git a/apps/discord-bot/src/services/wov.ts b/apps/discord-bot/src/services/wov.ts deleted file mode 100644 index b0dfeb7..0000000 --- a/apps/discord-bot/src/services/wov.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { env } from "~/env"; -import { redis } from "@lbf-bot/database"; - -export type QuestResult = { - quest: { - id: string; - promoImageUrl: string; - promoImagePrimaryColor: string; - }; - participants: Array; -}; - -export type QuestParticipant = { - playerId: string; - username: string; - xp: number; -}; - -export const getLatestQuest = async (): Promise => { - const response = await fetch( - `https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/quests/history`, - { - method: "GET", - headers: { Authorization: `Bot ${env.WOV_API_KEY}` }, - }, - ); - const history = (await response.json()) as Array; - return history[0]; -}; - -export const getActiveQuest = async (): Promise => { - const response = await fetch( - `https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/quests/active`, - { - method: "GET", - headers: { Authorization: `Bot ${env.WOV_API_KEY}` }, - }, - ); - if (response.status === 404) return null; - return (await response.json()) as QuestResult; -}; - -export const checkForNewQuest = async (): Promise => { - const lastQuest = await getLatestQuest(); - const lastId = lastQuest.quest.id; - - const cachedQuestId = await redis.get("quest:last_id"); - if (cachedQuestId === lastId || cachedQuestId === "IGNORE") { - return null; - } - - await redis.set("quest:last_id", lastId); - return lastQuest; -}; - -export const getClanMembers = async (): Promise< - Array<{ playerId: string; username: string }> -> => { - const cached = await redis.get("clan:members"); - if (cached) { - return JSON.parse(cached); - } - - const response = await fetch( - `https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/members`, - { - method: "GET", - headers: { Authorization: `Bot ${env.WOV_API_KEY}` }, - }, - ); - const data = (await response.json()) as Array<{ - playerId: string; - username: string; - }>; - - await redis.set("clan:members", JSON.stringify(data), "EX", 60 * 60); - return data; -}; - -export const searchPlayer = async (username: string) => { - try { - const response = await fetch( - `https://api.wolvesville.com//players/search?username=${username}`, - { - method: "GET", - headers: { Authorization: `Bot ${env.WOV_API_KEY}` }, - }, - ); - - if (response.status === 404) return null; - - const data = (await response.json()) as { - id: string; - clanId: string | null; - }; - - return data; - } catch { - return null; - } -}; - -export const getClanInfos = async (clanId: string) => { - const response = await fetch( - `https://api.wolvesville.com/clans/${clanId}/info`, - { - method: "GET", - headers: { Authorization: `Bot ${env.WOV_API_KEY}` }, - }, - ); - const data = (await response.json()) as { - name: string; - tag: string; - }; - - return data; -}; - -export async function getPlayer(playerId: string) { - try { - const response = await fetch( - `https://api.wolvesville.com/players/${playerId}`, - { - method: "GET", - headers: { Authorization: `Bot ${env.WOV_API_KEY}` }, - }, - ); - - if (response.status === 404) return null; - - const data = (await response.json()) as { - username: string; - }; - - return data; - } catch { - return null; - } -} diff --git a/apps/discord-bot/src/tracking.ts b/apps/discord-bot/src/tracking.ts new file mode 100644 index 0000000..b5f4d87 --- /dev/null +++ b/apps/discord-bot/src/tracking.ts @@ -0,0 +1,82 @@ +import type { Client } from "discord.js"; +import { createLogger, logger } from "@lbf-bot/utils"; +import { db, tables, eq } from "@lbf-bot/database"; +import { env } from "~/env"; +import { getPlayer } from "~/wov"; +import { createInfoEmbed } from "~/discord"; + +const trackingLogger = createLogger({ prefix: "tracking" }); + +export async function listTrackedPlayers(): Promise { + const players = await db.query.trackedPlayers.findMany({ + columns: { playerId: true }, + }); + return players.map((p) => p.playerId); +} + +export async function isTracked(playerId: string): Promise { + const player = await db.query.trackedPlayers.findFirst({ + where: eq(tables.trackedPlayers.playerId, playerId), + }); + return player !== undefined; +} + +export async function trackPlayer(playerId: string): Promise { + if (await isTracked(playerId)) return; + + const player = await getPlayer(playerId); + if (!player) return; + + await db.insert(tables.trackedPlayers).values({ playerId }); + await db.insert(tables.usernameHistory).values({ playerId, username: player.username }); +} + +export async function untrackPlayer(playerId: string): Promise { + await db.delete(tables.trackedPlayers).where(eq(tables.trackedPlayers.playerId, playerId)); +} + +export async function getPlayerUsernames(playerId: string): Promise { + const tracked = await db.query.trackedPlayers.findFirst({ + where: eq(tables.trackedPlayers.playerId, playerId), + with: { + usernameHistory: { orderBy: (history, { asc }) => [asc(history.firstSeenAt)] }, + }, + }); + if (!tracked) return []; + return tracked.usernameHistory.map((h) => h.username); +} + +export async function addUsernameToHistory(playerId: string, username: string): Promise { + await db.insert(tables.usernameHistory).values({ playerId, username }); + await db + .update(tables.trackedPlayers) + .set({ updatedAt: new Date() }) + .where(eq(tables.trackedPlayers.playerId, playerId)); +} + +export const trackingCron = async (client: Client) => { + trackingLogger.info("Checking for tracked players"); + const trackedPlayers = await listTrackedPlayers(); + trackingLogger.info(`${trackedPlayers.length} players to check`); + + for (const playerId of trackedPlayers) { + const player = await getPlayer(playerId); + if (!player) continue; + + const usernames = await getPlayerUsernames(playerId); + if (usernames.includes(player.username)) continue; + + await addUsernameToHistory(playerId, player.username); + + const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); + if (!chan?.isSendable()) { + return logger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'"); + } + + const lastUsername = usernames[usernames.length - 1]; + const description = `### [UPDATE] \`${lastUsername}\` -> \`${player.username}\` [\`${playerId}\`]\n\n**Nouveau pseudo:** \`${player.username}\`\n**Anciens pseudos:**\n${usernames.map((x) => `- \`${x}\``).join("\n")}`; + await chan.send(createInfoEmbed(description, 0x00ea00)); + + trackingLogger.info(`Username changed: ${lastUsername} -> ${player.username} [${playerId}]`); + } +}; diff --git a/apps/discord-bot/src/utils/cli.ts b/apps/discord-bot/src/utils/cli.ts deleted file mode 100644 index 75cf869..0000000 --- a/apps/discord-bot/src/utils/cli.ts +++ /dev/null @@ -1,32 +0,0 @@ -type Mode = { type: "bot" } | { type: "user"; channelId: string }; - -export const parseArgs = (args: string[]): Mode => { - let mode: Mode = { type: "bot" }; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - switch (arg) { - case "-u": - case "--as-user": { - const channelId = args[(i += 1)]; - - if (!channelId) { - console.error(`ERROR: ${arg} requires a channel ID`); - process.exit(1); - } - - mode = { type: "user", channelId }; - - break; - } - - default: { - console.error(`ERROR: Unrecognized argument: '${arg}'`); - process.exit(1); - } - } - } - - return mode; -}; diff --git a/apps/discord-bot/src/utils/discord.ts b/apps/discord-bot/src/utils/discord.ts deleted file mode 100644 index 10bc076..0000000 --- a/apps/discord-bot/src/utils/discord.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { getAccountBalance, setAccountBalance } from "~/services/account"; -import { env } from "~/env"; -import type { QuestResult } from "~/services/wov"; -import type { MessageCreateOptions, APIEmbed, Message } from "discord.js"; - -export const makeResultEmbed = async ( - result: QuestResult, - exclude: Array, -): Promise => { - const imageUrl = result.quest.promoImageUrl; - const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16); - const participants = result.participants.toSorted((a, b) => b.xp - a.xp); - - let rewardsEmbed: APIEmbed | undefined; - if (env.QUEST_REWARDS) { - const rewardedParticipants = participants - .map((x) => ({ id: x.playerId, username: x.username })) - .filter((x) => !exclude.includes(x.username)); - const medals = ["đŸ„‡", "đŸ„ˆ", "đŸ„‰"].concat( - new Array(rewardedParticipants.length).fill("🏅"), - ); - - const rewards = rewardedParticipants - .slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length)) - .map( - (x, i) => - `- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} ${env.QUEST_REWARDS_ARE_GEMS ? "gemmes" : ""}`, - ); - - if (env.QUEST_REWARDS_ARE_GEMS) { - const arr = rewardedParticipants.slice( - 0, - Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length), - ); - for (let i = 0; i < arr.length; i++) { - const balance = await getAccountBalance(arr[i].id); - await setAccountBalance( - arr[i].id, - balance + parseInt(env.QUEST_REWARDS![i]), - ); - } - } - - rewardsEmbed = { - title: "RĂ©compenses", - description: `${rewards.join("\n")}\n\n-# \`@LBF gemmes\` pour voir votre nombre de gemmes. Puis avec ${env.DISCORD_REWARDS_GIVER} pour Ă©changer contre des cadeaux !`, - color, - }; - } - - return { - content: `-# ||${env.DISCORD_MENTION}||`, - embeds: [ - { - description: `# RĂ©sultats de quĂȘte\n\nMerci Ă  toutes et Ă  tous d'avoir participĂ© đŸ«Ą`, - color, - image: { - url: imageUrl, - }, - }, - ...(rewardsEmbed ? [rewardsEmbed] : []), - { - title: "Classement", - description: participants - .filter((x) => !exclude.includes(x.username)) - .filter((_, i) => i < 8) - .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) - .join("\n"), - color, - }, - ], - }; -}; - -export const createErrorEmbed = ( - message: string, - color = 0xea0000, -): MessageCreateOptions => ({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n${message}`, - color, - }, - ], -}); - -export const createSuccessEmbed = ( - message: string, - color = 0x00ea00, -): MessageCreateOptions => ({ - embeds: [ - { - description: `### ✅ ${message}`, - color, - }, - ], -}); - -export const createInfoEmbed = ( - message: string, - color = 0x89cff0, -): MessageCreateOptions => ({ - embeds: [ - { - description: message, - color, - }, - ], -}); - -export const replyError = (message: Message, text: string, color?: number) => - message.reply(createErrorEmbed(text, color)); - -export const replySuccess = (message: Message, text: string, color?: number) => - message.reply(createSuccessEmbed(text, color)); - -export const replyInfo = (message: Message, text: string, color?: number) => - message.reply(createInfoEmbed(text, color)); diff --git a/apps/discord-bot/src/utils/quest.ts b/apps/discord-bot/src/utils/quest.ts deleted file mode 100644 index 7de88db..0000000 --- a/apps/discord-bot/src/utils/quest.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { ChannelType, type Client, type Message } from "discord.js"; -import { env } from "~/env"; -import { makeResultEmbed } from "~/utils/discord"; -import type { QuestResult } from "~/services/wov"; -import { createLogger } from "@lbf-bot/utils"; - -const questLogger = createLogger({ prefix: "quests" }); - -export const askForGrinders = async (quest: QuestResult, client: Client) => { - const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); - if (!adminChannel || adminChannel.type !== ChannelType.GuildText) { - return questLogger.fatal("Invalid 'DISCORD_ADMIN_CHANNEL'"); - } - - let exclude: string[] = []; - if (env.QUEST_REWARDS) { - const top10 = quest.participants - .filter((x) => !env.QUEST_EXCLUDE.includes(x.username)) - .sort((a, b) => b.xp - a.xp) - .slice(0, 10) - .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) - .join("\n"); - - const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); - - await adminChannel.send({ - content: `-# ||${env.DISCORD_ADMIN_MENTION}||`, - embeds: [ - { - title: "QuĂȘte terminĂ©e !", - color, - }, - { - title: "Top 10 XP", - description: top10, - color, - }, - { - title: "Qui a grind ?", - description: - "Merci d'entrer les pseudos des joueurs qui ont grind.\n\nFormat:```@LBF laulau,Yuno,...```\n**Attention les majuscules comptent**\nPour entrer la liste des joueurs, il faut __mentionner le bot__, si personne n'a grind, `@LBF tg`", - color, - }, - ], - }); - - const filter = (msg: Message) => - msg.channel.id === adminChannel.id && - !msg.author.bot && - msg.content.startsWith(`<@${client.user!.id}>`); - - let confirmed = false; - let answer: string | null = null; - while (!confirmed) { - const collected = await adminChannel.awaitMessages({ filter, max: 1 }); - answer = collected.first()?.content || null; - if (!answer) continue; - - answer = answer.replace(`<@${client.user!.id}>`, "").trim(); - if (answer.toLowerCase() === "tg") { - answer = ""; - break; - } - - const players = answer - .split(",") - .map((x) => x.trim()) - .filter(Boolean); - await adminChannel.send({ - embeds: [ - { - title: "Joueurs entrĂ©s", - description: players.length - ? players.map((name) => `- ${name}`).join("\n") - : "*Aucun joueur entrĂ©*", - color, - }, - ], - content: `Est-ce correct ? (oui/non)`, - }); - const confirmFilter = (msg: Message) => - msg.channel.id === adminChannel.id && - !msg.author.bot && - ["oui", "non", "yes", "no"].includes(msg.content.toLowerCase()); - const confirmCollected = await adminChannel.awaitMessages({ - filter: confirmFilter, - max: 1, - }); - const confirmation = confirmCollected.first()?.content.toLowerCase(); - if (confirmation === "oui" || confirmation === "yes") { - confirmed = true; - await adminChannel.send({ content: "Ok" }); - } else { - await adminChannel.send({ - content: "D'accord, veuillez rĂ©essayer. Qui a grind ?", - }); - } - } - - if (answer === null) { - return questLogger.fatal("Answer was 'null', this should be unreachable"); - } - - exclude = answer - .split(",") - .map((x) => x.trim()) - .filter(Boolean); - } - - const embed = await makeResultEmbed(quest, [ - ...env.QUEST_EXCLUDE, - ...exclude, - ]); - const rewardChannel = await client.channels.fetch( - env.DISCORD_REWARDS_CHANNEL, - ); - if (rewardChannel && rewardChannel.type === ChannelType.GuildText) { - await rewardChannel.send(embed); - } else { - return questLogger.fatal("Invalid 'DISCORD_REWARDS_CHANNEL'"); - } - - if (env.QUEST_EXCLUDE) { - await adminChannel.send("EnvoyĂ© !"); - } - questLogger.info(`Results posted at: ${new Date().toISOString()}`); -}; diff --git a/apps/discord-bot/src/wov.ts b/apps/discord-bot/src/wov.ts new file mode 100644 index 0000000..a5f2203 --- /dev/null +++ b/apps/discord-bot/src/wov.ts @@ -0,0 +1,89 @@ +import { env } from "~/env"; +import { redis } from "@lbf-bot/database"; + +export type QuestParticipant = { + playerId: string; + username: string; + xp: number; +}; + +export type QuestResult = { + quest: { + id: string; + promoImageUrl: string; + promoImagePrimaryColor: string; + }; + participants: Array; +}; + +export type Player = { + id: string; + clanId: string | null; +}; + +export type PlayerDetails = { + username: string; +}; + +export type ClanMember = { + playerId: string; + username: string; +}; + +export type ClanInfo = { + name: string; + tag: string; +}; + +const BASE_URL = "https://api.wolvesville.com"; + +const fetchWovApi = async (path: string): Promise => { + try { + const response = await fetch(`${BASE_URL}${path}`, { + headers: { Authorization: `Bot ${env.WOV_API_KEY}` }, + }); + if (!response.ok) return null; + return (await response.json()) as T; + } catch { + return null; + } +}; + +export const getLatestQuest = async (): Promise => { + const history = await fetchWovApi>(`/clans/${env.WOV_CLAN_ID}/quests/history`); + return history?.[0] ?? null; +}; + +export const getActiveQuest = async (): Promise => + fetchWovApi(`/clans/${env.WOV_CLAN_ID}/quests/active`); + +export const checkForNewQuest = async (): Promise => { + const lastQuest = await getLatestQuest(); + if (!lastQuest) return null; + + const cachedQuestId = await redis.get("quest:last_id"); + if (cachedQuestId === lastQuest.quest.id || cachedQuestId === "IGNORE") return null; + + await redis.set("quest:last_id", lastQuest.quest.id); + return lastQuest; +}; + +export const getClanMembers = async (): Promise> => { + const cached = await redis.get("clan:members"); + if (cached) return JSON.parse(cached) as Array; + + const data = await fetchWovApi>(`/clans/${env.WOV_CLAN_ID}/members`); + if (!data) return []; + + await redis.set("clan:members", JSON.stringify(data), "EX", 60 * 60); + return data; +}; + +export const searchPlayer = async (username: string): Promise => + fetchWovApi(`/players/search?username=${username}`); + +export const getClanInfo = async (clanId: string): Promise => + fetchWovApi(`/clans/${clanId}/info`); + +export const getPlayer = async (playerId: string): Promise => + fetchWovApi(`/players/${playerId}`); diff --git a/apps/discord-bot/tsconfig.build.json b/apps/discord-bot/tsconfig.build.json index a1d59a5..a30a392 100644 --- a/apps/discord-bot/tsconfig.build.json +++ b/apps/discord-bot/tsconfig.build.json @@ -1,14 +1,14 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "dist", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": false - }, - "tsc-alias": { - "resolveFullPaths": true - }, - "include": ["src"] + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false + }, + "tsc-alias": { + "resolveFullPaths": true + }, + "include": ["src"] } diff --git a/apps/discord-bot/tsconfig.json b/apps/discord-bot/tsconfig.json index 61dc659..c413468 100644 --- a/apps/discord-bot/tsconfig.json +++ b/apps/discord-bot/tsconfig.json @@ -1,28 +1,27 @@ { - "compilerOptions": { - "lib": ["ESNext"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "baseUrl": ".", - "paths": { - "~/*": ["./src/*"], - "~": ["./src/index"] + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "paths": { + "~/*": ["./src/*"], + "~": ["./src/index"] + }, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true, + + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false }, - - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true, - - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] + "include": ["src"], + "exclude": ["node_modules", "dist"] } diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7450547 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,25 @@ +import tseslint from "typescript-eslint"; + +export default tseslint.config( + [ + { + ignores: [ + "**/dist/**", + "**/node_modules/**", + "**/drizzle/**", + ] + }, + { + files: ["**/*.ts"], + extends: [tseslint.configs.recommendedTypeChecked], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + }, + },] +); diff --git a/package.json b/package.json index 9f75dcd..0065abb 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,10 @@ "name": "lbf-bot", "packageManager": "pnpm@10.24.0", "scripts": { - "format": "prettier --write --cache ." + "check": "pnpm -r check" }, - "dependencies": { - "prettier": "3.6.2" + "devDependencies": { + "eslint": "^10.3.0", + "typescript-eslint": "^8.59.2" } } diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index 261a541..a118a2a 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -2,10 +2,10 @@ import { defineConfig } from "drizzle-kit"; import { env } from "~/env"; export default defineConfig({ - dialect: "postgresql", - out: "./drizzle", - schema: "./src/schema/index.ts", - dbCredentials: { - url: env.DATABASE_URL, - }, + dialect: "postgresql", + out: "./drizzle", + schema: "./src/schema/index.ts", + dbCredentials: { + url: env.DATABASE_URL, + }, }); diff --git a/packages/database/package.json b/packages/database/package.json index 7209255..4ba0b45 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -1,36 +1,37 @@ { - "name": "@lbf-bot/database", - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" + "name": "@lbf-bot/database", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "drizzle" + ], + "scripts": { + "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "check": "tsc --noEmit && eslint src/", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "@lbf-bot/utils": "workspace:*", + "drizzle-orm": "0.44.7", + "ioredis": "5.8.2", + "pg": "8.16.3", + "zod": "4.1.11" + }, + "devDependencies": { + "drizzle-kit": "0.31.7", + "tsc-alias": "1.8.16", + "typescript": "5.9.3" } - }, - "files": [ - "dist", - "drizzle" - ], - "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", - "db:generate": "drizzle-kit generate", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "@lbf-bot/utils": "workspace:*", - "drizzle-orm": "0.44.7", - "ioredis": "5.8.2", - "pg": "8.16.3", - "zod": "4.1.11" - }, - "devDependencies": { - "drizzle-kit": "0.31.7", - "tsc-alias": "1.8.16", - "typescript": "5.9.3" - } } diff --git a/packages/database/tsconfig.build.json b/packages/database/tsconfig.build.json index a1d59a5..a30a392 100644 --- a/packages/database/tsconfig.build.json +++ b/packages/database/tsconfig.build.json @@ -1,14 +1,14 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "dist", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": false - }, - "tsc-alias": { - "resolveFullPaths": true - }, - "include": ["src"] + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false + }, + "tsc-alias": { + "resolveFullPaths": true + }, + "include": ["src"] } diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json index 9d039fe..fe4b96b 100644 --- a/packages/database/tsconfig.json +++ b/packages/database/tsconfig.json @@ -1,28 +1,27 @@ { - "compilerOptions": { - "lib": ["ESNext"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "baseUrl": ".", - "paths": { - "~/*": ["./src/*"], - "~": ["./src/index"] + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "paths": { + "~/*": ["./src/*"], + "~": ["./src/index"] + }, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true, + + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false }, - - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true, - - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - }, - "include": ["src", "drizzle.config.ts"], - "exclude": ["node_modules", "dist"] + "include": ["src", "drizzle.config.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/utils/package.json b/packages/utils/package.json index 8c0c3b5..3a713e7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,29 +1,30 @@ { - "name": "@lbf-bot/utils", - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" + "name": "@lbf-bot/utils", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "check": "tsc --noEmit && eslint src/" + }, + "dependencies": { + "dotenv": "17.2.3", + "zod": "4.1.11" + }, + "devDependencies": { + "@types/node": "22.19.1", + "tsc-alias": "1.8.16", + "typescript": "5.9.3" } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json" - }, - "dependencies": { - "dotenv": "17.2.3", - "zod": "4.1.11" - }, - "devDependencies": { - "@types/node": "22.19.1", - "tsc-alias": "1.8.16", - "typescript": "5.9.3" - } } diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts index ca9945c..eb65ec5 100644 --- a/packages/utils/src/env.ts +++ b/packages/utils/src/env.ts @@ -2,16 +2,16 @@ import "dotenv/config"; import { z } from "zod"; export const parseEnv = (vars: T) => { - const schema = z.object(vars); - const result = schema.safeParse(process.env); - if (!result.success) { - console.error("ERROR: Environment variable validation failed:"); - for (const issue of result.error.issues) { - console.error(`- ${issue.path.join(".")}: ${issue.message}`); + const schema = z.object(vars); + const result = schema.safeParse(process.env); + if (!result.success) { + console.error("ERROR: Environment variable validation failed:"); + for (const issue of result.error.issues) { + console.error(`- ${issue.path.join(".")}: ${issue.message}`); + } + + process.exit(1); } - process.exit(1); - } - - return result.data; + return result.data; }; diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index d2dbacb..2ab883e 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -1,101 +1,96 @@ type LogLevel = "debug" | "info" | "warn" | "error"; -interface LoggerOptions { - prefix?: string; - level?: LogLevel; +type LoggerOptions = { + prefix?: string; + level?: LogLevel; } const LOG_LEVELS = { - debug: 0, - info: 1, - warn: 2, - error: 3, + debug: 0, + info: 1, + warn: 2, + error: 3, } as const satisfies Record; const COLORS = { - debug: "\x1b[36m", // cyan - info: "\x1b[32m", // green - warn: "\x1b[33m", // yellow - error: "\x1b[31m", // red - reset: "\x1b[0m", - gray: "\x1b[90m", - bold: "\x1b[1m", + debug: "\x1b[36m", // cyan + info: "\x1b[32m", // green + warn: "\x1b[33m", // yellow + error: "\x1b[31m", // red + reset: "\x1b[0m", + gray: "\x1b[90m", + bold: "\x1b[1m", } as const; class Logger { - private prefix: string; - private minLevel: number; + private prefix: string; + private minLevel: number; - constructor(options: LoggerOptions = {}) { - this.prefix = options.prefix || ""; - this.minLevel = LOG_LEVELS[options.level || "info"]; - } + constructor(options: LoggerOptions = {}) { + this.prefix = options.prefix || ""; + this.minLevel = LOG_LEVELS[options.level || "info"]; + } - private formatTimestamp(): string { - const now = new Date(); - const hours = String(now.getHours()).padStart(2, "0"); - const minutes = String(now.getMinutes()).padStart(2, "0"); - const seconds = String(now.getSeconds()).padStart(2, "0"); - return `${hours}:${minutes}:${seconds}`; - } + private formatTimestamp(): string { + const now = new Date(); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + const seconds = String(now.getSeconds()).padStart(2, "0"); + return `${hours}:${minutes}:${seconds}`; + } - private log(level: LogLevel, message: string, ...args: unknown[]): void { - if (LOG_LEVELS[level] < this.minLevel) return; + private log(level: LogLevel, message: string, ...args: unknown[]): void { + if (LOG_LEVELS[level] < this.minLevel) return; - const timestamp = this.formatTimestamp(); - const color = COLORS[level]; - const levelStr = level.toUpperCase().padEnd(5); - const prefix = this.prefix ? `[${this.prefix}] ` : ""; + const timestamp = this.formatTimestamp(); + const color = COLORS[level]; + const levelStr = level.toUpperCase().padEnd(5); + const prefix = this.prefix ? `[${this.prefix}] ` : ""; - const formattedArgs = args.map((arg) => { - if (arg instanceof Error) { - return arg; - } - return arg; - }); + const formattedArgs = args.map((arg) => { + if (arg instanceof Error) { + return arg; + } + return arg; + }); - console.log( - `${COLORS.gray}${timestamp}${COLORS.reset} ${color}${COLORS.bold}${levelStr}${COLORS.reset} ${prefix}${message}`, - ...formattedArgs, - ); - } + console.log(`${COLORS.gray}${timestamp}${COLORS.reset} ${color}${COLORS.bold}${levelStr}${COLORS.reset} ${prefix}${message}`, ...formattedArgs); + } - debug(message: string, ...args: unknown[]): void { - this.log("debug", message, ...args); - } + debug(message: string, ...args: unknown[]): void { + this.log("debug", message, ...args); + } - info(message: string, ...args: unknown[]): void { - this.log("info", message, ...args); - } + info(message: string, ...args: unknown[]): void { + this.log("info", message, ...args); + } - warn(message: string, ...args: unknown[]): void { - this.log("warn", message, ...args); - } + warn(message: string, ...args: unknown[]): void { + this.log("warn", message, ...args); + } - error(message: string, ...args: unknown[]): void { - this.log("error", message, ...args); - } + error(message: string, ...args: unknown[]): void { + this.log("error", message, ...args); + } - fatal(message: string, ...args: unknown[]): never { - this.log("error", message, ...args); - process.exit(1); - } + fatal(message: string, ...args: unknown[]): never { + this.log("error", message, ...args); + process.exit(1); + } - child(prefix: string): Logger { - const childPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix; - return new Logger({ prefix: childPrefix, level: this.getLevel() }); - } + child(prefix: string): Logger { + const childPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix; + return new Logger({ prefix: childPrefix, level: this.getLevel() }); + } - private getLevel(): LogLevel { - const entry = Object.entries(LOG_LEVELS).find( - ([, value]) => value === this.minLevel, - ); - return (entry?.[0] as LogLevel) || "info"; - } + private getLevel(): LogLevel { + const entry = Object.entries(LOG_LEVELS).find(([, value]) => value === this.minLevel); + return (entry?.[0] as LogLevel) || "info"; + } } export const createLogger = (options?: LoggerOptions): Logger => { - return new Logger(options); + return new Logger(options); }; export const logger = createLogger(); diff --git a/packages/utils/tsconfig.build.json b/packages/utils/tsconfig.build.json index a1d59a5..a30a392 100644 --- a/packages/utils/tsconfig.build.json +++ b/packages/utils/tsconfig.build.json @@ -1,14 +1,14 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "dist", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": false - }, - "tsc-alias": { - "resolveFullPaths": true - }, - "include": ["src"] + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false + }, + "tsc-alias": { + "resolveFullPaths": true + }, + "include": ["src"] } diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index 61dc659..563bf69 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -1,28 +1,28 @@ { - "compilerOptions": { - "lib": ["ESNext"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", + "compilerOptions": { + "lib": ["ESNext"], + "types": ["node"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", - "moduleResolution": "bundler", - "allowImportingTsExtensions": false, - "baseUrl": ".", - "paths": { - "~/*": ["./src/*"], - "~": ["./src/index"] + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "paths": { + "~/*": ["./src/*"], + "~": ["./src/index"] + }, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true, + + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false }, - - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true, - - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] + "include": ["src"], + "exclude": ["node_modules", "dist"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7af3bb..0b46881 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,10 +7,13 @@ settings: importers: .: - dependencies: - prettier: - specifier: 3.6.2 - version: 3.6.2 + devDependencies: + eslint: + specifier: ^10.3.0 + version: 10.3.0 + typescript-eslint: + specifier: ^8.59.2 + version: 8.59.2(eslint@10.3.0)(typescript@5.9.3) apps/discord-bot: dependencies: @@ -23,12 +26,9 @@ importers: discord.js: specifier: ^14.21.0 version: 14.25.1 - dotenv: - specifier: ^17.2.3 - version: 17.2.3 zod: - specifier: ^3.24.4 - version: 3.25.76 + specifier: 4.1.11 + version: 4.1.11 devDependencies: '@types/node': specifier: ^22.10.2 @@ -575,6 +575,56 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@ioredis/commands@1.4.0': resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} @@ -602,16 +652,97 @@ packages: resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@22.19.1': resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vladfrangu/async_event_emitter@2.4.7': resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -620,10 +751,18 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -643,6 +782,10 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -652,6 +795,9 @@ packages: supports-color: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -787,6 +933,52 @@ packages: engines: {node: '>=18'} hasBin: true + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.3.0: + resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -794,13 +986,43 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -813,6 +1035,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -821,6 +1047,14 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + ioredis@5.8.2: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} @@ -841,6 +1075,29 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -864,6 +1121,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -871,10 +1132,33 @@ packages: resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} engines: {node: '>=16.0.0'} + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -917,6 +1201,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + plimit-lit@1.6.1: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} @@ -941,10 +1229,13 @@ packages: resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} queue-lit@1.5.2: resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} @@ -975,6 +1266,19 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -993,10 +1297,20 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-mixer@6.0.4: resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} @@ -1013,6 +1327,17 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.2: + resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1025,6 +1350,18 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -1041,8 +1378,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} zod@4.1.11: resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} @@ -1332,6 +1670,52 @@ snapshots: '@esbuild/win32-x64@0.27.0': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0)': + dependencies: + eslint: 10.3.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@ioredis/commands@1.4.0': {} '@nodelib/fs.scandir@2.1.5': @@ -1355,6 +1739,12 @@ snapshots: '@sapphire/snowflake@3.5.3': {} + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + '@types/node@22.19.1': dependencies: undici-types: 6.21.0 @@ -1363,8 +1753,112 @@ snapshots: dependencies: '@types/node': 22.19.1 + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0)(typescript@5.9.3))(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 10.3.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.2(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 10.3.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.59.2(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0)(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.3.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.2': {} + + '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.2(eslint@10.3.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + eslint: 10.3.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + eslint-visitor-keys: 5.0.1 + '@vladfrangu/async_event_emitter@2.4.7': {} + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -1372,8 +1866,14 @@ snapshots: array-union@2.1.0: {} + balanced-match@4.0.4: {} + binary-extensions@2.3.0: {} + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -1396,10 +1896,18 @@ snapshots: commander@9.5.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + debug@4.4.3: dependencies: ms: 2.1.3 + deep-is@0.1.4: {} + denque@2.1.0: {} dir-glob@3.0.1: @@ -1533,6 +2041,72 @@ snapshots: '@esbuild/win32-ia32': 0.27.0 '@esbuild/win32-x64': 0.27.0 + escape-string-regexp@4.0.0: {} + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.3.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -1543,14 +2117,38 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + fsevents@2.3.3: optional: true @@ -1562,6 +2160,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -1573,6 +2175,10 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + ioredis@5.8.2: dependencies: '@ioredis/commands': 1.4.0 @@ -1599,6 +2205,27 @@ snapshots: is-number@7.0.0: {} + isexe@2.0.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.defaults@4.2.0: {} lodash.isarguments@3.1.0: {} @@ -1616,12 +2243,39 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + ms@2.1.3: {} mylas@2.1.14: {} + natural-compare@1.4.0: {} + normalize-path@3.0.0: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + 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.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + path-type@4.0.0: {} pg-cloudflare@1.2.7: @@ -1661,6 +2315,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.4: {} + plimit-lit@1.6.1: dependencies: queue-lit: 1.5.2 @@ -1678,7 +2334,9 @@ snapshots: postgres@3.4.7: optional: true - prettier@3.6.2: {} + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} queue-lit@1.5.2: {} @@ -1702,6 +2360,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 + semver@7.8.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + slash@3.0.0: {} source-map-support@0.5.21: @@ -1715,10 +2381,19 @@ snapshots: standard-as-callback@2.1.0: {} + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-mixer@6.0.4: {} tsc-alias@1.8.16: @@ -1740,16 +2415,41 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.2(eslint@10.3.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0)(typescript@5.9.3))(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0)(typescript@5.9.3) + eslint: 10.3.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} undici-types@6.21.0: {} undici@6.21.3: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + ws@8.18.3: {} xtend@4.0.2: {} - zod@3.25.76: {} + yocto-queue@0.1.0: {} zod@4.1.11: {}