From 1e3f62d3c488d8b2aee907db451d27f9faf06ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Fri, 30 May 2025 09:38:11 +0200 Subject: [PATCH 01/11] Add workflow & helm --- .gitea/workflows/build-and-push.yml | 76 +++++++++++++++++++++++++++++ deploy/Chart.yaml | 12 +++++ deploy/templates/deployment.yaml | 34 +++++++++++++ deploy/values.yaml | 18 +++++++ 4 files changed, 140 insertions(+) create mode 100644 .gitea/workflows/build-and-push.yml create mode 100644 deploy/Chart.yaml create mode 100644 deploy/templates/deployment.yaml create mode 100644 deploy/values.yaml diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml new file mode 100644 index 0000000..0d1ccba --- /dev/null +++ b/.gitea/workflows/build-and-push.yml @@ -0,0 +1,76 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - master + tags: + - 'build_*' + pull_request: + branches: + - master + +env: + REGISTRY: rgy.angels-dev.fr + PATH: prod + IMAGE_NAME: bot_tamiseur + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} + tags: | + # Tag avec le nom de la branche + type=ref,event=branch + # Tag avec le nom du tag Git + type=ref,event=tag + # Tag avec le SHA du commit + type=sha,prefix={{branch}}- + # Tag latest pour la branche master + type=raw,value=latest,enable={{is_default_branch}} + labels: | + org.opencontainers.image.title=${{ env.IMAGE_NAME }} + org.opencontainers.image.description=Bot Discord + org.opencontainers.image.url=https://gitea.zac.ovh/zachary/bot_Tamiseur + org.opencontainers.image.source=https://gitea.zac.ovh/zachary/bot_Tamiseur + org.opencontainers.image.revision=${{ github.sha }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 # Multi-architecture si nécessaire + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true \ No newline at end of file diff --git a/deploy/Chart.yaml b/deploy/Chart.yaml new file mode 100644 index 0000000..df6856f --- /dev/null +++ b/deploy/Chart.yaml @@ -0,0 +1,12 @@ +# Version schéma helm (v2 = helm3) +apiVersion: v2 + +# Nom de l'application déployée +name: bot_tamiseur + +# Version du chart : doit changer si l'application change ou si la configuration du chart change +#version: 1 +version: "1" + +# icon (optionnel) mais génère un warning avec "helm lint" +icon: https://helm.sh/img/helm-logo.svg \ No newline at end of file diff --git a/deploy/templates/deployment.yaml b/deploy/templates/deployment.yaml new file mode 100644 index 0000000..017bfc8 --- /dev/null +++ b/deploy/templates/deployment.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} +spec: + replicas: 1 + revisionHistoryLimit: 0 + strategy: + type: {{ .Values.deployment.strategy }} + selector: + matchLabels: + pod: {{ .Release.Name }} + template: + metadata: + labels: + pod: {{ .Release.Name }} + spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + containers: + - name: {{ .Release.Name }} + image: "{{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag }}" + imagePullPolicy: {{ .Values.deployment.image.pullPolicy }} + env: + {{ range $envName, $envValue := .Values.deployment.env }} + - name: {{ $envName | quote}} + value: {{ $envValue | quote}} + {{ end }} + {{- if .Values.deployment.resources.enable }} + resources: + {{- toYaml .Values.deployment.resources | nindent 12 }} + {{- end }} \ No newline at end of file diff --git a/deploy/values.yaml b/deploy/values.yaml new file mode 100644 index 0000000..89f1bcd --- /dev/null +++ b/deploy/values.yaml @@ -0,0 +1,18 @@ +deployment: + replica: 1 + strategy: RollingUpdate + image: + repository: "rgy.angels-dev.fr/prod/bot_tamiseur" + tag: "3.0.4" + pullPolicy: IfNotPresent + env: + NODE_ENV: "production" + + ## Pas de limite CPU pour éviter latence + resources: + limits: + # cpu: "" + # Memory: "500Mi" + requests: + Cpu: "0.1" + Memory: "50Mi" \ No newline at end of file From 19119e5c77ded79a22af3bcb14833faf2967837a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Fri, 30 May 2025 11:06:00 +0200 Subject: [PATCH 02/11] Fix workflow & add makefile --- .gitea/workflows/build-and-push.yml | 5 +++++ Makefile | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 Makefile diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index 0d1ccba..faf5ea3 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -26,6 +26,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a13eb36 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# ===================== +# Build new application +# ===================== + + +.PHONY: tag-build +tag-build: ## DEV : Build a prod version with a timestamped tag + @export TIMESTAMP=build_`date +"%G-%m-%d_%Hh%M"`; \ + echo TAG = $$TIMESTAMP; \ + git tag $$TIMESTAMP; \ + git push origin $$TIMESTAMP; + +# =============== +# Deployment tags +# =============== + +.PHONY: tag-deploy +tag-deploy: ## DEV : Set tag to current HEAD to deploy in production + @export TIMESTAMP=deploy_`date +"%G-%m-%d_%Hh%M"`; \ + echo TAG = $$TIMESTAMP; \ + git tag $$TIMESTAMP; \ + git push origin $$TIMESTAMP; From eb6c40c2f5fad66c71f940680b65cd1485ab32aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Fri, 30 May 2025 11:07:50 +0200 Subject: [PATCH 03/11] Fix workflow --- .gitea/workflows/build-and-push.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index faf5ea3..7c87992 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -23,14 +23,14 @@ jobs: packages: write steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 66c5891510ff47c9d9e1135f9287cc20a8c2eb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Fri, 30 May 2025 11:33:06 +0200 Subject: [PATCH 04/11] Remove fix --- .gitea/workflows/build-and-push.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index 7c87992..0d1ccba 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -23,11 +23,6 @@ jobs: packages: write steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - - name: Checkout repository uses: actions/checkout@v4 From f2c6388da6faf6da07ed441ebf1f056fb10e9f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Fri, 30 May 2025 15:35:57 +0200 Subject: [PATCH 05/11] Fix env variable --- .gitea/workflows/build-and-push.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index 0d1ccba..358d4d3 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -12,7 +12,7 @@ on: env: REGISTRY: rgy.angels-dev.fr - PATH: prod + IMAGE_PATH: prod IMAGE_NAME: bot_tamiseur jobs: @@ -40,7 +40,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }} tags: | # Tag avec le nom de la branche type=ref,event=branch @@ -61,7 +61,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 # Multi-architecture si nécessaire + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -71,6 +71,6 @@ jobs: - name: Generate artifact attestation uses: actions/attest-build-provenance@v1 with: - subject-name: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build.outputs.digest }} - push-to-registry: true \ No newline at end of file + push-to-registry: true From ddd617317c2941e4d406f64b6a3e5740cc299e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Mon, 9 Jun 2025 16:29:12 +0200 Subject: [PATCH 06/11] =?UTF-8?q?R=C3=A9=C3=A9criture=20compl=C3=A8te=204.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 12 - .env.example | 29 + .gitea/workflows/build-and-push.yml | 18 +- .github/copilot-instructions.md | 22 + .gitignore | 5 +- .vscode/launch.json | 18 - .vscode/settings.json | 3 - Dockerfile | 19 - Makefile | 1 - README.md | 128 +- build/node.dockerfile | 24 + deploy/values.yaml | 2 +- docs/FREEBOX_LCD.md | 103 + eslint.config.mjs | 39 +- package-lock.json | 3664 +++++++++++---------- package.json | 65 +- src/buttons/freebox/index.ts | 11 + src/buttons/freebox/lcd_status.ts | 88 + src/buttons/freebox/refresh_status.ts | 76 + src/buttons/freebox/test_connection.ts | 71 + src/buttons/index.ts | 26 + src/buttons/loop.ts | 16 - src/buttons/pause.ts | 15 - src/buttons/player/disco_channel.ts | 15 + src/buttons/player/disco_disable.ts | 24 + src/buttons/player/disco_enable.ts | 31 + src/buttons/player/index.ts | 29 + src/buttons/player/loop.ts | 22 + src/buttons/player/pause.ts | 13 + src/buttons/player/previous.ts | 13 + src/buttons/player/resume.ts | 13 + src/buttons/player/shuffle.ts | 13 + src/buttons/player/skip.ts | 13 + src/buttons/player/stop.ts | 16 + src/buttons/player/volume_down.ts | 14 + src/buttons/player/volume_up.ts | 14 + src/buttons/previous.ts | 15 - src/buttons/resume.ts | 15 - src/buttons/shuffle.ts | 15 - src/buttons/skip.ts | 15 - src/buttons/stop.ts | 15 - src/buttons/twitch/channel.ts | 19 + src/buttons/twitch/disable.ts | 28 + src/buttons/twitch/enable.ts | 28 + src/buttons/twitch/index.ts | 17 + src/buttons/twitch/streamer_add.ts | 20 + src/buttons/twitch/streamer_list.ts | 51 + src/buttons/twitch/streamer_remove.ts | 46 + src/buttons/volume_down.ts | 16 - src/buttons/volume_up.ts | 16 - src/commands/global/amp.ts | 451 +-- src/commands/global/boost.ts | 64 +- src/commands/global/database.ts | 184 +- src/commands/global/freebox.ts | 353 ++ src/commands/global/index.ts | 17 + src/commands/global/ping.ts | 28 +- src/commands/global/twitch.ts | 198 ++ src/commands/index.ts | 26 + src/commands/player/disco.ts | 60 + src/commands/player/index.ts | 31 + src/commands/player/loop.ts | 50 +- src/commands/player/lyrics.ts | 106 +- src/commands/player/panel.ts | 51 +- src/commands/player/pause.ts | 30 +- src/commands/player/play.ts | 219 +- src/commands/player/previous.ts | 33 +- src/commands/player/queue.ts | 41 +- src/commands/player/resume.ts | 33 +- src/commands/player/shuffle.ts | 33 +- src/commands/player/skip.ts | 33 +- src/commands/player/stop.ts | 36 +- src/commands/player/volume.ts | 47 +- src/commands/salonpostam/crack.ts | 128 +- src/commands/salonpostam/freebox.ts | 151 - src/commands/salonpostam/index.ts | 15 + src/commands/salonpostam/papa.ts | 68 +- src/commands/salonpostam/parle.ts | 163 +- src/commands/salonpostam/spam.ts | 76 +- src/commands/salonpostam/update.ts | 41 +- src/events/client/error.ts | 11 +- src/events/client/guildCreate.ts | 26 +- src/events/client/guildMemberAdd.ts | 86 +- src/events/client/guildMemberRemove.ts | 43 +- src/events/client/guildMemberUpdate.ts | 72 +- src/events/client/guildUpdate.ts | 36 +- src/events/client/index.ts | 21 + src/events/client/interactionCreate.ts | 94 +- src/events/client/ready.ts | 256 +- src/events/client/voiceStateUpdate.ts | 61 - src/events/mongo/connected.ts | 12 +- src/events/mongo/connecting.ts | 12 +- src/events/mongo/disconnected.ts | 12 +- src/events/mongo/error.ts | 12 +- src/events/mongo/index.ts | 13 + src/events/player/audioTrackAdd.ts | 21 +- src/events/player/audioTracksAdd.ts | 20 +- src/events/player/debug.ts | 19 +- src/events/player/disconnect.ts | 37 +- src/events/player/emptyChannel.ts | 25 +- src/events/player/emptyQueue.ts | 23 +- src/events/player/error.ts | 18 +- src/events/player/index.ts | 25 + src/events/player/playerError.ts | 18 +- src/events/player/playerSkip.ts | 22 +- src/events/player/playerStart.ts | 32 +- src/index.ts | 242 +- src/locales/en.json | 537 +++ src/locales/fr.json | 534 +++ src/schemas/guild.ts | 52 +- src/selectmenus/index.ts | 20 + src/selectmenus/player/disco_channel.ts | 28 + src/selectmenus/player/index.ts | 7 + src/selectmenus/twitch/channel.ts | 32 + src/selectmenus/twitch/index.ts | 9 + src/selectmenus/twitch/streamer_remove.ts | 52 + src/types/amp.ts | 38 + src/types/freebox.ts | 112 + src/types/index.ts | 73 + src/types/player.ts | 27 + src/types/schemas.ts | 69 + src/utils/amp.ts | 116 +- src/utils/console.ts | 54 + src/utils/crack.ts | 96 +- src/utils/dbGuildInit.ts | 22 +- src/utils/freebox.ts | 239 +- src/utils/getUptime.ts | 10 - src/utils/i18n.ts | 111 + src/utils/player.ts | 446 ++- src/utils/rss.ts | 123 - src/utils/twitch.ts | 520 +-- src/utils/uptime.ts | 10 + tsconfig.json | 23 +- tsup.config.ts | 28 + 133 files changed, 8092 insertions(+), 4332 deletions(-) delete mode 100755 .dockerignore create mode 100644 .env.example create mode 100644 .github/copilot-instructions.md delete mode 100755 .vscode/launch.json delete mode 100755 .vscode/settings.json delete mode 100755 Dockerfile create mode 100644 build/node.dockerfile create mode 100644 docs/FREEBOX_LCD.md create mode 100644 src/buttons/freebox/index.ts create mode 100644 src/buttons/freebox/lcd_status.ts create mode 100644 src/buttons/freebox/refresh_status.ts create mode 100644 src/buttons/freebox/test_connection.ts create mode 100644 src/buttons/index.ts delete mode 100755 src/buttons/loop.ts delete mode 100755 src/buttons/pause.ts create mode 100644 src/buttons/player/disco_channel.ts create mode 100644 src/buttons/player/disco_disable.ts create mode 100644 src/buttons/player/disco_enable.ts create mode 100644 src/buttons/player/index.ts create mode 100644 src/buttons/player/loop.ts create mode 100644 src/buttons/player/pause.ts create mode 100644 src/buttons/player/previous.ts create mode 100644 src/buttons/player/resume.ts create mode 100644 src/buttons/player/shuffle.ts create mode 100644 src/buttons/player/skip.ts create mode 100644 src/buttons/player/stop.ts create mode 100644 src/buttons/player/volume_down.ts create mode 100644 src/buttons/player/volume_up.ts delete mode 100755 src/buttons/previous.ts delete mode 100755 src/buttons/resume.ts delete mode 100755 src/buttons/shuffle.ts delete mode 100755 src/buttons/skip.ts delete mode 100755 src/buttons/stop.ts create mode 100644 src/buttons/twitch/channel.ts create mode 100644 src/buttons/twitch/disable.ts create mode 100644 src/buttons/twitch/enable.ts create mode 100644 src/buttons/twitch/index.ts create mode 100644 src/buttons/twitch/streamer_add.ts create mode 100644 src/buttons/twitch/streamer_list.ts create mode 100644 src/buttons/twitch/streamer_remove.ts delete mode 100755 src/buttons/volume_down.ts delete mode 100755 src/buttons/volume_up.ts create mode 100644 src/commands/global/freebox.ts create mode 100644 src/commands/global/index.ts create mode 100644 src/commands/global/twitch.ts create mode 100644 src/commands/index.ts create mode 100644 src/commands/player/disco.ts create mode 100644 src/commands/player/index.ts delete mode 100644 src/commands/salonpostam/freebox.ts create mode 100644 src/commands/salonpostam/index.ts create mode 100644 src/events/client/index.ts delete mode 100755 src/events/client/voiceStateUpdate.ts create mode 100644 src/events/mongo/index.ts create mode 100644 src/events/player/index.ts create mode 100644 src/locales/en.json create mode 100644 src/locales/fr.json create mode 100644 src/selectmenus/index.ts create mode 100644 src/selectmenus/player/disco_channel.ts create mode 100644 src/selectmenus/player/index.ts create mode 100644 src/selectmenus/twitch/channel.ts create mode 100644 src/selectmenus/twitch/index.ts create mode 100644 src/selectmenus/twitch/streamer_remove.ts create mode 100644 src/types/amp.ts create mode 100644 src/types/freebox.ts create mode 100644 src/types/index.ts create mode 100644 src/types/player.ts create mode 100644 src/types/schemas.ts create mode 100644 src/utils/console.ts delete mode 100755 src/utils/getUptime.ts create mode 100644 src/utils/i18n.ts delete mode 100644 src/utils/rss.ts create mode 100644 src/utils/uptime.ts create mode 100644 tsup.config.ts diff --git a/.dockerignore b/.dockerignore deleted file mode 100755 index c29e504..0000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -.git -.vscode -node_modules -public/cracks/* -.dockerignore -.env -.gitignore -.ncurc.json -Dockerfile -eslint.config.mjs -README.md -tsconfig.json \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6429cc1 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +# Configuration du bot Discord +DISCORD_APP_ID=votre_app_id_discord +DISCORD_TOKEN=votre_token_discord +DISCORD_SPT_GUILD_ID=id_guild_salonpostam + +# Configuration Twitch +TWITCH_APP_ID=votre_app_id_twitch +TWITCH_APP_SECRET=votre_secret_twitch + +# Configuration Twurple (pour les webhooks) +TWURPLE_HOSTNAME=localhost +TWURPLE_PORT=3000 +TWURPLE_SECRET=VeryUnsecureSecretPleaseChangeMe + +# Configuration Ngrok (pour le développement) +NGROK_AUTHTOKEN=votre_token_ngrok + +# Configuration MongoDB +MONGOOSE_USER=utilisateur_mongodb +MONGOOSE_PASSWORD=mot_de_passe_mongodb +MONGOOSE_HOST=localhost:27017 +MONGOOSE_DATABASE=nom_base_de_donnees + +# Configuration d'environnement +NODE_ENV=development + +# Configuration des locales +DEFAULT_LOCALE=fr +CONSOLE_LOCALE=en diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index 358d4d3..04750bb 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -26,6 +26,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Build app + run: npm run build --if-present + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -36,20 +46,14 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - - name: Extract metadata + - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }} tags: | - # Tag avec le nom de la branche - type=ref,event=branch # Tag avec le nom du tag Git type=ref,event=tag - # Tag avec le SHA du commit - type=sha,prefix={{branch}}- - # Tag latest pour la branche master - type=raw,value=latest,enable={{is_default_branch}} labels: | org.opencontainers.image.title=${{ env.IMAGE_NAME }} org.opencontainers.image.description=Bot Discord diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..d4f9629 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,22 @@ +# Instructions pour GitHub Copilot + +## Contexte du projet +Ce projet est un bot appelé "bot_Tamiseur" développé en TypeScript. +Il s'agit d'un bot Discord pour des tâches de musique ou de notifications Twitch. +Des fonctionnalités spécifiques telles que amp, freebox... ont été implémentées par moi-même pour intérager avec mon infrastructure. + +## Conventions de code +- Utiliser des noms de variables en anglais +- Suivre les conventions de nommage du langage utilisé +- Commenter le code en français +- Utiliser des messages de commit en français +- Priviléger l'écriture sur une seule ligne pour les fonctions simples si la longueur n'est pas excessive + +## Préférences de développement +- Suivre les pratiques de développement du projet, telles que du code compressé en lisibilité et optimisé par exemple +- Comprendre les fonctionnalités du bot et les interactions avec Discord +- Éviter le spaghetti code et ne pas ajouter de code inutile +- Ne pas écrire énormément de code pour une seule demande, favoriser une approche simple et efficace +- Si demandé, proposer une solution plus développée +- Toujours repasser sur ce que tu as modifié pour retirer les erreurs potentielles +- Ne pas proposer une solution juste pour avoir une solution \ No newline at end of file diff --git a/.gitignore b/.gitignore index 677993a..84ea4a2 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ +.env dist/ node_modules/ -public/cracks/* -.env* -.ncurc.json \ No newline at end of file +public/cracks/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100755 index e073e42..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. - // Pointez pour afficher la description des attributs existants. - // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Nodemon", - "skipFiles": ["/**"], - "runtimeExecutable": "nodemon", - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "restart": true - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100755 index 935f4c9..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "docker.commands.build": "docker build --rm -f \"${dockerfile}\" -t bot_tamiseur:latest -t localhost:5000/bot_tamiseur:latest \"${context}\" && docker push localhost:5000/bot_tamiseur:latest" -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100755 index 9b7f9b5..0000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM node:lts-alpine - -RUN apk add --no-cache ffmpeg python3 make g++ -RUN npm install -g ts-node - -RUN mkdir -p /usr/src/app/node_modules -WORKDIR /usr/src/app -COPY package*.json ./ -RUN chown -R node:node /usr/src/app - -USER node -#ENV NODE_ENV=production -#RUN npm install --production --verbose -RUN npm install --verbose - -COPY --chown=node:node . . - -CMD ["npm", "start"] -#CMD ["npm", "run", "prod"] \ No newline at end of file diff --git a/Makefile b/Makefile index a13eb36..d8e3170 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ # Build new application # ===================== - .PHONY: tag-build tag-build: ## DEV : Build a prod version with a timestamped tag @export TIMESTAMP=build_`date +"%G-%m-%d_%Hh%M"`; \ diff --git a/README.md b/README.md index 3b13fe9..70d66bd 100755 --- a/README.md +++ b/README.md @@ -1,3 +1,129 @@ -# Discord +# Bot Tamiseur + +Bot Discord multifonction développé en TypeScript pour la musique, les notifications Twitch et l'intégration avec diverses infrastructures. + +## Fonctionnalités + +### 🎵 Lecteur de musique +- Lecture de musique depuis diverses sources (YouTube, Spotify, etc.) +- Gestion de file d'attente +- Mode disco avec sauvegarde automatique +- Contrôles avancés (boucle, mélange, volume) + +### 📺 Notifications Twitch +- Notifications automatiques de streams en direct +- Intégration EventSub avec Twurple +- Gestion multi-streamers + +### 🔧 Intégrations infrastructure +- **AMP**: Gestion des instances de serveurs de jeu +- **Freebox**: Intégration avec l'API Freebox +- **Crack**: Fonctionnalités spécifiques pour SalonPostam + +## Installation + +### Prérequis +- Node.js 22+ +- MongoDB +- Compte Discord Developer +- Compte Twitch Developer (optionnel) + +### Configuration + +1. Cloner le repository : +```bash +git clone +cd bot_Tamiseur +``` + +2. Installer les dépendances : +```bash +npm install +``` + +3. Configurer les variables d'environnement : +```bash +cp .env.example .env +# Éditer le fichier .env avec vos valeurs +``` + +### Variables d'environnement requises + +Voir le fichier `.env.example` pour la liste complète des variables. + +**Variables essentielles :** +- `DISCORD_APP_ID` et `DISCORD_TOKEN` : Configuration Discord +- `MONGOOSE_*` : Configuration base de données MongoDB + +**Variables optionnelles :** +- `TWITCH_*` : Pour les notifications Twitch +- `NGROK_AUTHTOKEN` : Pour le développement avec webhooks Twitch +- `DEFAULT_LOCALE` : Locale par défaut (défaut: `fr`) +- `CONSOLE_LOCALE` : Locale pour les logs console (défaut: `en`) + +## Utilisation + +### Développement +```bash +npm run dev +``` + +### Production +```bash +npm run build +npm run start:prod +``` + +### Scripts disponibles +- `npm run dev` : Démarrage en mode développement avec watch +- `npm run build` : Build de production +- `npm run start:prod` : Démarrage en production +- `npm run lint` : Vérification du code +- `npm run lint:fix` : Correction automatique des erreurs de linting + +## Architecture + +Le projet suit une architecture modulaire TypeScript : + +- `/src/commands/` : Commandes slash Discord organisées par catégories +- `/src/events/` : Gestionnaires d'événements (Discord, MongoDB, Player) +- `/src/buttons/` et `/src/selectmenus/` : Interactions utilisateur +- `/src/utils/` : Modules utilitaires (player, twitch, amp, freebox) +- `/src/types/` : Définitions TypeScript +- `/src/schemas/` : Schémas MongoDB + +## Déploiement + +Le projet inclut des configurations Docker et Helm pour le déploiement en Kubernetes : + +```bash +# Build et tag pour déploiement +make tag-build +make tag-deploy +``` + +## Développement + +Le code suit les conventions définies dans `.github/copilot-instructions.md` : +- Variables en anglais, commentaires en français +- Code optimisé et lisible +- Architecture modulaire stricte + +### Internationalisation + +Le bot supporte plusieurs langues via le système i18n : + +**Fonctions de traduction :** +- `t(locale, key, params)` : Traduction basée sur la locale utilisateur +- `fr(key, params)` : Traduction en français +- `en(key, params)` : Traduction en anglais + +**Configuration des locales :** +- `DEFAULT_LOCALE` : Locale par défaut pour les utilisateurs +- `CONSOLE_LOCALE` : Locale pour les messages de logs console + +**Fichiers de traduction :** +- `/src/locales/fr.json` : Traductions françaises +- `/src/locales/en.json` : Traductions anglaises diff --git a/build/node.dockerfile b/build/node.dockerfile new file mode 100644 index 0000000..53d6a63 --- /dev/null +++ b/build/node.dockerfile @@ -0,0 +1,24 @@ +# Starting from node +FROM node:22-alpine + +ENV NODE_ENV=production + +WORKDIR /app + +RUN apk add --no-cache python3 make g++ + +# Copy package files and install only production dependencies +COPY package.json package-lock.json* . +RUN npm ci --only=production --ignore-scripts && \ + npm install bufferutil zlib-sync + + +# Copy the builded files and the charts +COPY ./dist/* . + +# Set the permissions +RUN chown -R node:node /app +USER node + +# Start the application +CMD ["npm", "start"] diff --git a/deploy/values.yaml b/deploy/values.yaml index 89f1bcd..6ee8790 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -3,7 +3,7 @@ deployment: strategy: RollingUpdate image: repository: "rgy.angels-dev.fr/prod/bot_tamiseur" - tag: "3.0.4" + tag: "4.0.0" pullPolicy: IfNotPresent env: NODE_ENV: "production" diff --git a/docs/FREEBOX_LCD.md b/docs/FREEBOX_LCD.md new file mode 100644 index 0000000..77ba092 --- /dev/null +++ b/docs/FREEBOX_LCD.md @@ -0,0 +1,103 @@ +# Système de Contrôle des LEDs Freebox + +Ce module permet au bot de contrôler automatiquement les LEDs de l'écran LCD de la Freebox avec des timers programmables. + +## Fonctionnalités + +### Contrôle Manuel des LEDs +- Allumer/éteindre les LEDs instantanément via commande Discord +- Récupérer la configuration actuelle de l'écran LCD + +### Timer Automatique +- Programmation d'extinction automatique la nuit +- Programmation d'allumage automatique le matin +- Gestion par bot unique par serveur (évite les conflits) +- Persistance des paramètres en base de données + +## Configuration Prérequise + +1. **Module Freebox activé** : `/database edit guildFbx.enabled true` +2. **Hôte configuré** : `/database edit guildFbx.host ` +3. **Version API configurée** : `/database edit guildFbx.version ` +4. **Authentification** : `/freebox init` (suivre le processus d'autorisation) + +## Commandes Disponibles + +### Récupération de configuration +``` +/freebox get lcd +``` +Récupère et affiche la configuration actuelle de l'écran LCD. + +### Contrôle manuel des LEDs +``` +/freebox lcd leds enabled:true # Allumer les LEDs +/freebox lcd leds enabled:false # Éteindre les LEDs +``` + +### Gestion du timer +``` +# Activer le timer avec horaires +/freebox lcd timer action:enable morning_time:08:00 night_time:22:30 + +# Vérifier le statut du timer +/freebox lcd timer action:status + +# Désactiver le timer +/freebox lcd timer action:disable +``` + +## Fonctionnement du Timer + +### Programmation +- Le timer est programmé automatiquement au démarrage du bot +- Seul le bot configuré comme "gestionnaire" peut contrôler les LEDs d'un serveur +- Les horaires sont vérifiés et formatés (HH:MM, 24h) + +### Exécution +- **Matin** : Les LEDs s'allument à l'heure programmée +- **Soir** : Les LEDs s'éteignent à l'heure programmée +- **Reprogrammation** : Le cycle se répète automatiquement chaque jour + +### Logs +Les opérations sont loggées dans la console : +- Programmation des timers +- Exécution des commandes d'allumage/extinction +- Erreurs de connexion ou d'authentification + +## Gestion Multi-Bot + +### Système de Verrouillage +- Un seul bot peut gérer les LEDs par serveur Discord +- L'ID du bot gestionnaire est stocké en base de données +- Les autres bots reçoivent un message d'erreur s'ils tentent d'utiliser les commandes + +### Changement de Bot Gestionnaire +Pour changer de bot gestionnaire : +1. Désactiver le timer sur le bot actuel : `/freebox lcd timer action:disable` +2. Activer le timer sur le nouveau bot : `/freebox lcd timer action:enable` + +## Dépannage + +### Erreurs Communes +- **"Module Freebox désactivé"** : Activer avec `/database edit guildFbx.enabled true` +- **"Hôte non configuré"** : Définir avec `/database edit guildFbx.host ` +- **"Token d'app manquant"** : Refaire l'initialisation avec `/freebox init` +- **"Géré par un autre bot"** : Désactiver sur l'autre bot d'abord + +### Vérification de Configuration +1. Vérifier que la Freebox est accessible sur le réseau +2. S'assurer que l'application est autorisée dans l'interface Freebox +3. Vérifier les logs de la console pour les erreurs détaillées + +## API Freebox Utilisée + +- `GET /api/v8/lcd/config/` : Récupération de la configuration LCD +- `PUT /api/v8/lcd/config/` : Modification de la configuration LCD +- Propriété `led_strip_enabled` : Contrôle de l'état des LEDs + +## Sécurité + +- Les tokens d'authentification sont gérés automatiquement +- Les sessions sont créées à la demande +- Les erreurs d'authentification sont loggées mais les tokens ne sont pas exposés diff --git a/eslint.config.mjs b/eslint.config.mjs index 964d4ac..f8505d6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,23 +1,22 @@ -import typescriptEslint from "@typescript-eslint/eslint-plugin" -import tsParser from "@typescript-eslint/parser" -import { FlatCompat } from "@eslint/eslintrc" -import { fileURLToPath } from "node:url" -import path from "node:path" -import js from "@eslint/js" +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all -}) - -export default [ - ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, { - plugins: { "@typescript-eslint": typescriptEslint }, - languageOptions: { parser: tsParser }, - rules: { "prefer-const": "off" } + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + } + }, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + { ignores: ["dist/**", "eslint.config.mjs", "tsup.config.ts"] }, + { + rules: { "@typescript-eslint/restrict-template-expressions": "off" }, + files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"] } -] \ No newline at end of file +) diff --git a/package-lock.json b/package-lock.json index 479ce6a..15b283b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,66 +1,340 @@ { "name": "bot_tamiseur", - "version": "3.0.4", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bot_tamiseur", - "version": "3.0.4", + "version": "4.0.0", "dependencies": { - "@discord-player/equalizer": "^7.0.0", - "@discord-player/extractor": "^7.0.0", + "@discord-player/extractor": "^7.1.0", "@discordjs/voice": "^0.18.0", - "@evan/opus": "^1.0.3", - "axios": "^1.7.9", + "@twurple/api": "^7.3.0", + "@twurple/auth": "^7.3.0", + "@twurple/eventsub-http": "^7.3.0", + "@twurple/eventsub-ngrok": "^7.3.0", + "axios": "^1.9.0", "bufferutil": "^4.0.9", - "chalk": "^4.1.2", - "discord-player": "^7.0.0", - "discord-player-youtubei": "^1.3.7", - "discord.js": "^14.17.2", - "dotenv": "^16.4.7", + "chalk": "^5.4.1", + "discord-player": "^7.1.0", + "discord-player-youtubei": "^1.4.6", + "discord.js": "^14.19.3", "iconv-lite": "^0.6.3", - "jsdom": "^25.0.1", - "libsodium-wrappers": "^0.7.15", "mediaplex": "^1.0.0", - "mongoose": "^8.9.3", + "mongoose": "^8.15.1", "parse-torrent": "^9.1.5", - "require-all": "^3.0.0", - "rss-parser": "^3.13.0", - "utf-8-validate": "^6.0.5", - "websocket": "^1.0.35" + "zlib-sync": "^0.1.10" }, "devDependencies": { - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.17.0", - "@swc/core": "^1.10.4", - "@types/node": "^22.10.5", + "@eslint/js": "^9.28.0", + "@types/node": "^22.15.30", "@types/parse-torrent": "^5.8.7", - "@types/websocket": "^1.0.10", - "@typescript-eslint/eslint-plugin": "^8.19.0", - "@typescript-eslint/parser": "^8.19.0", - "eslint": "^9.17.0", - "nodemon": "^3.1.9", - "prettier": "^3.4.2", - "tsx": "^4.19.2" + "dotenv": "^16.5.0", + "eslint": "^9.28.0", + "tsup": "^8.5.0", + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.33.1" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, "node_modules/@bufbuild/protobuf": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz", - "integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.5.1.tgz", + "integrity": "sha512-lut4UTvKL8tqtend0UDu7R79/n9jA7Jtxf77RNPbxtmWqfWI4qQ9bTjf7KCS4vfqLmpQbuHr1ciqJumAgJODdw==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@d-fischer/cache-decorators": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@d-fischer/cache-decorators/-/cache-decorators-4.0.1.tgz", + "integrity": "sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA==", + "license": "MIT", + "dependencies": { + "@d-fischer/shared-utils": "^3.6.3", + "tslib": "^2.6.2" + } + }, + "node_modules/@d-fischer/cross-fetch": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-5.0.5.tgz", + "integrity": "sha512-symjDUPInTrkfIsZc2n2mo9hiAJLcTJsZkNICjZajEWnWpJ3s3zn50/FY8xpNUAf5w3eFuQii2wxztTGpvG1Xg==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/@d-fischer/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@d-fischer/cross-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@d-fischer/cross-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@d-fischer/cross-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@d-fischer/detect-node": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@d-fischer/detect-node/-/detect-node-3.0.1.tgz", + "integrity": "sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w==", + "license": "MIT" + }, + "node_modules/@d-fischer/logger": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@d-fischer/logger/-/logger-4.2.3.tgz", + "integrity": "sha512-mJUx9OgjrNVLQa4od/+bqnmD164VTCKnK5B4WOW8TX5y/3w2i58p+PMRE45gUuFjk2BVtOZUg55JQM3d619fdw==", + "license": "MIT", + "dependencies": { + "@d-fischer/detect-node": "^3.0.1", + "@d-fischer/shared-utils": "^3.2.0", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/qs": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@d-fischer/qs/-/qs-7.0.2.tgz", + "integrity": "sha512-yAu3xDooiL+ef84Jo8nLjDjWBRk7RXk163Y6aTvRB7FauYd3spQD/dWvgT7R4CrN54Juhrrc3dMY7mc+jZGurQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/rate-limiter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@d-fischer/rate-limiter/-/rate-limiter-1.1.0.tgz", + "integrity": "sha512-O5HgACwApyCZhp4JTEBEtbv/W3eAwEkrARFvgWnEsDmXgCMWjIHwohWoHre5BW6IYXFSHBGsuZB/EvNL3942kQ==", + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.2.3", + "@d-fischer/shared-utils": "^3.6.3", + "tslib": "^2.6.2" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@d-fischer/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-rtPTezQLROnTDdRij0Vo5OJ41aGvfKj9pQ7CkzFssQy+Jyc9BUVLV/DXLIGgvEGUaWt09Jq3im4WgvvPYqTomw==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@d-fischer/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@d-fischer/shared-utils": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@d-fischer/shared-utils/-/shared-utils-3.6.4.tgz", + "integrity": "sha512-BPkVLHfn2Lbyo/ENDBwtEB8JVQ+9OzkjJhUunLaxkw4k59YFlQxUUwlDBejVSFcpQT0t+D3CQlX+ySZnQj0wxw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/typed-event-emitter": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@d-fischer/typed-event-emitter/-/typed-event-emitter-3.3.3.tgz", + "integrity": "sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, "node_modules/@discord-player/equalizer": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@discord-player/equalizer/-/equalizer-7.0.0.tgz", - "integrity": "sha512-GfaG9Cy8+OnJpLILD2yNpCwAg40rMfU1QVJzUwjYMR3cIWqLHwLwgiG5QlPNnQ4B2062tBjbiG26zhzllogI/g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@discord-player/equalizer/-/equalizer-7.1.0.tgz", + "integrity": "sha512-bF5MRDDcm0BeZbTHMcG3UHdTZKHC7w21iEqrjt89KDzis6qz9oRsRWIG1w0NH3UYSCf1X3jdS/WiKdMqGHmmiA==", "license": "MIT" }, "node_modules/@discord-player/extractor": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@discord-player/extractor/-/extractor-7.0.0.tgz", - "integrity": "sha512-OAodbCQdL289wu4CGSp/noJxMJQ9VvzF0K1+HLWiMuKNOaBKiLpdY/kNsR56jFw1N+i+cHrkK6k7DmbfHhmHWw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@discord-player/extractor/-/extractor-7.1.0.tgz", + "integrity": "sha512-/ttNFkN0hacSS/KJNcPP8Dvk1W8+QGbdlbtJNIPHO1oBfEMazs6BimokMG5eCVmSLPb2MaWPGKTjhoQzHLlBlw==", "license": "MIT", "dependencies": { "file-type": "^16.5.4", @@ -72,36 +346,45 @@ } }, "node_modules/@discord-player/ffmpeg": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@discord-player/ffmpeg/-/ffmpeg-7.0.0.tgz", - "integrity": "sha512-CukfWpOskgDC07l15Xzz6TSK3XzupVEmaUo7ZoiTRthjlGJyqF5nmAjr1OtXVcs+SVCBhk6eRNxlHQz8vUc2wg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@discord-player/ffmpeg/-/ffmpeg-7.1.0.tgz", + "integrity": "sha512-4pKY6S5AwrM2aUWLklELOljKH1S2M2CN4vcel/15U6OGmg1sCJEWWVxLNvWIek/zH5Ua2yU4EN8gNxzZbjq/pw==", "license": "MIT" }, "node_modules/@discord-player/opus": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@discord-player/opus/-/opus-7.0.0.tgz", - "integrity": "sha512-QIYMphTKjmfXJn+U6r9YlB5tTLpJl62+hYH92H6IeVY5U1f3ZIB3aLCBZmo6pa/JFVDLMz8SrNalx1n2ir2QSQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@discord-player/opus/-/opus-7.1.0.tgz", + "integrity": "sha512-kQdkiCdyPKiKt+2YRgYfx8ymS8ZesvRZXC5Ok8UUoSYVl658yAxwLPlDrziZnVqcuSvGjnAFTcwAZcU00iuRZg==", "license": "MIT" }, "node_modules/@discord-player/utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@discord-player/utils/-/utils-7.0.0.tgz", - "integrity": "sha512-66hXEZCc8u0WX2Zjd2NH1MShvtsn3rwxEn49REI/XZzIjhG2GOHcY/BOTLAE6LzMGGEW7VTmNal2BChnoNnzRw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@discord-player/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-Glaj91FRoi6GRHLQ+rDZWmWL8GEnnUl1OXotIZ1A66flk/C+p99KZahBdHV9u24QZrU5mL+yqGYxqqIQYf4rxQ==", "license": "MIT", "dependencies": { "@discordjs/collection": "^1.1.0" } }, + "node_modules/@discord-player/utils/node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, "node_modules/@discordjs/builders": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz", - "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.2.tgz", + "integrity": "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==", "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.6.0", + "@discordjs/formatters": "^0.6.1", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.37.114", + "discord-api-types": "^0.38.1", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" @@ -114,53 +397,6 @@ } }, "node_modules/@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", - "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.37.114" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz", - "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.37.114", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", @@ -172,13 +408,42 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/rest/node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", - "license": "MIT", + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, "engines": { - "node": ">=18.17" + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.5.0.tgz", + "integrity": "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/util": { @@ -212,19 +477,25 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@discordjs/voice/node_modules/discord-api-types": { + "version": "0.37.120", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz", + "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", + "license": "MIT" + }, "node_modules/@discordjs/ws": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz", - "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.2.tgz", + "integrity": "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==", "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.4.1", + "@discordjs/rest": "^2.5.0", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "^0.37.114", + "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" }, @@ -235,22 +506,10 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], @@ -264,9 +523,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], @@ -280,9 +539,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], @@ -296,9 +555,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], @@ -312,9 +571,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], @@ -328,9 +587,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -344,9 +603,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -360,9 +619,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -376,9 +635,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -392,9 +651,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -408,9 +667,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -424,9 +683,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -440,9 +699,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -456,9 +715,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -472,9 +731,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -488,9 +747,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -504,9 +763,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -520,9 +779,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -536,9 +795,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -552,9 +811,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -568,9 +827,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -584,9 +843,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -600,9 +859,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -616,9 +875,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -632,9 +891,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -648,9 +907,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -666,6 +925,19 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", @@ -677,13 +949,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -691,10 +963,20 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -705,9 +987,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -729,19 +1011,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -749,24 +1034,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@evan/opus": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@evan/opus/-/opus-1.0.3.tgz", - "integrity": "sha512-ADfwIad83W1LuiZDNMjDMDNQRsPz8rj5xnDLExhVWTnA5wGJCLntOn12Ir5rxGBqdfo10QhnNVdd2+gXiZ6xCg==", - "license": "MIT" - }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -829,9 +1109,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -873,16 +1153,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -907,15 +1177,254 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" } }, + "node_modules/@ngrok/ngrok": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-0.5.2.tgz", + "integrity": "sha512-IDTLnK93UZlpiN0Ftr5aIXvMADioMEHFcydrvmP27kypHGmW5ww1883TWiASGTPUwBEVtnVqfUtCzbu+NRhyPQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@ngrok/ngrok-android-arm-eabi": "0.5.2", + "@ngrok/ngrok-android-arm64": "0.5.2", + "@ngrok/ngrok-darwin-arm64": "0.5.2", + "@ngrok/ngrok-darwin-universal": "0.5.2", + "@ngrok/ngrok-darwin-x64": "0.5.2", + "@ngrok/ngrok-freebsd-x64": "0.5.2", + "@ngrok/ngrok-linux-arm-gnueabihf": "0.5.2", + "@ngrok/ngrok-linux-arm64-gnu": "0.5.2", + "@ngrok/ngrok-linux-arm64-musl": "0.5.2", + "@ngrok/ngrok-linux-x64-gnu": "0.5.2", + "@ngrok/ngrok-linux-x64-musl": "0.5.2", + "@ngrok/ngrok-win32-ia32-msvc": "0.5.2", + "@ngrok/ngrok-win32-x64-msvc": "0.5.2" + } + }, + "node_modules/@ngrok/ngrok-android-arm-eabi": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm-eabi/-/ngrok-android-arm-eabi-0.5.2.tgz", + "integrity": "sha512-O8/qxTrtwvOLafnp2dRK2Jjbj7xf7bwTinXiAoEf8Y+/24p3EDCzMqcyFkJS3NuBQU/TiWloER5qCmOK/aX/UQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-android-arm64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm64/-/ngrok-android-arm64-0.5.2.tgz", + "integrity": "sha512-SFFlxHKCHqcJPD/nKzJGibXAtQDWy+R5VuCakvzPmWKer47QQ1B/2kLy7ua4tFEmARGHYWOHGZHzP7mkq73oMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-arm64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-arm64/-/ngrok-darwin-arm64-0.5.2.tgz", + "integrity": "sha512-6OcddF5wioQIuawXh1ONxmIywP5GskVT7H4MeKp5BjN2s9sIr7zhy7JBkwfAXFvNJtqw1dasV6JbYeGWXYCBnQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-universal": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-universal/-/ngrok-darwin-universal-0.5.2.tgz", + "integrity": "sha512-g3Q8qn5Z62m/z9zNxDGCMYVOgMBCXbZmNVpsfmhSBze5Rp1a1mUtloem8FvBeS9LyZYbXpbUbpeo2eJvZjF6Qg==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-darwin-x64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-x64/-/ngrok-darwin-x64-0.5.2.tgz", + "integrity": "sha512-/s+R2qGbkYUrzXkNBsrdpftGj80ZTnIzHVsvPsOhnSU0rOcWJ/+/MN/Be0f8AXw7uHBRr+i7smVYW9sSIfroKw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-freebsd-x64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-freebsd-x64/-/ngrok-freebsd-x64-0.5.2.tgz", + "integrity": "sha512-D2TVtW6ug8pFFR6Yrq/Q4XvYslIvQFbouSNyy7wvsZ/ZqSHwXR8XU3rG9D2+QnMG4qRS0DpUcf3qLQPWdWJAVw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm-gnueabihf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm-gnueabihf/-/ngrok-linux-arm-gnueabihf-0.5.2.tgz", + "integrity": "sha512-3euA0sbSI6+AX8qrpjgpJSjW16IKnFjLtzN9DZo/QLajc237Vq1gc1/8GWD/zI1zT+zquIFR9clq0SRRwO83pQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm64-gnu": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-gnu/-/ngrok-linux-arm64-gnu-0.5.2.tgz", + "integrity": "sha512-O5nEuB2wIG6IoX6bon7vAnhCqbC54bPfkul5wsNA9+A17GkmsFVwpKs0wI0a2OoskmyG2DRKo9DSXS+AwsJ6Xg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-arm64-musl": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-musl/-/ngrok-linux-arm64-musl-0.5.2.tgz", + "integrity": "sha512-BryCXIGzaA3YPJ3OgSbOX3VHlDm7c7tj1dYI3PfQyI2Od7HC1uRUv4FRZgwl+OjY/AckXFm02oV1H6ho+iJqgA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-x64-gnu": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-gnu/-/ngrok-linux-x64-gnu-0.5.2.tgz", + "integrity": "sha512-6S4m4Tqbf4vxFAFY7b9U2NuhujxcHKWR3lR4Na9aLWR9VBg2E/3Qa0nTvfWk+SGBilU03QELKaT3yYhLz/Y2zw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-linux-x64-musl": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-musl/-/ngrok-linux-x64-musl-0.5.2.tgz", + "integrity": "sha512-33tt/nOTUm/QN1xx2jvqQpBSv5tOn2LrU0MXuvoTQFOOr0XwrlqaZguyFhdGoU5J5bPaecw5lfhZb2JH3zJliQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-win32-ia32-msvc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-ia32-msvc/-/ngrok-win32-ia32-msvc-0.5.2.tgz", + "integrity": "sha512-rJ4+JP6TrWYmw6iaUwQEKoTIxX7vKGWJh9b2RL0ZcRbb8FMTZWY9KoyogkxSZDfx4BuTz2kSe4AMqe5RRhFt6Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngrok/ngrok-win32-x64-msvc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-x64-msvc/-/ngrok-win32-x64-msvc-0.5.2.tgz", + "integrity": "sha512-FMdljqqhbilwoY0FLb8iEW3179WkAHwb3i2e3U/XrqTlO1nvF8hbjoWLesrLzAICY9wSH5mgqC7i6qxHAA1Neg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -965,9 +1474,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", - "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", "cpu": [ "arm" ], @@ -978,9 +1487,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", - "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", "cpu": [ "arm64" ], @@ -991,9 +1500,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", - "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", "cpu": [ "arm64" ], @@ -1004,9 +1513,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", - "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", "cpu": [ "x64" ], @@ -1017,9 +1526,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", - "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", "cpu": [ "arm64" ], @@ -1030,9 +1539,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", - "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", "cpu": [ "x64" ], @@ -1043,9 +1552,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", - "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", "cpu": [ "arm" ], @@ -1056,9 +1565,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", - "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", "cpu": [ "arm" ], @@ -1069,9 +1578,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", - "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", "cpu": [ "arm64" ], @@ -1082,9 +1591,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", - "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", "cpu": [ "arm64" ], @@ -1095,9 +1604,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", - "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", "cpu": [ "loong64" ], @@ -1108,9 +1617,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", - "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", "cpu": [ "ppc64" ], @@ -1121,9 +1630,22 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", - "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", "cpu": [ "riscv64" ], @@ -1134,9 +1656,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", - "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", "cpu": [ "s390x" ], @@ -1147,9 +1669,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", - "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", "cpu": [ "x64" ], @@ -1160,9 +1682,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", - "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", "cpu": [ "x64" ], @@ -1173,9 +1695,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", - "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", "cpu": [ "arm64" ], @@ -1186,9 +1708,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", - "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", "cpu": [ "ia32" ], @@ -1199,9 +1721,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", - "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", "cpu": [ "x64" ], @@ -1235,9 +1757,9 @@ } }, "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", "license": "MIT", "engines": { "node": ">=v14.0.0", @@ -1245,15 +1767,16 @@ } }, "node_modules/@swc/core": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.4.tgz", - "integrity": "sha512-ut3zfiTLORMxhr6y/GBxkHmzcGuVpwJYX4qyXWuBKkpw/0g0S5iO1/wW7RnLnZbAi8wS/n0atRZoaZlXWBkeJg==", - "devOptional": true, + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.29.tgz", + "integrity": "sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.17" + "@swc/types": "^0.1.21" }, "engines": { "node": ">=10" @@ -1263,19 +1786,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.10.4", - "@swc/core-darwin-x64": "1.10.4", - "@swc/core-linux-arm-gnueabihf": "1.10.4", - "@swc/core-linux-arm64-gnu": "1.10.4", - "@swc/core-linux-arm64-musl": "1.10.4", - "@swc/core-linux-x64-gnu": "1.10.4", - "@swc/core-linux-x64-musl": "1.10.4", - "@swc/core-win32-arm64-msvc": "1.10.4", - "@swc/core-win32-ia32-msvc": "1.10.4", - "@swc/core-win32-x64-msvc": "1.10.4" + "@swc/core-darwin-arm64": "1.11.29", + "@swc/core-darwin-x64": "1.11.29", + "@swc/core-linux-arm-gnueabihf": "1.11.29", + "@swc/core-linux-arm64-gnu": "1.11.29", + "@swc/core-linux-arm64-musl": "1.11.29", + "@swc/core-linux-x64-gnu": "1.11.29", + "@swc/core-linux-x64-musl": "1.11.29", + "@swc/core-win32-arm64-msvc": "1.11.29", + "@swc/core-win32-ia32-msvc": "1.11.29", + "@swc/core-win32-x64-msvc": "1.11.29" }, "peerDependencies": { - "@swc/helpers": "*" + "@swc/helpers": ">=0.5.17" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -1284,171 +1807,171 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.4.tgz", - "integrity": "sha512-sV/eurLhkjn/197y48bxKP19oqcLydSel42Qsy2zepBltqUx+/zZ8+/IS0Bi7kaWVFxerbW1IPB09uq8Zuvm3g==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz", + "integrity": "sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.4.tgz", - "integrity": "sha512-gjYNU6vrAUO4+FuovEo9ofnVosTFXkF0VDuo1MKPItz6e2pxc2ale4FGzLw0Nf7JB1sX4a8h06CN16/pLJ8Q2w==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz", + "integrity": "sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.4.tgz", - "integrity": "sha512-zd7fXH5w8s+Sfvn2oO464KDWl+ZX1MJiVmE4Pdk46N3PEaNwE0koTfgx2vQRqRG4vBBobzVvzICC3618WcefOA==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz", + "integrity": "sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g==", "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.4.tgz", - "integrity": "sha512-+UGfoHDxsMZgFD3tABKLeEZHqLNOkxStu+qCG7atGBhS4Slri6h6zijVvf4yI5X3kbXdvc44XV/hrP/Klnui2A==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz", + "integrity": "sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.4.tgz", - "integrity": "sha512-cDDj2/uYsOH0pgAnDkovLZvKJpFmBMyXkxEG6Q4yw99HbzO6QzZ5HDGWGWVq/6dLgYKlnnmpjZCPPQIu01mXEg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz", + "integrity": "sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.4.tgz", - "integrity": "sha512-qJXh9D6Kf5xSdGWPINpLGixAbB5JX8JcbEJpRamhlDBoOcQC79dYfOMEIxWPhTS1DGLyFakAx2FX/b2VmQmj0g==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz", + "integrity": "sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.4.tgz", - "integrity": "sha512-A76lIAeyQnHCVt0RL/pG+0er8Qk9+acGJqSZOZm67Ve3B0oqMd871kPtaHBM0BW3OZAhoILgfHW3Op9Q3mx3Cw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz", + "integrity": "sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.4.tgz", - "integrity": "sha512-e6j5kBu4fIY7fFxFxnZI0MlEovRvp50Lg59Fw+DVbtqHk3C85dckcy5xKP+UoXeuEmFceauQDczUcGs19SRGSQ==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz", + "integrity": "sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.4.tgz", - "integrity": "sha512-RSYHfdKgNXV/amY5Tqk1EWVsyQnhlsM//jeqMLw5Fy9rfxP592W9UTumNikNRPdjI8wKKzNMXDb1U29tQjN0dg==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz", + "integrity": "sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg==", "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.4.tgz", - "integrity": "sha512-1ujYpaqfqNPYdwKBlvJnOqcl+Syn3UrQ4XE0Txz6zMYgyh6cdU6a3pxqLqIUSJ12MtXRA9ZUhEz1ekU3LfLWXw==", + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz", + "integrity": "sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -1457,15 +1980,17 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@swc/types": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", - "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", - "devOptional": true, + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.3" } @@ -1476,12 +2001,156 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@twurple/api": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@twurple/api/-/api-7.3.0.tgz", + "integrity": "sha512-QtaVgYi50E3AB/Nxivjou/u6w1cuQ6g4R8lzQawYDaQNtlP2Ue8vvYuSp2PfxSpe8vNiKhgV8hZAs+j4V29sxQ==", + "license": "MIT", + "dependencies": { + "@d-fischer/cache-decorators": "^4.0.0", + "@d-fischer/cross-fetch": "^5.0.1", + "@d-fischer/detect-node": "^3.0.1", + "@d-fischer/logger": "^4.2.1", + "@d-fischer/rate-limiter": "^1.1.0", + "@d-fischer/shared-utils": "^3.6.1", + "@d-fischer/typed-event-emitter": "^3.3.1", + "@twurple/api-call": "7.3.0", + "@twurple/common": "7.3.0", + "retry": "^0.13.1", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "@twurple/auth": "7.3.0" + } + }, + "node_modules/@twurple/api-call": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-7.3.0.tgz", + "integrity": "sha512-nx389kXjVphAeR3RfnzkRRf7Qa45wqHla067/mr3YxnUICCg4YOFv0Jb5UohQGHkj5h18mDZ3iUu/x2J49c1lA==", + "license": "MIT", + "dependencies": { + "@d-fischer/cross-fetch": "^5.0.1", + "@d-fischer/qs": "^7.0.2", + "@d-fischer/shared-utils": "^3.6.1", + "@twurple/common": "7.3.0", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/auth": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@twurple/auth/-/auth-7.3.0.tgz", + "integrity": "sha512-K68nFbQswfaEVCWP2MEPcxhHRR/N8kIHBP6AnRXzgSpmvWxhjOitz9oyP04di5DI1rJE+2NRauv1qFDyYia/qg==", + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.2.1", + "@d-fischer/shared-utils": "^3.6.1", + "@d-fischer/typed-event-emitter": "^3.3.1", + "@twurple/api-call": "7.3.0", + "@twurple/common": "7.3.0", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/common": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.3.0.tgz", + "integrity": "sha512-BGNniY7PBIohxfpRQ1bsOxUaktZcXZOExq8ojCtnsNBVDlchNEX2fYsere03ZwTLd48XBtxsdaUaeQXbx1aXLw==", + "license": "MIT", + "dependencies": { + "@d-fischer/shared-utils": "^3.6.1", + "klona": "^2.0.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/eventsub-base": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@twurple/eventsub-base/-/eventsub-base-7.3.0.tgz", + "integrity": "sha512-Wc/3qpyFfyvjabk/tQJVjAke+ixp5QWUT7LsuU+kMcCf46jsRQMB3InoXsZMRgX5sD1frBZzxUEJ7ujhxb8Ngw==", + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.2.1", + "@d-fischer/shared-utils": "^3.6.1", + "@d-fischer/typed-event-emitter": "^3.3.0", + "@twurple/api": "7.3.0", + "@twurple/auth": "7.3.0", + "@twurple/common": "7.3.0", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/eventsub-http": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@twurple/eventsub-http/-/eventsub-http-7.3.0.tgz", + "integrity": "sha512-JYh/Kl60AN/Yw/zYZ901gcaxi39NS8XdBdk4nVL7sK3i8Vvgu2y2RnPjBcTyugNDzpCarhloMoy9u9V95HgprQ==", + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.2.1", + "@d-fischer/raw-body": "^2.4.3", + "@d-fischer/shared-utils": "^3.6.1", + "@d-fischer/typed-event-emitter": "^3.3.0", + "@twurple/auth": "7.3.0", + "@twurple/common": "7.3.0", + "@twurple/eventsub-base": "7.3.0", + "@types/express-serve-static-core": "^4.17.24", + "httpanda": "^0.4.6", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "@twurple/api": "7.3.0" + } + }, + "node_modules/@twurple/eventsub-ngrok": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@twurple/eventsub-ngrok/-/eventsub-ngrok-7.3.0.tgz", + "integrity": "sha512-wTHMtHTpIKqttYVff8MGoYLFXAk+oQ+KvJhw+fgRiafIZkl8tivmcUBFZHBKtVeAALlnEpoZ812Ky3Byl2QKRA==", + "license": "MIT", + "dependencies": { + "@d-fischer/shared-utils": "^3.6.1", + "@ngrok/ngrok": "^0.5.1", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "@twurple/api": "7.3.0", + "@twurple/eventsub-http": "7.3.0" + } + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1499,13 +2168,19 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/parse-torrent": { @@ -1530,22 +2205,34 @@ "@types/node": "*" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", "license": "MIT" }, - "node_modules/@types/websocket": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz", - "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", @@ -1556,30 +2243,30 @@ } }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", - "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz", + "integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/type-utils": "8.19.0", - "@typescript-eslint/utils": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/type-utils": "8.33.1", + "@typescript-eslint/utils": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1589,22 +2276,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.1", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", + "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "engines": { @@ -1616,18 +2313,40 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", - "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", + "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0" + "@typescript-eslint/tsconfig-utils": "^8.33.1", + "@typescript-eslint/types": "^8.33.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", + "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1637,17 +2356,34 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", + "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", - "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz", + "integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1658,13 +2394,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", - "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", + "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", "dev": true, "license": "MIT", "engines": { @@ -1676,20 +2412,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", - "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", + "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/project-service": "8.33.1", + "@typescript-eslint/tsconfig-utils": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1699,7 +2437,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1729,16 +2467,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", - "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", + "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1749,17 +2487,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", - "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", + "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1770,19 +2508,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@vladfrangu/async_event_emitter": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", @@ -1802,10 +2527,22 @@ "node": ">=10.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1883,20 +2620,6 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1911,9 +2634,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1927,6 +2650,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bencode": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.3.tgz", @@ -1939,18 +2682,14 @@ "integrity": "sha512-ct6s33iiwRCUPp9KXnJ4QMWDgHIgaw36caK/5XEQ9L8dCzSQlJt1Vk6VmHh1VD4AlGCAI4C2zmtfItifBBPrhQ==", "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/bgutils-js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/bgutils-js/-/bgutils-js-3.2.0.tgz", + "integrity": "sha512-CacO15JvxbclbLeCAAm9DETGlLuisRGWpPigoRvNsccSCPEC4pwYwA2g2x/pv7Om/sk79d4ib35V5HHmxPBpDg==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "license": "MIT" }, "node_modules/blob-to-buffer": { "version": "1.2.9", @@ -2003,14 +2742,38 @@ } }, "node_modules/bson": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.1.tgz", - "integrity": "sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/bufferutil": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", @@ -2039,6 +2802,15 @@ "esbuild": ">=0.18" } }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2048,6 +2820,19 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2059,59 +2844,17 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2158,10 +2901,16 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, "node_modules/consola": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.3.3.tgz", - "integrity": "sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -2210,30 +2959,18 @@ } }, "node_modules/cssstyle": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", - "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", + "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", "license": "MIT", "dependencies": { - "rrweb-cssom": "^0.7.1" + "@asamuzakjp/css-color": "^3.1.2", + "rrweb-cssom": "^0.8.0" }, "engines": { "node": ">=18" } }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -2257,9 +2994,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2274,9 +3011,9 @@ } }, "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "license": "MIT" }, "node_modules/decompress-response": { @@ -2310,79 +3047,103 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/discord-api-types": { - "version": "0.37.115", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.115.tgz", - "integrity": "sha512-ivPnJotSMrXW8HLjFu+0iCVs8zP6KSliMelhr7HgcB2ki1QzpORkb26m71l1pzSnnGfm7gb5n/VtRTtpw8kXFA==", - "license": "MIT" + "version": "0.38.9", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.9.tgz", + "integrity": "sha512-LaVbou3fYrRbploSUNAWnyXBmUmVfH4FrQ8W2AVA7f/hUSPcUtkc1bDjELt2wrbIDBbSI4uMo/d/HxeIz7JvoQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] }, "node_modules/discord-player": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/discord-player/-/discord-player-7.0.0.tgz", - "integrity": "sha512-NMfdea/6qEOZ6rLSPdCyrOFVBc/3Zp7xrgtP0ZuiabkWZU4LKArCBYwkbc0EEbc4JJAjmbYEkAJHBx585tFmNw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/discord-player/-/discord-player-7.1.0.tgz", + "integrity": "sha512-bnEfvx5Ui0jLQjBw/17q8iYlw9C5aAjLiJ8379GQeF3Ln8ddeKRphAvthlKHBmq48aMySARwSxxL8147kQykyA==", "license": "MIT", "dependencies": { - "@discord-player/equalizer": "^7.0.0", - "@discord-player/ffmpeg": "^7.0.0", - "@discord-player/utils": "^7.0.0", + "@discord-player/equalizer": "^7.1.0", + "@discord-player/ffmpeg": "^7.1.0", + "@discord-player/utils": "^7.1.0", "@web-scrobbler/metadata-filter": "^3.1.0", - "discord-voip": "^7.0.0", + "discord-voip": "^7.1.0", "libsodium-wrappers": "^0.7.13" }, "funding": { "url": "https://github.com/Androz2091/discord-player?sponsor=1" }, "peerDependencies": { - "@discord-player/extractor": "^7.0.0", + "@discord-player/extractor": "^7.1.0", "mediaplex": "^1" } }, "node_modules/discord-player-youtubei": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/discord-player-youtubei/-/discord-player-youtubei-1.3.7.tgz", - "integrity": "sha512-EZ/ezfUC32VKtKY7CGcEdgeb/V/p9CyQoLwwjuiLxYSaeI4patGVGlCQP0v4cyK2yCJefDmfgjol0lXFwB1KrQ==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/discord-player-youtubei/-/discord-player-youtubei-1.4.6.tgz", + "integrity": "sha512-LM8nSrJkIGYUj6oQsce6vi/rxxkp6AaJgy3m5uE4foSRotPmfkPRNo/tidoddTezlyaI/Qnz9qgCQNmbA43AYA==", "license": "Creative Commons", "dependencies": { + "bgutils-js": "^3.2.0", + "jsdom": "^26.1.0", "tiny-typed-emitter": "^2.1.0", "undici": "^7.1.0", - "youtubei.js": "^12.2.0" + "youtubei.js": "^13.4.0" }, "bin": { "discord-player-youtubei": "bin/index.js" } }, + "node_modules/discord-player-youtubei/node_modules/undici": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/discord-voip": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/discord-voip/-/discord-voip-7.0.0.tgz", - "integrity": "sha512-XdwbGgaeW9E0yV1arAwXth2y78h9kse3UIUx/Ph2VTCZrFCoQi2T/sNtESu8zdGxcezEE2F0A94l/16GsIfo+w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/discord-voip/-/discord-voip-7.1.0.tgz", + "integrity": "sha512-6aQgI3QUj0Ee5tqaAV+pgBcOrgnkBh5/1UxAqbKMYQinQNs9LEZA7OoCxlWdiJVpVM2c1AxCdqhfryQMvM10+A==", "license": "MIT", "dependencies": { - "@discord-player/ffmpeg": "^7.0.0", - "@discord-player/opus": "^7.0.0", - "@discord-player/utils": "^7.0.0", + "@discord-player/ffmpeg": "^7.1.0", + "@discord-player/opus": "^7.1.0", + "@discord-player/utils": "^7.1.0", "@types/ws": "^8.5.10", "tsup": "^8.3.5", "typescript": "^5.5.4" } }, "node_modules/discord.js": { - "version": "14.17.2", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.2.tgz", - "integrity": "sha512-mrH6ziLVtNtId4bV4bsaUt5jE6NUaiHMPqO5VsSw1VVhFnjFi9duD8ctlo90/6cUH+8uyKBkoq9mSJ35SuuZ7Q==", + "version": "14.19.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz", + "integrity": "sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA==", "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.10.0", + "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.0", - "@discordjs/rest": "^2.4.2", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.5.0", "@discordjs/util": "^1.1.1", - "@discordjs/ws": "^1.2.0", + "@discordjs/ws": "^1.2.2", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.37.114", + "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", - "undici": "6.19.8" + "undici": "6.21.1" }, "engines": { "node": ">=18" @@ -2391,13 +3152,23 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/discord.js/node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "node_modules/discord.js/node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/discord.js/node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=v14.0.0", + "npm": ">=7.0.0" } }, "node_modules/dom-serializer": { @@ -2442,9 +3213,9 @@ } }, "node_modules/domutils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.1.tgz", - "integrity": "sha512-xWXmuRnN9OMP6ptPd2+H0cCbcYBULa5YDTbMm/2lvkWvNA3O4wcW+GvzooqBuNM8yy6pl3VIAeJTUUWUbfI5Fw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -2456,9 +3227,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2467,6 +3239,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2491,50 +3277,55 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">= 0.4" } }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "license": "ISC", - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=0.12" + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2544,31 +3335,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escape-string-regexp": { @@ -2585,22 +3376,23 @@ } }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -2608,7 +3400,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -2645,9 +3437,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2662,19 +3454,6 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", @@ -2687,19 +3466,21 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "license": "ISC", + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/espree": { @@ -2720,19 +3501,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2779,23 +3547,22 @@ "node": ">=0.10.0" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" + "engines": { + "node": ">=6" } }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "license": "ISC", - "dependencies": { - "type": "^2.7.2" + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" } }, "node_modules/fast-deep-equal": { @@ -2805,9 +3572,9 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -2815,7 +3582,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -2849,9 +3616,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2941,6 +3708,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -2956,9 +3734,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -2983,12 +3761,12 @@ } }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -2999,13 +3777,14 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -3038,6 +3817,52 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", @@ -3051,9 +3876,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3133,6 +3958,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3144,11 +3981,51 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3159,9 +4036,9 @@ } }, "node_modules/himalaya": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.0.tgz", - "integrity": "sha512-LLase1dHCRMel68/HZTFft0N0wti0epHr3nNY7ynpLbyZpmrKMQ8YIpiOV77TM97cNpC8Wb2n6f66IRggwdWPw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.1.tgz", + "integrity": "sha512-mJLY5tErGWtsw8hO2fJ2vK4IpG6S1AIgVkduRo4FqFJhgI2H3XLzgemRemk45zcnFyxNNpOfrIDle2KcnJM0lA==", "license": "ISC" }, "node_modules/html-encoding-sniffer": { @@ -3176,6 +4053,22 @@ "node": ">=18" } }, + "node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3189,6 +4082,25 @@ "node": ">= 14" } }, + "node_modules/httpanda": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/httpanda/-/httpanda-0.4.7.tgz", + "integrity": "sha512-NieTiR7kfOheL9OeEi6+JKFmJ2JP9ZRqUQ4tiXZ9J+EMMKxApHUQlEM5l4gZ+l67lxE9Er6oigZnujmhlodNCg==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.11.2", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/httpanda/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -3244,17 +4156,10 @@ "node": ">= 4" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3284,19 +4189,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3345,12 +4237,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3383,9 +4269,9 @@ } }, "node_modules/jintr": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.2.0.tgz", - "integrity": "sha512-psD1yf05kMKDNsUdW1l5YhO59pHScQ6OIHHb8W5SKSM2dCOFPsqolmIuSHgVA8+3Dc47NJR181CXZ4alCAPTkA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.3.1.tgz", + "integrity": "sha512-nnOzyhf0SLpbWuZ270Omwbj5LcXUkTcZkVnK8/veJXtSZOiATM5gMZMdmzN75FmTyj+NVgrGaPdH12zIJ24oIA==", "funding": [ "https://github.com/sponsors/LuanRT" ], @@ -3417,30 +4303,29 @@ } }, "node_modules/jsdom": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", - "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "license": "MIT", "dependencies": { - "cssstyle": "^4.1.0", + "cssstyle": "^4.2.1", "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", + "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", + "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", + "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, @@ -3448,7 +4333,7 @@ "node": ">=18" }, "peerDependencies": { - "canvas": "^2.11.2" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -3496,6 +4381,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3600,11 +4494,20 @@ "license": "ISC" }, "node_modules/magic-bytes.js": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", - "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/magnet-uri": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/magnet-uri/-/magnet-uri-6.2.0.tgz", @@ -3629,6 +4532,15 @@ "thirty-two": "^1.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mediaplex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mediaplex/-/mediaplex-1.0.0.tgz", @@ -3994,14 +4906,26 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, "node_modules/mongodb": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", - "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", + "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", + "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -4041,49 +4965,24 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "license": "MIT", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.9.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.3.tgz", - "integrity": "sha512-G50GNPdMqhoiRAJ/24GYAzg13yxXDD3FOOFeYiFwtHmHpAJem3hxbYIxAhLJGWbYEiUZL0qFMu2LXYkgGAmo+Q==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.1.tgz", + "integrity": "sha512-RhQ4DzmBi5BNGcS0w4u1vdMRIKcteXTCNzDt1j7XRcdWYBz1MjMjulBhPaeC5jBCHOD1yinuOFTTSOWLLGexWw==", "license": "MIT", "dependencies": { - "bson": "^6.10.1", + "bson": "^6.10.3", "kareem": "2.6.3", - "mongodb": "~6.12.0", + "mongodb": "~6.16.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -4135,6 +5034,12 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4142,16 +5047,11 @@ "dev": true, "license": "MIT" }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "license": "ISC" - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -4206,68 +5106,6 @@ "he": "1.2.0" } }, - "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4281,9 +5119,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", - "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "license": "MIT" }, "node_modules/object-assign": { @@ -4406,17 +5244,29 @@ } }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4452,6 +5302,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", @@ -4485,14 +5341,25 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", "engines": { "node": ">= 6" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", @@ -4545,22 +5412,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prism-media": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", @@ -4587,19 +5438,21 @@ } } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4630,26 +5483,28 @@ "license": "MIT" }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", "license": "MIT", "dependencies": { - "readable-stream": "^3.6.0" + "readable-stream": "^4.7.0" }, "engines": { "node": ">=8" @@ -4659,28 +5514,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-all": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/require-all/-/require-all-3.0.0.tgz", - "integrity": "sha512-jPGN876lc5exWYrMcgZSd7U42P0PmVQzxnQB13fCSzmyGnqQWW4WUz5DosZ/qe24hz+5o9lSvW2epBNZ1xa6Fw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4701,10 +5534,19 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -4764,12 +5606,12 @@ } }, "node_modules/rollup": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", - "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -4779,53 +5621,35 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" } }, "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "license": "MIT" }, - "node_modules/rss-parser": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz", - "integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==", - "license": "MIT", - "dependencies": { - "entities": "^2.0.3", - "xml2js": "^0.5.0" - } - }, - "node_modules/rss-parser/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4882,12 +5706,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -4901,9 +5719,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -4913,6 +5731,12 @@ "node": ">=10" } }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5007,19 +5831,6 @@ "rusha": "^0.8.13" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/soundcloud.ts": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/soundcloud.ts/-/soundcloud.ts-0.5.5.tgz", @@ -5029,15 +5840,6 @@ "undici": "^6.17.0" } }, - "node_modules/soundcloud.ts/node_modules/undici": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", - "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -5107,6 +5909,15 @@ "node": ">= 12" } }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5268,6 +6079,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -5324,22 +6136,25 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", - "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.2", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -5363,21 +6178,21 @@ } }, "node_modules/tldts": { - "version": "6.1.70", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", - "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.70" + "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.70", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", - "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "license": "MIT" }, "node_modules/to-regex-range": { @@ -5393,6 +6208,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/token-types": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", @@ -5410,20 +6234,10 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tough-cookie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", - "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" @@ -5433,9 +6247,9 @@ } }, "node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -5454,16 +6268,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { @@ -5485,26 +6299,27 @@ "license": "0BSD" }, "node_modules/tsup": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.5.tgz", - "integrity": "sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", + "integrity": "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==", "license": "MIT", "dependencies": { - "bundle-require": "^5.0.0", + "bundle-require": "^5.1.0", "cac": "^6.7.14", - "chokidar": "^4.0.1", - "consola": "^3.2.3", - "debug": "^4.3.7", - "esbuild": "^0.24.0", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.25.0", + "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", - "rollup": "^4.24.0", + "rollup": "^4.34.8", "source-map": "0.8.0-beta.0", "sucrase": "^3.35.0", - "tinyexec": "^0.3.1", - "tinyglobby": "^0.2.9", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "bin": { @@ -5551,12 +6366,12 @@ } }, "node_modules/tsup/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -5573,13 +6388,13 @@ } }, "node_modules/tsx": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", "devOptional": true, "license": "MIT", "dependencies": { - "esbuild": "~0.23.0", + "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -5592,460 +6407,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "devOptional": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "license": "ISC" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6059,19 +6420,10 @@ "node": ">= 0.8.0" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6081,26 +6433,48 @@ "node": ">=14.17" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "node_modules/typescript-eslint": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.1.tgz", + "integrity": "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==", "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.33.1", + "@typescript-eslint/parser": "8.33.1", + "@typescript-eslint/utils": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "license": "MIT" }, "node_modules/undici": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.2.0.tgz", - "integrity": "sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==", + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=18.17" } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unfetch": { @@ -6112,6 +6486,15 @@ "./packages/isomorphic-unfetch" ] }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6122,25 +6505,6 @@ "punycode": "^2.1.0" } }, - "node_modules/utf-8-validate": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz", - "integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -6171,51 +6535,6 @@ "node": ">=12" } }, - "node_modules/websocket": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", - "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", - "license": "Apache-2.0", - "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.63", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/websocket/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/websocket/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/websocket/node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -6238,12 +6557,12 @@ } }, "node_modules/whatwg-url": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", - "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^5.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { @@ -6370,9 +6689,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6399,43 +6718,12 @@ "node": ">=18" } }, - "node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, - "node_modules/yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", - "license": "MIT", - "engines": { - "node": ">=0.10.32" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6450,24 +6738,24 @@ } }, "node_modules/youtubei.js": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-12.2.0.tgz", - "integrity": "sha512-G+50qrbJCToMYhu8jbaHiS3Vf+RRul+CcDbz3hEGwHkGPh+zLiWwD6SS+YhYF+2/op4ZU5zDYQJrGqJ+wKh7Gw==", + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-13.4.0.tgz", + "integrity": "sha512-+fmIZU/dWAjsROONrASy1REwVpy6umAPVuoNLr/4iNmZXl84LyBef0n3hrd1Vn9035EuINToGyQcBmifwUEemA==", "funding": [ "https://github.com/sponsors/LuanRT" ], "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", - "jintr": "^3.2.0", + "jintr": "^3.3.1", "tslib": "^2.5.0", "undici": "^5.19.1" } }, "node_modules/youtubei.js/node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" @@ -6475,6 +6763,16 @@ "engines": { "node": ">=14.0" } + }, + "node_modules/zlib-sync": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/zlib-sync/-/zlib-sync-0.1.10.tgz", + "integrity": "sha512-t7/pYg5tLBznL1RuhmbAt8rNp5tbhr+TSrJFnMkRtrGIaPJZ6Dc0uR4u3OoQI2d6cGlVI62E3Gy6gwkxyIqr/w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.18.0" + } } } } diff --git a/package.json b/package.json index 8b7367f..3cae89f 100644 --- a/package.json +++ b/package.json @@ -1,57 +1,52 @@ { "name": "bot_tamiseur", "description": "Listen to music and use fun commands with your friends!", - "version": "3.0.4", + "version": "4.0.0", "author": { "name": "Zachary Guénot" }, "main": "src/index.ts", "scripts": { - "format": "prettier --write .", - "start": "npx tsx src/index.ts", - "dev": "nodemon -e ts src/index.ts", - "build": "tsc", - "lint": "eslint src/**/*.ts", - "prod": "node dist/index.js" + "start": "node index.js", + "start:prod": "NODE_ENV=production node dist/index.js", + "start:dev": "NODE_ENV=development tsx src/index.ts", + "dev": "NODE_ENV=development tsx watch src/index.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "build": "tsup", + "updateall": "ncu -u && npm i" }, "//": [ - "Garder chalk à la version 4.1.2 pour éviter un bug ESM avec la version >=5.0.0" + "Garder parse-torrent à la version 9.1.5 pour éviter un bug exports avec la version >=10.0.0" ], "dependencies": { - "@discord-player/equalizer": "^7.0.0", - "@discord-player/extractor": "^7.0.0", + "@discord-player/extractor": "^7.1.0", "@discordjs/voice": "^0.18.0", - "@evan/opus": "^1.0.3", - "axios": "^1.7.9", + "@twurple/api": "^7.3.0", + "@twurple/auth": "^7.3.0", + "@twurple/eventsub-http": "^7.3.0", + "@twurple/eventsub-ngrok": "^7.3.0", + "axios": "^1.9.0", "bufferutil": "^4.0.9", - "chalk": "^4.1.2", - "discord-player": "^7.0.0", - "discord-player-youtubei": "^1.3.7", - "discord.js": "^14.17.2", - "dotenv": "^16.4.7", + "chalk": "^5.4.1", + "discord-player": "^7.1.0", + "discord-player-youtubei": "^1.4.6", + "discord.js": "^14.19.3", "iconv-lite": "^0.6.3", - "jsdom": "^25.0.1", - "libsodium-wrappers": "^0.7.15", "mediaplex": "^1.0.0", - "mongoose": "^8.9.3", + "mongoose": "^8.15.1", "parse-torrent": "^9.1.5", - "require-all": "^3.0.0", - "rss-parser": "^3.13.0", - "utf-8-validate": "^6.0.5", - "websocket": "^1.0.35" + "zlib-sync": "^0.1.10" }, "devDependencies": { - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.17.0", - "@swc/core": "^1.10.4", - "@types/node": "^22.10.5", + "@eslint/js": "^9.28.0", + "@types/node": "^22.15.30", "@types/parse-torrent": "^5.8.7", - "@types/websocket": "^1.0.10", - "@typescript-eslint/eslint-plugin": "^8.19.0", - "@typescript-eslint/parser": "^8.19.0", - "eslint": "^9.17.0", - "nodemon": "^3.1.9", - "prettier": "^3.4.2", - "tsx": "^4.19.2" + "dotenv": "^16.5.0", + "eslint": "^9.28.0", + "tsup": "^8.5.0", + "tsx": "^4.19.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.33.1" } } diff --git a/src/buttons/freebox/index.ts b/src/buttons/freebox/index.ts new file mode 100644 index 0000000..82629aa --- /dev/null +++ b/src/buttons/freebox/index.ts @@ -0,0 +1,11 @@ +import * as lcd_status from "./lcd_status" +import * as refresh_status from "./refresh_status" +import * as test_connection from "./test_connection" + +import type { Button } from "@/types" + +export default [ + lcd_status, + refresh_status, + test_connection +] as Button[] diff --git a/src/buttons/freebox/lcd_status.ts b/src/buttons/freebox/lcd_status.ts new file mode 100644 index 0000000..3b38664 --- /dev/null +++ b/src/buttons/freebox/lcd_status.ts @@ -0,0 +1,88 @@ +import { EmbedBuilder, MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import crypto from "crypto" +import * as Freebox from "@/utils/freebox" +import type { APIResponseData, APIResponseDataError, GetChallenge, LcdConfig, OpenSession } from "@/types/freebox" +import type { GuildFbx } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "freebox_lcd_status" +export async function execute(interaction: ButtonInteraction) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }) + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.followUp({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildFbx") as GuildFbx + if (!dbData.enabled || !dbData.host || !dbData.appToken) { + return interaction.followUp({ content: t(interaction.locale, "freebox.general.incomplete_configuration"), flags: MessageFlags.Ephemeral }) + } + + try { + // Connexion à l'API Freebox + const challengeData = await Freebox.Login.Challenge(dbData.host) as APIResponseData + if (!challengeData.success) return await Freebox.handleError(challengeData as APIResponseDataError, interaction, false) + + const password = crypto.createHmac("sha1", dbData.appToken).update(challengeData.result.challenge).digest("hex") + const sessionData = await Freebox.Login.Session(dbData.host, password) as APIResponseData + if (!sessionData.success) return await Freebox.handleError(sessionData as APIResponseDataError, interaction, false) + + const sessionToken = sessionData.result.session_token + if (!sessionToken) return await interaction.followUp({ content: t(interaction.locale, "freebox.auth.session_token_failed"), flags: MessageFlags.Ephemeral }) + + // Récupération de la configuration LCD + const lcdData = await Freebox.Get.LcdConfig(dbData.host, sessionToken) as APIResponseData + if (!lcdData.success) return await Freebox.handleError(lcdData as APIResponseDataError, interaction, false) + + const lcdConfig = lcdData.result + + // Création de l'embed avec les informations LCD + const embed = new EmbedBuilder() + .setTitle(t(interaction.locale, "freebox.lcd.config_title")) + .setColor(lcdConfig.led_strip_enabled ? 0x00ff00 : 0xff0000) + .addFields( + { + name: t(interaction.locale, "freebox.lcd.led_strip"), + value: lcdConfig.led_strip_enabled ? t(interaction.locale, "freebox.lcd.led_strip_on") : t(interaction.locale, "freebox.lcd.led_strip_off"), + inline: true + }, + { + name: t(interaction.locale, "freebox.lcd.brightness"), + value: `${lcdConfig.brightness}%`, + inline: true + }, + { + name: t(interaction.locale, "freebox.lcd.orientation"), + value: lcdConfig.orientation === 0 ? t(interaction.locale, "freebox.lcd.orientation_portrait") : t(interaction.locale, "freebox.lcd.orientation_landscape"), + inline: true + } + ) + + // Informations du timer si configuré + if (dbData.lcd) { + const timerStatus = dbData.lcd.enabled ? t(interaction.locale, "freebox.status.timer_enabled") : t(interaction.locale, "freebox.status.timer_disabled") + const botManaged = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "freebox.status.timer_no_manager") + const morningTime = dbData.lcd.morningTime ?? t(interaction.locale, "freebox.status.timer_not_configured") + const nightTime = dbData.lcd.nightTime ?? t(interaction.locale, "freebox.status.timer_not_configured") + + embed.addFields({ + name: t(interaction.locale, "freebox.lcd.timer_auto"), + value: [ + t(interaction.locale, "freebox.timer.status_field", { status: timerStatus }), + t(interaction.locale, "freebox.timer.managed_by", { manager: botManaged }), + t(interaction.locale, "freebox.timer.morning", { time: morningTime }), + t(interaction.locale, "freebox.timer.night", { time: nightTime }) + ].join('\n'), + inline: false + }) + } + + embed.setTimestamp() + + return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) + } catch (error) { + console.error("Erreur lors de la récupération de l'état LCD:", error) + return interaction.followUp({ content: t(interaction.locale, "freebox.lcd.unexpected_error"), flags: MessageFlags.Ephemeral }) + } +} diff --git a/src/buttons/freebox/refresh_status.ts b/src/buttons/freebox/refresh_status.ts new file mode 100644 index 0000000..27bfc03 --- /dev/null +++ b/src/buttons/freebox/refresh_status.ts @@ -0,0 +1,76 @@ +import { EmbedBuilder, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import type { GuildFbx } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "freebox_refresh_status" +export async function execute(interaction: ButtonInteraction) { + await interaction.deferUpdate() + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.followUp({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildFbx") as GuildFbx + + // Reconstruire l'embed de statut actualisé + const embed = new EmbedBuilder() + .setTitle(t(interaction.locale, "freebox.status.title")) + .setColor(dbData.enabled ? 0x00ff00 : 0xff0000) + .addFields( + { + name: t(interaction.locale, "freebox.status.config_section"), + value: [ + t(interaction.locale, "freebox.status.module_field", { status: dbData.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }), + t(interaction.locale, "freebox.status.host_field", { value: dbData.host ? `\`${dbData.host}\`` : t(interaction.locale, "freebox.status.host_not_configured") }), + t(interaction.locale, "freebox.status.token_field", { status: dbData.appToken ? t(interaction.locale, "freebox.status.token_configured") : t(interaction.locale, "freebox.status.token_not_configured") }) + ].join('\n'), + inline: false + } + ) + + // Informations LCD si disponibles + if (dbData.lcd) { + const lcdStatus = dbData.lcd.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") + const botManaged = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "freebox.status.timer_no_manager") + const morningTime = dbData.lcd.morningTime ?? t(interaction.locale, "freebox.status.timer_not_configured") + const nightTime = dbData.lcd.nightTime ?? t(interaction.locale, "freebox.status.timer_not_configured") + + embed.addFields({ + name: t(interaction.locale, "freebox.status.timer_section"), + value: [ + t(interaction.locale, "freebox.timer.status_field", { status: lcdStatus }), + t(interaction.locale, "freebox.timer.managed_by", { manager: botManaged }), + t(interaction.locale, "freebox.timer.morning", { time: morningTime }), + t(interaction.locale, "freebox.timer.night", { time: nightTime }) + ].join('\n'), + inline: false + }) + } + + embed.setTimestamp() + + // Reconstruire les boutons + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("freebox_test_connection") + .setLabel(t(interaction.locale, "freebox.buttons.test_connection")) + .setEmoji("🔌") + .setStyle(ButtonStyle.Primary) + .setDisabled(!dbData.appToken), + new ButtonBuilder() + .setCustomId("freebox_lcd_status") + .setLabel(t(interaction.locale, "freebox.buttons.lcd_status")) + .setEmoji("💡") + .setStyle(ButtonStyle.Secondary) + .setDisabled(!dbData.appToken), + new ButtonBuilder() + .setCustomId("freebox_refresh_status") + .setLabel(t(interaction.locale, "freebox.buttons.refresh_status")) + .setEmoji("🔄") + .setStyle(ButtonStyle.Success) + ) + + return interaction.editReply({ embeds: [embed], components: [buttons] }) +} diff --git a/src/buttons/freebox/test_connection.ts b/src/buttons/freebox/test_connection.ts new file mode 100644 index 0000000..cd78347 --- /dev/null +++ b/src/buttons/freebox/test_connection.ts @@ -0,0 +1,71 @@ +import { EmbedBuilder, MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import crypto from "crypto" +import * as Freebox from "@/utils/freebox" +import type { APIResponseData, APIResponseDataError, APIResponseDataVersion, ConnectionStatus, GetChallenge, OpenSession } from "@/types/freebox" +import type { GuildFbx } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "freebox_test_connection" +export async function execute(interaction: ButtonInteraction) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }) + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.followUp({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildFbx") as GuildFbx + if (!dbData.enabled || !dbData.host || !dbData.appToken) { + return interaction.followUp({ content: t(interaction.locale, "freebox.general.incomplete_configuration"), flags: MessageFlags.Ephemeral }) + } + + try { + // Test de la version API + const versionData = await Freebox.Core.Version(dbData.host) as APIResponseDataVersion + + // Test d'authentification + const challengeData = await Freebox.Login.Challenge(dbData.host) as APIResponseData + if (!challengeData.success) return await Freebox.handleError(challengeData as APIResponseDataError, interaction, false) + + const password = crypto.createHmac("sha1", dbData.appToken).update(challengeData.result.challenge).digest("hex") + const sessionData = await Freebox.Login.Session(dbData.host, password) as APIResponseData + if (!sessionData.success) return await Freebox.handleError(sessionData as APIResponseDataError, interaction, false) + + const sessionToken = sessionData.result.session_token + if (!sessionToken) return await interaction.followUp({ content: t(interaction.locale, "freebox.auth.session_token_failed"), flags: MessageFlags.Ephemeral }) + + // Test de la connexion + const connectionData = await Freebox.Get.Connection(dbData.host, sessionToken) as APIResponseData + if (!connectionData.success) return await Freebox.handleError(connectionData as APIResponseDataError, interaction, false) + + // Création de l'embed de succès + const embed = new EmbedBuilder() + .setTitle(t(interaction.locale, "freebox.test.connection_success_title")) + .setColor(0x00ff00) + .addFields( + { + name: t(interaction.locale, "freebox.test.api_field"), + value: t(interaction.locale, "freebox.test.api_version", { version: versionData.api_version }), + inline: true + }, + { + name: t(interaction.locale, "freebox.test.auth_field"), + value: t(interaction.locale, "freebox.test.token_valid"), + inline: true + }, + { + name: t(interaction.locale, "freebox.test.connection_field"), + value: connectionData.result.state === "up" ? + t(interaction.locale, "freebox.test.connection_active") : + t(interaction.locale, "freebox.test.connection_inactive"), + inline: true + } + ) + .setTimestamp() + + return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) + } catch (error) { + console.error("Erreur lors du test de connexion Freebox:", error) + return interaction.followUp({ content: t(interaction.locale, "freebox.test.connection_error"), flags: MessageFlags.Ephemeral }) + } +} diff --git a/src/buttons/index.ts b/src/buttons/index.ts new file mode 100644 index 0000000..7d629d5 --- /dev/null +++ b/src/buttons/index.ts @@ -0,0 +1,26 @@ +import freebox from "./freebox" +import player from "./player" +import twitch from "./twitch" + +import type { Button, ButtonFolder } from "@/types" + +export const buttonFolders = [ + { + name: "freebox", + commands: freebox + }, + { + name: "player", + commands: player + }, + { + name: "twitch", + commands: twitch + } +] as ButtonFolder[] + +export default [ + ...freebox, + ...player, + ...twitch +] as Button[] diff --git a/src/buttons/loop.ts b/src/buttons/loop.ts deleted file mode 100755 index d6e59ff..0000000 --- a/src/buttons/loop.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'loop', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - let loop = queue.repeatMode === 0 ? 1 : queue.repeatMode === 1 ? 2 : queue.repeatMode === 2 ? 3 : 0 - await queue.setRepeatMode(loop) - await interaction.followUp({ content:`Boucle ${loop === 0 ? 'désactivée' : loop === 1 ? 'en mode Titre' : loop === 2 ? 'en mode File d\'Attente' : 'en autoplay'}.`, ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/pause.ts b/src/buttons/pause.ts deleted file mode 100755 index f4b8b3f..0000000 --- a/src/buttons/pause.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'pause', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - await queue.node.setPaused(!queue.node.isPaused()) - return interaction.followUp({ content: 'Musique mise en pause !', ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/player/disco_channel.ts b/src/buttons/player/disco_channel.ts new file mode 100644 index 0000000..56d8b98 --- /dev/null +++ b/src/buttons/player/disco_channel.ts @@ -0,0 +1,15 @@ +import { MessageFlags, ChannelType, ActionRowBuilder, ChannelSelectMenuBuilder } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { t } from "@/utils/i18n" + +export const id = "player_disco_channel" +export async function execute(interaction: ButtonInteraction) { + const channelSelect = new ChannelSelectMenuBuilder() + .setCustomId("player_disco_channel") + .setPlaceholder(t(interaction.locale, "selectmenus.placeholders.select_channel_disco")) + .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement) + + const row = new ActionRowBuilder().addComponents(channelSelect) + + return interaction.reply({ content: t(interaction.locale, "player.disco.select_channel"), components: [row], flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/disco_disable.ts b/src/buttons/player/disco_disable.ts new file mode 100644 index 0000000..f9d5ad4 --- /dev/null +++ b/src/buttons/player/disco_disable.ts @@ -0,0 +1,24 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { generateDiscoEmbed } from "@/utils/player" +import type { Disco } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "player_disco_disable" +export async function execute(interaction: ButtonInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildPlayer.disco") as Disco + dbData.enabled = false + + guildProfile.set("guildPlayer.disco", dbData) + guildProfile.markModified("guildPlayer.disco") + await guildProfile.save().catch(console.error) + + // Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour + const { embed, components } = generateDiscoEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + return interaction.update({ embeds: [embed], components: components }) +} diff --git a/src/buttons/player/disco_enable.ts b/src/buttons/player/disco_enable.ts new file mode 100644 index 0000000..9ccc603 --- /dev/null +++ b/src/buttons/player/disco_enable.ts @@ -0,0 +1,31 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { generateDiscoEmbed } from "@/utils/player" +import type { Disco } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "player_disco_enable" +export async function execute(interaction: ButtonInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildPlayer.disco") as Disco + + // Vérifier qu'un canal est configuré avant d'activer + if (!dbData.channelId) return interaction.reply({ + content: t(interaction.locale, "player.disco.configure_channel_first"), + flags: MessageFlags.Ephemeral + }) + + dbData.enabled = true + + guildProfile.set("guildPlayer.disco", dbData) + guildProfile.markModified("guildPlayer.disco") + await guildProfile.save().catch(console.error) + + // Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour + const { embed, components } = generateDiscoEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + return interaction.update({ embeds: [embed], components: components }) +} diff --git a/src/buttons/player/index.ts b/src/buttons/player/index.ts new file mode 100644 index 0000000..b62966f --- /dev/null +++ b/src/buttons/player/index.ts @@ -0,0 +1,29 @@ +import * as disco_disable from "./disco_disable" +import * as disco_enable from "./disco_enable" +import * as disco_channel from "./disco_channel" +import * as loop from "./loop" +import * as pause from "./pause" +import * as previous from "./previous" +import * as resume from "./resume" +import * as shuffle from "./shuffle" +import * as skip from "./skip" +import * as stop from "./stop" +import * as volume_down from "./volume_down" +import * as volume_up from "./volume_up" + +import type { Button } from "@/types" + +export default [ + disco_channel, + disco_disable, + disco_enable, + loop, + pause, + previous, + resume, + shuffle, + skip, + stop, + volume_down, + volume_up +] as Button[] diff --git a/src/buttons/player/loop.ts b/src/buttons/player/loop.ts new file mode 100644 index 0000000..fb07af2 --- /dev/null +++ b/src/buttons/player/loop.ts @@ -0,0 +1,22 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_loop" +export async function execute(interaction: ButtonInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + const loop = queue.repeatMode === 0 ? 1 : queue.repeatMode === 1 ? 2 : queue.repeatMode === 2 ? 3 : 0 + queue.setRepeatMode(loop) + + const loopModes = { + 0: t(interaction.locale, "player.loop_off"), + 1: t(interaction.locale, "player.loop_track"), + 2: t(interaction.locale, "player.loop_queue"), + 3: t(interaction.locale, "player.loop_autoplay") + } + + return interaction.followUp({ content: loopModes[loop], flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/pause.ts b/src/buttons/player/pause.ts new file mode 100644 index 0000000..c0dd5f1 --- /dev/null +++ b/src/buttons/player/pause.ts @@ -0,0 +1,13 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_pause" +export async function execute(interaction: ButtonInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + queue.node.setPaused(!queue.node.isPaused()) + return interaction.followUp({ content: t(interaction.locale, "player.paused"), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/previous.ts b/src/buttons/player/previous.ts new file mode 100644 index 0000000..2716bc4 --- /dev/null +++ b/src/buttons/player/previous.ts @@ -0,0 +1,13 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useHistory } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_previous" +export async function execute(interaction: ButtonInteraction) { + const history = useHistory(interaction.guild?.id ?? "") + if (!history) return + + await history.previous() + return interaction.followUp({ content: t(interaction.locale, "player.previous_played"), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/resume.ts b/src/buttons/player/resume.ts new file mode 100644 index 0000000..70403dd --- /dev/null +++ b/src/buttons/player/resume.ts @@ -0,0 +1,13 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_resume" +export async function execute(interaction: ButtonInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + queue.node.setPaused(!queue.node.isPaused()) + return interaction.followUp({ content: t(interaction.locale, "player.resumed"), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/shuffle.ts b/src/buttons/player/shuffle.ts new file mode 100644 index 0000000..c6c6063 --- /dev/null +++ b/src/buttons/player/shuffle.ts @@ -0,0 +1,13 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_shuffle" +export async function execute(interaction: ButtonInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + queue.tracks.shuffle() + return interaction.followUp({ content: t(interaction.locale, "player.shuffled"), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/skip.ts b/src/buttons/player/skip.ts new file mode 100644 index 0000000..65b46c7 --- /dev/null +++ b/src/buttons/player/skip.ts @@ -0,0 +1,13 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_skip" +export async function execute(interaction: ButtonInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + queue.node.skip() + return interaction.followUp({ content: t(interaction.locale, "player.skipped"), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/stop.ts b/src/buttons/player/stop.ts new file mode 100644 index 0000000..7aeb67d --- /dev/null +++ b/src/buttons/player/stop.ts @@ -0,0 +1,16 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { stopProgressSaving } from "@/utils/player" +import { t } from "@/utils/i18n" + +export const id = "player_stop" +export async function execute(interaction: ButtonInteraction) { + await stopProgressSaving(interaction.guild?.id ?? "", interaction.client.user.id) + + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + queue.delete() + return interaction.followUp({ content: t(interaction.locale, "player.stopped"), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/volume_down.ts b/src/buttons/player/volume_down.ts new file mode 100644 index 0000000..d0a741e --- /dev/null +++ b/src/buttons/player/volume_down.ts @@ -0,0 +1,14 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_volume_down" +export async function execute(interaction: ButtonInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + const volume = queue.node.volume - 10 + queue.node.setVolume(volume) + return interaction.followUp({ content: t(interaction.locale, "player.volume_changed_down", { volume: volume.toString() }), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/player/volume_up.ts b/src/buttons/player/volume_up.ts new file mode 100644 index 0000000..e688862 --- /dev/null +++ b/src/buttons/player/volume_up.ts @@ -0,0 +1,14 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const id = "player_volume_up" +export async function execute(interaction: ButtonInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return + + const volume = queue.node.volume + 10 + queue.node.setVolume(volume) + return interaction.followUp({ content: t(interaction.locale, "player.volume_changed", { volume: volume.toString() }), flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/previous.ts b/src/buttons/previous.ts deleted file mode 100755 index 310bb1a..0000000 --- a/src/buttons/previous.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useHistory } from 'discord-player' - -export default { - id: 'previous', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let history = useHistory(guild.id) - if (!history) return - - await history.previous() - return interaction.followUp({ content: 'Musique précédente jouée !', ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/resume.ts b/src/buttons/resume.ts deleted file mode 100755 index 16ad3ac..0000000 --- a/src/buttons/resume.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'resume', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - await queue.node.setPaused(!queue.node.isPaused()) - return interaction.followUp({ content: 'Musique reprise !', ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/shuffle.ts b/src/buttons/shuffle.ts deleted file mode 100755 index 1f212b3..0000000 --- a/src/buttons/shuffle.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'shuffle', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - await queue.tracks.shuffle() - return interaction.followUp({ content: 'File d\'attente mélangée !', ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/skip.ts b/src/buttons/skip.ts deleted file mode 100755 index 664fafe..0000000 --- a/src/buttons/skip.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'skip', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - await queue.node.skip() - return interaction.followUp({ content: 'Musique passée !', ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/stop.ts b/src/buttons/stop.ts deleted file mode 100755 index c1095a4..0000000 --- a/src/buttons/stop.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'stop', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - await queue.delete() - return interaction.followUp({ content: 'Musique arrêtée !', ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/twitch/channel.ts b/src/buttons/twitch/channel.ts new file mode 100644 index 0000000..d65dfdc --- /dev/null +++ b/src/buttons/twitch/channel.ts @@ -0,0 +1,19 @@ +import { MessageFlags, ChannelType, ActionRowBuilder, ChannelSelectMenuBuilder } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { t } from "@/utils/i18n" + +export const id = "twitch_channel" +export async function execute(interaction: ButtonInteraction) { + const channelSelect = new ChannelSelectMenuBuilder() + .setCustomId("twitch_channel") + .setPlaceholder(t(interaction.locale, "twitch.select_notification_channel_placeholder")) + .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement) + + const row = new ActionRowBuilder().addComponents(channelSelect) + + return interaction.reply({ + content: t(interaction.locale, "twitch.select_notification_channel"), + components: [row], + flags: MessageFlags.Ephemeral + }) +} diff --git a/src/buttons/twitch/disable.ts b/src/buttons/twitch/disable.ts new file mode 100644 index 0000000..b79a391 --- /dev/null +++ b/src/buttons/twitch/disable.ts @@ -0,0 +1,28 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { generateTwitchEmbed } from "@/utils/twitch" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "twitch_disable" +export async function execute(interaction: ButtonInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + dbData.enabled = false + dbData.botId = "" + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + // Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour + const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + return interaction.update({ + embeds: [embed], + components: components + }) +} diff --git a/src/buttons/twitch/enable.ts b/src/buttons/twitch/enable.ts new file mode 100644 index 0000000..c36f5ff --- /dev/null +++ b/src/buttons/twitch/enable.ts @@ -0,0 +1,28 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { generateTwitchEmbed } from "@/utils/twitch" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "twitch_enable" +export async function execute(interaction: ButtonInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + dbData.enabled = true + dbData.botId = interaction.client.user.id + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + // Utiliser la fonction utilitaire pour générer l'embed et les composants mis à jour + const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + return interaction.update({ + embeds: [embed], + components: components + }) +} diff --git a/src/buttons/twitch/index.ts b/src/buttons/twitch/index.ts new file mode 100644 index 0000000..90356f2 --- /dev/null +++ b/src/buttons/twitch/index.ts @@ -0,0 +1,17 @@ +import * as twitch_disable from "./disable" +import * as twitch_enable from "./enable" +import * as twitch_set_channel from "./channel" +import * as twitch_list_streamers from "./streamer_list" +import * as twitch_add_streamer from "./streamer_add" +import * as twitch_remove_streamer from "./streamer_remove" + +import type { Button } from "@/types" + +export default [ + twitch_disable, + twitch_enable, + twitch_set_channel, + twitch_list_streamers, + twitch_add_streamer, + twitch_remove_streamer +] as Button[] diff --git a/src/buttons/twitch/streamer_add.ts b/src/buttons/twitch/streamer_add.ts new file mode 100644 index 0000000..f186c59 --- /dev/null +++ b/src/buttons/twitch/streamer_add.ts @@ -0,0 +1,20 @@ +import { MessageFlags } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "twitch_streamer_add" +export async function execute(interaction: ButtonInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled"), flags: MessageFlags.Ephemeral }) + if (!dbData.channelId) return interaction.reply({ content: t(interaction.locale, "twitch.configure_channel_first"), flags: MessageFlags.Ephemeral }) + + return interaction.reply({ + content: t(interaction.locale, "twitch.add_streamer_command"), + flags: MessageFlags.Ephemeral + }) +} diff --git a/src/buttons/twitch/streamer_list.ts b/src/buttons/twitch/streamer_list.ts new file mode 100644 index 0000000..b2267f1 --- /dev/null +++ b/src/buttons/twitch/streamer_list.ts @@ -0,0 +1,51 @@ +import { MessageFlags, EmbedBuilder } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { twitchClient } from "@/utils/twitch" +import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" + +export const id = "twitch_streamer_list" +export async function execute(interaction: ButtonInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled"), flags: MessageFlags.Ephemeral }) + if (!dbData.streamers.length) { + const embed = new EmbedBuilder() + .setTitle(t(interaction.locale, "twitch.list.title")) + .setDescription(t(interaction.locale, "twitch.list.empty_description")) + .setColor(0x808080) + .setTimestamp() + + return interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }) + } + + const streamers = [] as string[] + await Promise.all(dbData.streamers.map(async (streamer, index) => { + try { + const user = await twitchClient.users.getUserById(streamer.twitchUserId) + if (user) { + const discordUser = streamer.discordUserId ? `<@${streamer.discordUserId}>` : t(interaction.locale, "twitch.list.discord_not_associated") + streamers.push(`**${index + 1}.** ${user.displayName}\n└ Discord: ${discordUser}\n└ ID: \`${streamer.twitchUserId}\``) + } else { + streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.user_not_found")}\n└ ID: \`${streamer.twitchUserId}\``) + } + } catch (error) { + logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId }) + console.error(error) + streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.fetch_error")}\n└ ID: \`${streamer.twitchUserId}\``) + } + })) + + const embed = new EmbedBuilder() + .setTitle(t(interaction.locale, "twitch.list.title")) + .setDescription(streamers.join("\n\n")) + .setColor(0x9146FF) + .setFooter({ text: t(interaction.locale, "twitch.list.footer", { count: streamers.length.toString() }) }) + .setTimestamp() + + return interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/twitch/streamer_remove.ts b/src/buttons/twitch/streamer_remove.ts new file mode 100644 index 0000000..e0be364 --- /dev/null +++ b/src/buttons/twitch/streamer_remove.ts @@ -0,0 +1,46 @@ +import { MessageFlags, ActionRowBuilder, StringSelectMenuBuilder } from "discord.js" +import type { ButtonInteraction } from "discord.js" +import { twitchClient } from "@/utils/twitch" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" + +export const id = "twitch_streamer_remove" +export async function execute(interaction: ButtonInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled"), flags: MessageFlags.Ephemeral }) + if (!dbData.streamers.length) return interaction.reply({ content: t(interaction.locale, "twitch.no_streamers_list"), flags: MessageFlags.Ephemeral }) + + // Créer la liste des streamers dans un menu de sélection + const options = await Promise.all(dbData.streamers.map(async (streamer) => { + try { + const user = await twitchClient.users.getUserById(streamer.twitchUserId) + return { + label: user ? user.displayName : `ID: ${streamer.twitchUserId}`, + value: streamer.twitchUserId, + description: user ? `@${user.name}` : t(interaction.locale, "twitch.user_not_found") + } + } catch (error) { + logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId }) + console.error(error) + return { + label: `ID: ${streamer.twitchUserId}`, + value: streamer.twitchUserId, + description: t(interaction.locale, "twitch.fetch_error") + } + } + })) + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId("twitch_streamer_remove") + .setPlaceholder(t(interaction.locale, "twitch.select_streamer_to_remove")) + .addOptions(options.slice(0, 25)) // Discord limite à 25 options + + const row = new ActionRowBuilder().addComponents(selectMenu) + + return interaction.reply({ content: t(interaction.locale, "twitch.select_streamer_prompt"), components: [row], flags: MessageFlags.Ephemeral }) +} diff --git a/src/buttons/volume_down.ts b/src/buttons/volume_down.ts deleted file mode 100755 index 943f39d..0000000 --- a/src/buttons/volume_down.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'volume_down', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - let volume = queue.node.volume - 10 - await queue.node.setVolume(volume) - return interaction.followUp({ content: `🔉 | Volume modifié à ${volume}% !`, ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/buttons/volume_up.ts b/src/buttons/volume_up.ts deleted file mode 100755 index e854d81..0000000 --- a/src/buttons/volume_up.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ButtonInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - id: 'volume_up', - async execute(interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return - let queue = useQueue(guild.id) - if (!queue) return - - let volume = queue.node.volume + 10 - await queue.node.setVolume(volume) - return interaction.followUp({ content: `🔊 | Volume modifié à ${volume}% !`, ephemeral: true }) - } -} \ No newline at end of file diff --git a/src/commands/global/amp.ts b/src/commands/global/amp.ts index 95647c7..ae06a4c 100755 --- a/src/commands/global/amp.ts +++ b/src/commands/global/amp.ts @@ -1,210 +1,241 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, EmbedBuilder, inlineCode, PermissionFlagsBits } from 'discord.js' -import dbGuild from '../../schemas/guild' -import * as AMP from '../../utils/amp' - -interface InstanceFields { - name: string - value: string - inline: boolean -} -interface InstanceResult { - status: string - data: [ - Host - ] -} -interface Host { - AvailableInstances: Instance[] - FriendlyName: string -} -interface Instance { - InstanceID: string - FriendlyName: string - Running: boolean - Module: string - Port: number -} -interface FailMsgData { - Title: string - Message: string -} -interface ErrorMsgData { - error_code: string -} - -function failMsg(data: FailMsgData) { return `La commande a échouée !\n${inlineCode(`${data.Title}: ${data.Message}`)}` } -function errorMsg(data: ErrorMsgData) { return `Y'a eu une erreur !\n${inlineCode(`${data.error_code}`)}` } - -export default { - data: new SlashCommandBuilder() - .setName('amp') - .setDescription('Accède à mon panel de jeu AMP !') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addSubcommand(subcommand => subcommand.setName('login').setDescription("Connectez-vous avant d'effectuer une autre commande !") - .addStringOption(option => option.setName('username').setDescription("Nom d'Utilisateur").setRequired(true)) - .addStringOption(option => option.setName('password').setDescription('Mot de Passe').setRequired(true)) - .addBooleanOption(option => option.setName('remember').setDescription('Mémoriser les identifiants').setRequired(true)) - .addStringOption(option => option.setName('otp').setDescription('Code de double authentification'))) - .addSubcommandGroup(subcommandgroup => subcommandgroup.setName('instances').setDescription('Intéragir avec les instances AMP.') - .addSubcommand(subcommand => subcommand.setName('list').setDescription('Liste toutes les instances disponibles.')) - .addSubcommand(subcommand => subcommand.setName('manage').setDescription('Gérer une instance.') - .addStringOption(option => option.setName('instance').setDescription("Nom de l'instance").setRequired(true).setAutocomplete(true))) - .addSubcommand(subcommand => subcommand.setName('restart').setDescription('Redémarre une instance.') - .addStringOption(option => option.setName('name').setDescription("Nom de l'instance").setRequired(true))) - ), - async autocompleteRun(interaction: AutocompleteInteraction) { - let query = interaction.options.getString('instance', true) - - let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id }) - if (!guildProfile) return await interaction.respond([]) - - let dbData = guildProfile.get('guildAmp') - if (!dbData?.enabled) return await interaction.respond([]) - - let host = dbData.host as string - let username = dbData.username as string - let sessionID = dbData.sessionID as string - let rememberMeToken = dbData.rememberMeToken as string - - // Check if the SessionID is still valid - let session = await AMP.CheckSession(host, sessionID) - if (session.status === 'fail') { - if (rememberMeToken) { - // Refresh the SessionID if the RememberMeToken is available - let details = { username, password: '', token: rememberMeToken, rememberMe: true } - let result = await AMP.Core.Login(host, details) - - if (result.status === 'success') sessionID = result.data.sessionID - else if (result.status === 'fail') return interaction.respond([]) - else if (result.status === 'error') return interaction.respond([]) - } else return await interaction.respond([]) - } - else if (session.status === 'error') return interaction.respond([]) - - let choices: ApplicationCommandOptionChoiceData[] = [] - let result = await AMP.ADSModule.GetInstances(host, sessionID) - if (result.status === 'success') { - let hosts = result.data.result as Host[] - hosts.forEach(host => { - let instances = host.AvailableInstances as Instance[] - instances.forEach(instance => { - if (instance.FriendlyName.includes(query)) choices.push({ name: `${host.FriendlyName} - ${instance.FriendlyName}`, value: instance.InstanceID }) - }) - }) - } - else if (result.status === 'fail') return interaction.respond([]) - else if (result.status === 'error') return interaction.respond([]) - - return interaction.respond(choices) - }, - async execute(interaction: ChatInputCommandInteraction) { - let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id }) - if (!guildProfile) return interaction.reply({ content: `Database data for **${interaction.guild?.name}** does not exist, please initialize with \`/database init\` !` }) - - let dbData = guildProfile.get('guildAmp') - if (!dbData?.enabled) return interaction.reply({ content: `AMP module is disabled for **${interaction.guild?.name}**, please activate with \`/database edit guildAmp.enabled True\` !` }) - - let host = dbData.host as string - let username = dbData.username as string - let sessionID = dbData.sessionID as string - let rememberMeToken = dbData.rememberMeToken as string - - // Let the user login - if (interaction.options.getSubcommand() == 'login') { - // Get a SessionID and a RememberMeToken if wanted - await interaction.deferReply({ ephemeral: true }) - - let details = { - username: interaction.options.getString('username') || '', - password: interaction.options.getString('password') || '', - rememberMe: interaction.options.getBoolean('remember') || '', - token: interaction.options.getString('otp') || '' - } - - let result = await AMP.Core.Login(host, details) - if (result.status === 'success') { - username = dbData['username'] = result.data.userInfo.Username - sessionID = dbData['sessionID'] = result.data.sessionID - rememberMeToken = dbData['rememberMeToken'] = result.data.rememberMeToken - - guildProfile.set('guildAmp', dbData) - guildProfile.markModified('guildAmp') - await guildProfile.save().catch(console.error) - - return await interaction.followUp(`Tu es connecté au panel sous **${username}** !`) - } - else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data)) - else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data)) - } - await interaction.deferReply() - - // Check if the SessionID is still valid - let session = await AMP.CheckSession(host, sessionID) - if (session.status === 'fail') { - if (rememberMeToken) { - // Refresh the SessionID if the RememberMeToken is available - let details = { username, password: '', token: rememberMeToken, rememberMe: true } - let result = await AMP.Core.Login(host, details) - - if (result.status === 'success') sessionID = result.data.sessionID - else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data)) - else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data)) - } - else return await interaction.followUp(`Tu dois te connecter avant d'effectuer une autre commande !`) - } - else if (session.status === 'error') return await interaction.followUp(errorMsg(session.data)) - - if (interaction.options.getSubcommandGroup() == 'instances') { - if (interaction.options.getSubcommand() == 'list') { - let result = await AMP.ADSModule.GetInstances(host, sessionID) as InstanceResult - - if (result.status === 'success') { - await interaction.followUp({ content: `${result.data.length} hôte(s) trouvé(s) !` }) - result.data.forEach(async host => { - let fields = [] as InstanceFields[] - host.AvailableInstances.forEach((instance: Instance) => { - fields.push({ - name: instance.FriendlyName, - value: `**Running:** ${instance.Running}\n**Port:** ${instance.Port}\n**Module:** ${instance.Module}`, - inline: true - }) - }) - let embed = new EmbedBuilder() - .setTitle(host.FriendlyName) - .setDescription(`Liste des ${host.AvailableInstances.length} instances :`) - .setColor(interaction.guild?.members.me?.displayColor || '#ffc370') - .setTimestamp() - .setFields(fields) - return await interaction.followUp({ embeds: [embed] }) - }) - } - else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data as unknown as FailMsgData)) - else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data as unknown as ErrorMsgData)) - } - else if (interaction.options.getSubcommand() == 'manage') { - let instanceID = interaction.options.getString('instance', true) - let result = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID) - - if (result.status === 'success') { - let server = await AMP.ADSModule.Servers(host, sessionID, instanceID) - - if (server.status === 'success') return await interaction.followUp(`Ok !`) - else if (server.status === 'fail') return await interaction.followUp(failMsg(server.data)) - else if (server.status === 'error') return await interaction.followUp(errorMsg(server.data)) - } - else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data)) - else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data)) - } - else if (interaction.options.getSubcommand() == 'restart') { - let query = interaction.options.getString('name') - if (!query) return - - let result = await AMP.ADSModule.RestartInstance(host, sessionID, query) - - if (result.status === 'success') return await interaction.followUp(`Ok !`) - else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data)) - else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data)) - } - } - } -} \ No newline at end of file +import { SlashCommandBuilder, EmbedBuilder, inlineCode, PermissionFlagsBits, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, Locale } from "discord.js" +import * as AMP from "@/utils/amp" +import type { Host, Instance, InstanceFields, InstanceResult, LoginSuccessData } from "@/types/amp" +import type { ReturnMsgData } from "@/types" +import type { GuildAmp } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +function returnMsg(result: ReturnMsgData, locale: Locale) { + if (result.status === "fail") return `${t(locale, "common.failed")}\n${inlineCode(`${result.Title}: ${result.Message}`)}` + if (result.status === "error") return `${t(locale, "common.error_occurred")}\n${inlineCode(`${result.error_code}`)}` +} + +export const data = new SlashCommandBuilder() + .setName("amp") + .setDescription("Access my AMP gaming panel") + .setDescriptionLocalizations({ fr: "Accède à mon panel de jeu AMP" }) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(subcommand => subcommand + .setName("login") + .setDescription("Log in before performing another command") + .setNameLocalizations({ fr: "connexion" }) + .setDescriptionLocalizations({ fr: "Connectez-vous avant d'effectuer une autre commande" }) + .addStringOption(option => option + .setName("username") + .setDescription("Username") + .setNameLocalizations({ fr: "nom_utilisateur" }) + .setDescriptionLocalizations({ fr: "Nom d'utilisateur" }) + .setRequired(true) + ) + .addStringOption(option => option + .setName("password") + .setDescription("Password") + .setNameLocalizations({ fr: "mot_de_passe" }) + .setDescriptionLocalizations({ fr: "Mot de passe" }) + .setRequired(true) + ) + .addBooleanOption(option => option + .setName("remember") + .setDescription("Remember credentials") + .setNameLocalizations({ fr: "memoriser" }) + .setDescriptionLocalizations({ fr: "Mémoriser les identifiants" }) + .setRequired(true) + ) + .addStringOption(option => option + .setName("otp") + .setDescription("Two-factor authentication code") + .setNameLocalizations({ fr: "otp" }) + .setDescriptionLocalizations({ fr: "Code d'authentification à 2 facteurs" }) + ) + ) + .addSubcommandGroup(subcommandgroup => subcommandgroup + .setName("instances") + .setDescription("Interact with AMP instances") + .setNameLocalizations({ fr: "instances" }) + .setDescriptionLocalizations({ fr: "Intéragir avec les instances AMP" }) + .addSubcommand(subcommand => subcommand + .setName("list") + .setDescription("List all available instances") + .setNameLocalizations({ fr: "liste" }) + .setDescriptionLocalizations({ fr: "Lister toutes les instances disponibles" }) + ) + .addSubcommand(subcommand => subcommand + .setName("manage") + .setDescription("Manage an instance") + .setNameLocalizations({ fr: "gerer" }) + .setDescriptionLocalizations({ fr: "Gérer une instance" }) + .addStringOption(option => option + .setName("instance") + .setDescription("Instance name") + .setNameLocalizations({ fr: "instance" }) + .setDescriptionLocalizations({ fr: "Nom de l'instance" }) + .setRequired(true) + .setAutocomplete(true) + ) + ) + .addSubcommand(subcommand => subcommand + .setName("restart") + .setDescription("Restart an instance") + .setNameLocalizations({ fr: "redemarrer" }) + .setDescriptionLocalizations({ fr: "Redémarrer une instance" }) + .addStringOption(option => option + .setName("name") + .setDescription("Instance name") + .setNameLocalizations({ fr: "nom" }) + .setDescriptionLocalizations({ fr: "Nom de l'instance" }) + .setRequired(true) + ) + ) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildAmp") as GuildAmp + if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "amp.module_disabled"), flags: MessageFlags.Ephemeral }) + + const host = dbData.host + if (!host) return interaction.reply({ content: t(interaction.locale, "amp.host_not_configured"), flags: MessageFlags.Ephemeral }) + + let username = dbData.username + let sessionID = dbData.sessionID + let rememberMeToken = dbData.rememberMeToken + + const subcommandGroup = interaction.options.getSubcommandGroup(false) + const subcommand = interaction.options.getSubcommand(true) + if (subcommand == "login") { + // Get a SessionID and a RememberMeToken if wanted + await interaction.deferReply({ flags: MessageFlags.Ephemeral }) + + const details = { + username: interaction.options.getString("username", true), + password: interaction.options.getString("password", true), + token: interaction.options.getString("otp") ?? "", + rememberMe: interaction.options.getBoolean("remember", true) + } + + const result = await AMP.Core.Login(host, details) + if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral }) + + const loginData = result.data as LoginSuccessData + username = dbData.username = loginData.userInfo.Username + sessionID = dbData.sessionID = loginData.sessionID + rememberMeToken = dbData.rememberMeToken = loginData.rememberMeToken + + guildProfile.set("guildAmp", dbData) + guildProfile.markModified("guildAmp") + await guildProfile.save().catch(console.error) + + return interaction.followUp({ content: t(interaction.locale, "amp.logged_in", { username }), flags: MessageFlags.Ephemeral }) + } + await interaction.deferReply() + + // Check if the SessionID is still valid + if (!sessionID) return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral }) + + const checkResult = await AMP.CheckSession(host, sessionID) + if (checkResult.status === "fail") { + if (rememberMeToken && username) { + // Refresh the SessionID if the RememberMeToken is available + const details = { username, password: "", token: rememberMeToken, rememberMe: true } + const loginResult = await AMP.Core.Login(host, details) + if (loginResult.status !== "success") return interaction.followUp({ content: returnMsg(loginResult, interaction.locale), flags: MessageFlags.Ephemeral }) + + const loginData = loginResult.data as LoginSuccessData + sessionID = loginData.sessionID + } + else return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral }) + } + else if (checkResult.status === "error") return interaction.followUp({ content: returnMsg(checkResult, interaction.locale), flags: MessageFlags.Ephemeral }) + + if (subcommandGroup == "instances") { + if (subcommand == "list") { + const result = (await AMP.ADSModule.GetInstances(host, sessionID)) as InstanceResult + if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral }) + + await interaction.followUp({ content: t(interaction.locale, "amp.hosts_found", { count: result.data.length }) }) + await Promise.all(result.data.map(async host => { + const fields = [] as InstanceFields[] + host.AvailableInstances.forEach((instance: Instance) => { + fields.push({ name: instance.FriendlyName, value: `**${t(interaction.locale, "amp.running")}:** ${instance.Running}\n**${t(interaction.locale, "amp.port")}:** ${instance.Port}\n**${t(interaction.locale, "amp.module")}:** ${instance.Module}`, inline: true }) + }) + const embed = new EmbedBuilder() + .setTitle(host.FriendlyName) + .setDescription(t(interaction.locale, "amp.instance_list", { count: host.AvailableInstances.length })) + .setColor(interaction.guild?.members.me?.displayColor ?? "#ffc370") + .setTimestamp() + .setFields(fields) + return interaction.followUp({ embeds: [embed] }) + })) + } + else if (subcommand == "manage") { + const instanceID = interaction.options.getString("instance", true) + + const manageResult = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID) + if (manageResult.status !== "success") return interaction.followUp({ content: returnMsg(manageResult, interaction.locale), flags: MessageFlags.Ephemeral }) + + const serversResult = await AMP.ADSModule.Servers(host, sessionID, instanceID) + if (serversResult.status !== "success") return interaction.followUp({ content: returnMsg(serversResult, interaction.locale), flags: MessageFlags.Ephemeral }) + + return interaction.followUp({ content: t(interaction.locale, "amp.manage_success") }) + } + else if (subcommand == "restart") { + const query = interaction.options.getString("name", true) + + const restartResult = await AMP.ADSModule.RestartInstance(host, sessionID, query) + if (restartResult.status !== "success") return interaction.followUp({ content: returnMsg(restartResult, interaction.locale), flags: MessageFlags.Ephemeral }) + + return interaction.followUp({ content: t(interaction.locale, "amp.restart_success") }) + } + } +} +export async function autocompleteRun(interaction: AutocompleteInteraction) { + const query = interaction.options.getString("instance", true) + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.respond([]) + + const dbData = guildProfile.get("guildAmp") as GuildAmp + if (!dbData.enabled) return interaction.respond([]) + + const host = dbData.host + if (!host) return interaction.respond([]) + + let sessionID = dbData.sessionID + if (!sessionID) return interaction.respond([]) + + const username = dbData.username + const rememberMeToken = dbData.rememberMeToken + + const checkResult = await AMP.CheckSession(host, sessionID) + if (checkResult.status === "fail") { + if (rememberMeToken && username) { + // Refresh the SessionID if the RememberMeToken is available + const details = { username, password: "", token: rememberMeToken, rememberMe: true } + const loginResult = await AMP.Core.Login(host, details) + if (loginResult.status !== "success") return interaction.respond([]) + + const loginData = loginResult.data as LoginSuccessData + sessionID = loginData.sessionID + } + else return interaction.respond([]) + } + else if (checkResult.status === "error") return interaction.respond([]) + + const instancesResult = (await AMP.ADSModule.GetInstances(host, sessionID)) as InstanceResult + if (instancesResult.status !== "success") return interaction.respond([]) + + const choices: ApplicationCommandOptionChoiceData[] = [] + const hosts = instancesResult.data as Host[] + hosts.forEach(host => { + const instances = host.AvailableInstances + instances.forEach(instance => { + if (instance.FriendlyName.includes(query)) choices.push({ name: `${host.FriendlyName} - ${instance.FriendlyName}`, value: instance.InstanceID }) + }) + }) + + return interaction.respond(choices) +} diff --git a/src/commands/global/boost.ts b/src/commands/global/boost.ts index 68896a2..c5e3cc5 100644 --- a/src/commands/global/boost.ts +++ b/src/commands/global/boost.ts @@ -1,37 +1,39 @@ -import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, TextChannel, PermissionFlagsBits } from 'discord.js' +import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" -module.exports = { - data: new SlashCommandBuilder() - .setName('boost') - .setDescription('Tester le boost du serveur !') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - async execute(interaction: ChatInputCommandInteraction) { - if (interaction.guild?.id !== '796327643783626782') return interaction.reply({ content: 'Non !' })// Jujul Community - let member = interaction.member - if (!member) return console.log(`\u001b[1;31m Aucun membre trouvé !`) +export const data = new SlashCommandBuilder() + .setName("boost") + .setDescription("Test the server boost") + .setDescriptionLocalizations({ fr: "Tester le boost du serveur" }) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - let guild = interaction.guild - if (!guild) return console.log(`\u001b[1;31m Aucun serveur trouvé !`) +export async function execute(interaction: ChatInputCommandInteraction) { + if (interaction.guild?.id !== "796327643783626782") return interaction.reply({ content: t(interaction.locale, "boost.not_authorized") }) // Jujul Community - let channel = guild.channels.cache.get('924353449930412153') as TextChannel - if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`) + const member = interaction.member + if (!member) { logConsole('discordjs', 'boost.no_member'); return } - let boostRole = guild.roles.premiumSubscriberRole - if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`) + const guild = interaction.guild + if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return } - if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`) - - let embed = new EmbedBuilder() - .setColor(guild.members.me.displayHexColor) - .setTitle(`Nouveau boost de ${member.user.username} !`) - .setDescription(` - Merci à toi pour ce boost.\n - Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts ! - `) - .setThumbnail(member.user.avatar) - .setTimestamp(new Date()) - - await channel.send({ embeds: [embed] }) - await interaction.reply({ content: 'Va voir dans <#924353449930412153> !' }) + const channel = await guild.channels.fetch("924353449930412153") + if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { + logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" }) + return } -} \ No newline at end of file + + const boostRole = guild.roles.premiumSubscriberRole + if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return } + + const embed = new EmbedBuilder() + .setColor(guild.members.me.displayHexColor) + .setTitle(t(interaction.locale, "boost.new_boost_title", { username: member.user.username })) + .setDescription(t(interaction.locale, "boost.new_boost_description", { count: (guild.premiumSubscriptionCount ?? 0).toString() })) + .setThumbnail(member.user.avatar) + .setTimestamp(new Date()) + + await channel.send({ embeds: [embed] }) + return interaction.reply({ content: t(interaction.locale, "boost.check_channel", { channelId: "924353449930412153" }) }) +} diff --git a/src/commands/global/database.ts b/src/commands/global/database.ts index e8fc979..4083bfb 100755 --- a/src/commands/global/database.ts +++ b/src/commands/global/database.ts @@ -1,76 +1,108 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, APIEmbedField, PermissionFlagsBits } from 'discord.js' - -import dbGuildInit from '../../utils/dbGuildInit' -import dbGuild from '../../schemas/guild' - -const parseObject = (obj: object, prefix = ''): { name: string, value: object | string | boolean }[] => { - let fields: { name: string, value: object | string | boolean }[] = [] - - for (let [key, value] of Object.entries(obj)) { - if (typeof value === 'object') fields.push(...parseObject(value, `${prefix}${key}.`)) - else { - if (typeof value === 'boolean') value = value ? 'True' : 'False' - else if (!value) value = 'None' - else value = value.toString() - - fields.push({ name: `${prefix}${key}`, value }) - } - } - return fields -} - -export default { - data: new SlashCommandBuilder() - .setName('database') - .setDescription('Communicate with the database') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addSubcommand(subcommand => subcommand.setName('info').setDescription('Returns information about the current guild')) - .addSubcommand(subcommand => subcommand.setName('init').setDescription('Force initialize an entry for the current guild in the database')) - .addSubcommand(subcommand => subcommand.setName('edit').setDescription('Modify parameters for the current guild') - .addStringOption(option => option.setName('key').setDescription('Key to modify').setRequired(true)) - .addStringOption(option => option.setName('value').setDescription('Value to set').setRequired(true))), - async execute(interaction: ChatInputCommandInteraction) { - let guild = interaction.guild - if (!guild) return await interaction.reply({ content: 'This command must be used in a server.', ephemeral: true }) - - let guildProfile = await dbGuild.findOne({ guildId: guild.id }) - - if (interaction.options.getSubcommand() === 'info') { - if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist !` }) - - let fields = parseObject(guildProfile.toObject()) - - let embed = new EmbedBuilder() - .setTitle('Database Information') - .setDescription(`Guild **${guildProfile.guildName}** (ID: ${guildProfile.guildId})`) - .setThumbnail(guildProfile.guildIcon as string) - .setTimestamp() - //.addFields(fields as APIEmbedField[]) - // Limit the number of fields to 25 - .addFields(fields.slice(0, 25) as APIEmbedField[]) - return await interaction.reply({ embeds: [embed] }) - - } else if (interaction.options.getSubcommand() === 'init') { - if (guildProfile) return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** already exists !` }) - - guildProfile = await dbGuildInit(guild) - if (!guildProfile) return await interaction.reply({ content: `An error occured while initializing database data for **${guild.name}** !` }) - - return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully initialized !` }) - - } else if (interaction.options.getSubcommand() === 'edit') { - if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist, please init with \`/database init\` !` }) - - let key = interaction.options.getString('key', true) - let value = interaction.options.getString('value', true) - - let oldValue = guildProfile.get(key) - if (!oldValue) oldValue = 'None' - - guildProfile.set(key, value) - await guildProfile.save().catch(console.error) - - return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully updated !\n**${key}**: ${oldValue} -> ${value}` }) - } - } -} \ No newline at end of file +import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction, APIEmbedField } from "discord.js" +import dbGuildInit from "@/utils/dbGuildInit" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +const parseObject = (obj: object, prefix = ""): { name: string, value: object | string | boolean }[] => { + const fields: { name: string, value: object | string | boolean }[] = [] + + for (const [key, value] of Object.entries(obj)) { + if (value !== null && typeof value === "object") fields.push(...parseObject(value as object, `${prefix}${key}.`)) + else { + let newValue: string + if (typeof value === "boolean") newValue = value ? "True" : "False" + else if (value === null || value === undefined) newValue = "None" + else newValue = String(value) + + fields.push({ name: `${prefix}${key}`, value: newValue }) + } + } + return fields +} + +export const data = new SlashCommandBuilder() + .setName("database") + .setDescription("Communicate with the database") + .setDescriptionLocalizations({ fr: "Communiquer avec la base de données" }) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(subcommand => subcommand + .setName("info") + .setDescription("Returns information about the current guild") + .setNameLocalizations({ fr: "info" }) + .setDescriptionLocalizations({ fr: "Retourne les informations sur le serveur actuel" }) + ) + .addSubcommand(subcommand => subcommand + .setName("init") + .setDescription("Force initialize an entry for the current guild in the database") + .setNameLocalizations({ fr: "init" }) + .setDescriptionLocalizations({ fr: "Initialiser de force une entrée pour le serveur actuel dans la base de données" }) + ) + .addSubcommand(subcommand => subcommand + .setName("edit") + .setDescription("Modify parameters for the current guild") + .setNameLocalizations({ fr: "modifier" }) + .setDescriptionLocalizations({ fr: "Modifier les paramètres pour le serveur actuel" }) + .addStringOption(option => option + .setName("key") + .setDescription("Key to modify") + .setNameLocalizations({ fr: "cle" }) + .setDescriptionLocalizations({ fr: "Clé à modifier" }) + .setRequired(true) + ) + .addStringOption(option => option + .setName("value") + .setDescription("Value to set") + .setNameLocalizations({ fr: "valeur" }) + .setDescriptionLocalizations({ fr: "Valeur à définir" }) + .setRequired(true) + ) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + if (interaction.user !== interaction.client.application.owner) return interaction.reply({ content: t(interaction.locale, "database.owner_only"), flags: MessageFlags.Ephemeral }) + + const guild = interaction.guild + if (!guild) return interaction.reply({ content: t(interaction.locale, "database.server_only"), flags: MessageFlags.Ephemeral }) + + let guildProfile = await dbGuild.findOne({ guildId: guild.id }) + + const subcommand = interaction.options.getSubcommand(true) + if (subcommand === "info") { + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const fields = parseObject(guildProfile.toObject()) + const embed = new EmbedBuilder() + .setTitle(t(interaction.locale, "database.info_title")) + .setDescription(t(interaction.locale, "database.guild_info", { name: guildProfile.guildName, id: guildProfile.guildId })) + .setThumbnail(guildProfile.guildIcon) + .setTimestamp() + //.addFields(fields as APIEmbedField[]) + // Limit the number of fields to 25 + .addFields(fields.slice(0, 25) as APIEmbedField[]) + + return interaction.reply({ embeds: [embed] }) + } + else if (subcommand === "init") { + if (guildProfile) return interaction.reply({ content: t(interaction.locale, "database.already_exists", { name: guildProfile.guildName }), flags: MessageFlags.Ephemeral }) + + guildProfile = await dbGuildInit(guild) + + return interaction.reply({ content: t(interaction.locale, "database.initialized", { name: guildProfile.guildName }), flags: MessageFlags.Ephemeral }) + } + else if (subcommand === "edit") { + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const key = interaction.options.getString("key", true) + const value = interaction.options.getString("value", true) + + let oldValue: string = guildProfile.get(key) as string + if (!oldValue) oldValue = t(interaction.locale, "common.none") + + guildProfile.set(key, value) + guildProfile.markModified(key) + await guildProfile.save().catch(console.error) + + return interaction.reply({ content: t(interaction.locale, "database.updated", { name: guildProfile.guildName, key, oldValue, value }), flags: MessageFlags.Ephemeral }) + } +} diff --git a/src/commands/global/freebox.ts b/src/commands/global/freebox.ts new file mode 100644 index 0000000..cc6abb9 --- /dev/null +++ b/src/commands/global/freebox.ts @@ -0,0 +1,353 @@ +import { SlashCommandBuilder, EmbedBuilder, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder, inlineCode } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import crypto from "crypto" +import * as Freebox from "@/utils/freebox" +import type { + APIResponseData, APIResponseDataError, APIResponseDataVersion, + ConnectionStatus, GetChallenge, LcdConfig, OpenSession, RequestAuthorization, TrackAuthorizationProgress +} from "@/types/freebox" +import type { GuildFbx } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("freebox") + .setDescription("Access FreeboxOS API") + .setDescriptionLocalizations({ fr: "Accéder à l'API FreeboxOS" }) + .addSubcommand(subcommand => subcommand + .setName("status") + .setDescription("Display Freebox configuration and status") + .setDescriptionLocalizations({ fr: "Afficher la configuration et l'état de la Freebox" }) + ) + .addSubcommand(subcommand => subcommand + .setName("setup") + .setDescription("Configure Freebox settings easily") + .setDescriptionLocalizations({ fr: "Configurer facilement les paramètres Freebox" }) + .addStringOption(option => option + .setName("host") + .setDescription("Freebox host (IP address)") + .setNameLocalizations({ fr: "hote" }) + .setDescriptionLocalizations({ fr: "Hôte Freebox (adresse IP)" }) + .setRequired(false) + ) + .addIntegerOption(option => option + .setName("version") + .setDescription("Freebox API version") + .setNameLocalizations({ fr: "version" }) + .setDescriptionLocalizations({ fr: "Version de l'API Freebox" }) + .setRequired(false) + ) + .addBooleanOption(option => option + .setName("enabled") + .setDescription("Enable or disable the Freebox module") + .setNameLocalizations({ fr: "active" }) + .setDescriptionLocalizations({ fr: "Activer ou désactiver le module Freebox" }) + .setRequired(false) + ) + ) + .addSubcommand(subcommand => subcommand + .setName("init") + .setDescription("Create an app on the Freebox to authenticate") + .setDescriptionLocalizations({ fr: "Créer une app sur la Freebox pour s'authentifier" }) + .addStringOption(option => option + .setName("host") + .setDescription("Freebox host (IP or domain)") + .setNameLocalizations({ fr: "hote" }) + .setDescriptionLocalizations({ fr: "Hôte Freebox (IP ou domaine)" }) + .setRequired(true) + ) + ) + .addSubcommandGroup(subcommandGroup => subcommandGroup + .setName("get") + .setDescription("Retrieve data") + .setNameLocalizations({ fr: "recuperer" }) + .setDescriptionLocalizations({ fr: "Récupérer des données" }) + .addSubcommand(subcommand => subcommand + .setName("version") + .setDescription("Display API version") + .setDescriptionLocalizations({ fr: "Afficher la version de l'API" }) + ) + .addSubcommand(subcommand => subcommand + .setName("connection") + .setDescription("Retrieve connection information") + .setNameLocalizations({ fr: "connexion" }) + .setDescriptionLocalizations({ fr: "Récupérer les informations de connexion" }) + ) + .addSubcommand(subcommand => subcommand + .setName("lcd") + .setDescription("Retrieve LCD configuration") + .setDescriptionLocalizations({ fr: "Récupérer la configuration de l'écran LCD" }) + ) + ) + .addSubcommandGroup(subcommandGroup => subcommandGroup + .setName("lcd") + .setDescription("Control LCD features") + .setDescriptionLocalizations({ fr: "Contrôler les fonctionnalités LCD" }) + .addSubcommand(subcommand => subcommand + .setName("leds") + .setDescription("Toggle LED strip on/off") + .setNameLocalizations({ fr: "leds" }) + .setDescriptionLocalizations({ fr: "Allumer/éteindre le bandeau LED" }) + .addBooleanOption(option => option + .setName("enabled") + .setDescription("Enable or disable LED strip") + .setNameLocalizations({ fr: "active" }) + .setDescriptionLocalizations({ fr: "Activer ou désactiver le bandeau LED" }) + .setRequired(true) + ) + ) + .addSubcommand(subcommand => subcommand + .setName("timer") + .setDescription("Setup automatic LED timer") + .setNameLocalizations({ fr: "minuteur" }) + .setDescriptionLocalizations({ fr: "Configurer le minuteur automatique des LEDs" }) + .addStringOption(option => option + .setName("action") + .setDescription("Timer action") + .setNameLocalizations({ fr: "action" }) + .setDescriptionLocalizations({ fr: "Action du minuteur" }) + .setRequired(true) + .addChoices( + { name: "Enable timer", value: "enable", name_localizations: { fr: "Activer minuteur" } }, + { name: "Disable timer", value: "disable", name_localizations: { fr: "Désactiver minuteur" } }, + { name: "Status", value: "status", name_localizations: { fr: "Statut" } } + ) + ) + .addStringOption(option => option + .setName("morning_time") + .setDescription("Morning time (HH:MM format, 24h)") + .setNameLocalizations({ fr: "heure_matin" }) + .setDescriptionLocalizations({ fr: "Heure du matin (format HH:MM, 24h)" }) + .setRequired(false) + ) + .addStringOption(option => option + .setName("night_time") + .setDescription("Night time (HH:MM format, 24h)") + .setNameLocalizations({ fr: "heure_nuit" }) + .setDescriptionLocalizations({ fr: "Heure du soir (format HH:MM, 24h)" }) + .setRequired(false) + ) + ) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildFbx") as GuildFbx + let host = dbData.host + let appToken = dbData.appToken + + const subcommandGroup = interaction.options.getSubcommandGroup(false) + const subcommand = interaction.options.getSubcommand(true) + + if (subcommand === "status") { + // Construire l'embed de statut + const embed = new EmbedBuilder() + .setTitle(t(interaction.locale, "freebox.status.title")) + .setColor(dbData.enabled ? 0x00ff00 : 0xff0000) + .addFields({ + name: t(interaction.locale, "freebox.status.config_section"), + value: [ + t(interaction.locale, "freebox.status.module_field", { status: dbData.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }), + t(interaction.locale, "freebox.status.host_field", { value: host ? `\`${host}\`` : t(interaction.locale, "freebox.status.host_not_configured") }), + t(interaction.locale, "freebox.status.token_field", { status: appToken ? t(interaction.locale, "freebox.status.token_configured") : t(interaction.locale, "freebox.status.token_not_configured") }) + ].join('\n'), + inline: false + }) + + // Informations LCD si disponibles + if (dbData.lcd) { + const lcdStatus = dbData.lcd.enabled ? t(interaction.locale, "freebox.status.timer_enabled") : t(interaction.locale, "freebox.status.timer_disabled") + const botManaged = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "freebox.status.timer_no_manager") + const morningTime = dbData.lcd.morningTime ?? t(interaction.locale, "freebox.status.timer_not_configured") + const nightTime = dbData.lcd.nightTime ?? t(interaction.locale, "freebox.status.timer_not_configured") + + embed.addFields({ + name: t(interaction.locale, "freebox.status.timer_section"), + value: [ + t(interaction.locale, "freebox.timer.status_field", { status: lcdStatus }), + t(interaction.locale, "freebox.timer.managed_by", { manager: botManaged }), + t(interaction.locale, "freebox.timer.morning", { time: morningTime }), + t(interaction.locale, "freebox.timer.night", { time: nightTime }) + ].join('\n'), + inline: false + }) + } + + // Boutons d'action + const buttons = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("freebox_test_connection") + .setLabel(t(interaction.locale, "freebox.buttons.test_connection")) + .setEmoji("🔌") + .setStyle(ButtonStyle.Primary) + .setDisabled(!appToken), + new ButtonBuilder() + .setCustomId("freebox_lcd_status") + .setLabel(t(interaction.locale, "freebox.buttons.lcd_status")) + .setEmoji("💡") + .setStyle(ButtonStyle.Secondary) + .setDisabled(!appToken), + new ButtonBuilder() + .setCustomId("freebox_refresh_status") + .setLabel(t(interaction.locale, "freebox.buttons.refresh_status")) + .setEmoji("🔄") + .setStyle(ButtonStyle.Success) + ) + + return interaction.reply({ embeds: [embed], components: [buttons], flags: MessageFlags.Ephemeral }) + } + + if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "common.module_disabled"), flags: MessageFlags.Ephemeral }) + + if (subcommand === "init") { + if (appToken) return interaction.reply({ content: t(interaction.locale, "freebox.auth.app_token_already_set"), flags: MessageFlags.Ephemeral }) + + host = interaction.options.getString("host", true) + if (host === "mafreebox.freebox.fr") return interaction.reply({ content: t(interaction.locale, "freebox.auth.host_not_allowed"), flags: MessageFlags.Ephemeral }) + + const initData = await Freebox.Core.Init(host) as APIResponseData + if (!initData.success) return Freebox.handleError(initData as APIResponseDataError, interaction) + + appToken = initData.result.app_token + const trackId = initData.result.track_id + if (!trackId) return interaction.reply({ content: t(interaction.locale, "freebox.auth.track_id_failed"), flags: MessageFlags.Ephemeral }) + + // Si l'utilisateur n'a pas encore autorisé l'application, on lui demande de le faire + await interaction.reply({ content: t(interaction.locale, "freebox.auth.init_in_progress", { trackId }), flags: MessageFlags.Ephemeral }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const initCheck = setInterval(async () => { + if (!host || !trackId) { clearInterval(initCheck); return } + + const trackData = await Freebox.Core.Init(host, trackId) as APIResponseData + if (!trackData.success) return Freebox.handleError(trackData as APIResponseDataError, interaction, false) + + const status = trackData.result.status + if (status === "granted") { + clearInterval(initCheck) + dbData.appToken = appToken + + guildProfile.set("guildFbx", dbData) + guildProfile.markModified("guildFbx") + await guildProfile.save().catch(console.error) + + return interaction.followUp({ content: t(interaction.locale, "common.success"), flags: MessageFlags.Ephemeral }) + } else if (status === "denied") { + clearInterval(initCheck) + + return interaction.followUp({ content: t(interaction.locale, "freebox.auth.user_denied_access"), flags: MessageFlags.Ephemeral }) + } else if (status === "pending") { console.log("Freebox authorization pending...") } + }, 2000) + } + else { + if (!host) return interaction.reply({ content: t(interaction.locale, "freebox.general.host_not_set"), flags: MessageFlags.Ephemeral }) + + if (subcommand === "version") { + const versionData = await Freebox.Core.Version(host) as APIResponseDataVersion + + const embed = new EmbedBuilder() + .setTitle("FreeboxOS API Version") + .setDescription(`Version: ${versionData.api_version || "Unknown"}`) + + return interaction.reply({ embeds: [embed] }) + } + + if (!appToken) return interaction.reply({ content: t(interaction.locale, "freebox.general.app_token_not_set"), flags: MessageFlags.Ephemeral }) + + const challengeData = await Freebox.Login.Challenge(host) as APIResponseData + if (!challengeData.success) return Freebox.handleError(challengeData as APIResponseDataError, interaction) + + const password = crypto.createHmac("sha1", appToken).update(challengeData.result.challenge).digest("hex") + const sessionData = await Freebox.Login.Session(host, password) as APIResponseData + if (!sessionData.success) return Freebox.handleError(sessionData as APIResponseDataError, interaction) + + const sessionToken = sessionData.result.session_token + if (!sessionToken) return interaction.reply({ content: t(interaction.locale, "freebox.auth.session_token_failed"), flags: MessageFlags.Ephemeral }) + + if (subcommandGroup === "get") { + if (subcommand === "connection") { + const connectionData = await Freebox.Get.Connection(host, sessionToken) as APIResponseData + if (!connectionData.success) return Freebox.handleError(connectionData as APIResponseDataError, interaction) + + return interaction.reply({ content: t(interaction.locale, "freebox.api.connection_details", { details: inlineCode(JSON.stringify(connectionData.result)) }), flags: MessageFlags.Ephemeral }) + } + else if (subcommand === "lcd") { + const lcdData = await Freebox.Get.LcdConfig(host, sessionToken) as APIResponseData + if (!lcdData.success) return Freebox.handleError(lcdData as APIResponseDataError, interaction) + + return interaction.reply({ content: t(interaction.locale, "freebox.api.lcd_details", { details: inlineCode(JSON.stringify(lcdData.result)) }), flags: MessageFlags.Ephemeral }) + } + } + else if (subcommandGroup === "lcd") { + // Initialiser l'objet LCD s'il n'existe pas + dbData.lcd ??= { enabled: false } + + // Vérifier si le bot est autorisé pour ce serveur + if (dbData.lcd.enabled && dbData.lcd.botId && dbData.lcd.botId !== interaction.client.user.id) { + return interaction.reply({ content: t(interaction.locale, "freebox.lcd.managed_by_other_bot"), flags: MessageFlags.Ephemeral }) + } + + if (subcommand === "leds") { + const enabled = interaction.options.getBoolean("enabled", true) + const lcdData = await Freebox.Set.LcdConfig(host, sessionToken, { led_strip_enabled: enabled }) as APIResponseData + if (!lcdData.success) return Freebox.handleError(lcdData as APIResponseDataError, interaction) + + return interaction.reply({ content: t(interaction.locale, "freebox.lcd.leds_success", { status: enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }), flags: MessageFlags.Ephemeral }) + } + else if (subcommand === "timer") { + const action = interaction.options.getString("action") + if (!action) return interaction.reply({ content: t(interaction.locale, "freebox.general.invalid_action"), flags: MessageFlags.Ephemeral }) + + if (action === "status") { + const status = dbData.lcd.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") + const managedBy = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "common.none") + + return interaction.reply({ content: t(interaction.locale, "freebox.timer.status_display", { status, managedBy }), flags: MessageFlags.Ephemeral }) + } + else if (action === "enable") { + const morningTime = interaction.options.getString("morning_time") + const nightTime = interaction.options.getString("night_time") + + if (!morningTime || !nightTime) return interaction.reply({ content: t(interaction.locale, "freebox.timer.times_required"), flags: MessageFlags.Ephemeral }) + + // Valider le format HH:MM + const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/ + if (!timeRegex.test(morningTime) || !timeRegex.test(nightTime)) return interaction.reply({ content: t(interaction.locale, "freebox.general.invalid_time_format"), flags: MessageFlags.Ephemeral }) + + // Activer le timer et enregistrer ce bot comme responsable + dbData.lcd.enabled = true + dbData.lcd.botId = interaction.client.user.id + dbData.lcd.morningTime = morningTime + dbData.lcd.nightTime = nightTime + + guildProfile.set("guildFbx", dbData) + guildProfile.markModified("guildFbx") + await guildProfile.save().catch(console.error) + + // Démarrer les timers automatiques + if (interaction.guildId) Freebox.Timer.schedule(interaction.client, interaction.guildId, dbData) + + return interaction.reply({ content: t(interaction.locale, "freebox.timer.enabled", { morningTime, nightTime }), flags: MessageFlags.Ephemeral }) + } + else if (action === "disable") { + // Arrêter les timers actifs avant de désactiver + if (interaction.guildId) Freebox.Timer.clear(interaction.guildId) + + // Désactiver le timer + dbData.lcd.enabled = false + dbData.lcd.botId = "" + dbData.lcd.morningTime = "" + dbData.lcd.nightTime = "" + + guildProfile.set("guildFbx", dbData) + guildProfile.markModified("guildFbx") + await guildProfile.save().catch(console.error) + + return interaction.reply({ content: t(interaction.locale, "freebox.timer.disabled"), flags: MessageFlags.Ephemeral }) + } + } + } + } +} diff --git a/src/commands/global/index.ts b/src/commands/global/index.ts new file mode 100644 index 0000000..bc1fd0f --- /dev/null +++ b/src/commands/global/index.ts @@ -0,0 +1,17 @@ +import * as amp from "./amp" +import * as boost from "./boost" +import * as database from "./database" +import * as freebox from "./freebox" +import * as ping from "./ping" +import * as twitch from "./twitch" + +import type { Command } from "@/types" + +export default [ + amp, + boost, + database, + freebox, + ping, + twitch +] as Command[] diff --git a/src/commands/global/ping.ts b/src/commands/global/ping.ts index 554fab8..3e1e4ec 100755 --- a/src/commands/global/ping.ts +++ b/src/commands/global/ping.ts @@ -1,11 +1,17 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' - -export default { - data: new SlashCommandBuilder() - .setName('ping') - .setDescription('Check the latency of the bot'), - async execute(interaction: ChatInputCommandInteraction) { - let sent = await interaction.reply({ content: 'Pinging...', fetchReply: true }) - interaction.editReply(`Websocket heartbeat: ${interaction.client.ws.ping}ms.\nRoundtrip latency: ${sent.createdTimestamp - interaction.createdTimestamp}ms`) - } -} \ No newline at end of file +import { SlashCommandBuilder } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("ping") + .setDescription("Check the latency of the bot") + .setDescriptionLocalizations({ fr: "Vérifier la latence du bot" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + await interaction.reply({ content: t(interaction.locale, "ping.pinging") }) + const sent = await interaction.fetchReply() + return interaction.editReply(t(interaction.locale, "ping.response", { + heartbeat: interaction.client.ws.ping.toString(), + latency: (sent.createdTimestamp - interaction.createdTimestamp).toString() + })) +} diff --git a/src/commands/global/twitch.ts b/src/commands/global/twitch.ts new file mode 100644 index 0000000..fcbaf1d --- /dev/null +++ b/src/commands/global/twitch.ts @@ -0,0 +1,198 @@ +import { SlashCommandBuilder, ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js" +import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData } from "discord.js" +import chalk from "chalk" +import { twitchClient, listener, onlineSub, offlineSub, generateTwitchEmbed } from "@/utils/twitch" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("twitch") + .setDescription("Manage streamers notifications") + .setDescriptionLocalizations({ fr: "Gérer les notifications des streameurs" }) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand(subcommand => subcommand + .setName("status") + .setDescription("Display Twitch module status") + .setNameLocalizations({ fr: "statut" }) + .setDescriptionLocalizations({ fr: "Afficher le statut du module Twitch" }) + ) + .addSubcommand(subcommand => subcommand + .setName("channel") + .setDescription("Set the channel to send notifications") + .setNameLocalizations({ fr: "canal" }) + .setDescriptionLocalizations({ fr: "Définir le canal pour envoyer les notifications" }) + .addChannelOption(option => option + .setName("channel") + .setDescription("The channel to send notifications") + .setNameLocalizations({ fr: "canal" }) + .setDescriptionLocalizations({ fr: "Le canal pour envoyer les notifications" }) + .setRequired(true) + ) + ) + .addSubcommandGroup(subcommandgroup => subcommandgroup + .setName("streamer") + .setDescription("Manage streamers") + .setNameLocalizations({ fr: "streameur" }) + .setDescriptionLocalizations({ fr: "Gérer les streameurs" }) + .addSubcommand(subcommand => subcommand + .setName("list") + .setDescription("List all streamers") + .setNameLocalizations({ fr: "liste" }) + .setDescriptionLocalizations({ fr: "Lister tous les streameurs" }) + ) + .addSubcommand(subcommand => subcommand + .setName("add") + .setDescription("Add a streamer") + .setNameLocalizations({ fr: "ajouter" }) + .setDescriptionLocalizations({ fr: "Ajouter un streameur" }) + .addStringOption(option => option + .setName("username") + .setDescription("The username of the streamer to add") + .setNameLocalizations({ fr: "nom_utilisateur" }) + .setDescriptionLocalizations({ fr: "Le nom d'utilisateur du streameur à ajouter" }) + .setRequired(true) + .setAutocomplete(true) + ) + .addUserOption(option => option + .setName("member") + .setDescription("The member on the guild to mention") + .setNameLocalizations({ fr: "membre" }) + .setDescriptionLocalizations({ fr: "Le membre sur le serveur à mentionner" }) + .setRequired(false) + ) + ) + .addSubcommand(subcommand => subcommand + .setName("remove") + .setDescription("Remove a streamer") + .setNameLocalizations({ fr: "supprimer" }) + .setDescriptionLocalizations({ fr: "Supprimer un streameur" }) + .addStringOption(option => option + .setName("username") + .setDescription("The username of the streamer to remove") + .setNameLocalizations({ fr: "nom_utilisateur" }) + .setDescriptionLocalizations({ fr: "Le nom d'utilisateur du streameur à supprimer" }) + .setRequired(true) + .setAutocomplete(true) + ) + ) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + + const subcommandGroup = interaction.options.getSubcommandGroup(false) + const subcommand = interaction.options.getSubcommand(true) + if (subcommand == "status") { + // Utiliser la fonction utilitaire pour générer l'embed et les composants + const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + return interaction.reply({ embeds: [embed], components: components, flags: MessageFlags.Ephemeral }) + } + + if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled_activate"), flags: MessageFlags.Ephemeral }) + + if (subcommand == "channel") { + const channel = interaction.options.getChannel("channel", true) + if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) + return interaction.reply({ content: t(interaction.locale, "common.invalid_text_channel"), flags: MessageFlags.Ephemeral }) + + dbData.channelId = channel.id + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + return interaction.reply({ content: t(interaction.locale, "twitch.notifications_channel_set", { channel: channel.name ?? channel.id }), flags: MessageFlags.Ephemeral }) + } + else if (subcommandGroup == "streamer") { + if (!dbData.channelId) return interaction.reply({ content: t(interaction.locale, "twitch.configure_channel_first"), flags: MessageFlags.Ephemeral }) + + if (subcommand == "list") { + if (!dbData.streamers.length) return interaction.reply({ content: t(interaction.locale, "twitch.no_streamers_list"), flags: MessageFlags.Ephemeral }) + + const streamers = [] as string[] + await Promise.all(dbData.streamers.map(async streamer => { + try { + const user = await twitchClient.users.getUserById(streamer.twitchUserId) + if (user) streamers.push(`- ${user.displayName} (${streamer.twitchUserId})`) + else streamers.push(`- ${t(interaction.locale, "twitch.user_not_found_id", { id: streamer.twitchUserId })}`) + } catch (error) { + console.log(chalk.magenta(`[Twitch] Error fetching user for ID ${streamer.twitchUserId}`)) + console.error(error) + } + })) + const streamerList = streamers.length > 0 ? streamers.join("\n") : t(interaction.locale, "twitch.no_streamers") + + return interaction.reply({ content: `${t(interaction.locale, "twitch.list.title")}:\n${streamerList}`, flags: MessageFlags.Ephemeral }) + } + else if (subcommand == "add") { + const username = interaction.options.getString("username", true) + const member = interaction.options.getUser("member", false) + + const user = await twitchClient.users.getUserByName(username) + if (!user) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_found", { username }), flags: MessageFlags.Ephemeral }) + + if (dbData.streamers.some(s => s.twitchUserId === user.id)) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_already_added", { username }), flags: MessageFlags.Ephemeral }) + dbData.streamers.push({ twitchUserId: user.id, discordUserId: member?.id ?? "", messageId: "" }) + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(user.id) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) listener.onStreamOnline(user.id, onlineSub) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.offline")) listener.onStreamOffline(user.id, offlineSub) + + return interaction.reply({ content: t(interaction.locale, "twitch.streamer_added", { username, id: user.id }), flags: MessageFlags.Ephemeral }) + } + else if (subcommand == "remove") { + const username = interaction.options.getString("username", true) + + const user = await twitchClient.users.getUserByName(username) + if (!user) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_found", { username }), flags: MessageFlags.Ephemeral }) + + const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === user.id) + if (streamerIndex === -1)return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_in_list", { username }), flags: MessageFlags.Ephemeral }) + + dbData.streamers.splice(streamerIndex, 1) + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + if (!await dbGuild.exists({ "guildTwitch.streamers.twitchUserId": user.id })) { + const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(user.id) + await Promise.all(userSubs.data.map(async sub => { if (sub.transportMethod === "webhook" && (sub.type === "stream.online" || sub.type === "stream.offline")) await sub.unsubscribe() })) + console.log(chalk.magenta(`[Twitch] Listener removed for ${user.displayName} (ID ${user.id})`)) + } + + return interaction.reply({ content: t(interaction.locale, "twitch.streamer_removed", { username }), flags: MessageFlags.Ephemeral }) + } + } +} + +export async function autocompleteRun(interaction: AutocompleteInteraction) { + const query = interaction.options.getString("username", true) + if (!query || query.length < 3) return interaction.respond([]) + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.respond([]) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbData.enabled) return interaction.respond([]) + + const choices: ApplicationCommandOptionChoiceData[] = [] + const searchResult = await twitchClient.search.searchChannels(query) + if (searchResult.data.length === 0) return interaction.respond([]) + + searchResult.data.forEach(streamerResult => { + if (dbData.streamers.some(s => s.twitchUserId === streamerResult.id)) return + choices.push({ name: streamerResult.displayName, value: streamerResult.name }) + }) + + return interaction.respond(choices) +} diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..057ff53 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,26 @@ +import global from "./global" +import player from "./player" +import salonpostam from "./salonpostam" + +import { Command, CommandFolder } from "@/types" + +export const commandFolders = [ + { + name: "global", + commands: global + }, + { + name: "player", + commands: player + }, + { + name: "salonpostam", + commands: salonpostam + } +] as CommandFolder[] + +export default [ + ...global, + ...player, + ...salonpostam +] as Command[] diff --git a/src/commands/player/disco.ts b/src/commands/player/disco.ts new file mode 100644 index 0000000..0c47944 --- /dev/null +++ b/src/commands/player/disco.ts @@ -0,0 +1,60 @@ +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ChannelType } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { generateDiscoEmbed } from "@/utils/player" +import type { Disco } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("disco") + .setDescription("Manage the Disco module") + .setDescriptionLocalizations({ fr: "Gérer le module Disco" }) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand(subcommand => subcommand + .setName("status") + .setDescription("Display Disco mode status") + .setNameLocalizations({ fr: "statut" }) + .setDescriptionLocalizations({ fr: "Afficher le statut du mode Disco" }) + ) + .addSubcommand(subcommand => subcommand + .setName("channel") + .setDescription("Configure the channel for Disco effects") + .setNameLocalizations({ fr: "canal" }) + .setDescriptionLocalizations({ fr: "Configurer le canal pour les effets Disco" }) + .addChannelOption(option => option + .setName("channel") + .setDescription("The channel where to apply Disco effects") + .setNameLocalizations({ fr: "canal" }) + .setDescriptionLocalizations({ fr: "Le canal où appliquer les effets Disco" }) + .setRequired(true) + ) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildPlayer.disco") as Disco + + const subcommand = interaction.options.getSubcommand(true) + if (subcommand === "status") { + const { embed, components } = generateDiscoEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + return interaction.reply({ embeds: [embed], components: components, flags: MessageFlags.Ephemeral }) + } + else if (subcommand === "channel") { + const channel = interaction.options.getChannel("channel", true) + if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) return interaction.reply({ + content: t(interaction.locale, "common.invalid_channel_type"), + flags: MessageFlags.Ephemeral + }) + + dbData.channelId = channel.id + + guildProfile.set("guildPlayer.disco", dbData) + guildProfile.markModified("guildPlayer.disco") + await guildProfile.save().catch(console.error) + + return interaction.reply({ content: t(interaction.locale, "player.disco.channel_configured_success", { channel: channel.name ?? "Inconnu" }), flags: MessageFlags.Ephemeral }) + } +} diff --git a/src/commands/player/index.ts b/src/commands/player/index.ts new file mode 100644 index 0000000..17052ba --- /dev/null +++ b/src/commands/player/index.ts @@ -0,0 +1,31 @@ +import * as disco from "./disco" +import * as loop from "./loop" +import * as lyrics from "./lyrics" +import * as panel from "./panel" +import * as pause from "./pause" +import * as play from "./play" +import * as previous from "./previous" +import * as queue from "./queue" +import * as resume from "./resume" +import * as shuffle from "./shuffle" +import * as skip from "./skip" +import * as stop from "./stop" +import * as volume from "./volume" + +import type { Command } from "@/types" + +export default [ + disco, + loop, + lyrics, + panel, + pause, + play, + previous, + queue, + resume, + shuffle, + skip, + stop, + volume +] as Command[] diff --git a/src/commands/player/loop.ts b/src/commands/player/loop.ts index 5186f53..b9ef472 100755 --- a/src/commands/player/loop.ts +++ b/src/commands/player/loop.ts @@ -1,21 +1,29 @@ -import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js' -import { useQueue } from'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('loop') - .setDescription('Boucler la musique en cours de lecture.') - .addIntegerOption(option => option.setName('loop') - .setDescription('Mode de boucle (0 = Off, 1 = Titre, 2 = File d\'Attente; 3 = Autoplay)') - .setRequired(true) - .setMinValue(0) - .setMaxValue(3)), - async execute(interaction: ChatInputCommandInteraction) { - let loop = interaction.options.getInteger('loop') - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - queue.setRepeatMode(loop as number) - return await interaction.reply(`Boucle ${loop === 0 ? 'désactivée' : loop === 1 ? 'en mode Titre' : loop === 2 ? 'en mode File d\'Attente' : 'en autoplay'}.`) - } -} \ No newline at end of file +import { SlashCommandBuilder } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import type { QueueRepeatMode } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("loop") + .setDescription("Loop the current music") + .setNameLocalizations({ fr: "boucle" }) + .setDescriptionLocalizations({ fr: "Boucler la musique en cours de lecture" }) + .addIntegerOption(option => option + .setName("mode") + .setDescription("Loop mode (0 = Off | 1 = Track | 2 = Queue | 3 = Autoplay)") + .setDescriptionLocalizations({ fr: "Mode de boucle (0 = Arrêt | 1 = Titre | 2 = File d'Attente | 3 = Autoplay)" }) + .setRequired(true) + .setMinValue(0) + .setMaxValue(3) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const mode = interaction.options.getInteger("mode", true) + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue") }) + + queue.setRepeatMode(mode as QueueRepeatMode) + + return interaction.reply(t(interaction.locale, mode === 0 ? "player.loop_off" : mode === 1 ? "player.loop_track" : mode === 2 ? "player.loop_queue" : "player.loop_autoplay")) +} diff --git a/src/commands/player/lyrics.ts b/src/commands/player/lyrics.ts index 948c966..513cbae 100755 --- a/src/commands/player/lyrics.ts +++ b/src/commands/player/lyrics.ts @@ -1,45 +1,61 @@ -import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js' -import { useQueue } from 'discord-player' -import { lyricsExtractor } from '@discord-player/extractor' - -export default { - data: new SlashCommandBuilder() - .setName('lyrics') - .setDescription('Rechercher les paroles d\'une musique.') - .addStringOption(option => option.setName('recherche').setDescription('Chercher une musique spécifique')), - async execute(interaction: ChatInputCommandInteraction) { - await interaction.deferReply() - - let query = interaction.options.getString('recherche', false) - if (!query) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - let track = queue.currentTrack - if (!track) return interaction.followUp({ content: 'Aucune musique en cours, recherche en une plutôt !' }) - - if (track.raw.source === 'spotify') query = `${track.author} ${track.title}` - else query = track.title - } - - let lyricsFinder = lyricsExtractor() - - let lyrics = await lyricsFinder.search(query).catch(() => null) - if (!lyrics) return interaction.followUp({ content: 'Pas de paroles trouvées !' }) - - let trimmedLyrics = lyrics.lyrics.substring(0, 1997) - - let embed = new EmbedBuilder() - .setColor('#ffc370') - .setTitle(lyrics.title) - .setURL(lyrics.url) - .setThumbnail(lyrics.thumbnail) - .setAuthor({ - name: lyrics.artist.name, - iconURL: lyrics.artist.image, - url: lyrics.artist.url - }) - .setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics) - - return interaction.followUp({ embeds: [embed] }) - } -} \ No newline at end of file +import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue, useMainPlayer } from "discord-player" +import type { LrcSearchResult } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("lyrics") + .setDescription("Search for song lyrics") + .setNameLocalizations({ fr: "paroles" }) + .setDescriptionLocalizations({ fr: "Rechercher les paroles d'une musique" }) + .addStringOption(option => option + .setName("search") + .setDescription("Search for a specific song") + .setNameLocalizations({ fr: "recherche" }) + .setDescriptionLocalizations({ fr: "Chercher une musique spécifique" }) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + await interaction.deferReply() + + const player = useMainPlayer() + const embed = new EmbedBuilder().setColor("#ffff64").setFooter({ text: "Powered by Genius" }) + let lyrics = [] as LrcSearchResult[] + + const query = interaction.options.getString("search", false) + if (!query) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue"), flags: MessageFlags.Ephemeral }) + + const track = queue.currentTrack + if (!track) return interaction.followUp({ content: t(interaction.locale, "player.no_current_track"), flags: MessageFlags.Ephemeral }) + + lyrics = await player.lyrics.search({ trackName: track.title, artistName: track.author }) + + if (!lyrics.length) return interaction.followUp({ content: t(interaction.locale, "player.no_lyrics_found"), flags: MessageFlags.Ephemeral }) + const trimmedLyrics = lyrics[0].plainLyrics.substring(0, 1997) + + embed + .setTitle(track.title) + .setURL(track.url) + .setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics) + .setThumbnail(track.thumbnail) + .setAuthor({ name: track.author, url: `https://genius.com/search?q=${track.author.replace(/ /g, "-")}` }) + } + else { + lyrics = await player.lyrics.search({ q: query }) + + if (!lyrics.length) return interaction.followUp({ content: t(interaction.locale, "player.no_lyrics_found"), flags: MessageFlags.Ephemeral }) + const trimmedLyrics = lyrics[0].plainLyrics.substring(0, 1997) + + embed + .setTitle(lyrics[0].name) + .setURL(`https://genius.com/search?q=${query.replace(/ /g, "%20")}`) + .setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics) + .setThumbnail("https://play-lh.googleusercontent.com/e6-dZlTM-gJ2sFxFFs3X15O84HEv6jc9PQGgHtVTn7FP6lUXeEAkDl9v4RfVOwbSuQ") + .setAuthor({ name: lyrics[0].artistName, url: `https://genius.com/search?q=${lyrics[0].artistName.replace(/ /g, "-")}` }) + } + + return interaction.followUp({ embeds: [embed] }) +} diff --git a/src/commands/player/panel.ts b/src/commands/player/panel.ts index 992d7b5..eb243ab 100755 --- a/src/commands/player/panel.ts +++ b/src/commands/player/panel.ts @@ -1,25 +1,26 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { playerGenerate } from '../../utils/player' -import getUptime from '../../utils/getUptime' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('panel') - .setDescription('Générer les infos de la lecture en cours.'), - async execute(interaction: ChatInputCommandInteraction) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - let guild = interaction.guild - if (!guild) return await interaction.reply({ content: 'Cette commande n\'est pas disponible en message privé.', ephemeral: true }) - - let client = guild.client - - let { embed, components } = await playerGenerate(guild) - if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)} \n ${embed.data.footer.text}` }) - else embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)}` }) - - return interaction.reply({ embeds: [embed] }) - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { generatePlayerEmbed } from "@/utils/player" +import uptime from "@/utils/uptime" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("panel") + .setDescription("Generate current playback info") + .setNameLocalizations({ fr: "panneau" }) + .setDescriptionLocalizations({ fr: "Générer les infos de la lecture en cours" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue"), flags: MessageFlags.Ephemeral }) + + const guild = interaction.guild + if (!guild) return interaction.reply({ content: t(interaction.locale, "common.private_message_not_available"), flags: MessageFlags.Ephemeral }) + + const { embed, components } = generatePlayerEmbed(guild, interaction.locale) + if (components && embed.data.footer) embed.setFooter({ text: `${t(interaction.locale, "player.uptime")}: ${uptime(guild.client.uptime)} \n ${embed.data.footer.text}` }) + else embed.setFooter({ text: `${t(interaction.locale, "player.uptime")}: ${uptime(guild.client.uptime)}` }) + + return interaction.reply({ embeds: [embed] }) +} diff --git a/src/commands/player/pause.ts b/src/commands/player/pause.ts index dc9a42a..5c28d0d 100755 --- a/src/commands/player/pause.ts +++ b/src/commands/player/pause.ts @@ -1,15 +1,17 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('pause') - .setDescription('Met en pause la musique.'), - async execute(interaction: ChatInputCommandInteraction) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - queue.node.setPaused(!queue.node.isPaused()) - return await interaction.reply('Musique mise en pause !') - } +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("pause") + .setDescription("Pause the music") + .setDescriptionLocalizations({ fr: "Met en pause la musique" }) + +export const execute = async (interaction: ChatInputCommandInteraction) => { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral }) + + queue.node.setPaused(!queue.node.isPaused()) + return interaction.reply(t(interaction.locale, "player.paused")) } \ No newline at end of file diff --git a/src/commands/player/play.ts b/src/commands/player/play.ts index 6b0ee0d..0ee67e3 100755 --- a/src/commands/player/play.ts +++ b/src/commands/player/play.ts @@ -1,96 +1,123 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from 'discord.js' -import { useMainPlayer, useQueue, QueryType } from 'discord-player' -import dbGuild from '../../schemas/guild' - -interface TrackSearchResult { name: string, value: string } - -export default { - data: new SlashCommandBuilder() - .setName('play') - .setDescription('Jouer une musique.') - .addStringOption(option => option.setName('recherche').setDescription('Titre de la musique à chercher').setRequired(true).setAutocomplete(true)), - async autocompleteRun(interaction: AutocompleteInteraction) { - let query = interaction.options.getString('recherche', true) - if (!query) return interaction.respond([]) - - let player = useMainPlayer() - - const resultsYouTube = await player.search(query, { searchEngine: QueryType.YOUTUBE }) - const resultsSpotify = await player.search(query, { searchEngine: QueryType.SPOTIFY_SEARCH }) - - const tracksYouTube = resultsYouTube.tracks.slice(0, 5).map((t) => ({ - name: `YouTube: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`, - value: t.url - })) - const tracksSpotify = resultsSpotify.tracks.slice(0, 5).map((t) => ({ - name: `Spotify: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`, - value: t.url - })) - - const tracks: TrackSearchResult[] = [] - tracksYouTube.forEach((t) => tracks.push({ name: t.name, value: t.value })) - tracksSpotify.forEach((t) => tracks.push({ name: t.name, value: t.value })) - - return interaction.respond(tracks) - }, - async execute(interaction: ChatInputCommandInteraction) { - let member = interaction.member as GuildMember - let voiceChannel = member.voice.channel - if (!voiceChannel) return await interaction.reply({ content: 'T\'es pas dans un vocal, idiot !', ephemeral: true }) - - let botChannel = interaction.guild?.members.me?.voice.channel - if (botChannel && voiceChannel.id !== botChannel.id) return await interaction.reply({ content: 'T\'es pas dans mon vocal !', ephemeral: true }) - - await interaction.deferReply() - - let query = interaction.options.getString('recherche', true) - let player = useMainPlayer() - let queue = useQueue(interaction.guild?.id ?? '') - - if (!queue) { - if (interaction.guild) queue = player.nodes.create(interaction.guild, { - metadata: { - channel: interaction.channel, - client: interaction.guild.members.me, - requestedBy: interaction.user - }, - selfDeaf: true, - volume: 20, - leaveOnEmpty: true, - leaveOnEmptyCooldown: 30000, - leaveOnEnd: true, - leaveOnEndCooldown: 300000 - }) - else return - } - try { if (!queue.connection) await queue.connect(voiceChannel) } - catch (error: unknown) { console.error(error) } - - - let guildProfile = await dbGuild.findOne({ guildId: queue.guild.id }) - if (!guildProfile) return console.log(`Database data for **${queue.guild.name}** does not exist !`) - - let dbData = guildProfile.get('guildPlayer.replay') - dbData['textChannelId'] = interaction.channel?.id - dbData['voiceChannelId'] = voiceChannel.id - - guildProfile.set('guildPlayer.replay', dbData) - await guildProfile.save().catch(console.error) - - - let result = await player.search(query, { requestedBy: interaction.user }) - if (!result.hasTracks()) return interaction.followUp(`Aucune musique trouvée pour **${query}** !`) - let track = result.tracks[0] - - let entry = queue.tasksQueue.acquire() - await entry.getTask() - queue.addTrack(track) - - try { - if (!queue.isPlaying()) await queue.node.play() - let track_source = track.source === 'youtube' ? 'Youtube' : track.source === 'spotify' ? 'Spotify' : 'Inconnu' - return interaction.followUp(`Chargement de la musique **${track.title}** de **${track.author}** sur **${track_source}**...`) - } catch (error: unknown) { console.error(error) } - finally { queue.tasksQueue.release() } - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from "discord.js" +import { useMainPlayer, useQueue } from "discord-player" +import { SpotifyExtractor } from "@discord-player/extractor" +import { YoutubeiExtractor } from "discord-player-youtubei" +import { startProgressSaving } from "@/utils/player" +import type { TrackSearchResult } from "@/types/player" +import type { GuildPlayer } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("play") + .setDescription("Play a song") + .setNameLocalizations({ fr: "jouer" }) + .setDescriptionLocalizations({ fr: "Jouer une musique" }) + .addStringOption(option => option + .setName("search") + .setDescription("Music title to search for") + .setNameLocalizations({ fr: "recherche" }) + .setDescriptionLocalizations({ fr: "Titre de la musique à chercher" }) + .setRequired(true) + .setAutocomplete(true) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const member = interaction.member as GuildMember + const voiceChannel = member.voice.channel + if (!voiceChannel) return interaction.reply({ content: t(interaction.locale, "player.not_in_voice"), flags: MessageFlags.Ephemeral }) + + const botChannel = interaction.guild?.members.me?.voice.channel + if (botChannel && voiceChannel.id !== botChannel.id) return interaction.reply({ content: t(interaction.locale, "player.not_in_same_voice"), flags: MessageFlags.Ephemeral }) + + await interaction.deferReply() + + const query = interaction.options.getString("search", true) + const player = useMainPlayer() + let queue = useQueue(interaction.guild?.id ?? "") + + if (!queue) { + if (interaction.guild) queue = player.nodes.create(interaction.guild, { + metadata: { + channel: interaction.channel, + client: interaction.guild.members.me, + requestedBy: interaction.user + }, + selfDeaf: true, + volume: 20, + leaveOnEmpty: true, + leaveOnEmptyCooldown: 30000, + leaveOnEnd: true, + leaveOnEndCooldown: 300000 + }) + else return + } + + try { if (!queue.connection) await queue.connect(voiceChannel) } + catch (error) { console.error(error) } + + const guildProfile = await dbGuild.findOne({ guildId: queue.guild.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const botId = interaction.client.user.id + const dbData = guildProfile.get("guildPlayer") as GuildPlayer + dbData.instances ??= [] + + const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId) + const instance = { botId, replay: { + textChannelId: interaction.channel?.id ?? "", + voiceChannelId: voiceChannel.id, + trackUrl: "", + progress: 0 + } } + + if (instanceIndex === -1) dbData.instances.push(instance) + else dbData.instances[instanceIndex] = instance + + guildProfile.set("guildPlayer", dbData) + guildProfile.markModified("guildPlayer") + await guildProfile.save().catch(console.error) + + const result = await player.search(query, { requestedBy: interaction.user }) + if (!result.hasTracks()) return interaction.followUp({ content: t(interaction.locale, "player.no_track_found", { query }), flags: MessageFlags.Ephemeral }) + const track = result.tracks[0] + + const entry = queue.tasksQueue.acquire() + await entry.getTask() + queue.addTrack(track) + + try { + if (!queue.isPlaying()) await queue.node.play() + startProgressSaving(queue.guild.id, botId) + const track_source = track.source === "spotify" ? t(interaction.locale, "player.sources.spotify") : track.source === "youtube" ? t(interaction.locale, "player.sources.youtube") : t(interaction.locale, "player.sources.unknown") + return await interaction.followUp(t(interaction.locale, "player.loading_track", { title: track.title, author: track.author, source: track_source })) + } + catch (error) { console.error(error) } + finally { queue.tasksQueue.release() } +} + +export async function autocompleteRun(interaction: AutocompleteInteraction) { + const query = interaction.options.getString("search", true) + if (!query) return interaction.respond([]) + + const player = useMainPlayer() + + const resultsSpotify = await player.search(query, { searchEngine: `ext:${SpotifyExtractor.identifier}` }) + const resultsYouTube = await player.search(query, { searchEngine: `ext:${YoutubeiExtractor.identifier}` }) + + const tracksSpotify = resultsSpotify.tracks.slice(0, 5).map(t => ({ + name: `Spotify: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`, + value: t.url + })) + const tracksYouTube = resultsYouTube.tracks.slice(0, 5).map(t => ({ + name: `YouTube: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`, + value: t.url + })) + + const tracks: TrackSearchResult[] = [] + tracksSpotify.forEach((t) => tracks.push({ name: t.name, value: t.value })) + tracksYouTube.forEach((t) => tracks.push({ name: t.name, value: t.value })) + + return interaction.respond(tracks) +} diff --git a/src/commands/player/previous.ts b/src/commands/player/previous.ts index 2c36bb8..7c90edd 100755 --- a/src/commands/player/previous.ts +++ b/src/commands/player/previous.ts @@ -1,15 +1,18 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useHistory } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('previous') - .setDescription('Joue la musique précédente.'), - async execute(interaction: ChatInputCommandInteraction) { - let history = useHistory(interaction.guild?.id ?? '') - if (!history) return await interaction.reply('Il n\'y a pas d\'historique de musique !') - - await history.previous() - return await interaction.reply('Musique précédente jouée !') - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useHistory } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("previous") + .setDescription("Play the previous song") + .setNameLocalizations({ fr: "precedent" }) + .setDescriptionLocalizations({ fr: "Joue la musique précédente" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + const history = useHistory(interaction.guild?.id ?? "") + if (!history) return interaction.reply({ content: t(interaction.locale, "player.no_session"), flags: MessageFlags.Ephemeral }) + + await history.previous() + return interaction.reply({ content: t(interaction.locale, "player.previous_played"), flags: MessageFlags.Ephemeral }) +} diff --git a/src/commands/player/queue.ts b/src/commands/player/queue.ts index 8890f9c..cef282b 100755 --- a/src/commands/player/queue.ts +++ b/src/commands/player/queue.ts @@ -1,19 +1,22 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('queue') - .setDescription("Récupérer la file d'attente."), - async execute(interaction: ChatInputCommandInteraction) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.reply({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - if (!queue.currentTrack) return interaction.reply({ content: 'Aucune musique en cours de lecture.' }) - - let track = `[${queue.currentTrack.title}](${queue.currentTrack.url})` - let tracks = queue.tracks.map((track, index) => { return `${index + 1}. [${track.title}](${track.url})` }) - if (tracks.length === 0) return interaction.reply({ content: `Lecture en cours : ${track} \nAucune musique dans la file d'attente.` }) - - return interaction.reply({ content: `Lecture en cours : ${track} \nFile d'attente actuelle : \n${tracks.join('\n')}` }) - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("queue") + .setDescription("Get the queue") + .setNameLocalizations({ fr: "file" }) + .setDescriptionLocalizations({ fr: "Récupérer la file d'attente." }) + +export async function execute(interaction: ChatInputCommandInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.reply({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral}) + if (!queue.currentTrack) return interaction.reply({ content: t(interaction.locale, "player.no_track_playing"), flags: MessageFlags.Ephemeral}) + + const track = `[${queue.currentTrack.title}](${queue.currentTrack.url})` + const tracks = queue.tracks.map((track, index) => { return `${index + 1}. [${track.title}](${track.url})` }) + if (tracks.length === 0) return interaction.reply({ content: t(interaction.locale, "player.now_playing_no_queue", { track }) }) + + return interaction.reply({ content: t(interaction.locale, "player.now_playing_with_queue", { track, tracks: tracks.join("\n") }) }) +} diff --git a/src/commands/player/resume.ts b/src/commands/player/resume.ts index 50d6349..195ca5b 100755 --- a/src/commands/player/resume.ts +++ b/src/commands/player/resume.ts @@ -1,15 +1,18 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('resume') - .setDescription('Reprendre la musique.'), - async execute(interaction: ChatInputCommandInteraction) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - queue.node.setPaused(!queue.node.isPaused()) - return await interaction.reply('Musique reprise !') - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("resume") + .setDescription("Resume the music") + .setNameLocalizations({ fr: "reprendre" }) + .setDescriptionLocalizations({ fr: "Reprendre la musique" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral}) + + queue.node.setPaused(!queue.node.isPaused()) + return interaction.reply(t(interaction.locale, "player.resumed")) +} diff --git a/src/commands/player/shuffle.ts b/src/commands/player/shuffle.ts index d91f3ab..bb23dbd 100755 --- a/src/commands/player/shuffle.ts +++ b/src/commands/player/shuffle.ts @@ -1,15 +1,18 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('shuffle') - .setDescription('Mélange la file d\'attente.'), - async execute(interaction: ChatInputCommandInteraction) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - queue.tracks.shuffle() - return await interaction.reply('File d\'attente mélangée !') - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("shuffle") + .setDescription("Shuffle the queue") + .setNameLocalizations({ fr: "melanger" }) + .setDescriptionLocalizations({ fr: "Mélange la file d'attente" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral}) + + queue.tracks.shuffle() + return interaction.reply(t(interaction.locale, "player.shuffled")) +} diff --git a/src/commands/player/skip.ts b/src/commands/player/skip.ts index a3fbe55..11f69c1 100755 --- a/src/commands/player/skip.ts +++ b/src/commands/player/skip.ts @@ -1,15 +1,18 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('skip') - .setDescription('Passer la musique en cours.'), - async execute(interaction: ChatInputCommandInteraction) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - queue.node.skip() - return await interaction.reply('Musique passée !') - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("skip") + .setDescription("Skip the current song") + .setNameLocalizations({ fr: "passer" }) + .setDescriptionLocalizations({ fr: "Passer la musique en cours" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral}) + + queue.node.skip() + return interaction.reply(t(interaction.locale, "player.skipped")) +} diff --git a/src/commands/player/stop.ts b/src/commands/player/stop.ts index af0cc6e..b115ae8 100755 --- a/src/commands/player/stop.ts +++ b/src/commands/player/stop.ts @@ -1,15 +1,21 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('stop') - .setDescription('Arrêter la musique.'), - async execute(interaction: ChatInputCommandInteraction) { - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - queue.delete() - return await interaction.reply('Musique arrêtée !') - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { stopProgressSaving } from "@/utils/player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("stop") + .setDescription("Stop the music") + .setNameLocalizations({ fr: "arreter" }) + .setDescriptionLocalizations({ fr: "Arrêter la musique" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + await stopProgressSaving(interaction.guild?.id ?? "", interaction.client.user.id) + + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral}) + + queue.delete() + return interaction.reply(t(interaction.locale, "player.stopped")) +} diff --git a/src/commands/player/volume.ts b/src/commands/player/volume.ts index e9ecd80..39ea6da 100755 --- a/src/commands/player/volume.ts +++ b/src/commands/player/volume.ts @@ -1,21 +1,26 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' -import { useQueue } from 'discord-player' - -export default { - data: new SlashCommandBuilder() - .setName('volume') - .setDescription('Modifie le volume de la musique.') - .addIntegerOption(option => option.setName('volume') - .setDescription('Le volume à mettre (%)') - .setRequired(true) - .setMinValue(1) - .setMaxValue(100)), - async execute(interaction: ChatInputCommandInteraction) { - let volume = interaction.options.getInteger('volume') - let queue = useQueue(interaction.guild?.id ?? '') - if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) - - queue.node.setVolume(volume as number) - return await interaction.reply(`Volume modifié à ${volume}% !`) - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { useQueue } from "discord-player" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("volume") + .setDescription("Change the music volume") + .setDescriptionLocalizations({ fr: "Modifie le volume de la musique" }) + .addIntegerOption(option => option + .setName("volume") + .setDescription("The volume to set (%)") + .setDescriptionLocalizations({ fr: "Le volume à mettre (%)" }) + .setRequired(true) + .setMinValue(0) + .setMaxValue(100) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const volume = interaction.options.getInteger("volume", true) + const queue = useQueue(interaction.guild?.id ?? "") + if (!queue) return interaction.followUp({ content: t(interaction.locale, "player.no_queue_search_instead"), flags: MessageFlags.Ephemeral}) + + queue.node.setVolume(volume) + return interaction.reply(t(interaction.locale, "player.volume_changed", { volume: volume.toString() })) +} diff --git a/src/commands/salonpostam/crack.ts b/src/commands/salonpostam/crack.ts index 201c501..aebed92 100755 --- a/src/commands/salonpostam/crack.ts +++ b/src/commands/salonpostam/crack.ts @@ -1,58 +1,70 @@ -import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, MessageReaction, User }from 'discord.js' -import * as crack from '../../utils/crack' - -export default { - data: new SlashCommandBuilder().setName('crack').setDescription('Télécharge un crack sur le site online-fix.me !') - .addStringOption(option => option.setName('jeu').setDescription('Quel jeu tu veux DL ?').setRequired(true)), - async execute(interaction: ChatInputCommandInteraction) { - await interaction.deferReply() - - let query = interaction.options.getString('jeu') - if (!query) return - - let games = await crack.search(query) as crack.Game[] - if (!Array.isArray(games)) { - //if (games.toString() == "TypeError: Cannot read properties of undefined (reading 'split')") return interaction.followUp({ content: `J'ai rien trouvé pour "${query}" !` }) - //else return interaction.followUp({ content: "Une erreur s'est produite ! ```" + games + "```" }) - return interaction.followUp({ content: `J'ai rien trouvé pour "${query}" !` }) - } - - let game = {} as crack.Game - if (games.length > 1) { - games = games.slice(0, 9) - let list = '' - for (let i = 0; i < games.length; i++) list += `\n${i + 1}. ${games[i].name} (${games[i].link})` - let message = await interaction.followUp({ content: `J'ai trouvé plusieurs jeux pour "${query}" ! ${list}` }) - - let emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'] - for (let i = 0; i < games.length; i++) await message.react(emojis[i]) - - // Wait for a reaction to be added by the interaction author. - const filter = (reaction: MessageReaction, user: User) => { if (reaction.emoji.name) { return emojis.includes(reaction.emoji.name) && user.id === interaction.user.id } return false } - await message.awaitReactions({ filter, max: 1, time: 5000, errors: ['time'] }).then(collected => { - console.log(collected) - if (!collected.first) return - let reaction = collected.first() - let index = emojis.indexOf(reaction?.emoji.name ?? '') - game = games[index] - }).catch(() => { return interaction.followUp({ content: "T'as mis trop de temps à choisir !" }) }) - } - else game = games[0] - - let url = await crack.repo(game) - if (!url) return - let file = await crack.torrent(url) - if (!file) return - let filePath = await crack.download(url, file) - if (!filePath) return - let link = await crack.magnet(filePath) - - let embed = new EmbedBuilder() - .setColor('#ffc370') - .setTitle(game.name) - .setURL(game.link) - .setDescription(`Voici ce que j'ai trouvé pour "${query}".\nTu peux aussi cliquer sur [ce lien](https://angels-dev.fr/magnet/${link}) pour pouvoir télécharger le jeu direct !`) - - await interaction.followUp({ embeds: [embed], files: [filePath] }) - } -} \ No newline at end of file +import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction, MessageReaction, User } from "discord.js" +import { search, repo, torrent, download, magnet } from "@/utils/crack" +import type { CrackGame } from "@/types" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("crack") + .setDescription("Download a crack from online-fix.me") + .setDescriptionLocalizations({ fr: "Télécharge un crack sur online-fix.me" }) + .addStringOption(option => option + .setName("game") + .setDescription("What game do you want to download?") + .setNameLocalizations({ fr: "jeu" }) + .setDescriptionLocalizations({ fr: "Quel jeu tu veux télécharger ?" }) + .setRequired(true) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + await interaction.deferReply() + + const query = interaction.options.getString("game", true) + let games = await search(query) + if (!Array.isArray(games)) return interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.no_games_found", { query }), flags: MessageFlags.Ephemeral }) + + let game = {} as CrackGame + if (games.length > 1) { + games = games.slice(0, 9) + + let list = "" + for (let i = 0; i < games.length; i++) list += `\n${i + 1}. ${games[i].name} (${games[i].link})` + const message = await interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.multiple_games_found", { query, list }) }) + + const emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"] + for (let i = 0; i < games.length; i++) await message.react(emojis[i]) + + // Wait for a reaction to be added by the interaction author. + const filter = (reaction: MessageReaction, user: User) => { + if (reaction.emoji.name) return (emojis.includes(reaction.emoji.name) && user.id === interaction.user.id) + return false + } + await message.awaitReactions({ filter, max: 1, time: 5000, errors: ["time"] }).then(collected => { + console.log(collected) + const reaction = collected.first() + const index = emojis.indexOf(reaction?.emoji.name ?? "") + + if (!games) return + game = games[index] + }) + .catch(() => { return interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.selection_timeout"), flags: MessageFlags.Ephemeral }) }) + } else game = games[0] + + const url = await repo(game) + if (!url) return + + const file = await torrent(url) + if (!file) return + + const filePath = await download(url, file) + if (!filePath) return + + const link = magnet(filePath) + const embed = new EmbedBuilder() + .setColor("#ffc370") + .setTitle(game.name) + .setURL(game.link) + .setDescription(t(interaction.locale, "salonpostam.crack.game_found", { query, link: `https://angels-dev.fr/magnet/${link}` })) + + await interaction.followUp({ embeds: [embed], files: [filePath] }) +} diff --git a/src/commands/salonpostam/freebox.ts b/src/commands/salonpostam/freebox.ts deleted file mode 100644 index 44297ba..0000000 --- a/src/commands/salonpostam/freebox.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, Message, inlineCode } from 'discord.js' -import * as Freebox from '../../utils/freebox' -import dbGuild from '../../schemas/guild' -import crypto from 'crypto' -import https from 'https' -//import path from 'path' -//import fs from 'fs' - -interface ReturnMsgData { - status: string - error_code?: string - Title?: string - Message?: string -} - -function returnMsg(result: ReturnMsgData) { - if (result.status === 'fail') return `La commande a échouée !\n${inlineCode(`${result.Title}: ${result.Message}`)}` - if (result.status === 'error') return `Y'a eu une erreur !\n${inlineCode(`${result.error_code}`)}` -} - -export default { - data: new SlashCommandBuilder().setName('freebox').setDescription("Accéder à l'API FreeboxOS !") - .addSubcommand(subcommand => subcommand.setName('import').setDescription("Envoyer un fichier d'autorité de certification.")) - .addSubcommand(subcommand => subcommand.setName('version').setDescription("Afficher la version de l'API.")) - .addSubcommand(subcommand => subcommand.setName('init').setDescription("Créer une app sur la Freebox pour s'authentifier.")) - .addSubcommandGroup(subcommandGroup => subcommandGroup.setName('get').setDescription('Récupérer des données.') - .addSubcommand(subcommand => subcommand.setName('connection').setDescription('Récupérer les informations de connexion.')) - ), - - async execute(interaction: ChatInputCommandInteraction) { - let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id }) - if (!guildProfile) return interaction.reply({ content: `Database data for **${interaction.guild?.name}** does not exist, please initialize with \`/database init\` !` }) - - let dbData = guildProfile.get('guildFbx') - if (!dbData?.enabled) return interaction.reply({ content: `Freebox module is disabled for **${interaction.guild?.name}**, please activate with \`/database edit guildFbx.enabled True\` !` }) - - let host = dbData.host as string - if (!host) return interaction.reply({ content: `Freebox host is not set for **${interaction.guild?.name}**, please set with \`/database edit guildFbx.host \` !` }) - let version = dbData.version as number - if (!version) return interaction.reply({ content: `Freebox API version is not set for **${interaction.guild?.name}**, please set with \`/database edit guildFbx.version \` !` }) - - let httpsOptions = {} - //let caCrt = fs.readFileSync(path.resolve(__dirname, '../../static/freebox-ecc-root-ca.crt')) - // MIME Type : application/x-x509-ca-cert - //if (caCrt) httpsOptions = { ca: caCrt } - let httpsAgent = new https.Agent(httpsOptions) - - - if (interaction.options.getSubcommand() === 'import') { - let filter = (m: Message) => m.author.id === interaction.user.id - - await interaction.reply({ content: 'Please send another message with the CA file attached, you have one minute.', fetchReply: true }).then(async () => { - console.log('waiting for message') - await interaction.channel?.awaitMessages({ filter, time: 60_000, errors: ['time'] }).then(async collected => { - console.log(collected) - let message = collected.first() - if (!message?.attachments.size) return interaction.followUp('No file was sent in your message!') - - let attachment = message.attachments.first() - console.log(attachment) - - // Save the file to the database // TODO - - interaction.followUp(`File saved, you can now interact with your Freebox!`) - }).catch(() => interaction.followUp('No message was sent before the time limit!')) - }) - } - else if (interaction.options.getSubcommand() === 'version') { - let result = await Freebox.Core.Version(host, httpsAgent) - if (result.status === 'success') { - let embed = new EmbedBuilder() - embed.setTitle('FreeboxOS API Version') - embed.setDescription(`Version: ${result.data.api_version}`) - return await interaction.reply({ embeds: [embed] }) - } - else if (result.status === 'fail') return await interaction.reply({ content: `Failed to retrieve the API version: ${result.data}`, ephemeral: true }) - else if (result.status === 'error') return await interaction.reply({ content: `An error occurred while retrieving the API version: ${result.data}`, ephemeral: true }) - } - else if (interaction.options.getSubcommand() === 'init') { - await interaction.deferReply({ ephemeral: true }) - - let app = { - app_id: 'fr.angels.bot_tamiseur', - app_name: 'Bot Tamiseur', - app_version: '2.3.0', - device_name: 'Bot Discord NodeJS' - } - let result = await Freebox.Core.Init(host, version, httpsAgent, app, '') - if (result.status === 'success') { - let appToken = result.data.app_token - let trackId = result.data.track_id - - let initCheck = setInterval(async () => { - let result = await Freebox.Core.Init(host, version, httpsAgent, app, trackId) - if (result.status !== 'success') return await interaction.followUp(returnMsg(result) as string) - - let status = result.data.status - if (status === 'granted') { - clearInterval(initCheck) - let password_salt = result.data.password_salt - - if (!dbData) return - dbData['appToken'] = appToken - dbData['password_salt'] = password_salt - - if (!guildProfile) return - guildProfile.set('guildFbx', dbData) - guildProfile.markModified('guildFbx') - await guildProfile.save().catch(console.error) - - return await interaction.followUp('Done !') - } - else if (status === 'denied') { - clearInterval(initCheck) - return await interaction.followUp('The user denied the app access to the Freebox.') - } - else if (status === 'pending') return - }, 2000) - } else return await interaction.followUp({ content: returnMsg(result) as string, ephemeral: true }) - } - else if (interaction.options.getSubcommandGroup() === 'get') { - let appToken = dbData.appToken as string - if (!appToken) return await interaction.reply({ content: `Freebox appToken is not set for **${interaction.guild?.name}**, please init the app with \`/freebox init\` !` }) - console.log(appToken) - - let challengeData = await Freebox.Login.Challenge(host, version, httpsAgent) - if (!challengeData) return await interaction.reply({ content: `Failed to retrieve the challenge for **${interaction.guild?.name}** !` }) - let challenge = challengeData.data.challenge - console.log(challenge) - - let password = crypto.createHmac('sha1', appToken).update(challenge).digest('hex') - console.log(password) - - let session = await Freebox.Login.Session(host, version, httpsAgent, 'fr.angels.bot_tamiseur', password) - if (!session) return await interaction.reply({ content: `Failed to retrieve the session for **${interaction.guild?.name}** !` }) - - let sessionToken = dbData['sessionToken'] = session.data.session_token - - guildProfile.set('guildFbx', dbData) - guildProfile.markModified('guildFbx') - await guildProfile.save().catch(console.error) - - if (interaction.options.getSubcommand() === 'connection') { - let connection = await Freebox.Get.Connection(host, version, httpsAgent, sessionToken) - if (!connection) return await interaction.reply({ content: `Failed to retrieve the connection details for **${interaction.guild?.name}** !` }) - - return await interaction.reply({ content: `Connection details for **${interaction.guild?.name}**:\n${inlineCode(JSON.stringify(connection))}` }) - } - } - } -} \ No newline at end of file diff --git a/src/commands/salonpostam/index.ts b/src/commands/salonpostam/index.ts new file mode 100644 index 0000000..3dd5f1e --- /dev/null +++ b/src/commands/salonpostam/index.ts @@ -0,0 +1,15 @@ +import * as crack from "./crack" +import * as papa from "./papa" +import * as parle from "./parle" +import * as spam from "./spam" +import * as update from "./update" + +import type { Command } from "@/types" + +export default [ + crack, + papa, + parle, + spam, + update +] as Command[] diff --git a/src/commands/salonpostam/papa.ts b/src/commands/salonpostam/papa.ts index 849744d..2ebc1e5 100755 --- a/src/commands/salonpostam/papa.ts +++ b/src/commands/salonpostam/papa.ts @@ -1,34 +1,34 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js' -import { getVoiceConnection, joinVoiceChannel } from '@discordjs/voice' - -export default { - data: new SlashCommandBuilder() - .setName('papa') - .setDescription('Si papa m\'appelle, je le rejoins !'), - async execute(interaction: ChatInputCommandInteraction) { - if (interaction.user.id !== '223831938346123275') return interaction.reply({ content: 'T\'es pas mon père, dégage !' }) - - let guild = interaction.guild - if (!guild) return interaction.reply({ content: 'Je ne peux pas rejoindre ton vocal en message privé, papa !' }) - - let member = interaction.member as GuildMember - - let botChannel = guild.members.me?.voice.channel - let papaChannel = member.voice.channel - - if (!papaChannel && botChannel) { - const voiceConnection = getVoiceConnection(guild.id); - if (voiceConnection) voiceConnection.destroy() - return interaction.reply({ content: 'Je quitte le vocal, papa !' }) - } - else if (papaChannel && (!botChannel || botChannel.id !== papaChannel.id)) { - joinVoiceChannel({ - channelId: papaChannel.id, - guildId: papaChannel.guild.id, - adapterCreator: papaChannel.guild.voiceAdapterCreator, - }) - return interaction.reply({ content: 'Je rejoins ton vocal, papa !' }) - } - else return interaction.reply({ content: 'Je suis déjà dans ton vocal, papa !' }) - } -} \ No newline at end of file +import { SlashCommandBuilder } from "discord.js" +import type { ChatInputCommandInteraction, GuildMember } from "discord.js" +import { getVoiceConnection, joinVoiceChannel } from "@discordjs/voice" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("papa") + .setDescription("If daddy calls me, I join him") + .setDescriptionLocalizations({ fr: "Si papa m'appelle, je le rejoins" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + if (interaction.user.id !== "223831938346123275") return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.not_your_father") }) + + const guild = interaction.guild + if (!guild) return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.no_dm") }) + + const member = interaction.member as GuildMember + + const botChannel = guild.members.me?.voice.channel + const papaChannel = member.voice.channel + + if (!papaChannel && botChannel) { + const voiceConnection = getVoiceConnection(guild.id) + if (voiceConnection) voiceConnection.destroy() + return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.leaving_voice") }) + } else if (papaChannel && (!botChannel || botChannel.id !== papaChannel.id)) { + joinVoiceChannel({ + channelId: papaChannel.id, + guildId: papaChannel.guild.id, + adapterCreator: papaChannel.guild.voiceAdapterCreator, + }) + return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.joining_voice") }) + } else return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.already_connected") }) +} diff --git a/src/commands/salonpostam/parle.ts b/src/commands/salonpostam/parle.ts index 3a53fa6..ee71b04 100755 --- a/src/commands/salonpostam/parle.ts +++ b/src/commands/salonpostam/parle.ts @@ -1,79 +1,84 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js' -import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, EndBehaviorType } from '@discordjs/voice' - -export default { - data: new SlashCommandBuilder() - .setName('parle') - .setDescription('Fais moi parler par dessus quelqu\'un de chiant dans le vocal') - .addUserOption(option => option.setName('user').setDescription('La personne en question').setRequired(true)), - async execute(interaction: ChatInputCommandInteraction) { - if (interaction.user.id !== '223831938346123275') return await interaction.reply({ content: 'Tu n\'as pas le droit d\'utiliser cette commande !', ephemeral: true }) - - let user = interaction.options.getUser('user') - if (!user) return - let guild = interaction.guild - if (!guild) return - let member = guild.members.cache.get(user.id) as GuildMember - if (!member) return - let caller = interaction.member as GuildMember - if (!caller) return - - if (!caller.voice.channel) return await interaction.reply({ content: 'You must be in a voice channel to use this command.', ephemeral: true }) - if (!member.voice.channel) return await interaction.reply({ content: 'The member must be in a voice channel to use this command.', ephemeral: true }) - if (caller.voice.channelId !== member.voice.channelId) return await interaction.reply({ content: 'You must be in the same voice channel than the member to use this command.', ephemeral: true }) - - await interaction.reply({ content: 'Je vais parler par dessus cette personne !', ephemeral: true }) - - /* - // Searches for audio files uploaded in the channel - let messages = await interaction.channel.messages.fetch({ limit: 10, cache: false }) - messages = messages.filter(m => m.attachments.size > 0) - - let files = [] - await messages.forEach(m => m.attachments.forEach(a => { - if (a.contentType === 'audio/mpeg') files.push(a) - })) - if (files.size === 0) return await interaction.editReply({ content: 'Aucun fichier audio trouvé dans ce channel.', ephemeral: true }) - - // Limit the number of files to the last 10 - //files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10) - - // Ask the user to choose a file - let file = await interaction.channel.send({ content: 'Choisissez un fichier audio :', files: files }) - let filter = m => m.author.id === interaction.user.id && !isNaN(m.content) && parseInt(m.content) > 0 && parseInt(m.content) <= files.size - let response = await interaction.channel.awaitMessages({ filter, max: 1, time: 30000, errors: ['time'] }) - file = files.get(files.keyArray()[response.first().content - 1]) - */ - - let playing = false - let player = createAudioPlayer() - player.on(AudioPlayerStatus.Idle, () => { playing = false }) - - let connection = joinVoiceChannel({ - channelId: caller.voice.channelId as string, - guildId: interaction.guildId as string, - adapterCreator: guild.voiceAdapterCreator, - selfDeaf: false - }) - connection.subscribe(player) - - let stream = connection.receiver.subscribe(user.id, { end: { behavior: EndBehaviorType.Manual } }) - stream.on('data', () => { - if (!user) return - if (connection.receiver.speaking.users.has(user.id) && !playing) { - playing = true - let resource = createAudioResource('../../static/parle.mp3', { inlineVolume: true }) - //let resource = createAudioResource(file.attachments.first().url, { inlineVolume: true }) - if (resource.volume) resource.volume.setVolume(0.2) - player.play(resource) - } - }) - - interaction.client.on('voiceStateUpdate', (oldState, newState) => { - if (oldState.id === member.id && newState.channelId !== caller.voice.channelId) { - stream.destroy() - connection.disconnect() - } - }) - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction, GuildMember } from "discord.js" +import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, EndBehaviorType } from "@discordjs/voice" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("speak") + .setDescription("Make me talk over someone annoying in voice chat") + .setNameLocalizations({ fr: "parle" }) + .setDescriptionLocalizations({ fr: "Fais moi parler par dessus quelqu'un d'ennuyant dans le vocal" }) + .addUserOption(option => option + .setName("user") + .setDescription("The person in question") + .setNameLocalizations({ fr: "utilisateur" }) + .setDescriptionLocalizations({ fr: "La personne en question" }) + .setRequired(true) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild + if (!guild) return + + const user = interaction.options.getUser("user", true) + const member = await guild.members.fetch(user.id) + const caller = interaction.member as GuildMember + + if (!caller.voice.channel) return interaction.reply({ content: t(interaction.locale, "salonpostam.parle.not_in_voice"), flags: MessageFlags.Ephemeral }) + if (!member.voice.channel) return interaction.reply({ content: t(interaction.locale, "salonpostam.parle.member_not_in_voice"), flags: MessageFlags.Ephemeral }) + if (caller.voice.channelId !== member.voice.channelId) return interaction.reply({ content: t(interaction.locale, "salonpostam.parle.not_same_channel"), flags: MessageFlags.Ephemeral }) + + await interaction.reply({ content: t(interaction.locale, "salonpostam.parle.will_speak_over"), flags: MessageFlags.Ephemeral }) + + /* + // Searches for audio files uploaded in the channel + const messages = await interaction.channel.messages.fetch({ limit: 10, cache: false }).filter(m => m.attachments.size > 0) + + const files = [] + await messages.forEach(m => m.attachments.forEach(a => { + if (a.contentType === 'audio/mpeg') files.push(a) + })) + if (files.size === 0) return interaction.editReply({ content: t(interaction.locale, "player.no_audio_found"), flags: MessageFlags.Ephemeral }) + + // Limit the number of files to the last 10 + //files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10) + + // Ask the user to choose a file + let file = await interaction.channel.send({ content: 'Choisissez un fichier audio :', files }) + const filter = m => m.author.id === interaction.user.id && !isNaN(m.content) && parseInt(m.content) > 0 && parseInt(m.content) <= files.size + const response = await interaction.channel.awaitMessages({ filter, max: 1, time: 30000, errors: ['time'] }) + file = files.get(files.keyArray()[response.first().content - 1]) + */ + + let playing = false + const player = createAudioPlayer() + player.on(AudioPlayerStatus.Idle, () => { playing = false }) + + const connection = joinVoiceChannel({ + channelId: caller.voice.channelId ?? "", + guildId: interaction.guildId ?? "", + adapterCreator: guild.voiceAdapterCreator, + selfDeaf: false + }) + connection.subscribe(player) + + const stream = connection.receiver.subscribe(user.id, { + end: { behavior: EndBehaviorType.Manual } + }) + stream.on("data", () => { + if (connection.receiver.speaking.users.has(user.id) && !playing) { + playing = true + const resource = createAudioResource("@/static/parle.mp3", { inlineVolume: true }) + //const resource = createAudioResource(file.attachments.first().url, { inlineVolume: true }) + if (resource.volume) resource.volume.setVolume(0.2) + player.play(resource) + } + }) + + interaction.client.on("voiceStateUpdate", (oldState, newState) => { + if (oldState.id === member.id && newState.channelId !== caller.voice.channelId ) { + stream.destroy() + connection.disconnect() + } + }) +} diff --git a/src/commands/salonpostam/spam.ts b/src/commands/salonpostam/spam.ts index 85dd3f1..b125203 100755 --- a/src/commands/salonpostam/spam.ts +++ b/src/commands/salonpostam/spam.ts @@ -1,29 +1,47 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' - -export default { - data: new SlashCommandBuilder() - .setName('spam') - .setDescription('Spam') - .addUserOption(option => option.setName('user').setDescription('Spam').setRequired(true)) - .addStringOption(option => option.setName('string').setDescription('Spam').setRequired(true)) - .addIntegerOption(option => option.setName('integer').setDescription('Spam').setRequired(true)), - async execute(interaction: ChatInputCommandInteraction) { - let user = interaction.options.getUser('user') - let string = interaction.options.getString('string') - let integer = interaction.options.getInteger('integer') - - await interaction.reply({ content: 'Spam', ephemeral: true }) - let i = 0 - function myLoop() { - setTimeout(function () { - if (!user) return - if (!string) return - if (!integer) return - user.send(string).catch(error => console.error(error)) - i++ - if (i < integer) myLoop() - }, 1000) - } - myLoop() - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("spam") + .setDescription("Spam a user with a message") + .setDescriptionLocalizations({ fr: "Spammer un utilisateur avec un message" }) + .addUserOption(option => option + .setName("user") + .setDescription("Target user") + .setNameLocalizations({ fr: "utilisateur" }) + .setDescriptionLocalizations({ fr: "Utilisateur cible" }) + .setRequired(true) + ) + .addStringOption(option => option + .setName("message") + .setDescription("Message to spam") + .setDescriptionLocalizations({ fr: "Message à spammer" }) + .setRequired(true) + ) + .addIntegerOption(option => option + .setName("count") + .setDescription("Number of times to spam") + .setNameLocalizations({ fr: "nombre" }) + .setDescriptionLocalizations({ fr: "Nombre de fois à spammer" }) + .setRequired(true) + .setMinValue(1) + .setMaxValue(100) + ) + +export async function execute(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user", true) + const string = interaction.options.getString("message", true) + const integer = interaction.options.getInteger("count", true) + + await interaction.reply({ content: t(interaction.locale, "salonpostam.spam.started"), flags: MessageFlags.Ephemeral }) + let i = 0 + function myLoop() { + setTimeout(() => { + user.send(string).catch(console.error) + i++ + if (i < integer) myLoop() + }, 1000) + } + myLoop() +} diff --git a/src/commands/salonpostam/update.ts b/src/commands/salonpostam/update.ts index 46d63da..9171b62 100755 --- a/src/commands/salonpostam/update.ts +++ b/src/commands/salonpostam/update.ts @@ -1,19 +1,22 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction, Guild } from 'discord.js' - -export default { - data: new SlashCommandBuilder() - .setName('update') - .setDescription('Update the member count channel.'), - async execute(interaction: ChatInputCommandInteraction) { - let guild = interaction.guild as Guild - - guild.members.fetch().then(() => { - let i = 0 - guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) - let channel = guild.channels.cache.get('1091140609139560508') - if (!channel) return - channel.setName(`${i} Gens Posés`) - interaction.reply(`${i} Gens Posés !`) - }).catch(console.error) - } -} \ No newline at end of file +import { SlashCommandBuilder, MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction } from "discord.js" +import { t } from "@/utils/i18n" + +export const data = new SlashCommandBuilder() + .setName("update") + .setDescription("Update the member count channel") + .setDescriptionLocalizations({ fr: "Mettre à jour le canal de nombre de membres" }) + +export async function execute(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild + if (!guild) return interaction.reply({ content: t(interaction.locale, "common.command_server_only"), flags: MessageFlags.Ephemeral }) + + guild.members.fetch().then(async () => { + let i = 0 + guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) + const channel = guild.channels.cache.get("1091140609139560508") + if (!channel) return + await channel.setName(`${i} Gens Posés`) + return interaction.reply(t(interaction.locale, "salonpostam.update.members_updated", { count: i })) + }).catch(console.error) +} diff --git a/src/events/client/error.ts b/src/events/client/error.ts index a3b0ad5..8224a23 100755 --- a/src/events/client/error.ts +++ b/src/events/client/error.ts @@ -1,8 +1,7 @@ -import { Events } from 'discord.js' +import { Events } from "discord.js" +import { logConsoleError } from "@/utils/console" -export default { - name: Events.Error, - execute(error: Error) { - console.error(error) - } +export const name = Events.Error +export function execute(error: Error) { + logConsoleError('discordjs', 'error', { message: error.message }, error) } \ No newline at end of file diff --git a/src/events/client/guildCreate.ts b/src/events/client/guildCreate.ts index 19738de..7973479 100755 --- a/src/events/client/guildCreate.ts +++ b/src/events/client/guildCreate.ts @@ -1,14 +1,12 @@ -import { Events, Guild } from 'discord.js' -import dbGuildInit from '../../utils/dbGuildInit' - -export default { - name: Events.GuildCreate, - async execute(guild: Guild) { - console.log(`Joined "${guild.name}" with ${guild.memberCount} members`) - - let guildProfile = await dbGuildInit(guild) - if (!guildProfile) return console.log(`An error occured while initializing database data for "${guild.name}" !`) - - console.log(`Database data for new guild "${guildProfile.guildName}" successfully initialized !`) - } -} \ No newline at end of file +import { Events, Guild } from "discord.js" +import dbGuildInit from "@/utils/dbGuildInit" +import { logConsole } from "@/utils/console" + +export const name = Events.GuildCreate +export async function execute(guild: Guild) { + logConsole('discordjs', 'guild_create', { name: guild.name, count: guild.memberCount.toString() }) + + const guildProfile = await dbGuildInit(guild) + + logConsole('mongoose', 'guild_create', { name: guildProfile.guildName }) +} diff --git a/src/events/client/guildMemberAdd.ts b/src/events/client/guildMemberAdd.ts index 11d5bb8..c8da743 100755 --- a/src/events/client/guildMemberAdd.ts +++ b/src/events/client/guildMemberAdd.ts @@ -1,44 +1,42 @@ -import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js' - -export default { - name: Events.GuildMemberAdd, - async execute(member: GuildMember) { - if (member.guild.id === '1086577543651524699') { // Salon posé tamisé - let guild = member.guild - - guild.members.fetch().then(() => { - let i = 0 - guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) - - let channel = guild.channels.cache.get('1091140609139560508') - if (!channel) return - - channel.setName('Changement...') - channel.setName(`${i} Gens Posés`) - }).catch(console.error) - } else if (member.guild.id === '796327643783626782') { // Jujul Community - let guild = member.guild - - let channel = guild.channels.cache.get('837248593609097237') as TextChannel - if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "837248593609097237" !`) - - if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`) - - let embed = new EmbedBuilder() - .setColor(guild.members.me.displayHexColor) - .setTitle(`Salut ${member.user.username} !`) - .setDescription(` - Bienvenue sur le serveur de **Jujul** ! - Nous sommes actuellement ${guild.memberCount} membres !\n - N'hésite pas à aller lire le <#797471924367786004> et à aller te présenter dans <#837138238417141791> !\n - Si tu as des questions, - n'hésite pas à les poser dans le <#837110617315344444> !\n - Bon séjour parmi nous ! - `) - .setThumbnail(member.user.avatarURL()) - .setTimestamp(new Date()) - - await channel.send({ embeds: [embed] }) - } - } -} \ No newline at end of file +import { Events, EmbedBuilder, ChannelType } from "discord.js" +import type { GuildMember } from "discord.js" +import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" + +export const name = Events.GuildMemberAdd +export async function execute(member: GuildMember) { + if (member.guild.id === "1086577543651524699") { + // Salon posé tamisé + const guild = member.guild + + guild.members.fetch().then(async () => { + let i = 0 + guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) + + const channel = guild.channels.cache.get("1091140609139560508") + if (!channel) return + + await channel.setName("Changement...") + await channel.setName(`${i} Gens Posés`) + }).catch(console.error) + } else if (member.guild.id === "796327643783626782") { + // Jujul Community + const guild = member.guild + if (!guild.members.me) return + + const channel = guild.channels.cache.get("837248593609097237") + if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { + logConsole('discordjs', 'guild_member_add', { channelId: '837248593609097237' }) + return + } + + const embed = new EmbedBuilder() + .setColor(guild.members.me.displayHexColor) + .setTitle(t(guild.preferredLocale, "welcome.title", { username: member.user.username })) + .setDescription(t(guild.preferredLocale, "welcome.description", { memberCount: guild.memberCount.toString() })) + .setThumbnail(member.user.avatarURL()) + .setTimestamp(new Date()) + + return channel.send({ embeds: [embed] }) + } +} diff --git a/src/events/client/guildMemberRemove.ts b/src/events/client/guildMemberRemove.ts index 2eca32e..ff703c7 100755 --- a/src/events/client/guildMemberRemove.ts +++ b/src/events/client/guildMemberRemove.ts @@ -1,21 +1,22 @@ -import { Events, GuildMember } from 'discord.js' - -export default { - name: Events.GuildMemberRemove, - async execute(member: GuildMember) { - if (member.guild.id === '1086577543651524699') { // Salon posé tamisé - let guild = member.guild - - guild.members.fetch().then(() => { - let i = 0 - guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) - - let channel = guild.channels.cache.get('1091140609139560508') - if (!channel) return - - channel.setName('Changement...') - channel.setName(`${i} Gens Posés`) - }).catch(console.error) - } - } -} \ No newline at end of file +import { Events } from "discord.js" +import type { GuildMember } from "discord.js" +import { t } from "@/utils/i18n" + +export const name = Events.GuildMemberRemove +export function execute(member: GuildMember) { + if (member.guild.id === "1086577543651524699") { + // Salon posé tamisé + const guild = member.guild + + guild.members.fetch().then(async () => { + let i = 0 + guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) + + const channel = guild.channels.cache.get("1091140609139560508") + if (!channel) return + + await channel.setName(t(guild.preferredLocale, "salonpostam.update.loading")) + await channel.setName(t(guild.preferredLocale, "salonpostam.update.members_updated", { count: i.toString() })) + }).catch(console.error) + } +} diff --git a/src/events/client/guildMemberUpdate.ts b/src/events/client/guildMemberUpdate.ts index f7c7452..b953313 100644 --- a/src/events/client/guildMemberUpdate.ts +++ b/src/events/client/guildMemberUpdate.ts @@ -1,35 +1,37 @@ -import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js' - -export default { - name: Events.GuildMemberUpdate, - async execute(oldMember: GuildMember, newMember: GuildMember) { - if (newMember.guild.id === '796327643783626782') { // Jujul Community - let guild = newMember.guild - - let channel = guild.channels.cache.get('924353449930412153') as TextChannel - if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`) - - let boostRole = guild.roles.premiumSubscriberRole - if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`) - - const hadRole = oldMember.roles.cache.find(role => role.id === boostRole.id) - const hasRole = newMember.roles.cache.find(role => role.id === boostRole.id) - - if (!hadRole && hasRole) { - if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`) - - let embed = new EmbedBuilder() - .setColor(guild.members.me.displayHexColor) - .setTitle(`Nouveau boost de ${newMember.user.username} !`) - .setDescription(` - Merci à toi pour ce boost.\n - Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts ! - `) - .setThumbnail(newMember.user.avatarURL()) - .setTimestamp(new Date()) - - await channel.send({ embeds: [embed] }) - } - } - } -} \ No newline at end of file +import { Events, EmbedBuilder, ChannelType } from "discord.js" +import type { GuildMember } from "discord.js" +import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" + +export const name = Events.GuildMemberUpdate +export async function execute(oldMember: GuildMember, newMember: GuildMember) { + if (newMember.guild.id === "796327643783626782") { + // Jujul Community + const guild = newMember.guild + + const channel = await guild.channels.fetch("924353449930412153") + if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { + logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" }) + return + } + + const boostRole = guild.roles.premiumSubscriberRole + if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return } + + const hadRole = oldMember.roles.cache.find(role => role.id === boostRole.id) + const hasRole = newMember.roles.cache.find(role => role.id === boostRole.id) + + if (!hadRole && hasRole) { + if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return } + + const embed = new EmbedBuilder() + .setColor(guild.members.me.displayHexColor) + .setTitle(t(guild.preferredLocale, "boost.new_boost_title", { username: newMember.user.username })) + .setDescription(t(guild.preferredLocale, "boost.new_boost_description", { count: guild.premiumSubscriptionCount?.toString() ?? "0" })) + .setThumbnail(newMember.user.avatarURL()) + .setTimestamp(new Date()) + + return channel.send({ embeds: [embed] }) + } + } +} diff --git a/src/events/client/guildUpdate.ts b/src/events/client/guildUpdate.ts index 78ce416..3d8c08e 100755 --- a/src/events/client/guildUpdate.ts +++ b/src/events/client/guildUpdate.ts @@ -1,18 +1,18 @@ -import { Events, Guild } from 'discord.js' -import dbGuildInit from '../../utils/dbGuildInit' -import dbGuild from '../../schemas/guild' - -export default { - name: Events.GuildUpdate, - async execute(oldGuild: Guild, newGuild: Guild) { - console.log(`Guild ${oldGuild.name} updated`) - - let guildProfile = await dbGuild.findOne({ guildId: newGuild.id }) - if (!guildProfile) guildProfile = await dbGuildInit(newGuild) - else { - guildProfile.guildName = newGuild.name - guildProfile.guildIcon = newGuild.iconURL() ?? 'None' - await guildProfile.save().catch(console.error) - } - } -} \ No newline at end of file +import { Events } from "discord.js" +import type { Guild } from "discord.js" +import dbGuildInit from "@/utils/dbGuildInit" +import dbGuild from "@/schemas/guild" +import { logConsole } from "@/utils/console" + +export const name = Events.GuildUpdate +export async function execute(oldGuild: Guild, newGuild: Guild) { + logConsole('discordjs', 'guild_update', { name: oldGuild.name }) + + let guildProfile = await dbGuild.findOne({ guildId: newGuild.id }) + if (!guildProfile) guildProfile = await dbGuildInit(newGuild) + else { + guildProfile.guildName = newGuild.name + guildProfile.guildIcon = newGuild.iconURL() ?? "None" + await guildProfile.save().catch(console.error) + } +} diff --git a/src/events/client/index.ts b/src/events/client/index.ts new file mode 100644 index 0000000..5ee87f0 --- /dev/null +++ b/src/events/client/index.ts @@ -0,0 +1,21 @@ +import * as error from "./error" +import * as guildCreate from "./guildCreate" +import * as guildMemberAdd from "./guildMemberAdd" +import * as guildMemberRemove from "./guildMemberRemove" +import * as guildMemberUpdate from "./guildMemberUpdate" +import * as guildUpdate from "./guildUpdate" +import * as interactionCreate from "./interactionCreate" +import * as ready from "./ready" + +import type { Event } from "@/types" + +export default [ + error, + guildCreate, + guildMemberAdd, + guildMemberRemove, + guildMemberUpdate, + guildUpdate, + interactionCreate, + ready +] as Event[] diff --git a/src/events/client/interactionCreate.ts b/src/events/client/interactionCreate.ts index a62be52..2d8a0e6 100755 --- a/src/events/client/interactionCreate.ts +++ b/src/events/client/interactionCreate.ts @@ -1,45 +1,49 @@ -import { Events, Interaction, ChatInputCommandInteraction, AutocompleteInteraction, ButtonInteraction } from 'discord.js' -import { playerButtons, playerEdit } from '../../utils/player' - -export default { - name: Events.InteractionCreate, - async execute(interaction: Interaction) { - //if (!interaction.isAutocomplete() && !interaction.isChatInputCommand() && !interaction.isButton()) return console.error(`Interaction ${interaction.commandName} is not a command.`) - - if (interaction.isChatInputCommand()) { - interaction = interaction as ChatInputCommandInteraction - - let chatInputCommand = interaction.client.commands.get(interaction.commandName) - if (!chatInputCommand) return console.error(`No chat input command matching ${interaction.commandName} was found.`) - - console.log(`Command '${interaction.commandName}' launched by ${interaction.user.tag}`) - - try { await chatInputCommand.execute(interaction) } - catch (error) { console.error(`Error executing ${interaction.commandName}:`, error) } - } - else if (interaction.isAutocomplete()) { - interaction = interaction as AutocompleteInteraction - - let autoCompleteRun = interaction.client.commands.get(interaction.commandName) - if (!autoCompleteRun) return console.error(`No autoCompleteRun matching ${interaction.commandName} was found.`) - - console.log(`AutoCompleteRun '${interaction.commandName}' launched by ${interaction.user.tag}`) - - try { await autoCompleteRun.autocompleteRun(interaction) } - catch (error) { console.error(`Error autocompleting ${interaction.commandName}:`, error) } - } - else if (interaction.isButton()) { - interaction = interaction as ButtonInteraction - - let button = interaction.client.buttons.get(interaction.customId) - if (!button) return console.error(`No button id matching ${interaction.customId} was found.`) - - console.log(`Button '${interaction.customId}' clicked by ${interaction.user.tag}`) - - if (playerButtons.includes(interaction.customId)) { await playerEdit(interaction) } - - try { await button.execute(interaction) } - catch (error) { console.error(`Error clicking ${interaction.customId}:`, error) } - } - } -} \ No newline at end of file +import { Events } from "discord.js" +import type { Interaction } from "discord.js" +import commands from "@/commands" +import buttons, { buttonFolders } from "@/buttons" +import selectMenus from "@/selectmenus" +import { playerEdit } from "@/utils/player" +import { logConsole, logConsoleError } from "@/utils/console" + +export const name = Events.InteractionCreate +export async function execute(interaction: Interaction) { + if (interaction.isChatInputCommand()) { + const chatInputCommand = commands.find(cmd => cmd.data.name == interaction.commandName) + if (!chatInputCommand) { logConsole('discordjs', 'interaction_create.command_not_found', { command: interaction.commandName }); return } + + logConsole('discordjs', 'interaction_create.command_launched', { command: interaction.commandName, user: interaction.user.tag }) + + try { await chatInputCommand.execute(interaction) } + catch (error) { logConsoleError('discordjs', 'interaction_create.command_error', { command: interaction.commandName }, error as Error) } + } + else if (interaction.isAutocomplete()) { + const autocompleteRun = commands.find(cmd => cmd.data.name == interaction.commandName) + if (!autocompleteRun?.autocompleteRun) { logConsole('discordjs', 'interaction_create.autocomplete_not_found', { command: interaction.commandName }); return } + + logConsole('discordjs', 'interaction_create.autocomplete_launched', { command: interaction.commandName, user: interaction.user.tag }) + + try { await autocompleteRun.autocompleteRun(interaction) } + catch (error) { logConsoleError('discordjs', 'interaction_create.autocomplete_error', { command: interaction.commandName }, error as Error) } + } + else if (interaction.isButton()) { + const button = buttons.find(btn => btn.id === interaction.customId) + if (!button) { logConsole('discordjs', 'interaction_create.button_not_found', { id: interaction.customId }); return } + + logConsole('discordjs', 'interaction_create.button_clicked', { id: interaction.customId, user: interaction.user.tag }) + + try { await button.execute(interaction) } + catch (error) { logConsoleError('discordjs', 'interaction_create.button_error', { id: interaction.customId }, error as Error) } + + if (buttonFolders.find(folder => folder.name === "player" ? folder.commands.some(cmd => cmd.id === interaction.customId) : false)) await playerEdit(interaction) + } + else if (interaction.isAnySelectMenu()) { + const selectMenu = selectMenus.find(menu => menu.id === interaction.customId) + if (!selectMenu) { logConsole('discordjs', 'interaction_create.selectmenu_not_found', { id: interaction.customId }); return } + + logConsole('discordjs', 'interaction_create.selectmenu_used', { id: interaction.customId, user: interaction.user.tag }) + + try { await selectMenu.execute(interaction) } + catch (error) { logConsoleError('discordjs', 'interaction_create.selectmenu_error', { id: interaction.customId }, error as Error) } + } +} diff --git a/src/events/client/ready.ts b/src/events/client/ready.ts index b6d6cb6..6a3f1e6 100755 --- a/src/events/client/ready.ts +++ b/src/events/client/ready.ts @@ -1,117 +1,139 @@ -import { Events, Client, ActivityType } from 'discord.js' -import { SpotifyExtractor } from '@discord-player/extractor' -import { YoutubeiExtractor } from 'discord-player-youtubei' -import { useMainPlayer } from 'discord-player' -import { connect } from 'mongoose' -import WebSocket from 'websocket' -import chalk from 'chalk' -import 'dotenv/config' - -import dbGuildInit from '../../utils/dbGuildInit' -import dbGuild from '../../schemas/guild' -import { playerDisco, playerReplay } from '../../utils/player' -import * as Twitch from '../../utils/twitch' -import rss from '../../utils/rss' - -export default { - name: Events.ClientReady, - once: true, - async execute(client: Client) { - console.log(chalk.blue(`[DiscordJS] Connected to Discord ! Logged in as ${client.user?.tag ?? 'unknown'}`)) - client.user?.setActivity('some bangers...', { type: ActivityType.Listening }) - - await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => console.log(chalk.blue('[Discord-Player] Spotify extractor loaded.'))).catch(console.error) - await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => console.log(chalk.blue('[Discord-Player] Youtube extractor loaded.'))).catch(console.error) - - let mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}` - await connect(mongo_url).catch(console.error) - - - let guilds = client.guilds.cache - guilds.forEach(async guild => { - let guildProfile = await dbGuild.findOne({ guildId: guild.id }) - - if (!guildProfile) guildProfile = await dbGuildInit(guild) - if (guildProfile.guildPlayer?.replay?.enabled && guildProfile.guildPlayer?.replay?.textChannelId) await playerReplay(client, guildProfile) - - client.disco = { interval: {} as NodeJS.Timeout } - client.disco.interval = setInterval(async () => { - let guildProfile = await dbGuild.findOne({ guildId: guild.id }) - - if (guildProfile?.guildPlayer?.disco?.enabled) { - let state = await playerDisco(client, guildProfile) - if (state === 'clear') clearInterval(client.disco.interval) - } - }, 3000) - - client.rss = { interval: {} as NodeJS.Timeout } - client.rss.interval = setInterval(async () => { - let guildProfile = await dbGuild.findOne({ guildId: guild.id }) - - if (guildProfile?.guildRss?.enabled) { - let state = await rss(client, guildProfile) - if (state === 'clear') clearInterval(client.rss.interval) - } - }, 30000) - - // TWITCH EVENTSUB - if (process.env['TWITCH_RUNNING_' + guild.id]) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Already running...`)) - console.log(chalk.magenta(`[Twitch] {${guild.name}} Not running, starting...`)) - process.env['TWITCH_RUNNING_' + guild.id] = 'true' - - let client_id = process.env.TWITCH_APP_ID as string - let client_secret = process.env.TWITCH_APP_SECRET as string - if (!client_id || !client_secret) return console.log(chalk.magenta(`[Twitch] {${guild.name}} App ID or Secret is not defined !`)) - - let dbData = guildProfile.get('guildTwitch') - if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Module is disabled, please activate with \`/database edit guildTwitch.enabled True\` !`)) - - let twitch = new WebSocket.client().on('connect', async connection => { - console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub WebSocket Connected !`)) - - connection.on('message', async message => { if (message.type === 'utf8') { try { - let data = JSON.parse(message.utf8Data) - let channel_access_token = guildProfile.get('guildTwitch')?.channelAccessToken as string - - // Check when Twitch asks to login - if (data.metadata.message_type === 'session_welcome') { - - // Check if the channel access token is still valid before connecting - channel_access_token = await Twitch.checkChannel(client_id, client_secret, channel_access_token, guild) as string - if (!channel_access_token) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't refresh channel access token !`)) - - // Get broadcaster user id and reward id - let broadcaster_user_id = await Twitch.getUserInfo(client_id, channel_access_token, 'id') as string - - let topics: { [key: string]: { version: string; condition: { broadcaster_user_id: string } } } = { - 'stream.online': { version: '1', condition: { broadcaster_user_id } }, - 'stream.offline': { version: '1', condition: { broadcaster_user_id } } - } - - // Subscribe to all events required - for (let type in topics) { - console.log(chalk.magenta(`[Twitch] {${guild.name}} Creating ${type}...`)) - let { version, condition } = topics[type] - - let status = await Twitch.subscribeToEvents(client_id, channel_access_token, data.payload.session.id, type, version, condition) - if (!status) return console.error(chalk.magenta(`[Twitch] {${guild.name}} Failed to create ${type}`)) - else if (status.error) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Erreur de connexion EventSub, veuillez vous reconnecter !`)) - else console.log(chalk.magenta(`[Twitch] {${guild.name}} Successfully created ${type}`)) - } - } - - // Handle notification messages - else if (data.metadata.message_type === 'notification') Twitch.notification(client_id, client_secret, channel_access_token, data, guild) - - } catch (error) { console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)) } } }) - .on('error', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error))) - .on('close', () => { - console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub Connection Closed !`)) - twitch.connect('wss://eventsub.wss.twitch.tv/ws') - }) - }).on('connectFailed', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error))) - - twitch.connect('wss://eventsub.wss.twitch.tv/ws') - }) - } -} \ No newline at end of file +import { Events, ActivityType, ChannelType } from "discord.js" +import type { Client } from "discord.js" +import { useMainPlayer } from "discord-player" +import { SpotifyExtractor } from "@discord-player/extractor" +import { YoutubeiExtractor } from "discord-player-youtubei" +import { connect } from "mongoose" +import type { Document } from "mongoose" +import { playerDisco, playerReplay } from "@/utils/player" +import { twitchClient, listener, onlineSub, offlineSub, startStreamWatching } from "@/utils/twitch" +import { logConsole } from "@/utils/console" +import type { GuildPlayer, Disco, GuildTwitch, GuildFbx } from "@/types/schemas" +import * as Freebox from "@/utils/freebox" +import dbGuildInit from "@/utils/dbGuildInit" +import dbGuild from "@/schemas/guild" + +export const name = Events.ClientReady +export const once = true +export async function execute(client: Client) { + logConsole('discordjs', 'ready', { tag: client.user?.tag ?? "unknown" }) + client.user?.setActivity("some bangers...", { type: ActivityType.Listening }) + + await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Spotify' }) }).catch(console.error) + await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Youtube' }) }).catch(console.error) + + const mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}` + await connect(mongo_url).catch(console.error) + + if (process.env.NODE_ENV === "development") await twitchClient.eventSub.deleteAllSubscriptions() + const streamerIds: string[] = [] + + await Promise.all(client.guilds.cache.map(async guild => { + let guildProfile = await dbGuild.findOne({ guildId: guild.id }) + guildProfile ??= await dbGuildInit(guild) + + const dbDataPlayer = guildProfile.get("guildPlayer") as GuildPlayer + const botInstance = dbDataPlayer.instances?.find(instance => instance.botId === client.user?.id) + if (botInstance?.replay.trackUrl) await playerReplay(client, dbDataPlayer) + + client.disco = { interval: {} as NodeJS.Timeout } + // eslint-disable-next-line @typescript-eslint/no-misused-promises + client.disco.interval = setInterval(async () => { + const guildProfile = await dbGuild.findOne({ guildId: guild.id }) + const dbDataDisco = guildProfile?.get("guildPlayer.disco") as Disco + + if (dbDataDisco.enabled) { + const state = await playerDisco(client, guild, dbDataDisco) + if (state === "clear") clearInterval(client.disco.interval) + } + }, 3000) + + // Gestion du timer LCD Freebox + const dbDataFbx = guildProfile.get("guildFbx") as GuildFbx + if (dbDataFbx.enabled && dbDataFbx.lcd) { + if (dbDataFbx.lcd.enabled && dbDataFbx.lcd.botId === client.user?.id) { + logConsole('freebox', 'lcd_timer_restored', { guild: guild.name }) + Freebox.Timer.schedule(client, guild.id, dbDataFbx) + } + } + + const dbDataTwitch = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbDataTwitch.enabled) return + if (!dbDataTwitch.streamers.length) { logConsole('twitch', 'ready.no_streamers_configured', { guild: guild.name }); return } + + await Promise.all(dbDataTwitch.streamers.map(async streamer => { + if (streamerIds.includes(streamer.twitchUserId)) return + streamerIds.push(streamer.twitchUserId) + + const user = await twitchClient.users.getUserById(streamer.twitchUserId) + if (!user) { logConsole('twitch', 'ready.user_not_found', { guild: guild.name, userId: streamer.twitchUserId }); return } + + const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(streamer.twitchUserId) + if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + listener.onStreamOnline(streamer.twitchUserId, onlineSub) + logConsole('twitch', 'listener_registered', { type: 'stream.online', name: user.name, id: streamer.twitchUserId }) + } + if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.offline")) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + listener.onStreamOffline(streamer.twitchUserId, offlineSub) + logConsole('twitch', 'listener_registered', { type: 'stream.offline', name: user.name, id: streamer.twitchUserId }) + } + + logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) + + const stream = await user.getStream() + if (stream && streamer.messageId) { + logConsole('twitch', 'ready.stream_restoration', { guild: guild.name, userName: user.name, userId: streamer.twitchUserId }) + + // Vérifier que le message existe encore + if (!dbDataTwitch.channelId) return + const channel = await guild.channels.fetch(dbDataTwitch.channelId) + if (channel && (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement)) { + try { + await channel.messages.fetch(streamer.messageId) + startStreamWatching(guild.id, streamer.twitchUserId, user.name, streamer.messageId) + logConsole('twitch', 'ready.monitoring_restored', { guild: guild.name, userName: user.name }) + } catch (error) { + logConsole('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name }) + console.error(error) + await cleanupMessageId(guildProfile, streamer.twitchUserId) + } + } + } else if (streamer.messageId) { + // Il y a un messageId mais le stream n'est plus en ligne, nettoyer + logConsole('twitch', 'ready.stream_offline_cleanup', { guild: guild.name, userName: user.name }) + await cleanupMessageId(guildProfile, streamer.twitchUserId) + } + + logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) + })) + })) + + const subs = await twitchClient.eventSub.getSubscriptions() + await Promise.all(subs.data.map(async sub => { + if (streamerIds.includes(sub.condition.broadcaster_user_id as string)) return + if (sub.type !== "stream.online" && sub.type !== "stream.offline") return + + await sub.unsubscribe().catch(console.error) + logConsole('twitch', 'unsubscribed', { type: sub.type, id: sub.condition.broadcaster_user_id as string }) + })) +} + +async function cleanupMessageId(guildProfile: Document, twitchUserId: string) { + try { + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + + const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === twitchUserId) + if (streamerIndex === -1) return + + dbData.streamers[streamerIndex].messageId = "" + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save() + } catch (error) { + logConsole('twitch', 'ready.cleanup_error', { userId: twitchUserId }) + console.error(error) + } +} diff --git a/src/events/client/voiceStateUpdate.ts b/src/events/client/voiceStateUpdate.ts deleted file mode 100755 index df3112c..0000000 --- a/src/events/client/voiceStateUpdate.ts +++ /dev/null @@ -1,61 +0,0 @@ -//import { Events, VoiceState } from 'discord.js' -import { Events } from 'discord.js' - -export default { - name: Events.VoiceStateUpdate, - async execute() { - //async execute(oldState: VoiceState, newState: VoiceState) { - /* - let oldMute = oldState.serverMute - let newMute = newState.serverMute - let oldDeaf = oldState.serverDeaf - let newDeaf = newState.serverDeaf - let oldChannel = oldState.channelId - let newChannel = newState.channelId - console.log(oldChannel) - console.log(newChannel) - let guild = newState.guild - let member = newState.member - let channel = guild.channels.cache.get('1076215868863819848') - let angels = guild.members.cache.get('223831938346123275') - if (oldChannel !== newChannel) { - let executor = await logMoveOrKick('channel_id') - //if (!executor) channel.send(`Impossible de savoir qui a déplacé <@${member.id}> !`) - //else if (member.id === executor.id) channel.send(`<@${member.id}> s'est déplacé lui-même le con...`) - //else { - // channel.send(`<@${member.id}> a été mis en sourdine par <@${executor.id}> !`) - //} - } else if (!oldMute && newMute) { - let executor = await logMuteOrDeaf('mute') - if (!executor) channel.send(`Impossible de savoir qui a muté <@${member.id}> !`) - else if (member.id === executor.id) channel.send(`<@${member.id}> s'est muté lui-même le con...`) - else { - channel.send(`<@${member.id}> a été muté par <@${executor.id}> !`) - } - } else if (!oldDeaf && newDeaf) { - let executor = await logMuteOrDeaf('deaf') - if (!executor) channel.send(`Impossible de savoir qui a mis en sourdine <@${member.id}> !`) - else if (member.id === executor.id) channel.send(`<@${member.id}> s'est mis en sourdine lui-même le con...`) - else { - channel.send(`<@${member.id}> a été mis en sourdine par <@${executor.id}> !`) - } - } - async function logMoveOrKick() { - let auditLogs = await guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MemberMove }) - console.log(auditLogs.entries.find(entry => { return entry })) - let log = await auditLogs.entries.find(entry => { return entry.extra.channel.id === newChannel }) - console.log(log) - if (!log) return undefined - let executor = await guild.members.cache.get(log.executor.id) - return executor - } - async function logMuteOrDeaf(type) { - let auditLogs = await guild.fetchAuditLogs({ limit: 1, type: AuditLogEvent.MemberUpdate }) - let log = await auditLogs.entries.find(entry => { return entry.target.id === member.id && entry.changes[0].key === type && entry.changes[0].new === true }) - if (!log) return undefined - let executor = await guild.members.cache.get(log.executor.id) - return executor - } - */ - } -} \ No newline at end of file diff --git a/src/events/mongo/connected.ts b/src/events/mongo/connected.ts index 7e089a2..3a62005 100644 --- a/src/events/mongo/connected.ts +++ b/src/events/mongo/connected.ts @@ -1,8 +1,6 @@ -import chalk from 'chalk' +import { logConsole } from "@/utils/console" -export default { - name: 'connected', - async execute() { - console.log(chalk.green('[Mongoose] Connected to MongoDB !')) - } -} \ No newline at end of file +export const name = "connected" +export function execute() { + logConsole('mongoose', 'connected') +} diff --git a/src/events/mongo/connecting.ts b/src/events/mongo/connecting.ts index 94f0359..2302f5e 100644 --- a/src/events/mongo/connecting.ts +++ b/src/events/mongo/connecting.ts @@ -1,8 +1,6 @@ -import chalk from 'chalk' +import { logConsole } from "@/utils/console" -export default { - name: 'connecting', - async execute() { - console.log(chalk.green('[Mongoose] Connecting to MongoDB...')) - } -} \ No newline at end of file +export const name = "connecting" +export function execute() { + logConsole('mongoose', 'connecting') +} diff --git a/src/events/mongo/disconnected.ts b/src/events/mongo/disconnected.ts index 688ecbc..15b3a26 100644 --- a/src/events/mongo/disconnected.ts +++ b/src/events/mongo/disconnected.ts @@ -1,8 +1,6 @@ -import chalk from 'chalk' +import { logConsole } from "@/utils/console" -export default { - name: 'disconnected', - async execute() { - console.log(chalk.green('[Mongoose] Disconnected from MongoDB !')) - } -} \ No newline at end of file +export const name = "disconnected" +export function execute() { + logConsole('mongoose', 'disconnected') +} diff --git a/src/events/mongo/error.ts b/src/events/mongo/error.ts index 379f253..96c13ba 100644 --- a/src/events/mongo/error.ts +++ b/src/events/mongo/error.ts @@ -1,8 +1,6 @@ -import chalk from 'chalk' +import { logConsoleError } from "@/utils/console" -export default { - name: 'error', - async execute(error: Error) { - console.log(chalk.red('[Mongoose] An error occured with the database conenction :\n' + error)) - } -} \ No newline at end of file +export const name = "error" +export function execute(error: Error) { + logConsoleError('mongoose', 'error', { message: error.message }, error) +} diff --git a/src/events/mongo/index.ts b/src/events/mongo/index.ts new file mode 100644 index 0000000..0eb8088 --- /dev/null +++ b/src/events/mongo/index.ts @@ -0,0 +1,13 @@ +import * as connected from "./connected" +import * as connecting from "./connecting" +import * as disconnected from "./disconnected" +import * as error from "./error" + +import type { Event } from "@/types" + +export default [ + connected, + connecting, + disconnected, + error +] as Event[] diff --git a/src/events/player/audioTrackAdd.ts b/src/events/player/audioTrackAdd.ts index 7544b97..6477e3c 100755 --- a/src/events/player/audioTrackAdd.ts +++ b/src/events/player/audioTrackAdd.ts @@ -1,10 +1,11 @@ -import { GuildQueue, Track } from 'discord-player' -import { PlayerMetadata } from '../../utils/player' - -export default { - name: 'audioTrackAdd', - async execute(queue: GuildQueue, track: Track) { - // Emitted when the player adds a single song to its queue - queue.metadata.channel.send(`Musique **${track.title}** de **${track.author}** ajoutée à la file d'attente !`) - } -} \ No newline at end of file +import type { GuildQueue, Track } from "discord-player" +import type { PlayerMetadata } from "@/types/player" +import { t } from "@/utils/i18n" + +export const name = "audioTrackAdd" +export async function execute(queue: GuildQueue, track: Track) { + // Emitted when the player adds a single song to its queue + if (!queue.metadata.channel) return + + if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added", { title: track.title }) }) +} diff --git a/src/events/player/audioTracksAdd.ts b/src/events/player/audioTracksAdd.ts index d3befc5..3bd0da7 100755 --- a/src/events/player/audioTracksAdd.ts +++ b/src/events/player/audioTracksAdd.ts @@ -1,10 +1,10 @@ -import { GuildQueue, Track } from 'discord-player' -import { PlayerMetadata } from '../../utils/player' - -export default { - name: 'audioTracksAdd', - async execute(queue: GuildQueue, track: Array) { - // Emitted when the player adds multiple songs to its queue - queue.metadata.channel.send(`Ajout de ${track.length} musiques à la file d'attente !`) - } -} \ No newline at end of file +import type { GuildQueue, Track } from "discord-player" +import type { PlayerMetadata } from "@/types/player" +import { t } from "@/utils/i18n" + +export const name = "audioTracksAdd" +export async function execute(queue: GuildQueue, track: Track[]) { + // Emitted when the player adds multiple songs to its queue + if (!queue.metadata.channel) return + if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added_playlist", { count: track.length.toString() }) }) +} diff --git a/src/events/player/debug.ts b/src/events/player/debug.ts index e56e316..7706933 100755 --- a/src/events/player/debug.ts +++ b/src/events/player/debug.ts @@ -1,10 +1,9 @@ -import { GuildQueue } from 'discord-player' - -export default { - name: 'debug', - async execute(queue: GuildQueue, message: string) { - // Emitted when the player queue sends debug info - // Useful for seeing what state the current queue is at - console.log(`Player debug event: ${message}`) - } -} \ No newline at end of file +import type { GuildQueue } from "discord-player" +import { logConsoleDev } from "@/utils/console" + +export const name = "debug" +export function execute(queue: GuildQueue, message: string) { + // Emitted when the player queue sends debug info + // Useful for seeing what state the current queue is at + logConsoleDev('discord_player', 'debug', { message }) +} diff --git a/src/events/player/disconnect.ts b/src/events/player/disconnect.ts index c3ac43a..d6d5153 100755 --- a/src/events/player/disconnect.ts +++ b/src/events/player/disconnect.ts @@ -1,24 +1,13 @@ -import { GuildQueue } from 'discord-player' -import { PlayerMetadata } from '../../utils/player' -import dbGuild from '../../schemas/guild' - -export default { - name: 'disconnect', - async execute(queue: GuildQueue) { - // Emitted when the bot leaves the voice channel - queue.metadata.channel.send("J'ai quitté le vocal !") - - let guildProfile = await dbGuild.findOne({ guildId: queue.guild.id }) - if (!guildProfile) return console.log(`Database data for **${queue.guild.name}** does not exist !`) - - let dbData = guildProfile.get('guildPlayer.replay') - dbData['textChannelId'] = '' - dbData['voiceChannelId'] = '' - dbData['trackUrl'] = '' - dbData['progress'] = '' - - guildProfile.set('guildPlayer.replay', dbData) - guildProfile.markModified('guildPlayer.replay') - return await guildProfile.save().catch(console.error) - } -} \ No newline at end of file +import type { GuildQueue } from "discord-player" +import type { PlayerMetadata } from "@/types/player" +import { stopProgressSaving } from "@/utils/player" +import { t } from "@/utils/i18n" + +export const name = "disconnect" +export async function execute(queue: GuildQueue) { + // Emitted when the bot leaves the voice channel + await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") + + if (!queue.metadata.channel) return + if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.disconnect") }) +} diff --git a/src/events/player/emptyChannel.ts b/src/events/player/emptyChannel.ts index 9fde003..7dd8bb7 100755 --- a/src/events/player/emptyChannel.ts +++ b/src/events/player/emptyChannel.ts @@ -1,11 +1,14 @@ -import { GuildQueue } from 'discord-player' -import { PlayerMetadata } from '../../utils/player' - -export default { - name: 'emptyChannel', - async execute(queue: GuildQueue) { - // Emitted when the voice channel has been empty for the set threshold - // Bot will automatically leave the voice channel with this event - queue.metadata.channel.send(`Je quitte le vocal car il est vide depuis trop longtemps.`) - } -} \ No newline at end of file +import type { GuildQueue } from "discord-player" +import type { PlayerMetadata } from "@/types/player" +import { stopProgressSaving } from "@/utils/player" +import { t } from "@/utils/i18n" + +export const name = "emptyChannel" +export async function execute(queue: GuildQueue) { + // Emitted when the voice channel has been empty for the set threshold + // Bot will automatically leave the voice channel with this event + await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") + + if (!queue.metadata.channel) return + if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.leaving_empty_channel") }) +} diff --git a/src/events/player/emptyQueue.ts b/src/events/player/emptyQueue.ts index 947931d..5ffb8e7 100755 --- a/src/events/player/emptyQueue.ts +++ b/src/events/player/emptyQueue.ts @@ -1,10 +1,13 @@ -import { GuildQueue } from 'discord-player' -import { PlayerMetadata } from '../../utils/player' - -export default { - name: 'emptyQueue', - async execute(queue: GuildQueue) { - // Emitted when the player queue has finished - queue.metadata.channel.send("File d'attente vide !") - } -} \ No newline at end of file +import type { GuildQueue } from "discord-player" +import type { PlayerMetadata } from "@/types/player" +import { stopProgressSaving } from "@/utils/player" +import { t } from "@/utils/i18n" + +export const name = "emptyQueue" +export async function execute(queue: GuildQueue) { + // Emitted when the player queue has finished + await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") + + if (!queue.metadata.channel) return + if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.queue_empty") }) +} diff --git a/src/events/player/error.ts b/src/events/player/error.ts index efd59f2..e6c2170 100755 --- a/src/events/player/error.ts +++ b/src/events/player/error.ts @@ -1,10 +1,8 @@ -import { GuildQueue } from 'discord-player' - -export default { - name: 'error', - async execute(queue: GuildQueue, error: Error) { - // Emitted when the player queue encounters error - console.log(`General player error event: ${error.message}`) - console.error(error) - } -} \ No newline at end of file +import type { GuildQueue } from "discord-player" +import { logConsoleError } from "@/utils/console" + +export const name = "error" +export function execute(queue: GuildQueue, error: Error) { + // Emitted when the player queue encounters error + logConsoleError('discord_player', 'error', { message: error.message }, error) +} diff --git a/src/events/player/index.ts b/src/events/player/index.ts new file mode 100644 index 0000000..76b976c --- /dev/null +++ b/src/events/player/index.ts @@ -0,0 +1,25 @@ +import * as audioTrackAdd from "./audioTrackAdd" +import * as audioTracksAdd from "./audioTracksAdd" +import * as debug from "./debug" +import * as disconnect from "./disconnect" +import * as emptyChannel from "./emptyChannel" +import * as emptyQueue from "./emptyQueue" +import * as error from "./error" +import * as playerError from "./playerError" +import * as playerSkip from "./playerSkip" +import * as playerStart from "./playerStart" + +import type { Event } from "@/types" + +export default [ + audioTrackAdd, + audioTracksAdd, + debug, + disconnect, + emptyChannel, + emptyQueue, + error, + playerError, + playerSkip, + playerStart +] as Event[] diff --git a/src/events/player/playerError.ts b/src/events/player/playerError.ts index f084529..9a3a204 100755 --- a/src/events/player/playerError.ts +++ b/src/events/player/playerError.ts @@ -1,10 +1,8 @@ -import { GuildQueue } from 'discord-player' - -export default { - name: 'playerError', - async execute(queue: GuildQueue, error: Error) { - // Emitted when the audio player errors while streaming audio track - console.log(`\u001b[1;31m Player error event: ${error.message}`) - console.error(error) - } -} \ No newline at end of file +import type { GuildQueue } from "discord-player" +import { logConsoleError } from "@/utils/console" + +export const name = "playerError" +export function execute(queue: GuildQueue, error: Error) { + // Emitted when the audio player errors while streaming audio track + logConsoleError('discord_player', 'player_error', { message: error.message }, error) +} diff --git a/src/events/player/playerSkip.ts b/src/events/player/playerSkip.ts index 6df9437..eb58b8a 100755 --- a/src/events/player/playerSkip.ts +++ b/src/events/player/playerSkip.ts @@ -1,10 +1,12 @@ -import { GuildQueue, Track } from 'discord-player' -import { PlayerMetadata } from '../../utils/player' - -export default { - name: 'playerSkip', - async execute(queue: GuildQueue, track: Track) { - // Emitted when the audio player fails to load the stream for a song - queue.metadata.channel.send(`Musique **${track.title}** de **${track.author}** passée !`) - } -} \ No newline at end of file +import type { GuildQueue, Track } from "discord-player" +import type { PlayerMetadata } from "@/types/player" +import { t } from "@/utils/i18n" + +export const name = "playerSkip" +export async function execute(queue: GuildQueue, track: Track) { + // Emitted when the audio player fails to load the stream for a song + if (!queue.metadata.channel) return + if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ + content: t(queue.guild.preferredLocale, "player.track_skipped", { title: track.title, author: track.author }) + }) +} diff --git a/src/events/player/playerStart.ts b/src/events/player/playerStart.ts index 17ab358..f8a7d65 100755 --- a/src/events/player/playerStart.ts +++ b/src/events/player/playerStart.ts @@ -1,22 +1,10 @@ -import { GuildQueue, Track } from 'discord-player' -import { PlayerMetadata } from '../../utils/player' -import dbGuild from '../../schemas/guild' - -export default { - name: 'playerStart', - async execute(queue: GuildQueue, track: Track) { - // Emitted when the player starts to play a song - queue.metadata.channel.send(`Lecture de **${track.title}** de **${track.author}** !`) - - let guildProfile = await dbGuild.findOne({ guildId: queue.guild.id }) - if (!guildProfile) return console.log(`Database data for **${queue.guild.name}** does not exist !`) - - let dbData = guildProfile.get('guildPlayer.replay') - dbData['trackUrl'] = track.url - dbData['progress'] = '0' - - guildProfile.set('guildPlayer.replay', dbData) - guildProfile.markModified('guildPlayer.replay') - return await guildProfile.save().catch(console.error) - } -} \ No newline at end of file +import type { GuildQueue, Track } from "discord-player" +import type { PlayerMetadata } from "@/types/player" +import { t } from "@/utils/i18n" + +export const name = "playerStart" +export async function execute(queue: GuildQueue, track: Track) { + // Emitted when the player starts to play a song + if (!queue.metadata.channel) return + if ("send" in queue.metadata.channel) await queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.now_playing", { title: track.title, author: track.author }) }) +} diff --git a/src/index.ts b/src/index.ts index 6d7b99d..b6c1c1e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,141 +1,101 @@ -// PACKAGES -import { Client, Collection, GatewayIntentBits, REST, Routes, ChatInputCommandInteraction, AutocompleteInteraction, ButtonInteraction, SlashCommandBuilder } from 'discord.js' -import { Player } from 'discord-player' -import { connection, Connection } from 'mongoose' -import path from 'path' -import fs from 'fs' -import 'dotenv/config' - -// CUSTOM TYPES -interface CConnection extends Connection { - once: (event: string, listener: (...args: unknown[]) => void) => this - on: (event: string, listener: (...args: unknown[]) => void) => this -} -interface Command { - name: string - description: string - data: SlashCommandBuilder - autocompleteRun: (interaction: AutocompleteInteraction) => unknown - execute: (interaction: ChatInputCommandInteraction) => unknown -} -interface Button { - name: string - description: string - id: string - execute: (interaction: ButtonInteraction) => unknown -} -declare module 'discord.js' { - export interface Client { - commands: Collection - buttons: Collection - disco: { interval: NodeJS.Timeout } - rss: { interval: NodeJS.Timeout } - } -} - - -// CLIENT INITIALIZATION -const client = new Client({ - intents: [ - GatewayIntentBits.AutoModerationConfiguration, - GatewayIntentBits.AutoModerationExecution, - GatewayIntentBits.DirectMessageReactions, - GatewayIntentBits.DirectMessageTyping, - GatewayIntentBits.DirectMessages, - GatewayIntentBits.GuildEmojisAndStickers, - GatewayIntentBits.GuildIntegrations, - GatewayIntentBits.GuildInvites, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.GuildMessageTyping, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildModeration, - GatewayIntentBits.GuildPresences, - GatewayIntentBits.GuildScheduledEvents, - GatewayIntentBits.GuildVoiceStates, - GatewayIntentBits.GuildWebhooks, - GatewayIntentBits.Guilds, - GatewayIntentBits.MessageContent - ], - allowedMentions: { parse: ['roles', 'users', 'everyone'] } -}) -client.commands = new Collection() -client.buttons = new Collection() - -// PLAYER INITIALIZATION -const player = new Player(client) - - -// COMMANDS HANDLING -let commands = [] as Command[] -let commandsParsed = 0 -let commandsTotal = 0 - -let commandFolders = fs.readdirSync(path.join(__dirname, './commands')) -commandFolders.forEach(folder => { - if (folder === 'salonpostam' && process.env.DISCORD_APP_ID === '660961595006124052') return - - let folderPath = path.join(__dirname, './commands', folder) - let commandFiles = fs.readdirSync(folderPath).filter(file => file.endsWith('.ts')) - commandsTotal += commandFiles.length - - commandFiles.forEach(async file => { - let command = await import(path.join(folderPath, file)) - command = command.default - if ('data' in command && 'execute' in command) { - let commandData = command.data.toJSON() - if (commandData) { client.commands.set(commandData.name, command); commands.push(commandData) } - } else console.log(`[WARNING] The command at ${`${folderPath}/${file}`} is missing a required "data" or "execute" property.`) - commandsParsed++ - - if (commandsParsed === commandsTotal) { - console.log(`[INFO] ${commandsParsed} commands parsed.`) - // COMMANDS REGISTRATION - const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN as string); - (async () => { - try { await rest.put(Routes.applicationCommands(process.env.DISCORD_APP_ID as string), { body: commands }) } - catch (error) { console.error(error) } - })() - } - }) -}) - -// BUTTONS HANDLING -let buttonFiles = fs.readdirSync(path.join(__dirname, './buttons')).filter(file => file.endsWith('.ts')) -buttonFiles.forEach(async file => { - let button = await import(path.join(__dirname, './buttons', file)) - button = button.default - if ('id' in button && 'execute' in button) client.buttons.set(button.id, button) - else console.log(`[WARNING] The button ${file} is missing a required "id" or "execute" property.`) -}) - -// EVENTS HANDLING -let eventClientFiles = fs.readdirSync(path.join(__dirname, './events/client')).filter(file => file.endsWith('.ts')) -eventClientFiles.forEach(async file => { - let event = await import(path.join(__dirname, './events/client', file)) - event = event.default - if (event.once) client.once(event.name, (...args: unknown[]) => { event.execute(...args) }) - else client.on(event.name, (...args: unknown[]) => { event.execute(...args) }) -}) - -// PLAYER EVENTS HANDLING -let eventsPlayer = fs.readdirSync(path.join(__dirname, './events/player')).filter(file => file.endsWith('.ts')) -eventsPlayer.forEach(async file => { - let event = await import(path.join(__dirname, './events/player', file)) - event = event.default - if (event.name === 'debug') return - player.events.on(event.name, (...args: unknown[]) => event.execute(...args)) -}) - -// MONGO EVENTS HANDLING -let eventsMongo = fs.readdirSync(path.join(__dirname, './events/mongo')).filter(file => file.endsWith('.ts')) -eventsMongo.forEach(async file => { - let event = await import(path.join(__dirname, './events/mongo', file)) - event = event.default - if (event.once) (connection as CConnection).once(event.name, (...args: unknown[]) => { event.execute(...args, client) }) - else (connection as CConnection).on(event.name, (...args: unknown[]) => { event.execute(...args, client) }) -}) - - -// LAUNCH -client.login() \ No newline at end of file +// ENVIRONMENT VARIABLES +import "dotenv/config" + +const appId = process.env.DISCORD_APP_ID +const token = process.env.DISCORD_TOKEN +if (!appId || !token) { + console.warn(chalk.red("[DiscordJS] Missing DISCORD_APP_ID or DISCORD_TOKEN in environment variables!")) + process.exit(1) +} + +// PACKAGES +import { Client, GatewayIntentBits, REST, Routes } from "discord.js" +import { Player } from "discord-player" +import { connection } from "mongoose" +import type { GuildQueueEvents } from "discord-player" +import chalk from "chalk" +import { commandFolders } from "./commands" +import clientEvents from "./events/client" +import mongoEvents from "./events/mongo" +import playerEvents from "./events/player" +import { logConsole, logConsoleDev } from "@/utils/console" + +// CLIENT INITIALIZATION +const client = new Client({ + intents: [ + GatewayIntentBits.AutoModerationConfiguration, + GatewayIntentBits.AutoModerationExecution, + GatewayIntentBits.DirectMessageReactions, + GatewayIntentBits.DirectMessageTyping, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.GuildExpressions, + GatewayIntentBits.GuildIntegrations, + GatewayIntentBits.GuildInvites, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.GuildMessageTyping, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildModeration, + GatewayIntentBits.GuildPresences, + GatewayIntentBits.GuildScheduledEvents, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.GuildWebhooks, + GatewayIntentBits.Guilds, + GatewayIntentBits.MessageContent + ], + allowedMentions: { parse: ["roles", "users", "everyone"] } +}) + +// PLAYER INITIALIZATION +const player = new Player(client) + +// COMMANDS REGISTRATION +const rest = new REST({ version: "10" }).setToken(token) +void (async () => { + try { + const commands = commandFolders.flatMap(folder => folder.name !== "salonpostam" ? folder.commands.map(command => command.data) : []) + logConsole('discordjs', 'commands_registered', { count: commands.length.toString() }) + await rest.put(Routes.applicationCommands(process.env.DISCORD_APP_ID ?? ""), { body: commands }) + + const sptGuildId = process.env.DISCORD_SPT_GUILD_ID + if (!sptGuildId) return + + const commandsSpt = commandFolders.flatMap(folder => folder.name === "salonpostam" ? folder.commands.map(command => command.data) : []) + logConsole('discordjs', 'commands_registered_guild', { count: commandsSpt.length.toString(), guild: 'salonpostam' }) + await rest.put(Routes.applicationGuildCommands(appId, sptGuildId), { body: commandsSpt }) + } + catch (error) { console.error(error) } +})() + +// CLIENT EVENTS REGISTRATION +clientEvents.forEach(event => { + const callback = (...args: unknown[]) => { + logConsoleDev('discordjs', 'event_triggered', { event: event.name.toString() }) + event.execute(...args) + } + if (event.once) client.once(event.name, callback) + else client.on(event.name, callback) +}) +// MONGO EVENTS REGISTRATION +mongoEvents.forEach(event => { + const callback = (...args: unknown[]) => { + logConsoleDev('mongoose', 'event_triggered', { event: event.name.toString() }) + event.execute(...args) + } + if (event.once) connection.once(event.name, callback) + else connection.on(event.name, callback) +}) +// PLAYER EVENTS REGISTRATION +playerEvents.forEach(event => { + if (event.name === "debug") return + const callback = (...args: unknown[]) => { + logConsoleDev('discord_player', 'event_triggered', { event: event.name.toString() }) + event.execute(...args) + } + player.events.on(event.name as keyof GuildQueueEvents, callback) +}) + +// LAUNCH +void client.login() + +export default client diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..0437c5e --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,537 @@ +{ + "common": { + "database_not_found": "Database data does not exist!", + "database_exists": "Database data already exists!", + "database_initialized": "Database data successfully initialized!", + "command_owner_only": "This command can only be used by the bot owner.", + "command_server_only": "This command must be used in a server.", + "private_message_not_available": "This command is not available in private messages.", + "error_occurred": "An error occurred!", + "success": "Success!", + "failed": "Failed!", + "none": "None", + "unknown": "Unknown", + "configure": "Configure", + "enable": "Enable", + "disable": "Disable", + "add": "Add", + "remove": "Remove", + "list": "List", + "status": "Status", + "channel": "Channel", + "no_channel_selected": "No channel selected!", + "channel_configured_success": "✅ Channel configured successfully!", + "invalid_text_channel": "Please provide a valid text channel!", + "invalid_channel_type": "Please provide a valid text channel!", + "no_permission": "You don't have permission to use this command." + }, + "player": { + "common": { + "enabled": "✅ Enabled", + "disabled": "❌ Disabled", + "not_configured": "Not configured", + "channel_not_found": "Channel not found", + "configure_channel": "Configure Channel" + }, + "track_skipped": "Song **{title}** by **{author}** skipped!", + "disconnect": "I've left the voice channel!", + "not_in_voice": "You're not in a voice channel, idiot!", + "not_in_same_voice": "You're not in my voice channel!", + "no_queue": "No queue is currently running, search for a song instead!", + "no_current_track": "No music is currently playing, search for one instead!", + "no_session": "No listening session in progress!", + "no_track": "No music currently playing!", + "track_added": "🎵 **{title}** has been added to the queue!", + "track_added_playlist": "🎵 **{count}** songs have been added to the queue!", + "invalid_search": "No results found for your search!", + "paused_status": "Playback paused", + "resumed_status": "Playback resumed", + "stopped_status": "Playback stopped", + "skipped": "Next song", + "previous": "Previous song", + "shuffled": "Queue shuffled", + "volume_max": "Volume is already at maximum!", + "volume_min": "Volume is already at minimum!", + "loop_off": "Loop disabled", + "loop_track": "Track loop enabled", + "loop_queue": "Queue loop enabled", + "loop_autoplay": "Autoplay enabled", + "no_lyrics_found": "No lyrics found!", + "no_audio_found": "No audio files found in this channel.", + "choose_audio_file": "Choose an audio file:", + "took_too_long": "You took too long to choose!", + "paused": "Music paused!", + "resumed": "Music resumed!", + "stopped": "Music stopped!", + "volume_changed": "🔊 | Volume changed to {volume}%!", + "volume_changed_down": "🔉 | Volume changed to {volume}%!", + "previous_played": "Previous music played!", + "uptime": "Uptime", + "requested_by": "Requested by {user}", + "next_track": "Next track", + "no_next_track": "None", + "no_track_found": "No music found for **{query}**!", + "music_restarted": "Restarting music after my restart...", + "now_playing": "Now playing **{title}** by **{author}**!", + "queue_empty": "Queue empty!", + "leaving_empty_channel": "I'm leaving the voice channel because it has been empty for too long.", + "no_queue_search_instead": "No queue currently active, search for music instead!", + "no_track_playing": "No music currently playing.", + "now_playing_no_queue": "Now playing: {track} \nNo music in the queue.", + "now_playing_with_queue": "Now playing: {track} \nCurrent queue: \n{tracks}", + "loading_track": "Loading music **{title}** by **{author}** on **{source}**...", + "duration": "Duration", + "source": "Source", + "volume": "Volume", + "progression": "Progress", + "progression_paused": "Progress (paused)", + "loop": "Loop", + "loop_modes": { + "off": "Off", + "track": "Track", + "queue": "Queue", + "autoplay": "Autoplay" + }, + "sources": { + "spotify": "Spotify", + "youtube": "Youtube", + "unknown": "Unknown" + }, + "disco": { + "title": "🪩 Disco Module Configuration", + "description": "Disco mode automatically updates the playback panel every 3 seconds", + "description_enabled": "Disco mode is enabled! Visual and audio effects will be applied during music playback.", + "description_disabled": "Disco mode is disabled. Enable it to enjoy visual and audio effects during music playback.", + "channel_not_configured": "No channel configured", + "channel_not_found": "Channel not found", + "enabled": "✅ Enabled", + "disabled": "❌ Disabled", + "configure_channel": "Configure Channel", + "configure_channel_first": "❌ Cannot enable Disco mode! Please first configure a channel with the **Configure Channel** button.", + "effects_applied": "Disco effects will be applied in {channel}.", + "select_channel": "Please select the channel where to apply Disco effects:", + "channel_configured_success": "Disco channel configured successfully! Effects will be applied in **{channel}**." + }, + "queue": { + "title": "🎵 Queue", + "empty": "The queue is empty", + "current_track": "**Currently playing:** {track}", + "next_tracks": "**Next:**", + "track_entry": "{index}. {title} - {author}" + } + }, + "twitch": { + "common": { + "enabled": "✅ Enabled", + "disabled": "❌ Disabled", + "not_configured": "Not configured", + "channel_not_found": "Channel not found", + "configure_channel": "Configure Channel" + }, + "title": "🎮 Twitch Module Configuration", + "module_disabled": "The Twitch module is disabled!", + "module_disabled_activate": "Twitch module is disabled, please activate with `/twitch enable`!", + "channel_not_configured": "No channel configured", + "streamers_count": "{count} configured", + "no_streamers": "No streamers configured at the moment", + "streamer_not_found": "**{username}** is not a valid Twitch username!", + "user_not_found_id": "User not found for ID {id}", + "streamer_already_added": "**{username}** is already added as a streamer!", + "streamer_not_in_list": "**{username}** is not a streamer!", + "streamer_added": "**{username}** (ID {id}) has been added as a streamer!", + "streamer_removed": "**{username}** has been removed as a streamer!", + "no_streamers_to_remove": "No streamers to remove!", + "notifications_channel_set": "Twitch notifications will be sent to **{channel}**!", + "configure_channel_first": "Please configure a channel first with the **Configure Channel** button!", + "select_notification_channel": "Please select the channel where to send Twitch notifications:", + "select_notification_channel_placeholder": "Select a channel for notifications", + "select_streamer_to_remove": "Select the streamer to remove:", + "select_streamer_prompt": "Select the streamer to remove:", + "user_not_found": "User not found", + "fetch_error": "Fetch error", + "no_streamers_list": "No streamers are currently added!", + "streamer_removed_success": "✅ **{streamer}** has been successfully removed from the streamers list!", + "streamer_not_found_list": "Streamer not found!", + "no_streamer_selected": "No streamer selected!", + "add_streamer_command": "To add a streamer, use the command `/twitch streamer add ` with autocomplete.\nYou can also select a member to mention them in notifications.", + "managed_by": "Managed by {bot}", + "notifications_sent_to": "Twitch notifications will be sent to {channel}.", + "list": { + "title": "📋 Streamers List", + "empty_description": "No streamers configured at the moment", + "discord_not_associated": "Not associated", + "user_not_found": "User not found", + "fetch_error": "Fetch error", + "footer": "{count} streamer(s) configured" + }, + "notification": { + "online": { + "everyone": "Hey @everyone!\n{streamer} is live on **Twitch**, come!", + "everyone_with_mention": "Hey @everyone!\n<@{discordId}> is live on **Twitch**, come!", + "title_unknown": "Unknown stream title", + "author": "🔴 {streamer} IS CURRENTLY LIVE! 🎥", + "description": "Playing {game} with {viewers} viewers" + }, + "offline": { + "everyone": "Re @everyone!\n{streamer} has ended their stream on **Twitch**!", + "everyone_with_mention": "Re @everyone!\n<@{discordId}> has ended their stream on **Twitch**!", + "author": "⚫ IT'S OVER, THE STREAM LASTED {duration}! 📼", + "duration_unknown": "I DON'T KNOW HOW LONG" + } + }, + "ready": { + "no_streamers_configured": "Twitch module is enabled but no channel is configured, please use `/twitch channel`!", + "user_not_found": "User with ID {userId} not found, skipping...", + "stream_restoration": "Restoring monitoring for {userName} (ID {userId}) - Active stream detected", + "monitoring_restored": "Monitoring successfully restored for {userName}", + "message_not_found": "Message not found for {userName}, cleaning up messageId", + "stream_offline_cleanup": "Offline stream detected for {userName}, cleaning up messageId", + "cleanup_error": "Error while cleaning up messageId for {userId}" + }, + "logs": { + "user_fetch_error": "Error while fetching user for ID {userId}", + "listener_removed": "Listener removed for {streamerName} (ID {userId})", + "listener_removal_error": "Error while removing listener for {streamerName}" + } + }, + "amp": { + "module_disabled": "AMP module is disabled, please activate with `/database edit guildAmp.enabled True`!", + "host_not_configured": "AMP host is not configured, please configure it first!", + "login_required": "You must login before performing another command!", + "logged_in": "You are connected to the panel as **{username}**!", + "instance_list": "List of {count} instances:", + "running": "Running", + "port": "Port", + "module": "Module", + "hosts_found": "{count} host(s) found!", + "success": "Ok!", + "manage_success": "Ok!", + "restart_success": "Ok!" + }, + "freebox": { + "common": { + "enabled": "✅ Enabled", + "disabled": "❌ Disabled", + "not_configured": "Not configured", + "module_enabled": "The module is enabled!", + "module_disabled": "The module is disabled!", + "configured": "✅ Configured", + "not_configured_error": "❌ Not configured", + "general_error": "An error occurred while processing your request!" + }, + "general": { + "module_disabled": "Freebox module is disabled, please activate with `/database edit guildFbx.enabled True`!", + "host_not_set": "Freebox host is not set, please set with `/database edit guildFbx.host `!", + "version_not_set": "Freebox API version is not set, please set with `/database edit guildFbx.version `!", + "app_token_not_set": "Freebox appToken is not set, please init the app with `/freebox init`!", + "incomplete_configuration": "Incomplete Freebox configuration! Check that module, host, version and token are configured.", + "user_denied_access": "The user denied the app access to the Freebox.", + "file_saved": "File saved, you can now interact with your Freebox!", + "no_file_sent": "No file was sent in your message!", + "no_message_sent": "No message was sent before the time limit!", + "send_ca_file": "Please send another message with the CA file attached, you have one minute.", + "invalid_action": "Invalid action!", + "invalid_enabled_value": "Invalid enabled value!" + }, + "auth": { + "challenge_failed": "Failed to retrieve the challenge!", + "session_failed": "Failed to retrieve the session!", + "session_token_failed": "Failed to retrieve the session token!", + "track_id_failed": "Failed to retrieve the track ID, please try again later." + }, + "api": { + "version_failed": "An error occurred while retrieving the API version: {error}", + "connection_failed": "Failed to retrieve the connection details!", + "connection_details": "Connection details:\n{details}" + }, + "lcd": { + "details": "LCD configuration:\n{details}", + "config_failed": "Failed to retrieve LCD configuration!", + "managed_by_other_bot": "LED control is managed by another bot on this server!", + "leds_failed": "Failed to control LEDs!", + "leds_success": "LEDs {status} successfully!", + "config_title": "💡 Freebox LCD Status", + "led_strip": "🌈 LED Strip", + "led_strip_on": "✅ On", + "led_strip_off": "❌ Off", + "brightness": "🔆 Brightness", + "orientation": "📱 Orientation", + "orientation_portrait": "Portrait", + "orientation_landscape": "Landscape", + "auth_challenge_error": "❌ **Authentication Error**\nUnable to get challenge", + "auth_challenge_not_found": "❌ **Authentication Error**\nChallenge not found", + "auth_session_error": "❌ **Authentication Error**\nUnable to create session", + "auth_token_not_found": "❌ **Authentication Error**\nSession token not found", + "config_error": "❌ **LCD Error**\nUnable to retrieve LCD configuration", + "unexpected_error": "❌ **Error**\nAn unexpected error occurred" + }, + "timer": { + "auto": "⏰ Automatic Timer", + "status": "**LED Timer Status:**\n- Status: {status}\n- Managed by: {managedBy}", + "times_required": "Morning and night times are required to enable the timer!", + "invalid_time_format": "Invalid time format! Use HH:MM format (e.g., 08:00, 22:30)", + "enabled": "✅ LED Timer enabled!\n🌅 Turn on: {morningTime}\n🌙 Turn off: {nightTime}", + "disabled": "❌ LED Timer disabled!", + "status_field": "**Status:** {status}", + "managed_by": "**Managed by:** {manager}", + "morning": "**Morning time:** `{time}`", + "night": "**Night time:** `{time}`", + "not_configured": "Not configured", + "no_manager": "None" + }, + "status": { + "title": "📊 Freebox Status", + "config_section": "🔧 Configuration", + "module_field": "**Module:** {status}", + "host_field": "**Host:** {value}", + "version_field": "**API Version:** {value}", + "token_field": "**App Token:** {status}", + "host_not_configured": "❌ Not configured", + "version_not_configured": "❌ Not configured", + "token_configured": "✅ Configured", + "token_not_configured": "❌ Not configured", + "timer_section": "💡 LCD Timer", + "timer_enabled": "✅ Enabled", + "timer_disabled": "❌ Disabled", + "timer_no_manager": "None", + "timer_not_configured": "Not configured" + }, + "test": { + "connection_failed": "❌ Connection test failed", + "auth_challenge_failed": "❌ Authentication challenge failed", + "auth_challenge_not_found": "❌ Authentication challenge not found", + "auth_session_failed": "❌ Authentication session failed", + "auth_token_not_found": "❌ Session token not found", + "api_failed": "❌ API call failed", + "connection_success_title": "✅ Connection test successful!", + "api_field": "API Version", + "api_version": "v{version}", + "auth_field": "Authentication", + "token_valid": "✅ Valid token", + "connection_field": "Connection Status", + "connection_active": "✅ Connection active", + "connection_inactive": "❌ Connection inactive", + "connection_error": "❌ Connection test error" + }, + "buttons": { + "test_connection": "🔌 Test Connection", + "lcd_status": "💡 LCD Status", + "refresh_status": "🔄 Refresh Status", + "testing_connection": "🔌 Testing connection...", + "connection_success": "✅ Connection successful!", + "connection_test_failed": "❌ Connection test failed", + "connection_error": "Connection error: {error}", + "lcd_status_title": "💡 LCD Configuration", + "lcd_status_error": "❌ Failed to retrieve LCD status", + "lcd_status_error_details": "Error retrieving LCD status: {error}", + "status_refreshed": "🔄 Status refreshed successfully!", + "status_test_connection": "Test Connection", + "status_lcd_status": "LCD Status", + "status_refresh": "Refresh" + }, + "error": { + "inval": "invalid request or parameters", + "nodev": "no device found with this name/id", + "noent": "no entity found with this name/id", + "netdown": "network is down", + "busy": "device is busy", + "invalid_port": "invalid port", + "insecure_password": "the password is too weak to enable remote access", + "invalid_provider": "invalid ddns provider name", + "invalid_next_hop": "invalid next hop address (should be a link local address)", + "auth_required": "Invalid session token, or not session token sent", + "invalid_token": "The app token you are trying to use is invalid or has been revoked", + "pending_token": "The app token you are trying to use has not been validated by user yet", + "insufficient_rights": "Your app permissions does not allow accessing this API", + "denied_from_external_ip": "You are trying to get an app_token from a remote IP", + "invalid_request": "Your request is invalid", + "ratelimited": "Too many auth error have been made from your IP", + "new_apps_denied": "New application token request has been disabled", + "apps_denied": "API access from apps has been disabled", + "internal_error": "Internal error", + "no_panel": "No screen detected", + "setup": "Unable to setup screen", + "notsup": "Operation is not supported" + } + }, + "boost": { + "not_authorized": "This command is only allowed on the Jujul Community server!", + "new_boost_title": "New boost from {username}!", + "new_boost_description": "Thank you for this boost.\nThanks to you, we reached {count} boosts!", + "check_channel": "Go check in <#{channelId}>!" + }, + "welcome": { + "title": "Hello {username}!", + "description": "Welcome to **Jujul's** server!\nWe are currently {memberCount} members!\nFeel free to read the <#797471924367786004> and introduce yourself in <#837138238417141791>!\nIf you have questions,\nfeel free to ask them in <#837110617315344444>!\nEnjoy your stay with us!" + }, + "ping": { + "pinging": "Pinging...", + "response": "Websocket heartbeat: {heartbeat}ms.\nRoundtrip latency: {latency}ms" + }, + "salonpostam": { + "papa": { + "not_your_father": "You're not my father, get out!", + "no_dm": "I can't join your voice channel in private message, papa!", + "leaving_voice": "I'm leaving the voice channel, papa!", + "joining_voice": "I'm joining your voice channel, papa!", + "already_connected": "I'm already in your voice channel, papa!" + }, + "crack": { + "multiple_games_found": "I found multiple games for \"{query}\"! {list}", + "game_found": "Here's what I found for \"{query}\".\nYou can also click on [this link]({link}) to download the game directly!", + "no_games_found": "I found nothing for \"{query}\"!", + "selection_timeout": "You took too long to choose!" + }, + "spam": { + "started": "Spam" + }, + "parle": { + "not_in_voice": "You must be in a voice channel!", + "member_not_in_voice": "This person is not in a voice channel!", + "not_same_channel": "This person is not in the same channel as you!", + "will_speak_over": "I'll speak over this person whenever they talk!" + }, + "update": { + "loading": "Updating...", + "members_updated": "{count} Cool People!" + } + }, + "buttons": { + "labels": { + "previous": "⏮️", + "play_pause": "▶️", + "pause": "⏸️", + "stop": "⏹️", + "skip": "⏭️", + "volume_down": "🔉", + "volume_up": "🔊", + "shuffle": "🔀", + "loop": "🔁", + "configure_channel": "Configure Channel", + "enable": "Enable", + "disable": "Disable", + "list": "List", + "add": "Add", + "remove": "Remove" + } + }, + "selectmenus": { + "placeholders": { + "select_channel_notifications": "Select a channel for notifications", + "select_channel_disco": "Select a channel for Disco effects", + "select_streamer_remove": "Select a streamer to remove" + } + }, + "database": { + "owner_only": "This command can only be used by the bot owner!", + "server_only": "This command must be used in a server!", + "info_title": "Database Information", + "guild_info": "Guild **{name}** (ID: {id})", + "already_exists": "Database data for **{name}** already exists!", + "initialized": "Database data for **{name}** successfully initialized!", + "updated": "Database data for **{name}** successfully updated!\n**{key}**: {oldValue} -> {value}" + }, + "console": { + "discordjs": { + "ready": "[DiscordJS] Ready - Connected to Discord! Logged in as {tag}", + "commands_registered": "[DiscordJS] Registering {count} commands...", + "commands_registered_guild": "[DiscordJS] Registering {count} commands for {guild}...", + "event_triggered": "[DiscordJS] Event {event} triggered", + "guild_create": "[DiscordJS] GuildCreate - Joined \"{name}\" with {count} members", + "guild_update": "[DiscordJS] GuildUpdate - Guild {name} updated", + "guild_member_add": "[DiscordJS] GuildMemberAdd - No channel found with id \"{channelId}\"!", + "interaction_create": { + "command_not_found": "[DiscordJS] InteractionCreate - No ChatInputCommand name matching {command} was found.", + "command_launched": "[DiscordJS] InteractionCreate - ChatInputCommand '{command}' launched by {user}", + "command_error": "[DiscordJS] InteractionCreate - Error executing {command}", + "autocomplete_not_found": "[DiscordJS] InteractionCreate - No AutocompleteRun name matching {command} was found.", + "autocomplete_launched": "[DiscordJS] InteractionCreate - AutocompleteRun '{command}' launched by {user}", + "autocomplete_error": "[DiscordJS] InteractionCreate - Error Autocompleting {command}", + "button_not_found": "[DiscordJS] InteractionCreate - No Button id matching {id} was found.", + "button_clicked": "[DiscordJS] InteractionCreate - Button '{id}' clicked by {user}", + "button_error": "[DiscordJS] InteractionCreate - Error clicking {id}", + "selectmenu_not_found": "[DiscordJS] InteractionCreate - No SelectMenu id matching {id} was found.", + "selectmenu_used": "[DiscordJS] InteractionCreate - SelectMenu '{id}' used by {user}", + "selectmenu_error": "[DiscordJS] InteractionCreate - Error using {id}" + }, + "error": "[DiscordJS] Error - An error occurred: {message}", + "boost": { + "no_member": "[DiscordJS] Boost - No member found!", + "not_in_guild": "[DiscordJS] Boost - I'm not in the server!", + "no_channel": "[DiscordJS] Boost - No channel found with id \"{channelId}\"!", + "no_boost_role": "[DiscordJS] Boost - No boost role found!" + }, + "replay": { + "no_data": "[DiscordJS] Replay - No replay data for bot {botId}", + "no_text_channel_id": "[DiscordJS] Replay - No textChannelId configured for bot {botId}", + "no_voice_channel_id": "[DiscordJS] Replay - No voiceChannelId configured for bot {botId}", + "text_channel_not_found": "[DiscordJS] Replay - No textChannel found with id {channelId} for bot {botId}", + "voice_channel_not_found": "[DiscordJS] Replay - No voiceChannel found with id {channelId} for bot {botId}" + } + }, + "discord_player": { + "extractor_loaded": "[Discord-Player] Ready - {extractor} extractor loaded", + "event_triggered": "[Discord-Player] Event {event} triggered", + "error": "[Discord-Player] Error - General player error event: {message}", + "player_error": "[Discord-Player] PlayerError - Player error event: {message}", + "debug": "[Discord-Player] Debug - Player debug event: {message}", + "disco": { + "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Channel is not configured!", + "channel_not_found": "[Discord-Player] PlayerDisco - {guild} No channel found with id {channelId}" + }, + "progress_saving": { + "missing_ids": "[Discord-Player] ProgressSaving - GuildId or BotId is missing!", + "start": "[Discord-Player] ProgressSaving - Starting save for server {guildId} (bot {botId})", + "stop": "[Discord-Player] ProgressSaving - Stopping save for server {guildId} (bot {botId})", + "error": "[Discord-Player] ProgressSaving - Error saving progress for guild {guildId} (bot {botId})", + "database_not_exist": "[Discord-Player] ProgressSaving - Database data does not exist!" + } + }, + "mongoose": { + "connecting": "[Mongoose] Connecting to MongoDB...", + "connected": "[Mongoose] Connected to MongoDB!", + "disconnected": "[Mongoose] Disconnected from MongoDB!", + "error": "[Mongoose] An error occurred with the database connection: {message}", + "event_triggered": "[Mongoose] Event {event} triggered", + "guild_init": "[Mongoose] Initializing guild profile for {name} ({id})", + "guild_create": "[Mongoose] GuildCreate - Database data for new guild \"{name}\" successfully initialized!" + }, + "twitch": { + "starting_listener": "[Twitch] Starting listener with {adapter}...", + "stream_online": "[Twitch] Stream from {streamer} (ID {id}) is now online, sending Discord messages...", + "stream_offline": "[Twitch] Stream from {streamer} (ID {id}) is now offline, editing Discord messages...", + "processing_guild": "[Twitch] Processing guild: {name} (ID: {id}) for streamer {streamer}", + "notification_failed": "[Twitch] StreamWatching - {guild} Notification generation failed with status: {status}", + "no_db_data": "[Twitch] StreamWatching - {guild} No dbData found", + "streamer_not_found": "[Twitch] StreamWatching - {guild} Streamer {streamer} not found in this guild", + "message_exists": "[Twitch] StreamWatching - {guild} Message already exists for {streamer}, skipping", + "sending_notification": "[Twitch] StreamWatching - {guild} Sending notification for {streamer}", + "message_sent": "[Twitch] StreamWatching - {guild} Message sent with ID: {id}", + "error_processing_guild": "[Twitch] Error processing guild {name}", + "guild_failed": "[Twitch] Guild {index} failed:", + "user_operational": "[Twitch] User {name} (ID {id}) is operational", + "listener_registered": "[Twitch] Listener \"{type}\" registered for {name} (ID {id})", + "listener_removed": "[Twitch] Listener removed for {name} (ID {id})", + "unsubscribed": "[Twitch] Unsubscribed from {type} for {id}", + "start_watching": "[Twitch] StreamWatching - Starting watch for {streamer} (ID {id}) on {guildId}", + "stop_watching": "[Twitch] StreamWatching - Stopping watch for {streamer} (ID {id})", + "embed_missing": "[Twitch] StreamWatching - {guild} Embed is missing", + "error_editing_message": "[Twitch] StreamWatching - {guild} Error editing message for {streamer} (ID {id})", + "error_watching": "[Twitch] StreamWatching - Error watching {streamer} (ID {id}) on {guildId}", + "database_not_exist": "[Twitch] StreamWatching - {guild} Database data does not exist!", + "module_disabled": "[Twitch] StreamWatching - {guild} Twitch module is not enabled or channel ID is missing", + "channel_not_found": "[Twitch] StreamWatching - {guild} Channel with ID {channelId} not found for Twitch notifications", + "user_data_not_found": "[Twitch] StreamWatching - {guild} User data not found for {streamer} (ID {id})", + "stream_data_not_found": "[Twitch] StreamWatching - {guild} Stream data not found for {streamer} (ID {id})", + "message_id_not_found": "[Twitch] StreamWatching - {guild} Message ID not found for {streamer} (ID {id})", + "user_fetch_error": "[Twitch] Error fetching user for ID {id}", + "user_fetch_error_detailed": "[Twitch] Error while fetching user for ID {id}", + "starting_listener_ngrok": "[Twitch] Starting listener with ngrok...", + "user_fetch_error_buttons": "[Twitch] Error fetching user for ID {id} in buttons/selectmenu", + "listener_removal_error": "[Twitch] Error removing listener for {streamerName}" + }, + "freebox": { + "lcd_timer_restored": "Timers restored successfully for {guild}!" + } + } +} diff --git a/src/locales/fr.json b/src/locales/fr.json new file mode 100644 index 0000000..c720ac9 --- /dev/null +++ b/src/locales/fr.json @@ -0,0 +1,534 @@ +{ + "common": { + "database_not_found": "Données de base de données inexistantes !", + "database_exists": "Les données de la base de données existent déjà !", + "database_initialized": "Les données de la base de données ont été initialisées avec succès !", + "command_owner_only": "Cette commande ne peut être utilisée que par le propriétaire du bot.", + "command_server_only": "Cette commande doit être utilisée sur un serveur.", + "private_message_not_available": "Cette commande n'est pas disponible en message privé.", + "error_occurred": "Une erreur s'est produite !", + "success": "Succès !", + "failed": "Échec !", + "none": "Aucune", + "unknown": "Inconnu", + "configure": "Configurer", + "enable": "Activer", + "disable": "Désactiver", + "add": "Ajouter", + "remove": "Supprimer", + "list": "Liste", + "status": "Statut", + "channel": "Canal", + "no_channel_selected": "Aucun canal sélectionné !", + "channel_configured_success": "✅ Canal configuré avec succès !", + "invalid_text_channel": "Veuillez fournir un canal textuel valide !", + "invalid_channel_type": "Veuillez fournir un canal textuel valide !", + "no_permission": "Vous n'avez pas la permission d'utiliser cette commande." + }, + "player": { + "common": { + "enabled": "✅ Activé", + "disabled": "❌ Désactivé", + "not_configured": "Non configuré", + "channel_not_found": "Canal introuvable", + "configure_channel": "Configurer le canal" + }, + "track_skipped": "Musique **{title}** de **{author}** passée !", + "disconnect": "J'ai quitté le vocal !", + "not_in_voice": "T'es pas dans un vocal, idiot !", + "not_in_same_voice": "T'es pas dans mon vocal !", + "no_queue": "Aucune file d'attente en cours, recherche une musique plutôt !", + "no_current_track": "Aucune musique en cours, recherche en une plutôt !", + "no_session": "Aucune session d'écoute en cours !", + "no_track": "Aucune musique en cours de lecture !", + "track_added": "🎵 **{title}** a été ajouté à la file d'attente !", + "track_added_playlist": "🎵 **{count}** musiques ont été ajoutées à la file d'attente !", + "invalid_search": "Aucun résultat trouvé pour votre recherche !", + "paused_status": "Lecture mise en pause", + "resumed_status": "Lecture reprise", + "stopped_status": "Lecture arrêtée", + "skipped": "Musique suivante", + "previous": "Musique précédente", + "shuffled": "File d'attente mélangée", + "volume_max": "Le volume est déjà au maximum !", + "volume_min": "Le volume est déjà au minimum !", + "loop_off": "Répétition désactivée", + "loop_track": "Répétition du titre activée", + "loop_queue": "Répétition de la file d'attente activée", + "loop_autoplay": "Lecture automatique activée", + "no_lyrics_found": "Pas de paroles trouvées !", + "no_audio_found": "Aucun fichier audio trouvé dans ce channel.", + "choose_audio_file": "Choisissez un fichier audio :", + "took_too_long": "T'as mis trop de temps à choisir !", + "paused": "Musique mise en pause !", + "resumed": "Musique reprise !", + "stopped": "Musique arrêtée !", + "volume_changed": "🔊 | Volume modifié à {volume}% !", + "volume_changed_down": "🔉 | Volume modifié à {volume}% !", + "previous_played": "Musique précédente jouée !", + "uptime": "Uptime", + "requested_by": "Demandé par {user}", + "next_track": "Musique suivante", + "no_next_track": "Aucune", + "no_track_found": "Aucune musique trouvée pour **{query}** !", + "music_restarted": "Relancement de la musique suite à mon redémarrage...", + "now_playing": "Lecture de **{title}** de **{author}** !", + "queue_empty": "File d'attente vide !", + "leaving_empty_channel": "Je quitte le vocal car il est vide depuis trop longtemps.", + "no_queue_search_instead": "Aucune file d'attente en cours, recherche une musique plutôt !", + "no_track_playing": "Aucune musique en cours de lecture.", + "now_playing_no_queue": "Lecture en cours : {track} \nAucune musique dans la file d'attente.", + "now_playing_with_queue": "Lecture en cours : {track} \nFile d'attente actuelle : \n{tracks}", + "loading_track": "Chargement de la musique **{title}** de **{author}** sur **{source}**...", + "duration": "Durée", + "source": "Source", + "volume": "Volume", + "progression": "Progression", + "progression_paused": "Progression (en pause)", + "loop": "Loop", + "loop_modes": { + "off": "Off", + "track": "Titre", + "queue": "File d'Attente", + "autoplay": "Autoplay" + }, + "sources": { + "spotify": "Spotify", + "youtube": "Youtube", + "unknown": "Inconnu" + }, + "disco": { + "title": "🪩 Configuration du Module Disco", + "description": "Le mode Disco met à jour automatiquement le panneau de lecture toutes les 3 secondes", + "description_enabled": "Le mode Disco est activé ! Les effets visuels et sonores seront appliqués lors de la lecture de musique.", + "description_disabled": "Le mode Disco est désactivé. Activez-le pour profiter des effets visuels et sonores lors de la lecture de musique.", + "channel_not_configured": "Aucun canal configuré", + "configure_channel_first": "❌ Impossible d'activer le mode Disco ! Veuillez d'abord configurer un canal avec le bouton **Configurer Canal**.", + "effects_applied": "Les effets Disco seront appliqués dans {channel}.", + "select_channel": "Veuillez sélectionner le canal où appliquer les effets Disco :", + "channel_configured_success": "Canal Disco configuré avec succès ! Les effets seront appliqués dans **{channel}**." + }, + "queue": { + "title": "🎵 File d'attente", + "empty": "La file d'attente est vide", + "current_track": "**En cours :** {track}", + "next_tracks": "**Suivant :**", + "track_entry": "{index}. {title} - {author}" + } + }, + "twitch": { + "common": { + "enabled": "✅ Activé", + "disabled": "❌ Désactivé", + "not_configured": "Non configuré", + "channel_not_found": "Canal introuvable", + "configure_channel": "Configurer le canal" + }, + "title": "🎮 Configuration du Module Twitch", + "module_disabled": "Le module Twitch est désactivé !", + "module_disabled_activate": "Le module Twitch est désactivé, veuillez l'activer avec `/twitch enable` !", + "channel_not_configured": "Aucun canal configuré", + "streamers_count": "{count} configuré(s)", + "no_streamers": "Aucun streamer configuré pour le moment", + "streamer_not_found": "**{username}** n'est pas un nom d'utilisateur Twitch valide !", + "user_not_found_id": "Utilisateur introuvable pour l'ID {id}", + "streamer_already_added": "**{username}** est déjà ajouté comme streamer !", + "streamer_not_in_list": "**{username}** n'est pas un streamer !", + "streamer_added": "**{username}** (ID {id}) a été ajouté comme streamer !", + "streamer_removed": "**{username}** a été supprimé de la liste des streamers !", + "no_streamers_to_remove": "Aucun streamer à supprimer !", + "notifications_channel_set": "Les notifications Twitch seront envoyées dans **{channel}** !", + "configure_channel_first": "Veuillez d'abord configurer un canal avec le bouton **Configurer Canal** !", + "select_notification_channel": "Veuillez sélectionner le canal où envoyer les notifications Twitch :", + "select_notification_channel_placeholder": "Sélectionner un canal pour les notifications", + "select_streamer_to_remove": "Sélectionnez le streamer à supprimer :", + "select_streamer_prompt": "Sélectionnez le streamer à supprimer :", + "user_not_found": "Utilisateur introuvable", + "fetch_error": "Erreur de récupération", + "no_streamers_list": "Aucun streamer n'est actuellement ajouté !", + "streamer_removed_success": "✅ **{streamer}** a été supprimé avec succès de la liste des streamers !", + "streamer_not_found_list": "Streamer introuvable !", + "no_streamer_selected": "Aucun streamer sélectionné !", + "add_streamer_command": "Pour ajouter un streamer, utilisez la commande `/twitch streamer add ` avec l'autocomplétion.\nVous pouvez également sélectionner un membre pour le mentionner dans les notifications.", + "managed_by": "Géré par {bot}", + "notifications_sent_to": "Les notifications Twitch seront envoyées dans {channel}.", + "list": { + "title": "📋 Liste des Streamers", + "empty_description": "Aucun streamer configuré pour le moment", + "discord_not_associated": "Non associé", + "user_not_found": "Utilisateur introuvable", + "fetch_error": "Erreur de récupération", + "footer": "{count} streamer(s) configuré(s)" + }, + "notification": { + "online": { + "everyone": "Hey @everyone !\n{streamer} est en live sur **Twitch**, venez !", + "everyone_with_mention": "Hey @everyone !\n<@{discordId}> est en live sur **Twitch**, venez !", + "title_unknown": "Nom du live inconnu", + "author": "🔴 {streamer} EST ACTUELLEMENT EN LIVE ! 🎥", + "description": "Joue à {game} avec {viewers} viewers" + }, + "offline": { + "everyone": "Re @everyone !\n{streamer} a terminé son live sur **Twitch** !", + "everyone_with_mention": "Re @everyone !\n<@{discordId}> a terminé son live sur **Twitch** !", + "author": "⚫ C'EST FINI, LE LIVE A DURÉ {duration} ! 📼", + "duration_unknown": "JE SAIS PAS COMBIEN DE TEMPS" + } + }, + "ready": { + "no_streamers_configured": "Le module Twitch est activé mais aucun canal n'est configuré, veuillez utiliser `/twitch channel` !", + "user_not_found": "Utilisateur avec l'ID {userId} introuvable, ignoré...", + "stream_restoration": "Restauration de la surveillance pour {userName} (ID {userId}) - Stream en cours détecté", + "monitoring_restored": "Surveillance restaurée avec succès pour {userName}", + "message_not_found": "Message introuvable pour {userName}, nettoyage du messageId", + "stream_offline_cleanup": "Stream hors ligne détecté pour {userName}, nettoyage du messageId", + "cleanup_error": "Erreur lors du nettoyage du messageId pour {userId}" + }, + "logs": { + "user_fetch_error": "Erreur lors de la récupération de l'utilisateur pour l'ID {userId}", + "listener_removed": "Listener supprimé pour {streamerName} (ID {userId})", + "listener_removal_error": "Erreur lors de la suppression du listener pour {streamerName}" + } + }, + "amp": { + "module_disabled": "Le module AMP est désactivé, veuillez l'activer avec `/database edit guildAmp.enabled True` !", + "host_not_configured": "L'hôte AMP n'est pas configuré, veuillez le configurer d'abord !", + "login_required": "Tu dois te connecter avant d'effectuer une autre commande !", + "logged_in": "Tu es connecté au panel sous **{username}** !", + "instance_list": "Liste des {count} instances :", + "running": "En cours", + "port": "Port", + "module": "Module", + "hosts_found": "{count} hôte(s) trouvé(s) !", + "success": "Ok !", + "manage_success": "Ok !", + "restart_success": "Ok !" + }, + "freebox": { + "common": { + "enabled": "✅ Activé", + "disabled": "❌ Désactivé", + "not_configured": "Non configuré", + "module_enabled": "Le module est activé !", + "module_disabled": "Le module est désactivé !", + "configured": "✅ Configuré", + "not_configured_error": "❌ Non configuré", + "general_error": "Une erreur s'est produite lors de l'exécution de la commande !" + }, + "general": { + "module_disabled": "Le module Freebox est désactivé, veuillez l'activer avec `/database edit guildFbx.enabled True` !", + "host_not_set": "L'hôte Freebox n'est pas configuré, veuillez le configurer avec `/database edit guildFbx.host ` !", + "version_not_set": "La version de l'API Freebox n'est pas configurée, veuillez la configurer avec `/database edit guildFbx.version ` !", + "app_token_not_set": "Le token d'app Freebox n'est pas configuré, veuillez initialiser l'app avec `/freebox init` !", + "incomplete_configuration": "Configuration Freebox incomplète ! Vérifiez que le module, l'hôte, la version et le token sont configurés.", + "user_denied_access": "L'utilisateur a refusé l'accès de l'app à la Freebox.", + "file_saved": "Fichier sauvegardé, vous pouvez maintenant interagir avec votre Freebox !", + "no_file_sent": "Aucun fichier n'a été envoyé dans votre message !", + "no_message_sent": "Aucun message n'a été envoyé avant la limite de temps !", + "send_ca_file": "Veuillez envoyer un autre message avec le fichier CA attaché, vous avez une minute.", + "invalid_action": "Action invalide !", + "invalid_enabled_value": "Valeur d'activation invalide !" + }, + "auth": { + "challenge_failed": "Échec de récupération du challenge !", + "session_failed": "Échec de récupération de la session !", + "session_token_failed": "Échec de récupération du token de session !", + "track_id_failed": "Échec de récupération du track ID, veuillez réessayer plus tard." + }, + "api": { + "version_failed": "Erreur lors de la récupération de la version de l'API : {error}", + "connection_failed": "Échec de récupération des détails de connexion !", + "connection_details": "Détails de connexion :\n{details}" + }, + "lcd": { + "details": "Configuration LCD :\n{details}", + "config_failed": "Échec de récupération de la configuration LCD !", + "managed_by_other_bot": "Le contrôle des LEDs est géré par un autre bot sur ce serveur !", + "leds_failed": "Échec du contrôle des LEDs !", + "leds_success": "LEDs {status} avec succès !", + "config_title": "💡 État LCD Freebox", + "led_strip": "🌈 Bandeau LED", + "led_strip_on": "✅ Allumé", + "led_strip_off": "❌ Éteint", + "brightness": "🔆 Luminosité", + "orientation": "📱 Orientation", + "orientation_portrait": "Portrait", + "orientation_landscape": "Paysage", + "auth_challenge_error": "❌ **Erreur d'authentification**\nImpossible d'obtenir le challenge", + "auth_challenge_not_found": "❌ **Erreur d'authentification**\nChallenge introuvable", + "auth_session_error": "❌ **Erreur d'authentification**\nImpossible de créer la session", + "auth_token_not_found": "❌ **Erreur d'authentification**\nToken de session introuvable", + "config_error": "❌ **Erreur LCD**\nImpossible de récupérer la configuration LCD", + "unexpected_error": "❌ **Erreur**\nUne erreur inattendue s'est produite" + }, + "timer": { + "auto": "⏰ Timer Automatique", + "status": "**Statut du minuteur LED :**\n- État : {status}\n- Géré par : {managedBy}", + "times_required": "Les heures du matin et du soir sont requises pour activer le minuteur !", + "invalid_time_format": "Format d'heure invalide ! Utilisez le format HH:MM (ex: 08:00, 22:30)", + "enabled": "✅ Minuteur LED activé !\n🌅 Allumage : {morningTime}\n🌙 Extinction : {nightTime}", + "disabled": "❌ Minuteur LED désactivé !", + "status_field": "**Statut:** {status}", + "managed_by": "**Géré par:** {manager}", + "morning": "**Heure matin:** `{time}`", + "night": "**Heure soir:** `{time}`", + "not_configured": "Non configuré", + "no_manager": "Aucun" + }, + "status": { + "title": "📊 Freebox Status", + "config_section": "🔧 Configuration", + "module_field": "**Module:** {status}", + "host_field": "**Host:** {value}", + "version_field": "**Version API:** {value}", + "token_field": "**App Token:** {status}", + "host_not_configured": "❌ Non configuré", + "version_not_configured": "❌ Non configurée", + "token_configured": "✅ Configuré", + "token_not_configured": "❌ Non configuré", + "timer_section": "💡 Timer LCD", + "timer_enabled": "✅ Activé", + "timer_disabled": "❌ Désactivé", + "timer_no_manager": "Aucun", + "timer_not_configured": "Non configuré" + }, + "test": { + "connection_failed": "❌ Test de connexion échoué", + "auth_challenge_failed": "❌ Échec du challenge d'authentification", + "auth_challenge_not_found": "❌ Challenge d'authentification introuvable", + "auth_session_failed": "❌ Échec de la session d'authentification", + "auth_token_not_found": "❌ Token de session introuvable", + "api_failed": "❌ Échec de l'appel API", + "connection_success_title": "✅ Test de connexion réussi !", + "api_field": "Version API", + "api_version": "v{version}", + "auth_field": "Authentification", + "token_valid": "✅ Token valide", + "connection_field": "État de la connexion", + "connection_active": "✅ Connexion active", + "connection_inactive": "❌ Connexion inactive", + "connection_error": "❌ Erreur lors du test de connexion" + }, + "buttons": { + "test_connection": "🔌 Tester la connexion", + "lcd_status": "💡 Statut LCD", + "refresh_status": "🔄 Actualiser le statut", + "testing_connection": "🔌 Test de connexion en cours...", + "connection_success": "✅ Connexion réussie !", + "connection_test_failed": "❌ Échec du test de connexion", + "connection_error": "Erreur de connexion : {error}", + "lcd_status_title": "💡 Configuration LCD", + "lcd_status_error": "❌ Échec de récupération du statut LCD", + "lcd_status_error_details": "Erreur lors de la récupération du statut LCD : {error}", + "status_refreshed": "🔄 Statut actualisé avec succès !", + "status_test_connection": "Test Connexion", + "status_lcd_status": "État LCD", + "status_refresh": "Actualiser" + }, + "error": { + "inval": "invalid request or parameters", + "nodev": "no device found with this name/id", + "noent": "no entity found with this name/id", + "netdown": "network is down", + "busy": "device is busy", + "invalid_port": "invalid port", + "insecure_password": "the password is too weak to enable remote access", + "invalid_provider": "invalid ddns provider name", + "invalid_next_hop": "invalid next hop address (should be a link local address)", + "auth_required": "Invalid session token, or not session token sent", + "invalid_token": "The app token you are trying to use is invalid or has been revoked", + "pending_token": "The app token you are trying to use has not been validated by user yet", + "insufficient_rights": "Your app permissions does not allow accessing this API", + "denied_from_external_ip": "You are trying to get an app_token from a remote IP", + "invalid_request": "Your request is invalid", + "ratelimited": "Too many auth error have been made from your IP", + "new_apps_denied": "New application token request has been disabled", + "apps_denied": "API access from apps has been disabled", + "internal_error": "Internal error", + "no_panel": "No screen detected", + "setup": "Unable to setup screen", + "notsup": "Operation is not supported" + } + }, + "boost": { + "not_authorized": "Cette commande n'est autorisée que sur le serveur de Jujul Community !", + "new_boost_title": "Nouveau boost de {username} !", + "new_boost_description": "Merci à toi pour ce boost.\nGrâce à toi, on a atteint {count} boosts !", + "check_channel": "Va voir dans <#{channelId}> !" + }, + "welcome": { + "title": "Salut {username} !", + "description": "Bienvenue sur le serveur de **Jujul** !\nNous sommes actuellement {memberCount} membres !\nN'hésite pas à aller lire le <#797471924367786004> et à aller te présenter dans <#837138238417141791> !\nSi tu as des questions,\nn'hésite pas à les poser dans le <#837110617315344444> !\nBon séjour parmi nous !" + }, + "ping": { + "pinging": "Ping en cours...", + "response": "Websocket heartbeat: {heartbeat}ms.\nRoundtrip latency: {latency}ms" + }, + "salonpostam": { + "papa": { + "not_your_father": "T'es pas mon père, dégage !", + "no_dm": "Je ne peux pas rejoindre ton vocal en message privé, papa !", + "leaving_voice": "Je quitte le vocal, papa !", + "joining_voice": "Je rejoins ton vocal, papa !", + "already_connected": "Je suis déjà dans ton vocal, papa !" + }, + "crack": { + "multiple_games_found": "J'ai trouvé plusieurs jeux pour \"{query}\" ! {list}", + "game_found": "Voici ce que j'ai trouvé pour \"{query}\".\nTu peux aussi cliquer sur [ce lien]({link}) pour pouvoir télécharger le jeu direct !", + "no_games_found": "J'ai rien trouvé pour \"{query}\" !", + "selection_timeout": "T'as mis trop de temps à choisir !" + }, + "spam": { + "started": "Spam" + }, + "parle": { + "not_in_voice": "Tu dois être dans un salon vocal !", + "member_not_in_voice": "Cette personne n'est pas dans un salon vocal !", + "not_same_channel": "Cette personne n'est pas dans le même salon que toi !", + "will_speak_over": "Je vais parler par dessus cette personne, pendant tout le temps où elle parlera !" + }, + "update": { + "loading": "Changement...", + "members_updated": "{count} Gens Posés !" + } + }, + "buttons": { + "labels": { + "previous": "⏮️", + "play_pause": "▶️", + "pause": "⏸️", + "stop": "⏹️", + "skip": "⏭️", + "volume_down": "🔉", + "volume_up": "🔊", + "shuffle": "🔀", + "loop": "🔁", + "configure_channel": "Configurer Canal", + "enable": "Activer", + "disable": "Désactiver", + "list": "Liste", + "add": "Ajouter", + "remove": "Supprimer" + } + }, + "selectmenus": { + "placeholders": { + "select_channel_notifications": "Sélectionner un canal pour les notifications", + "select_channel_disco": "Sélectionner un canal pour les effets Disco", + "select_streamer_remove": "Sélectionner un streamer à supprimer" + } + }, + "database": { + "owner_only": "Cette commande ne peut être utilisée que par le propriétaire du bot !", + "server_only": "Cette commande doit être utilisée sur un serveur !", + "info_title": "Informations de la Base de Données", + "guild_info": "Serveur **{name}** (ID: {id})", + "already_exists": "Les données de la base de données pour **{name}** existent déjà !", + "initialized": "Les données de la base de données pour **{name}** ont été initialisées avec succès !", + "updated": "Les données de la base de données pour **{name}** ont été mises à jour avec succès !\n**{key}**: {oldValue} -> {value}" + }, + "console": { + "discordjs": { + "ready": "[DiscordJS] Ready - Connecté à Discord ! Connecté en tant que {tag}", + "commands_registered": "[DiscordJS] Enregistrement de {count} commandes...", + "commands_registered_guild": "[DiscordJS] Enregistrement de {count} commandes pour {guild}...", + "event_triggered": "[DiscordJS] Événement {event} déclenché", + "guild_create": "[DiscordJS] GuildCreate - Rejoint \"{name}\" avec {count} membres", + "guild_update": "[DiscordJS] GuildUpdate - Serveur {name} mis à jour", + "guild_member_add": "[DiscordJS] GuildMemberAdd - Aucun canal trouvé avec l'id \"{channelId}\" !", + "interaction_create": { + "command_not_found": "[DiscordJS] InteractionCreate - Aucune ChatInputCommand correspondant à {command} trouvée.", + "command_launched": "[DiscordJS] InteractionCreate - ChatInputCommand '{command}' lancée par {user}", + "command_error": "[DiscordJS] InteractionCreate - Erreur lors de l'exécution de {command}", + "autocomplete_not_found": "[DiscordJS] InteractionCreate - Aucune AutocompleteRun correspondant à {command} trouvée.", + "autocomplete_launched": "[DiscordJS] InteractionCreate - AutocompleteRun '{command}' lancée par {user}", + "autocomplete_error": "[DiscordJS] InteractionCreate - Erreur lors de l'autocomplétion de {command}", + "button_not_found": "[DiscordJS] InteractionCreate - Aucun Button avec l'id {id} trouvé.", + "button_clicked": "[DiscordJS] InteractionCreate - Button '{id}' cliqué par {user}", + "button_error": "[DiscordJS] InteractionCreate - Erreur lors du clic sur {id}", + "selectmenu_not_found": "[DiscordJS] InteractionCreate - Aucun SelectMenu avec l'id {id} trouvé.", + "selectmenu_used": "[DiscordJS] InteractionCreate - SelectMenu '{id}' utilisé par {user}", + "selectmenu_error": "[DiscordJS] InteractionCreate - Erreur lors de l'utilisation de {id}", + "selectmenu_invalid_type": "[DiscordJS] InteractionCreate - Type de SelectMenu invalide pour {id} reçu '{type}'" + }, + "error": "[DiscordJS] Error - Une erreur s'est produite : {message}", + "boost": { + "no_member": "[DiscordJS] Boost - Aucun membre trouvé !", + "not_in_guild": "[DiscordJS] Boost - Je ne suis pas sur le serveur !", + "no_channel": "[DiscordJS] Boost - Aucun channel trouvé avec l'id \"{channelId}\" !", + "no_boost_role": "[DiscordJS] Boost - Aucun rôle de boost trouvé !" + }, + "replay": { + "no_data": "[DiscordJS] Replay - Aucune donnée de replay pour le bot {botId}", + "no_text_channel_id": "[DiscordJS] Replay - Aucun textChannelId configuré pour le bot {botId}", + "no_voice_channel_id": "[DiscordJS] Replay - Aucun voiceChannelId configuré pour le bot {botId}", + "text_channel_not_found": "[DiscordJS] Replay - Aucun textChannel trouvé avec l'id {channelId} pour le bot {botId}", + "voice_channel_not_found": "[DiscordJS] Replay - Aucun voiceChannel trouvé avec l'id {channelId} pour le bot {botId}" + } + }, + "discord_player": { + "extractor_loaded": "[Discord-Player] Ready - Extracteur {extractor} chargé", + "event_triggered": "[Discord-Player] Événement {event} déclenché", + "error": "[Discord-Player] Error - Événement d'erreur général du lecteur : {message}", + "player_error": "[Discord-Player] PlayerError - Événement d'erreur du lecteur : {message}", + "debug": "[Discord-Player] Debug - Événement de débogage du lecteur : {message}", + "disco": { + "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Le canal n'est pas configuré !", + "channel_not_found": "[Discord-Player] PlayerDisco - {guild} Aucun canal trouvé avec l'id {channelId}" + }, + "progress_saving": { + "missing_ids": "[Discord-Player] ProgressSaving - GuildId ou BotId manquant !", + "start": "[Discord-Player] ProgressSaving - Démarrage de la sauvegarde pour le serveur {guildId} (bot {botId})", + "stop": "[Discord-Player] ProgressSaving - Arrêt de la sauvegarde pour le serveur {guildId} (bot {botId})", + "error": "[Discord-Player] ProgressSaving - Erreur lors de la sauvegarde pour le serveur {guildId} (bot {botId})", + "database_not_exist": "[Discord-Player] ProgressSaving - Les données de base n'existent pas !" + } + }, + "mongoose": { + "connecting": "[Mongoose] Connexion à MongoDB...", + "connected": "[Mongoose] Connecté à MongoDB !", + "disconnected": "[Mongoose] Déconnecté de MongoDB !", + "error": "[Mongoose] Une erreur s'est produite avec la connexion à la base de données : {message}", + "event_triggered": "[Mongoose] Événement {event} déclenché", + "guild_init": "[Mongoose] Initialisation du profil de serveur pour {name} ({id})", + "guild_create": "[Mongoose] GuildCreate - Données de base pour le nouveau serveur \"{name}\" initialisées avec succès !" + }, + "twitch": { + "starting_listener": "[Twitch] Démarrage du listener avec {adapter}...", + "stream_online": "[Twitch] Stream de {streamer} (ID {id}) maintenant en ligne, envoi des messages Discord...", + "stream_offline": "[Twitch] Stream de {streamer} (ID {id}) maintenant hors ligne, édition des messages Discord...", + "processing_guild": "[Twitch] Traitement du serveur : {name} (ID: {id}) pour le streamer {streamer}", + "notification_failed": "[Twitch] StreamWatching - {guild} Génération de notification échouée avec le statut : {status}", + "no_db_data": "[Twitch] StreamWatching - {guild} Aucune donnée BD trouvée", + "streamer_not_found": "[Twitch] StreamWatching - {guild} Streamer {streamer} non trouvé dans ce serveur", + "message_exists": "[Twitch] StreamWatching - {guild} Message existe déjà pour {streamer}, ignoré", + "sending_notification": "[Twitch] StreamWatching - {guild} Envoi de notification pour {streamer}", + "message_sent": "[Twitch] StreamWatching - {guild} Message envoyé avec l'ID : {id}", + "error_processing_guild": "[Twitch] Erreur lors du traitement du serveur {name}", + "guild_failed": "[Twitch] Serveur {index} échoué :", + "user_operational": "[Twitch] Utilisateur {name} (ID {id}) opérationnel", + "listener_registered": "[Twitch] Listener \"{type}\" enregistré pour {name} (ID {id})", + "listener_removed": "[Twitch] Listener supprimé pour {name} (ID {id})", + "unsubscribed": "[Twitch] Désabonné de {type} pour {id}", + "start_watching": "[Twitch] StreamWatching - Démarrage du visionnage de {streamer} (ID {id}) sur {guildId}", + "stop_watching": "[Twitch] StreamWatching - Arrêt du visionnage de {streamer} (ID {id})", + "embed_missing": "[Twitch] StreamWatching - {guild} Embed manquant", + "error_editing_message": "[Twitch] StreamWatching - {guild} Erreur lors de l'édition du message pour {streamer} (ID {id})", + "error_watching": "[Twitch] StreamWatching - Erreur lors du visionnage de {streamer} (ID {id}) sur {guildId}", + "database_not_exist": "[Twitch] StreamWatching - {guild} Les données de base n'existent pas !", + "module_disabled": "[Twitch] StreamWatching - {guild} Module Twitch non activé ou ID de canal manquant", + "channel_not_found": "[Twitch] StreamWatching - {guild} Canal avec l'ID {channelId} non trouvé pour les notifications Twitch", + "user_data_not_found": "[Twitch] StreamWatching - {guild} Données utilisateur non trouvées pour {streamer} (ID {id})", + "stream_data_not_found": "[Twitch] StreamWatching - {guild} Données de stream non trouvées pour {streamer} (ID {id})", + "message_id_not_found": "[Twitch] StreamWatching - {guild} ID de message non trouvé pour {streamer} (ID {id})", + "user_fetch_error": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id}", + "user_fetch_error_detailed": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id}", + "starting_listener_ngrok": "[Twitch] Démarrage du listener avec ngrok...", + "user_fetch_error_buttons": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id} dans buttons/selectmenu", + "listener_removal_error": "[Twitch] Erreur lors de la suppression du listener pour {streamerName}" + }, + "freebox": { + "lcd_timer_restored": "Minuteurs restaurés avec succès pour {guild} !" + } + } +} diff --git a/src/schemas/guild.ts b/src/schemas/guild.ts index 0116cd2..51ab4f4 100644 --- a/src/schemas/guild.ts +++ b/src/schemas/guild.ts @@ -1,4 +1,4 @@ -import { Schema, Types, model } from 'mongoose' +import { Schema, Types, model } from "mongoose" const guildSchema = new Schema({ _id: Types.ObjectId, @@ -6,27 +6,20 @@ const guildSchema = new Schema({ guildName: { type: String, required: true }, guildIcon: { type: String, required: true }, guildPlayer: { - replay: { - enabled: { type: Boolean, required: true }, - textChannelId: { type: String, required: false }, - voiceChannelId: { type: String, required: false }, - trackUrl: { type: String, required: false }, - progress: { type: Number, required: false } - }, + instances: [{ + botId: { type: String, required: true }, + replay: { + textChannelId: { type: String, required: false }, + voiceChannelId: { type: String, required: false }, + trackUrl: { type: String, required: false }, + progress: { type: Number, required: false } + } + }], disco: { enabled: { type: Boolean, required: true }, channelId: { type: String, required: false } } }, - guildRss: { - enabled: { type: Boolean, required: true }, - channelId: { type: String, required: false }, - feeds: [{ - name: { type: String, required: false }, - url: { type: String, required: false }, - token: { type: String, required: false } - }] - }, guildAmp: { enabled: { type: Boolean, required: true }, host: { type: String, required: false }, @@ -40,17 +33,26 @@ const guildSchema = new Schema({ version: { type: Number, required: false }, appToken: { type: String, required: false }, sessionToken: { type: String, required: false }, - password_salt: { type: String, required: false } + password_salt: { type: String, required: false }, + lcd: { + enabled: { type: Boolean, required: true }, + botId: { type: String, required: false }, + morningTime: { type: String, required: false }, + nightTime: { type: String, required: false } + } }, guildTwitch: { enabled: { type: Boolean, required: true }, - channelName: { type: String, required: false }, - channelAccessToken: { type: String, required: false }, - channelRefreshToken: { type: String, required: false }, - liveChannelId: { type: String, required: false }, - liveMessageId: { type: String, required: false }, - liveBroadcasterId: { type: String, required: false } + botId: { type: String, required: false }, + channelId: { type: String, required: false }, + streamers: [ + { + twitchUserId: { type: String, required: true }, + discordUserId: { type: String, required: false }, + messageId: { type: String, required: false } + } + ] } }) -export default model('Guild', guildSchema, 'guilds') \ No newline at end of file +export default model("Guild", guildSchema, "guilds") diff --git a/src/selectmenus/index.ts b/src/selectmenus/index.ts new file mode 100644 index 0000000..bf531ea --- /dev/null +++ b/src/selectmenus/index.ts @@ -0,0 +1,20 @@ +import player from "./player" +import twitch from "./twitch" + +import type { SelectMenu, SelectMenuFolder } from "@/types" + +export const buttonFolders = [ + { + name: "player", + commands: player + }, + { + name: "twitch", + commands: twitch + } +] as SelectMenuFolder[] + +export default [ + ...player, + ...twitch +] as SelectMenu[] diff --git a/src/selectmenus/player/disco_channel.ts b/src/selectmenus/player/disco_channel.ts new file mode 100644 index 0000000..fc90934 --- /dev/null +++ b/src/selectmenus/player/disco_channel.ts @@ -0,0 +1,28 @@ +import { MessageFlags } from "discord.js" +import type { ChannelSelectMenuInteraction } from "discord.js" +import { generateDiscoEmbed } from "@/utils/player" +import type { Disco } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "player_disco_channel" +export async function execute(interaction: ChannelSelectMenuInteraction) { + const channel = interaction.channels.first() + if (!channel) return interaction.reply({ content: t(interaction.locale, "common.no_channel_selected"), flags: MessageFlags.Ephemeral }) + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildPlayer.disco") as Disco + dbData.channelId = channel.id + + guildProfile.set("guildPlayer.disco", dbData) + guildProfile.markModified("guildPlayer.disco") + await guildProfile.save().catch(console.error) + + // Générer l'embed mis à jour avec la fonction utilitaire + const { embed, components } = generateDiscoEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + // Mettre à jour l'embed original avec les nouvelles données + return interaction.update({ content: t(interaction.locale, "common.channel_configured_success"), embeds: [embed], components }) +} diff --git a/src/selectmenus/player/index.ts b/src/selectmenus/player/index.ts new file mode 100644 index 0000000..8b16209 --- /dev/null +++ b/src/selectmenus/player/index.ts @@ -0,0 +1,7 @@ +import * as disco_channel from "./disco_channel" + +import type { SelectMenu } from "@/types" + +export default [ + disco_channel +] as SelectMenu[] diff --git a/src/selectmenus/twitch/channel.ts b/src/selectmenus/twitch/channel.ts new file mode 100644 index 0000000..9abf8a6 --- /dev/null +++ b/src/selectmenus/twitch/channel.ts @@ -0,0 +1,32 @@ +import { MessageFlags } from "discord.js" +import type { ChannelSelectMenuInteraction } from "discord.js" +import { generateTwitchEmbed } from "@/utils/twitch" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" + +export const id = "twitch_channel" +export async function execute(interaction: ChannelSelectMenuInteraction) { + const channel = interaction.channels.first() + if (!channel) return interaction.reply({ content: t(interaction.locale, "common.no_channel_selected"), flags: MessageFlags.Ephemeral }) + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + dbData.channelId = channel.id + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + // Générer l'embed mis à jour avec la fonction utilitaire + const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale) + + // Mettre à jour l'embed original avec les nouvelles données + return interaction.update({ + content: t(interaction.locale, "common.channel_configured_success") + ` ${t(interaction.locale, "twitch.notifications_sent_to", { channel: `<#${channel.id}>` })}`, + embeds: [embed], + components + }) +} diff --git a/src/selectmenus/twitch/index.ts b/src/selectmenus/twitch/index.ts new file mode 100644 index 0000000..2579ce8 --- /dev/null +++ b/src/selectmenus/twitch/index.ts @@ -0,0 +1,9 @@ +import * as channel from "./channel" +import * as streamer_remove from "./streamer_remove" + +import type { SelectMenu } from "@/types" + +export default [ + channel, + streamer_remove +] as SelectMenu[] diff --git a/src/selectmenus/twitch/streamer_remove.ts b/src/selectmenus/twitch/streamer_remove.ts new file mode 100644 index 0000000..4e22138 --- /dev/null +++ b/src/selectmenus/twitch/streamer_remove.ts @@ -0,0 +1,52 @@ +import { MessageFlags } from "discord.js" +import type { StringSelectMenuInteraction } from "discord.js" +import { twitchClient } from "@/utils/twitch" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" + +export const id = "twitch_streamer_remove" +export async function execute(interaction: StringSelectMenuInteraction) { + const twitchUserId = interaction.values[0] + if (!twitchUserId) return interaction.reply({ content: t(interaction.locale, "twitch.no_streamer_selected"), flags: MessageFlags.Ephemeral }) + + const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id }) + if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + + // Trouver et supprimer le streamer + const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === twitchUserId) + if (streamerIndex === -1) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_found_list"), flags: MessageFlags.Ephemeral }) + + // Récupérer le nom du streamer avant suppression + let streamerName = `ID: ${twitchUserId}` + try { + const user = await twitchClient.users.getUserById(twitchUserId) + if (user) streamerName = user.displayName + } catch { + logConsole('twitch', 'user_fetch_error_buttons', { id: twitchUserId }) + } + + // Supprimer le streamer + dbData.streamers.splice(streamerIndex, 1) + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + // Vérifier s'il faut supprimer les listeners + if (!await dbGuild.exists({ "guildTwitch.streamers.twitchUserId": twitchUserId })) { + try { + const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(twitchUserId) + await Promise.all(userSubs.data.map(async sub => { + if (sub.transportMethod === "webhook" && (sub.type === "stream.online" || sub.type === "stream.offline")) await sub.unsubscribe() + })) + logConsole('twitch', 'listener_removed', { name: streamerName, id: twitchUserId }) + } catch { + logConsole('twitch', 'listener_removal_error', { streamerName }) + } + } + + return interaction.update({ content: t(interaction.locale, "twitch.streamer_removed_success", { streamer: streamerName }), components: [] }) +} diff --git a/src/types/amp.ts b/src/types/amp.ts new file mode 100644 index 0000000..1463035 --- /dev/null +++ b/src/types/amp.ts @@ -0,0 +1,38 @@ +export interface Host { + AvailableInstances: Instance[] + FriendlyName: string +} + +export interface Instance { + InstanceID: string + FriendlyName: string + Running: boolean + Module: string + Port: number +} + +export interface InstanceFields { + name: string + value: string + inline: boolean +} + +export interface InstanceResult { + status: string + data: [Host] +} + +export interface LoginDetails { + username: string + password: string + remember?: boolean + otp?: string +} + +export interface LoginSuccessData { + userInfo: { + Username: string + } + sessionID: string + rememberMeToken: string +} diff --git a/src/types/freebox.ts b/src/types/freebox.ts new file mode 100644 index 0000000..6c16af3 --- /dev/null +++ b/src/types/freebox.ts @@ -0,0 +1,112 @@ +export interface APIResponseData { + success: boolean + result: Result +} +export interface APIResponseDataError extends APIResponseData { + error_code: string + msg: string +} +export interface APIResponseDataVersion { + api_version: string // The current API version on the Freebox + api_base_url: string // The API root path on the HTTP server + uid: string // The device unique id + api_domain: string // The domain to use in place of hardcoded Freebox ip + device_name: string // The device name + box_model: string // Box model + box_model_name: string // Box model display name + https_available: boolean // Tells if https has been configured on the Freebox + https_port: number // Port to use for remote https access to the Freebox Api +} + +// -------------- // +// AUTHENTICATION // +// -------------- // +export interface TokenRequest { + app_id: string + app_name: string + app_version: string + device_name: string +} + +export interface RequestAuthorization { + app_token: string + track_id: string +} + +export interface TrackAuthorizationProgress { + status: "unknown" // the app_token is invalid or has been revoked + | "pending" // the user has not confirmed the authorization request yet + | "timeout" // the user did not confirmed the authorization within the given time + | "granted" // the app_token is valid and can be used to open a session + | "denied" // the user denied the authorization request + + challenge: string +} + +export interface GetChallenge { + logged_in: boolean + challenge: string +} + +export interface OpenSession { + challenge: string + session_token?: string + permissions?: SessionPermissions +} +interface SessionPermissions { + settings?: boolean // Allow modifying the Freebox settings (reading settings is always allowed) + contacts?: boolean // Access to contact list + calls?: boolean // Access to call logs + explorer?: boolean // Access to filesystem + downloader?: boolean // Access to downloader + parental?: boolean // Access to parental control (obsolete) + pvr?: boolean // Access personal video recorder + profile?: boolean // Access to user profile management +} + +export interface SessionStart { + app_id: string + app_version: string + password: string +} +// -------------- // +// -------------- // +// -------------- // + +export interface ConnectionStatus { + state: "going_up" // connection is initializing + | "up" // connection is active + | "going_down" // connection is about to become inactive + | "down" // connection is inactive + + type: "ethernet" // FTTH/ethernet + | "rfc2684" // xDSL (unbundled) + | "pppoatm" // xDSL + + media: "ftth" // FTTH + | "ethernet" // Ethernet + | "xdsl" // xDSL + | "backup_4g" // Internet Backup + + ipv4: string // Freebox IPv4 address + ipv6: string // Freebox IPv6 address + rate_up: number // current upload rate in byte/s + rate_down: number // current download rate in byte/s + bandwidth_up: number // available upload bandwidth in bit/s + bandwidth_down: number // available download bandwidth in bit/s + bytes_up: number // total uploaded bytes since last connection + bytes_down: number // total downloaded bytes since last connection + ipv4_port_range: string // limited range of ports available for the Freebox to use for NAT +} + +export interface LcdConfig { + brightness?: number + orientation?: number + orientation_forced?: boolean + hide_wifi_key?: boolean + hide_status_led?: boolean + led_strip_enabled?: boolean + led_strip_brightness?: number + led_strip_animation?: string + available_led_strip_animations?: string[] +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..ddf59ae --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,73 @@ +import type { + SlashCommandBuilder, Guild, TextBasedChannel, VoiceChannel, + ChatInputCommandInteraction, AutocompleteInteraction, ButtonInteraction, + ChannelSelectMenuInteraction, MentionableSelectMenuInteraction, RoleSelectMenuInteraction, StringSelectMenuInteraction, UserSelectMenuInteraction +} from "discord.js" + +// Discord.js +declare module "discord.js" { + export interface Client { + disco: { interval: NodeJS.Timeout } + } +} + +export interface Command { + data: SlashCommandBuilder + execute: (interaction: ChatInputCommandInteraction) => unknown + autocompleteRun?: (interaction: AutocompleteInteraction) => unknown +} + +export interface CommandFolder { + name: string + commands: Command[] +} + +export interface Button { + id: string + execute: (interaction: ButtonInteraction) => unknown +} + +export interface ButtonFolder { + name: string + commands: Button[] +} + +export interface SelectMenu { + id: string + execute: (interaction: ChannelSelectMenuInteraction | MentionableSelectMenuInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | UserSelectMenuInteraction) => unknown +} + +export interface SelectMenuFolder { + name: string + commands: SelectMenu[] +} + +export interface Event { + name: string + once?: boolean + execute: (...args: unknown[]) => unknown +} + +export interface ChannelInferrable { + channel?: TextBasedChannel | VoiceChannel + guild?: Guild +} + +// Utils +export interface APIResponse { + result?: boolean + success?: boolean + [key: string]: unknown +} + +export interface ReturnMsgData { + status: string + error_code?: string + Title?: string + Message?: string +} + +export interface CrackGame { + name: string + link: string +} diff --git a/src/types/player.ts b/src/types/player.ts new file mode 100644 index 0000000..27f6804 --- /dev/null +++ b/src/types/player.ts @@ -0,0 +1,27 @@ +import { CommandInteraction } from "discord.js" +import type { GuildChannel } from "discord.js" +import type { ChannelInferrable } from "@/types" + +export class PlayerMetadata { + public constructor(public data: ChannelInferrable) { + if (!data.channel) throw new Error("PlayerMetadata can only be created from a channel") + if (data.channel.isDMBased()) throw new Error("PlayerMetadata cannot be created from a DM") + } + public get channel() { return this.data.channel } + public get guild() { return this.data.guild ?? (this.data.channel as GuildChannel).guild } + + public static create(data: ChannelInferrable | CommandInteraction) { + if (data instanceof CommandInteraction) { + if (!data.inGuild()) throw new Error("PlayerMetadata cannot be created from a DM") + if (!data.channel) throw new Error("PlayerMetadata can only be created from a channel") + if (!data.guild) throw new Error("PlayerMetadata can only be created from a guild") + return new PlayerMetadata({ channel: data.channel, guild: data.guild }) + } + return new PlayerMetadata(data) + } +} + +export interface TrackSearchResult { + name: string + value: string +} diff --git a/src/types/schemas.ts b/src/types/schemas.ts new file mode 100644 index 0000000..6c53587 --- /dev/null +++ b/src/types/schemas.ts @@ -0,0 +1,69 @@ +import type { Types } from "mongoose" + +export interface GuildSchema { + _id: Types.ObjectId + guildId: string + guildName: string + guildIcon: string + guildPlayer: GuildPlayer + guildAmp: GuildAmp + guildFbx: GuildFbx + guildTwitch: GuildTwitch +} + +export interface GuildPlayer { + disco: Disco + instances?: GuildPlayerInstance[] +} + +export interface Disco { + enabled: boolean + channelId?: string +} + +export interface GuildPlayerInstance { + botId: string + replay: Replay +} + +export interface Replay { + textChannelId?: string + voiceChannelId?: string + trackUrl?: string + progress?: number +} + +export interface GuildAmp { + enabled: boolean + host?: string + username?: string + sessionID?: string + rememberMeToken?: string +} + +export interface GuildFbx { + enabled: boolean + host?: string + appToken?: string + lcd?: GuildLcd +} + +export interface GuildLcd { + enabled: boolean + botId?: string + morningTime?: string + nightTime?: string +} + +export interface GuildTwitch { + enabled: boolean + botId?: string + channelId?: string + streamers: Streamer[] +} + +export interface Streamer { + twitchUserId: string + discordUserId?: string + messageId?: string +} diff --git a/src/utils/amp.ts b/src/utils/amp.ts index a976e0c..c2a1c4e 100644 --- a/src/utils/amp.ts +++ b/src/utils/amp.ts @@ -1,89 +1,57 @@ -import axios from 'axios' - -export interface LoginDetails { - username: string - password: string - remember?: boolean - otp?: string -} +import axios from "axios" +import type { AxiosResponse } from "axios" +import type { APIResponse } from "@/types" +import type { LoginDetails } from "@/types/amp" export const ADSModule = { - async GetInstances(host: string, SESSIONID: string) { - return await axios.post(host + '/API/ADSModule/GetInstances', { - SESSIONID - }).then(response => { - if (!Array.isArray(response.data)) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + async GetInstances(host: string, SESSIONID: string) { + return axios.post(host + "/API/ADSModule/GetInstances", { SESSIONID }) + .then((response: AxiosResponse) => { + if (!Array.isArray(response.data)) return { status: "fail", data: response.data } + return { status: "success", data: response.data } + }) + .catch((error: unknown) => { console.error(error); return { status: "error", data: error } }) }, - async ManageInstance(host: string, SESSIONID: string, InstanceId: string) { - return await axios.post(host + '/API/ADSModule/ManageInstance', { - SESSIONID, - InstanceId - }).then(response => { - if (!response.data.result) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + return axios.post(host + "/API/ADSModule/ManageInstance", { SESSIONID, InstanceId }) + .then((response: AxiosResponse) => { + if (!response.data.result) return { status: "fail", data: response.data } + return { status: "success", data: response.data } + }) + .catch((error: unknown) => { console.error(error); return { status: "error", data: error } }) }, - async RestartInstance(host: string, SESSIONID: string, InstanceName: string) { - return await axios.post(host + '/API/ADSModule/RestartInstance', { - SESSIONID, - InstanceName - }).then(response => { - //if (!response.data.success) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + return axios.post(host + "/API/ADSModule/RestartInstance", { SESSIONID, InstanceName }) + .then((response: AxiosResponse) => { + //if (!response.data.success) return { status: "fail", data: response.data } + return { status: "success", data: response.data } + }) + .catch((error: unknown) => { console.error(error); return { status: "error", data: error } }) }, - async Servers(host: string, SESSIONID: string, InstanceId: string) { - return await axios.get(host + '/API/ADSModule/Servers', { - data: { - SESSIONID, - InstanceId - } - }).then(response => { - if (!response.data.result) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + return axios.get(host + "/API/ADSModule/Servers", { data: { SESSIONID, InstanceId } }) + .then((response: AxiosResponse) => { + if (!response.data.result) return { status: "fail", data: response.data } + return { status: "success", data: response.data } + }) + .catch((error: unknown) => { console.error(error); return { status: "error", data: error } }) } } - export const Core = { async Login(host: string, details: LoginDetails) { - return await axios.post(host + '/API/Core/Login', - details - ).then(response => { - if (!response.data.success) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + return axios.post(host + "/API/Core/Login", details) + .then((response: AxiosResponse) => { + if (!response.data.success) return { status: "fail", data: response.data } + return { status: "success", data: response.data } + }) + .catch((error: unknown) => { console.error(error); return { status: "error", data: error } }) } } - export async function CheckSession(host: string, SESSIONID: string) { - return await axios.post(host + '/API/ADSModule/GetInstances', { - SESSIONID - }).then(response => { - if (!Array.isArray(response.data)) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) -} \ No newline at end of file + return axios.post(host + "/API/ADSModule/GetInstances", { SESSIONID }) + .then((response: AxiosResponse) => { + if (!Array.isArray(response.data)) return { status: "fail", data: response.data } + return { status: "success", data: response.data } + }) + .catch((error: unknown) => { console.error(error); return { status: "error", data: error } }) +} diff --git a/src/utils/console.ts b/src/utils/console.ts new file mode 100644 index 0000000..f1e2fef --- /dev/null +++ b/src/utils/console.ts @@ -0,0 +1,54 @@ +import chalk from "chalk" +import { t, CONSOLE_LOCALE } from "./i18n" + +/** + * Fonction utilitaire pour les logs console localisés avec couleurs + * @param service - Le service (discordjs, discord_player, mongoose, twitch) + * @param key - La clé de traduction sans le préfixe "console.{service}." + * @param params - Les paramètres à remplacer dans le message + */ +export function logConsole(service: 'discordjs' | 'discord_player' | 'mongoose' | 'twitch' | 'freebox', key: string, params?: Record) { + const locale = CONSOLE_LOCALE // Utilise la locale configurée pour les logs console + const fullKey = `console.${service}.${key}` + const message = t(locale, fullKey, params) + + // Couleurs selon le service + switch (service) { + case 'discordjs': + console.log(chalk.blue(message)) + break + case 'discord_player': + console.log(chalk.cyan(message)) + break + case 'mongoose': + console.log(chalk.green(message)) + break + case 'twitch': + console.log(chalk.magenta(message)) + break + case 'freebox': + console.log(chalk.yellow(message)) + break + default: + console.log(message) + } +} + +/** + * Fonction pour les logs d'erreur console + * @param service - Le service + * @param key - La clé de traduction + * @param params - Les paramètres + * @param error - L'erreur à logger après le message + */ +export function logConsoleError(service: 'discordjs' | 'discord_player' | 'mongoose' | 'twitch' | 'freebox', key: string, params?: Record, error?: Error) { + logConsole(service, key, params) + if (error) console.error(error) +} + +/** + * Log conditionnel en mode développement + */ +export function logConsoleDev(service: 'discordjs' | 'discord_player' | 'mongoose' | 'twitch' | 'freebox', key: string, params?: Record) { + if (process.env.NODE_ENV === "development") logConsole(service, key, params) +} diff --git a/src/utils/crack.ts b/src/utils/crack.ts index ee61dce..c816987 100644 --- a/src/utils/crack.ts +++ b/src/utils/crack.ts @@ -1,98 +1,102 @@ -import parseTorrent, { toMagnetURI } from 'parse-torrent' -import iconv from 'iconv-lite' -import axios from 'axios' -import path from 'path' -import fs from 'fs' - -export interface Game { - name: string - link: string -} +import parseTorrent, { toMagnetURI } from "parse-torrent" +import axios from "axios" +import iconv from "iconv-lite" +import type { AxiosResponse } from "axios" +import type { Readable } from "stream" +import { createWriteStream, readFileSync } from "fs" +import { join } from "path" +import type { CrackGame } from "@/types" const headers = { h1: { - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "content-type": "application/x-www-form-urlencoded charset=UTF-8", "x-requested-with": "XMLHttpRequest" }, h2: { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", + "accept": "text/html,application/xhtml+xml,application/xmlq=0.9,image/avif,image/webp,image/apng,*/*q=0.8,application/signed-exchangev=b3q=0.7", + "accept-language": "fr-FR,frq=0.9,en-USq=0.8,enq=0.7", "cache-control": "no-cache", "pragma": "no-cache", - "sec-ch-ua": "\"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"110\", \"Opera GX\";v=\"96\"", + "sec-ch-ua": '"Not=A?Brand"v="8", "Chromium"v="110", "Opera GX"v="96"', "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", + "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "none", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1", - "cookie": "online_fix_auth=gAAAAABkKM0s9WNLe_V6euTnJD7UQjppVty9B7OOyHBYOyVcbcejj8F6KveBcLxlf3mlx_vE7JEFPHlrpj-Aq6BFJyKPGzxds_wpcPV2MdXPyDGQLsz4mAvt3qgTgGg25MapWo_fSIOMiAAsF4Gv_uh4kUOiR_jgbHCZWJGPgpNQwU2HFFyvahYR6MzR7nYE9-fCmrev3obkRbro43vIVTTX4UyJMRHadrsY5Q-722TzinCZVmAuJfc=; dle_password=89465c26673e0199e5272e4730772c35; _ym_uid=1670534560361937997; _ym_d=1680394955; _ym_isad=2; dle_user_id=2619796; PHPSESSID=3v8sd281sr0n1n9f1p66q25sa2", + "cookie": "online_fix_auth=gAAAAABkKM0s9WNLe_V6euTnJD7UQjppVty9B7OOyHBYOyVcbcejj8F6KveBcLxlf3mlx_vE7JEFPHlrpj-Aq6BFJyKPGzxds_wpcPV2MdXPyDGQLsz4mAvt3qgTgGg25MapWo_fSIOMiAAsF4Gv_uh4kUOiR_jgbHCZWJGPgpNQwU2HFFyvahYR6MzR7nYE9-fCmrev3obkRbro43vIVTTX4UyJMRHadrsY5Q-722TzinCZVmAuJfc= dle_password=89465c26673e0199e5272e4730772c35 _ym_uid=1670534560361937997 _ym_d=1680394955 _ym_isad=2 dle_user_id=2619796 PHPSESSID=3v8sd281sr0n1n9f1p66q25sa2", "Referer": "https://online-fix.me/", "Referrer-Policy": "strict-origin-when-cross-origin" } } export async function search(query: string) { - let body = await fetch("https://online-fix.me/engine/ajax/search.php", { headers: headers.h1, body: `query=${query}`, method: "POST" }) + const body = await fetch("https://online-fix.me/engine/ajax/search.php", { headers: headers.h1, body: `query=${query}`, method: "POST" }) .then(response => response.arrayBuffer()) - .then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), 'win1251') }) + .then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), "win1251") }) .catch(console.error) + try { if (!body) return - let matches = body.split('')[1].split('')[0].split('') - let games = [] as Game[] + const matches = body.split("")[1].split('')[0].split("") + const games = [] as CrackGame[] matches.pop() - matches.forEach(async match => { - let name = match.split('">')[1].split('')[0].slice(0, -8) - let link = match.split('')[0] + + for (const match of matches) { + const name = match.split('">')[1].split("")[0].slice(0, -8) + const link = match.split('')[0] games.push({ name, link }) - }) + } return games - } catch (error) { return error } + } catch (error) { console.error(error) } } -export async function repo(game: Game) { - let body = await fetch(game.link, { headers: headers.h2, body: null, method: "GET" }) +export async function repo(game: CrackGame) { + const body = await fetch(game.link, { headers: headers.h2, body: null, method: "GET" }) .then(response => response.arrayBuffer()) - .then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), 'win1251') }) + .then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), "win1251") }) .catch(console.error) + try { if (!body) return - let name = body.split('https://uploads.online-fix.me:2053/torrents/')[1].split('"')[0] - let url = `https://uploads.online-fix.me:2053/torrents/${name}` + const name = body.split("https://uploads.online-fix.me:2053/torrents/")[1].split('"')[0] + const url = `https://uploads.online-fix.me:2053/torrents/${name}` return url } catch (error) { console.error(error) } } export async function torrent(url: string) { - let response = await fetch(url, { headers: headers.h2, body: null, method: "GET" }).catch(console.error) + const response = await fetch(url, { headers: headers.h2, body: null, method: "GET" }) + .catch(console.error) + try { if (!response) return - let body = await response.text() - let file = body.split('')[0] + const body = await response.text() + const file = body.split('')[0] return file } catch (error) { console.error(error) } } export async function download(url: string, file: string) { - let filePath = path.join(__dirname, '../../public/cracks/', file) - let writer = fs.createWriteStream(filePath) + const filePath = join(__dirname, "@/public/cracks/", file) + const writer = createWriteStream(filePath) try { - await axios({ url: url + file, method: 'GET', responseType: 'stream', headers: headers.h2 }).then(response => { - return new Promise((resolve, reject) => { - response.data.pipe(writer) - let error = null as unknown as Error - writer.on('error', err => { error = err; writer.close(); reject(err) }) - writer.on('close', () => { if (!error) resolve(true) }) + await axios({ url: url + file, method: "GET", responseType: "stream", headers: headers.h2 }) + .then((response: AxiosResponse) => { + return new Promise((resolve, reject) => { + response.data.pipe(writer) + writer.on("error", err => { writer.close(); reject(err) }) + writer.on("close", () => { resolve(true) }) + }) }) - }).catch(console.error) + .catch(console.error) return filePath } catch (error) { console.error(error) } } -export async function magnet(filePath: string) { - let torrentData = parseTorrent(fs.readFileSync(filePath)) - let uri = toMagnetURI(torrentData) +export function magnet(filePath: string) { + const torrentData = parseTorrent(readFileSync(filePath)) + const uri = toMagnetURI(torrentData) return uri -} \ No newline at end of file +} diff --git a/src/utils/dbGuildInit.ts b/src/utils/dbGuildInit.ts index 432b305..9bf0004 100644 --- a/src/utils/dbGuildInit.ts +++ b/src/utils/dbGuildInit.ts @@ -1,22 +1,26 @@ -import { Guild } from 'discord.js' -import { Types } from 'mongoose' -import dbGuild from '../schemas/guild' +import { Guild } from "discord.js" +import { Types } from "mongoose" +import dbGuild from "@/schemas/guild" +import { logConsole } from "@/utils/console" export default async (guild: Guild) => { - let guildProfile = new dbGuild({ + logConsole('mongoose', 'guild_init', { name: guild.name, id: guild.id }) + + const guildProfile = new dbGuild({ _id: new Types.ObjectId(), guildId: guild.id, guildName: guild.name, - guildIcon: guild.iconURL() ?? 'None', + guildIcon: guild.iconURL() ?? "None", guildPlayer: { - replay: { enabled: false }, disco: { enabled: false } }, - guildRss: { enabled: false, feeds: [] }, guildAmp: { enabled: false }, - guildFbx: { enabled: false }, + guildFbx: { enabled: false, + lcd: { enabled: false } + }, guildTwitch: { enabled: false } }) + await guildProfile.save().catch(console.error) return guildProfile -} \ No newline at end of file +} diff --git a/src/utils/freebox.ts b/src/utils/freebox.ts index 6c1aec1..c5a6a55 100644 --- a/src/utils/freebox.ts +++ b/src/utils/freebox.ts @@ -1,79 +1,192 @@ -import axios from 'axios' -import https from 'https' +import axios from "axios" +import type { AxiosError } from "axios" +import crypto from "crypto" +import { MessageFlags } from "discord.js" +import type { ChatInputCommandInteraction, ButtonInteraction, Client } from "discord.js" +import type { + APIResponseData, APIResponseDataError, APIResponseDataVersion, + ConnectionStatus, GetChallenge, LcdConfig, OpenSession, RequestAuthorization, TokenRequest, TrackAuthorizationProgress +} from "@/types/freebox" +import type { GuildFbx } from "@/types/schemas" +import { t } from "@/utils/i18n" -export interface App { - app_id: string - app_name: string - app_version: string - device_name: string +const app: TokenRequest = { + app_id: "fr.angels-dev.tamiseur", + app_name: "Bot Tamiseur", + app_version: process.env.npm_package_version ?? "1.0.0", + device_name: "Bot Discord NodeJS", } export const Core = { - async Version(host: string, httpsAgent: https.Agent) { - let request = axios.get(host + '/api_version', { httpsAgent }) - - return await request.then(response => { - if (response.status !== 200) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + async Version(host: string) { + try { + const res = await axios.get(host + `/api_version`) + return res.data + } catch (error) { return (error as AxiosError).response?.data } }, - async Init(host: string, version: number, httpsAgent: https.Agent, app: App, trackId: string) { + async Init(host: string, trackId?: string) { let request - - if (trackId) request = axios.get(host + `/api/v${version}/login/authorize/` + trackId, { httpsAgent }) - else request = axios.post(host + `/api/v${version}/login/authorize/`, app, { httpsAgent }) - - return await request.then(response => { - if (!response.data.success) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data.result } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + if (trackId) request = axios.get>(host + `/api/v8/login/authorize/` + trackId) + else request = axios.post>(host + `/api/v8/login/authorize/`, app) + + try { + const res = await request + return res.data + } catch (error) { return (error as AxiosError).response?.data } } } export const Login = { - async Challenge(host: string, version: number, httpsAgent: https.Agent) { - let request = axios.get(host + `/api/v${version}/login/`, { httpsAgent }) - - return await request.then(response => { - console.log(response.data) - if (response.status !== 200) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data.result } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + async Challenge(host: string) { + try { + const res = await axios.get>(host + `/api/v8/login/`) + return res.data + } catch (error) { return (error as AxiosError).response?.data } }, - async Session(host: string, version: number, httpsAgent: https.Agent, app_id: string, password: string) { - let request = axios.post(host + `/api/v${version}/login/session/`, { app_id, password }, { httpsAgent }) - - return await request.then(response => { - console.log(response.data) - if (response.status !== 200) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data.result } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + async Session(host: string, password: string) { + try { + const res = await axios.post>(host + `/api/v8/login/session/`, { app_id: app.app_id, password }) + return res.data + } catch (error) { return (error as AxiosError).response?.data } } } export const Get = { - async Connection(host: string, version: number, httpsAgent: https.Agent, sessionToken: string) { - let request = axios.get(host + `/api/v${version}/connection/`, { httpsAgent, headers: { 'X-Fbx-App-Auth': sessionToken } }) - - return await request.then(response => { - console.log(response.data) - if (!response.data.success) return { status: 'fail', data: response.data } - return { status: 'success', data: response.data.result } - }).catch(error => { - console.error(error) - return { status: 'error', data: error } - }) + async Connection(host: string, sessionToken: string) { + try { + const res = await axios.get>(host + `/api/v11/connection/`, { headers: { "X-Fbx-App-Auth": sessionToken } }) + return res.data + } catch (error) { return (error as AxiosError).response?.data } + }, + async LcdConfig(host: string, sessionToken: string) { + try { + const res = await axios.get>(host + `/api/v8/lcd/config/`, { headers: { "X-Fbx-App-Auth": sessionToken } }) + return res.data + } catch (error) { return (error as AxiosError).response?.data } } -} \ No newline at end of file +} + +export const Set = { + async LcdConfig(host: string, sessionToken: string, config: LcdConfig) { + try { + const res = await axios.put>(host + `/api/v8/lcd/config/`, config, { headers: { "X-Fbx-App-Auth": sessionToken } }) + return res.data + } catch (error) { return (error as AxiosError).response?.data } + } +} + +export async function handleError(error: APIResponseDataError, interaction: ChatInputCommandInteraction | ButtonInteraction, reply = true) { + if (reply) return await interaction.reply({ content: t(interaction.locale, `freebox.error.${error.error_code}`), flags: MessageFlags.Ephemeral }) + else return await interaction.followUp({ content: t(interaction.locale, `freebox.error.${error.error_code}`), flags: MessageFlags.Ephemeral }) +} + +// Stockage des timers actifs pour chaque guild +const activeTimers = new Map() + +// Gestion du timer LCD +export const Timer = { + // Fonction pour programmer le timer LCD Freebox + schedule(client: Client, guildId: string, dbData: GuildFbx) { + const lcd = dbData.lcd + if (!lcd?.morningTime || !lcd.nightTime) return + + // Nettoyer les anciens timers pour cette guild + Timer.clear(guildId) + + const now = new Date() + const [morningHour, morningMinute] = lcd.morningTime.split(':').map(Number) + const [nightHour, nightMinute] = lcd.nightTime.split(':').map(Number) + + // Calculer le prochain allumage (matin) + const nextMorning = new Date(now.getFullYear(), now.getMonth(), now.getDate(), morningHour, morningMinute, 0, 0) + if (nextMorning <= now) nextMorning.setDate(nextMorning.getDate() + 1) + + // Calculer la prochaine extinction (nuit) + const nextNight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), nightHour, nightMinute, 0, 0) + if (nextNight <= now) nextNight.setDate(nextNight.getDate() + 1) + + // Programmer l'allumage le matin + const morningDelay = nextMorning.getTime() - now.getTime() + const morningTimer = setTimeout(() => { + void Timer.controlLeds(guildId, dbData, true) + // Reprogrammer pour le cycle suivant + Timer.schedule(client, guildId, dbData) + }, morningDelay) + + // Programmer l'extinction le soir + const nightDelay = nextNight.getTime() - now.getTime() + const nightTimer = setTimeout(() => { + void Timer.controlLeds(guildId, dbData, false) + // Note: pas besoin de reprogrammer ici car le timer du matin s'en charge + }, nightDelay) + + // Stocker les références des timers + activeTimers.set(guildId, { morning: morningTimer, night: nightTimer }) + + console.log(`[Freebox LCD] Timer programmé pour ${guildId} - Allumage: ${nextMorning.toLocaleString()}, Extinction: ${nextNight.toLocaleString()}`) + }, + + // Fonction utilitaire pour calculer la prochaine occurrence d'une heure donnée + getNextOccurrence(now: Date, hour: number, minute: number): Date { + const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0) + + // Si l'heure cible est déjà passée aujourd'hui, programmer pour demain + if (target <= now) { + target.setDate(target.getDate() + 1) + } + + return target + }, + + // Fonction pour nettoyer les timers d'une guild + clear(guildId: string) { + const timers = activeTimers.get(guildId) + if (timers) { + if (timers.morning) clearTimeout(timers.morning) + if (timers.night) clearTimeout(timers.night) + activeTimers.delete(guildId) + console.log(`[Freebox LCD] Timers nettoyés pour ${guildId}`) + } + }, + + // Fonction pour nettoyer tous les timers (utile lors de l'arrêt du bot) + clearAll() { + for (const [guildId] of activeTimers) { + Timer.clear(guildId) + } + console.log(`[Freebox LCD] Tous les timers ont été nettoyés`) + }, + + // Fonction pour contrôler les LEDs + async controlLeds(guildId: string, dbDataFbx: GuildFbx, enabled: boolean) { + if (!dbDataFbx.host || !dbDataFbx.appToken) { + console.error(`[Freebox LCD] Configuration manquante pour le serveur ${guildId}`) + return + } + + try { + // Obtenir le challenge + const challengeData = await Login.Challenge(dbDataFbx.host) as APIResponseData + if (!challengeData.success) { console.error(`[Freebox LCD] Erreur lors de la récupération du challenge pour ${guildId}`); return } + + const challenge = challengeData.result.challenge + if (!challenge) { console.error(`[Freebox LCD] Challenge introuvable pour ${guildId}`); return } + + // Créer la session + const password = crypto.createHmac("sha1", dbDataFbx.appToken).update(challenge).digest("hex") + const sessionData = await Login.Session(dbDataFbx.host, password) as APIResponseData + if (!sessionData.success) { console.error(`[Freebox LCD] Erreur lors de la création de la session pour ${guildId}`); return } + + const sessionToken = sessionData.result.session_token + if (!sessionToken) { console.error(`[Freebox LCD] Token de session introuvable pour ${guildId}`); return } + + // Contrôler les LEDs + const lcdData = await Set.LcdConfig(dbDataFbx.host, sessionToken, { led_strip_enabled: enabled }) as APIResponseData + if (!lcdData.success) { console.error(`[Freebox LCD] Erreur lors du contrôle des LEDs pour ${guildId}:`, lcdData); return } + + console.log(`[Freebox LCD] LEDs ${enabled ? 'allumées' : 'éteintes'} avec succès pour ${guildId}`) + } catch (error) { + console.error(`[Freebox LCD] Erreur lors du contrôle des LEDs pour ${guildId}:`, error) + } + } +} diff --git a/src/utils/getUptime.ts b/src/utils/getUptime.ts deleted file mode 100755 index 89c25f9..0000000 --- a/src/utils/getUptime.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Client } from 'discord.js' - -export default function (uptime: Client["uptime"]) { - if (!uptime) return '0J, 0H, 0M et 0S' - let days = Math.floor(uptime / 86400000) - let hours = Math.floor(uptime / 3600000) % 24 - let minutes = Math.floor(uptime / 60000) % 60 - let seconds = Math.floor(uptime / 1000) % 60 - return `${days}J, ${hours}H, ${minutes}M et ${seconds}S` -} \ No newline at end of file diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..f079343 --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,111 @@ +import type { Locale } from "discord.js" +import frLocale from "@/locales/fr.json" +import enLocale from "@/locales/en.json" + +// Variables d'environnement pour les locales avec valeurs par défaut +const DEFAULT_LOCALE = process.env.DEFAULT_LOCALE ?? 'fr' +const CONSOLE_LOCALE = process.env.CONSOLE_LOCALE ?? 'en' + +// Types pour l'internationalisation +type LocaleData = Record +type ReplacementParams = Record +type TranslationKey = string + +// Conversion des imports en type LocaleData +const frLocaleData = frLocale as unknown as LocaleData +const enLocaleData = enLocale as unknown as LocaleData + +// Locales supportées +const locales = { + 'fr': frLocaleData, + 'en-US': enLocaleData, + 'en-GB': enLocaleData +} as const + +type SupportedLocale = keyof typeof locales + +/** + * Récupère une traduction basée sur la locale et la clé + */ +function getNestedValue(obj: LocaleData, path: string): string | undefined { + try { + const keys = path.split('.') + let current: unknown = obj + + for (const key of keys) { + if (current !== null && typeof current === 'object' && key in current) current = (current as Record)[key] + else return undefined + } + + return typeof current === 'string' ? current : undefined + } catch { return undefined } +} + +/** + * Remplace les paramètres dans une chaîne de caractères + * Exemple: "Hello {name}" avec {name: "World"} devient "Hello World" + */ +function replaceParams(text: string, params: ReplacementParams = {}): string { + return text.replace(/\{(\w+)\}/g, (match, key: string) => { + if (Object.prototype.hasOwnProperty.call(params, key)) return params[key].toString() + return match + }) +} + +/** + * Fonction principale de localisation + * @param locale - La locale Discord de l'utilisateur + * @param key - La clé de traduction (ex: "player.not_in_voice") + * @param params - Les paramètres à remplacer dans la traduction + * @returns La chaîne traduite + */ +export function t(locale: Locale | string, key: TranslationKey, params: ReplacementParams = {}): string { + // Détermine la locale à utiliser (par défaut de la config) + const supportedLocale: SupportedLocale = (Object.keys(locales).includes(locale)) ? locale as SupportedLocale : DEFAULT_LOCALE as SupportedLocale + + // Récupère les données de locale + const localeData = locales[supportedLocale] + + // Récupère la traduction + const translation = getNestedValue(localeData, key) + + if (!translation) { + console.warn(`[Localization] Translation not found for key: ${key} in locale: ${supportedLocale}`) + return key // Retourne la clé si la traduction n'est pas trouvée + } + + // Remplace les paramètres et retourne la traduction + return replaceParams(translation, params) +} + +/** + * Fonction helper pour obtenir la locale française par défaut + */ +export function fr(key: TranslationKey, params: ReplacementParams = {}): string { + return t('fr', key, params) +} + +/** + * Fonction helper pour obtenir la locale anglaise + */ +export function en(key: TranslationKey, params: ReplacementParams = {}): string { + return t('en-US', key, params) +} + +/** + * Obtient les locales supportées pour une commande + * Utilisé pour les localisations des commandes slash + */ +export function getCommandLocalizations(baseKey: string) { + return { + fr: getNestedValue(frLocaleData, baseKey) ?? baseKey, + 'en-US': getNestedValue(enLocaleData, baseKey) ?? baseKey, + 'en-GB': getNestedValue(enLocaleData, baseKey) ?? baseKey + } +} + +// Export des constantes de locale +export { DEFAULT_LOCALE, CONSOLE_LOCALE } + +// Export des types pour utilisation externe +export type { TranslationKey, ReplacementParams, SupportedLocale } diff --git a/src/utils/player.ts b/src/utils/player.ts index b0fd5b0..2359124 100644 --- a/src/utils/player.ts +++ b/src/utils/player.ts @@ -1,178 +1,118 @@ -import { Client, TextChannel, CommandInteraction, Guild, GuildChannel, TextBasedChannel, VoiceChannel, EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonInteraction } from 'discord.js' -import { useMainPlayer, useQueue } from 'discord-player' -import { Document } from 'mongoose' -import getUptime from './getUptime' +import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, MessageFlags, ComponentType } from "discord.js" +import type { ButtonInteraction, Client, Guild, GuildChannel, Locale } from "discord.js" +import { useMainPlayer, useQueue } from "discord-player" +import type { GuildPlayer, Disco } from "@/types/schemas" +import type { PlayerMetadata } from "@/types/player" +import uptime from "./uptime" +import dbGuild from "@/schemas/guild" +import { t } from "./i18n" +import { logConsole } from "./console" -type ChannelInferrable = { - channel: TextBasedChannel | VoiceChannel - guild?: Guild -} +const progressIntervals = new Map() -export class PlayerMetadata { - public constructor(public data: ChannelInferrable) { - if (data.channel.isDMBased()) { throw new Error('PlayerMetadata cannot be created from a DM') } - if (!data.channel) { throw new Error('PlayerMetadata can only be created from a channel') } +export function startProgressSaving(guildId: string, botId: string) { + if (!guildId || !botId) { logConsole('discord_player', 'progress_saving.missing_ids'); return } + + logConsole('discord_player', 'progress_saving.start', { guildId, botId }) + + const key = `${guildId}-${botId}` + if (progressIntervals.has(key)) { + clearInterval(progressIntervals.get(key)) + progressIntervals.delete(key) } - public get channel() { return this.data.channel! } - public get guild() { return this.data.guild || (this.data.channel as GuildChannel).guild } - public static create(data: ChannelInferrable | CommandInteraction) { - if (data instanceof CommandInteraction) { - if (!data.inGuild()) { throw new Error('PlayerMetadata cannot be created from a DM') } + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const interval = setInterval(async () => { + try { + const queue = useQueue(guildId) + if (!queue || !queue.isPlaying() || !queue.currentTrack) { startProgressSaving(guildId, botId); return } - return new PlayerMetadata({ channel: data.channel!, guild: data.guild! }) - } - return new PlayerMetadata(data); - } -} + const guildProfile = await dbGuild.findOne({ guildId }) + if (!guildProfile) { await stopProgressSaving(guildId, botId); return } -export const bots = ['1065047326860783636', '1119343522059927684', '1119344050412204032', '1210714000321548329', '660961595006124052'] -export const playerButtons = ['loop', 'pause', 'previous', 'resume', 'shuffle', 'skip', 'stop', 'volume_down', 'volume_up'] + const dbData = guildProfile.get("guildPlayer") as GuildPlayer + dbData.instances ??= [] -export async function playerDisco (client: Client, guildProfile: Document) { - try { - let guild = client.guilds.cache.get(guildProfile.get('guildId')) - if (!guild) { - clearInterval(client.disco.interval) - return 'clear' - } + const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId) + if (instanceIndex === -1) { + dbData.instances.push({ botId, replay: { + textChannelId: (queue.metadata as PlayerMetadata).channel?.id ?? "", + voiceChannelId: queue.connection?.joinConfig.channelId ?? "", + trackUrl: queue.currentTrack.url, + progress: queue.node.playbackTime + } }) + } else { + dbData.instances[instanceIndex].replay.trackUrl = queue.currentTrack.url + dbData.instances[instanceIndex].replay.progress = queue.node.playbackTime + } - let dbData = guildProfile.get('guildPlayer.disco') - let queue = useQueue(guild.id) - if (queue) if (queue.isPlaying()) { - dbData['progress'] = queue.node.playbackTime.toString() - - guildProfile.set('guildPlayer.disco', dbData) - guildProfile.markModified('guildPlayer.disco') + guildProfile.set("guildPlayer", dbData) + guildProfile.markModified("guildPlayer") await guildProfile.save().catch(console.error) + } catch (error) { + logConsole('discord_player', 'progress_saving.error', { guildId, botId }) + console.error(error) + await stopProgressSaving(guildId, botId) } + }, 3000) - let channel = client.channels.cache.get(dbData.channelId) as TextChannel - if (!channel) { - console.log(`Aucun channel trouvé avec l'id \`${dbData.channelId}\`, veuillez utiliser la commande \`/database edit 'value': guildPlayer.disco.channelId\` !`) - clearInterval(client.disco.interval) - return 'clear' + progressIntervals.set(key, interval) +} + +export async function stopProgressSaving(guildId: string, botId: string) { + if (!guildId || !botId) { logConsole('discord_player', 'progress_saving.missing_ids'); return } + + logConsole('discord_player', 'progress_saving.stop', { guildId, botId }) + + const key = `${guildId}-${botId}` + if (progressIntervals.has(key)) { + clearInterval(progressIntervals.get(key)) + progressIntervals.delete(key) + } + + const guildProfile = await dbGuild.findOne({ guildId: guildId }) + if (!guildProfile) { logConsole('discord_player', 'progress_saving.database_not_exist'); return } + + const dbData = guildProfile.get("guildPlayer") as GuildPlayer + if (dbData.instances) { + const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId) + if (instanceIndex === -1) return + + dbData.instances[instanceIndex].replay = { + textChannelId: "", + voiceChannelId: "", + trackUrl: "", + progress: 0 } - - let { embed, components } = await playerGenerate(guild) - if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)} \n ${embed.data.footer.text}` }) - else embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)}` }) - - let messages = await channel.messages.fetch() - messages.forEach(msg => { if (!bots.includes(msg.author.id)) msg.delete() }) - - let botMessage = messages.find(msg => client.user && msg.author.id === client.user.id) - if (botMessage) { - if (!components && botMessage.components.length > 0) { - await botMessage.delete() - return channel.send({ embeds: [embed] }) - - } else if (components) return botMessage.edit({ embeds: [embed], components }) - - else return botMessage.edit({ embeds: [embed] }) - - } else return channel.send({ embeds: [embed] }) - } catch (error) { - console.error(error); - return 'clear' + + guildProfile.set("guildPlayer", dbData) + guildProfile.markModified("guildPlayer") + await guildProfile.save().catch(console.error) } } -export async function playerEdit (interaction: ButtonInteraction) { - let guild = interaction.guild - if (!guild) return await interaction.reply({ content: 'Cette commande n\'est pas disponible en message privé.', ephemeral: true }) - - let { components } = await playerGenerate(guild) - if (!components) return - - components.forEach((actionRow) => actionRow.components.forEach((button) => button.setDisabled(true))) - await interaction.update({ components }) -} - -export async function playerGenerate (guild: Guild) { - let embed = new EmbedBuilder().setColor('#ffc370') - - let queue = useQueue(guild.id) - if (!queue) { - embed.setTitle('Aucune session d\'écoute en cours !') - return ({ embed, components: null }) +export async function playerReplay(client: Client, dbData: GuildPlayer) { + const botId = client.user?.id ?? "" + const instance = dbData.instances?.find(instance => instance.botId === botId) + if (!instance) { logConsole('discordjs', 'replay.no_data', { botId }); return } + + if (!instance.replay.textChannelId) { logConsole('discordjs', 'replay.no_text_channel_id', { botId }); return } + if (!instance.replay.voiceChannelId) { logConsole('discordjs', 'replay.no_voice_channel_id', { botId }); return } + + const textChannel = await client.channels.fetch(instance.replay.textChannelId) + if (!textChannel || (textChannel.type !== ChannelType.GuildText && textChannel.type !== ChannelType.GuildAnnouncement)) { + logConsole('discordjs', 'replay.text_channel_not_found', { channelId: instance.replay.textChannelId, botId }) + return } - let track = queue.currentTrack - if (!track) { - embed.setTitle('Aucune musique en cours de lecture !') - return ({ embed, components: null }) + const voiceChannel = await client.channels.fetch(instance.replay.voiceChannelId) + if (!voiceChannel || (voiceChannel.type !== ChannelType.GuildVoice && voiceChannel.type !== ChannelType.GuildStageVoice)) { + logConsole('discordjs', 'replay.voice_channel_not_found', { channelId: instance.replay.textChannelId, botId }) + return } - embed.setTitle(track.title) - .setAuthor({ name: track.author }) - .setURL(track.url) - .setImage(track.thumbnail) - .addFields( - { name: 'Durée', value: track.duration, inline: true }, - { name: 'Source', value: track.source === 'youtube' ? 'Youtube' : track.source === 'spotify' ? 'Spotify' : 'Inconnu', inline: true }, - { name: 'Volume', value: `${queue.node.volume}%`, inline: true }, - { name: queue.node.isPaused() ? 'Progression (en pause)' : 'Progression', value: queue.node.createProgressBar() || 'Aucune' }, - { name: 'Loop', value: queue.repeatMode === 3 ? 'Autoplay' : queue.repeatMode === 2 ? 'File d\'Attente' : queue.repeatMode === 1 ? 'Titre' : 'Off', inline: true } - ) - .setDescription(`**Musique suivante :** ${queue.tracks.data[0] ? queue.tracks.data[0].title : 'Aucune'}`) - .setFooter({ text: `Demandé par ${track.requestedBy ? track.requestedBy.tag : 'Inconnu'}` }) - - let components = [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel(queue.node.isPaused() ? '▶️' : '⏸️') - .setStyle(2) - .setCustomId(queue.node.isPaused() ? 'resume' : 'pause'), - new ButtonBuilder() - .setLabel('⏹️') - .setStyle(2) - .setCustomId('stop'), - new ButtonBuilder() - .setLabel('⏭️') - .setStyle(2) - .setCustomId('skip') - .setDisabled(queue.tracks.data.length !== 0), - new ButtonBuilder() - .setLabel('🔉') - .setStyle(2) - .setCustomId('volume_down') - .setDisabled(queue.node.volume === 0), - new ButtonBuilder() - .setLabel('🔊') - .setStyle(2) - .setCustomId('volume_up') - .setDisabled(queue.node.volume === 100) - ), - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel('🔀') - .setStyle(2) - .setCustomId('shuffle'), - new ButtonBuilder() - .setLabel('🔁') - .setStyle(2) - .setCustomId('loop'), - new ButtonBuilder() - .setLabel('⏮️') - .setStyle(2) - .setCustomId('previous') - .setDisabled(queue.history.previousTrack ? false : true) - ) - ] - return ({ embed, components }) -} - -export async function playerReplay (client: Client, guildProfile: Document) { - let dbData = guildProfile.get('guildPlayer.replay') - - let textChannel = client.channels.cache.get(dbData.textChannelId) as TextChannel - if (!textChannel) return console.log(`Aucun channel trouvé avec l'id \`${dbData.textChannelId}\`, veuillez utiliser la commande \`/setchannel\` !`) - let voiceChannel = client.channels.cache.get(dbData.voiceChannelId) as VoiceChannel - if (!voiceChannel) return console.log(`Aucun channel trouvé avec l'id \`${dbData.voiceChannelId}\`, veuillez utiliser la commande \`/setchannel\` !`) - - let player = useMainPlayer() - let queue = player.nodes.create(textChannel.guild, { + const player = useMainPlayer() + const queue = player.nodes.create((textChannel as GuildChannel).guild, { metadata: { channel: textChannel, client: textChannel.guild.members.me, @@ -188,19 +128,193 @@ export async function playerReplay (client: Client, guildProfile: Document) { try { if (!queue.connection) await queue.connect(voiceChannel) } catch (error) { console.error(error) } + + if (!instance.replay.trackUrl) return - let result = await player.search(dbData.trackUrl as string, { requestedBy: client.user || undefined }) - if (!result.hasTracks()) await textChannel.send(`Aucune musique trouvée pour **${dbData.trackUrl}** !`) - let track = result.tracks[0] - - let entry = queue.tasksQueue.acquire() + const result = await player.search(instance.replay.trackUrl, { requestedBy: client.user ?? undefined }) + if (!result.hasTracks()) await textChannel.send(t(queue.guild.preferredLocale, "player.no_track_found", { url: instance.replay.trackUrl })) + const track = result.tracks[0] + + const entry = queue.tasksQueue.acquire() await entry.getTask() queue.addTrack(track) try { - await queue.node.play() - await queue.node.seek(Number(dbData.progress) / 1000) - await textChannel.send(`Relancement de la musique suite à mon redémarrage...`) - } catch (error) { console.error(error) } + if (!queue.isPlaying()) await queue.node.play() + if (instance.replay.progress) await queue.node.seek(instance.replay.progress) + startProgressSaving(queue.guild.id, botId) + await textChannel.send(t(queue.guild.preferredLocale, "player.music_restarted")) + } + catch (error) { console.error(error) } finally { queue.tasksQueue.release() } -} \ No newline at end of file +} + +export async function playerDisco(client: Client, guild: Guild, dbData: Disco) { + try { + if (!dbData.channelId) { + logConsole('discord_player', 'disco.channel_not_configured', { guild: guild.name }) + clearInterval(client.disco.interval) + return "clear" + } + + const channel = await client.channels.fetch(dbData.channelId) + if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { + logConsole('discord_player', 'disco.channel_not_found', { guild: guild.name, channelId: dbData.channelId }) + clearInterval(client.disco.interval) + return "clear" + } + + const { embed, components } = generatePlayerEmbed(guild, guild.preferredLocale) + if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${uptime(client.uptime)} \n ${embed.data.footer.text}` }) + else embed.setFooter({ text: `Uptime: ${uptime(client.uptime)}` }) + + const messages = await channel.messages.fetch() + const bots = ["1065047326860783636", "1119343522059927684", "1119344050412204032", "1210714000321548329", "660961595006124052"] + await Promise.all(messages.map(async msg => { if (!bots.includes(msg.author.id)) await msg.delete() })) + + const botMessage = messages.find(msg => client.user && msg.author.id === client.user.id) + if (botMessage) { + if (!components && botMessage.components.length > 0) { + await botMessage.delete() + return await channel.send({ embeds: [embed] }) + } + else if (components) return await botMessage.edit({ embeds: [embed], components }) + else return await botMessage.edit({ embeds: [embed] }) + } + else return await channel.send({ embeds: [embed] }) + } catch (error) { + console.error(error) + return "clear" + } +} + +export async function playerEdit(interaction: ButtonInteraction) { + const guild = interaction.guild + if (!guild) return interaction.reply({ content: t(interaction.locale, "common.no_dm"), flags: MessageFlags.Ephemeral }) + + const { components } = generatePlayerEmbed(guild, interaction.locale) + if (!components) return + + components.forEach(actionRow => { actionRow.components.forEach((button) => button.setDisabled(true)) }) + await interaction.update({ components }) +} + +export function generatePlayerEmbed(guild: Guild, locale: Locale) { + const embed = new EmbedBuilder().setColor("#ffc370") + + const queue = useQueue(guild.id) + if (!queue) { + embed.setTitle(t(locale, "player.no_session")) + return { embed, components: null } + } + + const track = queue.currentTrack + if (!track) { + embed.setTitle(t(locale, "player.no_track")) + return { embed, components: null } + } + + const sourceValue = track.source === "spotify" ? t(locale, "player.sources.spotify") : + track.source === "youtube" ? t(locale, "player.sources.youtube") : + t(locale, "player.sources.unknown") + + const loopValue = queue.repeatMode === 3 ? t(locale, "player.loop_modes.autoplay") : + queue.repeatMode === 2 ? t(locale, "player.loop_modes.queue") : + queue.repeatMode === 1 ? t(locale, "player.loop_modes.track") : + t(locale, "player.loop_modes.off") + + const progressionName = queue.node.isPaused() ? + t(locale, "player.progression_paused") : + t(locale, "player.progression") + + embed + .setTitle(track.title) + .setAuthor({ name: track.author }) + .setURL(track.url) + .setImage(track.thumbnail) + .addFields( + { name: t(locale, "player.duration"), value: track.duration, inline: true }, + { name: t(locale, "player.source"), value: sourceValue, inline: true }, + { name: t(locale, "player.volume"), value: `${queue.node.volume}%`, inline: true }, + { name: progressionName, value: queue.node.createProgressBar() ?? t(locale, "common.none") }, + { name: t(locale, "player.loop"), value: loopValue, inline: true } + ) + .setDescription(`**${t(locale, "player.next_track")} :** ${queue.tracks.data[0] ? queue.tracks.data[0].title : t(locale, "player.no_next_track")}`) + .setFooter({ text: t(locale, "player.requested_by", { user: track.requestedBy ? track.requestedBy.tag : t(locale, "common.unknown") }) }) + + const components = [ + new ActionRowBuilder().addComponents( + new ButtonBuilder().setLabel(queue.node.isPaused() ? "▶️" : "⏸️").setStyle(2).setCustomId(queue.node.isPaused() ? "player_resume" : "player_pause"), + new ButtonBuilder().setLabel("⏹️").setStyle(2).setCustomId("player_stop"), + new ButtonBuilder().setLabel("⏭️").setStyle(2).setCustomId("player_skip").setDisabled(queue.tracks.data.length !== 0), + new ButtonBuilder().setLabel("🔉").setStyle(2).setCustomId("player_volume_down").setDisabled(queue.node.volume === 0), + new ButtonBuilder().setLabel("🔊").setStyle(2).setCustomId("player_volume_up").setDisabled(queue.node.volume === 100) + ), + new ActionRowBuilder().addComponents( + new ButtonBuilder().setLabel("🔀").setStyle(2).setCustomId("player_shuffle"), + new ButtonBuilder().setLabel("🔁").setStyle(2).setCustomId("player_loop"), + new ButtonBuilder().setLabel("⏮️").setStyle(2).setCustomId("player_previous").setDisabled(queue.history.previousTrack ? false : true) + ) + ] + return { embed, components } +} + +/** + * Génère l'embed et les composants pour la configuration du module Disco + * @param dbData - Données du module Disco depuis la base de données + * @param client - Client Discord + * @param guildId - ID de la guilde + * @param locale - Locale pour la traduction + * @returns Objet contenant l'embed et les composants + */ +export function generateDiscoEmbed(dbData: Disco, client: Client, guildId: string, locale: Locale) { + // Récupérer les informations du canal + let channelInfo = t(locale, "player.disco.channel_not_configured") + if (dbData.channelId) { + const guild = client.guilds.cache.get(guildId) + const channel = guild?.channels.cache.get(dbData.channelId) + channelInfo = channel ? `<#${channel.id}>` : t(locale, "player.common.channel_not_found") + } + + // Créer l'embed principal + const embed = new EmbedBuilder() + .setTitle(t(locale, "player.disco.title")) + .setColor(dbData.enabled ? 0xFFC370 : 0x808080) + .setDescription(dbData.enabled ? + t(locale, "player.disco.description_enabled") : + t(locale, "player.disco.description_disabled") + ) + .addFields( + { name: t(locale, "common.status"), value: dbData.enabled ? t(locale, "player.common.enabled") : t(locale, "player.common.disabled"), inline: true }, + { name: t(locale, "common.channel"), value: channelInfo, inline: true } + ) + .setTimestamp() + + // Bouton toggle - désactivé si pas de canal configuré pour l'activation + const toggleButton = new ButtonBuilder() + .setCustomId(dbData.enabled ? "player_disco_disable" : "player_disco_enable") + .setLabel(dbData.enabled ? t(locale, "common.disable") : t(locale, "common.enable")) + .setStyle(dbData.enabled ? ButtonStyle.Danger : ButtonStyle.Success) + .setEmoji(dbData.enabled ? "❌" : "✅") + + // Désactiver le bouton d'activation si aucun canal n'est configuré + if (!dbData.enabled && !dbData.channelId) { + toggleButton.setDisabled(true) + } + + // Bouton de configuration du canal + const channelButton = new ButtonBuilder() + .setCustomId("player_disco_channel") + .setLabel(t(locale, "player.common.configure_channel")) + .setStyle(ButtonStyle.Secondary) + .setEmoji("📺") + + const components = [ + { + type: ComponentType.ActionRow, + components: [toggleButton, channelButton] + } + ] + + return { embed, components } +} diff --git a/src/utils/rss.ts b/src/utils/rss.ts deleted file mode 100644 index fadd181..0000000 --- a/src/utils/rss.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Client, TextChannel } from 'discord.js' -import { Document } from 'mongoose' -import Parser from 'rss-parser' -import 'dotenv/config' - -export interface Feed { - name: string - url: string - token?: string -} - -export default async (client: Client, guildProfile: Document) => { - try { - let guild = client.guilds.cache.get(guildProfile.get('guildId')) - if (!guild) { - clearInterval(client.disco.interval) - console.log(`Aucun serveur trouvé avec l'id \`${guildProfile.get('guildId')}\`, veuillez utiliser la commande \`/setchannel\` !`) - return 'clear' - } - - let dbData = guildProfile.get('guildRss') - let channel = client.channels.cache.get(dbData.channelId) as TextChannel - if (!channel) { - clearInterval(client.disco.interval) - console.log(`Aucun channel trouvé avec l'id \`${dbData.channelId}\`, veuillez utiliser la commande \`/setchannel\` !`) - return 'clear' - } - - let feeds = [ - { - name: 'Nautiljon - Actualités', - url: 'https://www.nautiljon.com/actualite/rss.php' - }, - { - name: 'Nautiljon - Les Brèves', - url: 'https://www.nautiljon.com/breves/rss.php' - }, - { - name: 'YGG - Application', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2144&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - Audio', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2139&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - eBook', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2140&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - Emulation', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2141&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - Flim/Vidéo', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2145&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - GPS', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2143&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - Imprimante 3D', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2200&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - Jeu Vidéo', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2142&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - Nulled', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2300&passkey=', - token: process.env.YGG_PASSKEY - }, - { - name: 'YGG - XXX', - url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2188&passkey=', - token: process.env.YGG_PASSKEY - } - ] - - if (!dbData.feeds) dbData.feeds = feeds - dbData.feeds.forEach((feed: Feed, i: number) => { setTimeout(async () => { - let parser = new Parser() - if (feed.token) feed.url += feed.token - let feedData = await parser.parseURL(feed.url) - - let thread = channel.threads.cache.find(thread => thread.name === feed.name) - if (!thread) { - thread = await channel.threads.create({ - name: feed.name, - autoArchiveDuration: 60, - reason: 'Création du fil RSS' - }) - await thread.send({ content: `Fil RSS créé !\nVisionnage de **${feedData.title}** sur ${feedData.link}...` }) - - feedData.items.reverse().forEach(async (item, i) => { - setTimeout(async () => await thread?.send({ content: `**${item.title}**\n${item.link}` }), i * 1000) - }) - } - - let lastItem = feedData.items[0] - if (!lastItem) return console.log('No last item found for ' + feed.name) - - let messages = await thread.messages.fetch({ limit: 1 }) - if (!messages) return console.log('No messages found for ' + feed.name) - - let lastMessage = messages.first() - if (!lastMessage) return console.log('No last message found for ' + feed.name) - - if (lastMessage.content !== `**${lastItem.title}**\n${lastItem.link}`) await thread.send({ content: `**${lastItem.title}**\n${lastItem.link}` }) - //else console.log('No new item found for ' + feed.name) - }, Number(i) * 1000) }) - } catch (error) { console.error(error); return 'clear' } -} \ No newline at end of file diff --git a/src/utils/twitch.ts b/src/utils/twitch.ts index e446e04..e08ae5b 100644 --- a/src/utils/twitch.ts +++ b/src/utils/twitch.ts @@ -1,193 +1,327 @@ -import { Guild, TextChannel, EmbedBuilder, hyperlink } from 'discord.js' -import axios, { AxiosHeaderValue } from 'axios' -import dbGuild from '../schemas/guild' -import chalk from 'chalk' - -interface NotificationData { - metadata: { - message_type: string - } - payload: { - subscription: { - type: string - } - event: { - broadcaster_user_name: string - broadcaster_user_login: string - } - session: { - id: string - } - } -} - -interface Condition { - broadcaster_user_id: string -} - -export async function checkChannel (client_id: string, client_secret: string, channel_access_token: string, guild: Guild) { - let guildProfile = await dbGuild.findOne({ guildId: guild?.id }) - if (!guildProfile) return console.log(chalk.magenta(`[Twitch] Database data for ${guild?.name} does not exist, please initialize with \`/database init\` !`)) - - let dbData = guildProfile.get('guildTwitch') - if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] module is disabled for "${guild?.name}", please activate with \`/database edit guildTwitch.enabled True\` !`)) - - if (!await validateToken(channel_access_token)) { - let channel_refresh_token = dbData.channelRefreshToken - if (!channel_refresh_token) return console.log(chalk.magenta('[Twitch] No refresh token found in database !')) - - let GetAccessFromRefresh = await refreshToken(client_id, client_secret, channel_refresh_token) - if (!GetAccessFromRefresh) return false; - - [channel_access_token, channel_refresh_token] = [dbData.channelAccessToken, dbData.channelRefreshToken] = GetAccessFromRefresh - - guildProfile.set('guildTwitch', dbData) - guildProfile.markModified('guildTwitch') - await guildProfile.save().catch(console.error) - } - return channel_access_token -} - -export async function validateToken (access_token: string) { - return await axios.get('https://id.twitch.tv/oauth2/validate', { - headers: { - 'Authorization': `OAuth ${access_token}`, - } - }).then(() => { - return true - }).catch(error => { console.error(error.response.data) }) -} - -export async function refreshToken (client_id: string, client_secret: string, refresh_token: string) { - return await axios.post('https://id.twitch.tv/oauth2/token', { - client_id, - client_secret, - refresh_token, - grant_type: 'refresh_token' - }).then(response => { - if (response.data.token_type === 'bearer') return [response.data.access_token, response.data.refresh_token] - }).catch(error => { console.error(error.response.data) }) -} - -export async function getStreams (client_id: string, access_token: string, user_login: string) { - return await axios.get(`https://api.twitch.tv/helix/streams?user_login=${user_login}`, { - headers: { - 'Authorization': `Bearer ${access_token}`, - 'Client-Id': client_id as AxiosHeaderValue - } - }).then(response => { - return response.data.data[0] - }).catch(error => { console.error(error.response) }) -} - -export async function getUserInfo (client_id: string, access_token: string, type: string) { - return await axios.get('https://api.twitch.tv/helix/users', { - headers: { - 'Authorization': `Bearer ${access_token}`, - 'Client-Id': client_id as AxiosHeaderValue - } - }).then(response => { - if (type === 'login') return response.data.data[0].login - if (type === 'id') return response.data.data[0].id - if (type === 'profile_image_url') return response.data.data[0].profile_image_url - }).catch(error => { console.error(error.response.data) }) -} - -export async function notification (client_id: string, client_secret: string, channel_access_token: string, data: NotificationData, guild: Guild) { - if (!guild) return console.log(chalk.magenta('[Twitch] Can\'t find guild !')) - let { subscription, event } = data.payload - - let guildProfile = await dbGuild.findOne({ guildId: guild.id }) - if (!guildProfile) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Database data does not exist, please initialize with \`/database init\` !`)) - - let dbData = guildProfile.get('guildTwitch') - if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Module is disabled, please activate with \`/database edit guildTwitch.enabled True\` !`)) - - let liveChannelId = dbData.liveChannelId - if (!liveChannelId) return console.log(chalk.magenta(`[Twitch] {${guild.name}} No live channel id found in database !`)) - - let liveChannel = guild.channels.cache.get(liveChannelId) as TextChannel - if (!liveChannel) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find channel with id ${liveChannelId}`)) - - let liveMessageInterval - - // Check if the channel access token is still valid before connecting - channel_access_token = await checkChannel(client_id, client_secret, channel_access_token, guild) as string - if (!channel_access_token) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't refresh channel access token !`)) - - if (subscription.type === 'stream.online') { - console.log(chalk.magenta(`[Twitch] {${guild.name}} Stream from ${event.broadcaster_user_name} is now online, sending Discord message...`)) - - let stream_data = await getStreams(client_id, channel_access_token, event.broadcaster_user_login) - let user_profile_image_url = await getUserInfo(client_id, channel_access_token, 'profile_image_url') - - let embed = new EmbedBuilder() - .setColor('#6441a5') - .setTitle(stream_data.title) - .setURL(`https://twitch.tv/${event.broadcaster_user_login}`) - .setAuthor({ name: `🔴 ${event.broadcaster_user_name.toUpperCase()} EST ACTUELLEMENT EN LIVE ! 🎥`, iconURL: user_profile_image_url }) - .setDescription(`Joue à ${stream_data.game_name} avec ${stream_data.viewer_count} viewers`) - .setImage(stream_data.thumbnail_url.replace('{width}', '1920').replace('{height}', '1080')) - .setTimestamp() - let hidden = hyperlink('démarre un live', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&pp=ygUJcmljayByb2xs') - let message = await liveChannel.send({ content: `Hey @everyone ! <@${dbData.liveBroadcasterId}> ${hidden} sur **Twitch**, venez !`, embeds: [embed] }) - - dbData.liveMessageId = message.id - guildProfile.set('guildTwitch', dbData) - guildProfile.markModified('guildTwitch') - await guildProfile.save().catch(console.error) - - liveMessageInterval = setInterval(async () => { - let stream_data = await getStreams(client_id, channel_access_token, event.broadcaster_user_login) - if (!stream_data) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find stream data for ${event.broadcaster_user_name}`)) - embed.setTitle(stream_data.title) - .setDescription(`Joue à ${stream_data.game_name} avec ${stream_data.viewer_count} viewers`) - .setImage(stream_data.thumbnail_url.replace('{width}', '1920').replace('{height}', '1080')) - .setTimestamp() - message.edit({ content: `Hey @everyone !\n<@${dbData.liveBroadcasterId}> est en live sur **Twitch**, venez !`, embeds: [embed] }).catch(console.error) - }, 60000) - } - else if (subscription.type === 'stream.offline') { - console.log(chalk.magenta(`[Twitch] {${guild.name}} Stream from ${event.broadcaster_user_name} is now offline, editing Discord message...`)) - - let message = await liveChannel.messages.fetch(dbData.liveMessageId as string) - if (!message) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find message with id ${dbData.liveMessageId}`)) - if (!message.embeds[0]) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find embed in message with id ${dbData.liveMessageId}`)) - - let duration = new Date().getTime() - new Date(message.embeds[0].data.timestamp ?? 0).getTime() - let seconds = Math.floor(duration / 1000) - let minutes = Math.floor(seconds / 60) - let hours = Math.floor(minutes / 60) - let duration_string = `${hours ? hours + 'H ' : ''}${minutes % 60 ? minutes % 60 + 'M ' : ''}${seconds % 60 ? seconds % 60 + 'S' : ''}` - - let user_profile_image_url = await getUserInfo(client_id, channel_access_token, 'profile_image_url') - - let embed = new EmbedBuilder() - .setColor('#6441a5') - .setAuthor({ name: `⚫ C'EST FINI, LE LIVE A DURÉ ${duration_string} ! 📼`, iconURL: user_profile_image_url }) - .setTimestamp() - - message.edit({ content: `Re @everyone !\n<@${dbData.liveBroadcasterId}> a terminé son live sur **Twitch** !`, embeds: [embed] }).catch(console.error) - clearInterval(liveMessageInterval) - } -} - -export async function subscribeToEvents (client_id: string, access_token: string, session_id: string, type: string, version: string, condition: Condition) { - return await axios.post('https://api.twitch.tv/helix/eventsub/subscriptions', { - type, - version, - condition, - transport: { - method: 'websocket', - session_id - } - }, { - headers: { - 'Authorization': `Bearer ${access_token}`, - 'Client-Id': client_id, - 'Content-Type': 'application/json' - } - }).then(response => { - return response.data.data[0].status - }).catch(error => { return error.response.data }) -} \ No newline at end of file +// ENVIRONMENT VARIABLES +const clientId = process.env.TWITCH_APP_ID +const clientSecret = process.env.TWITCH_APP_SECRET +if (!clientId || !clientSecret) { + console.warn(chalk.red("[Twitch] Missing TWITCH_APP_ID or TWITCH_APP_SECRET in environment variables!")) + process.exit(1) +} + +// PACKAGES +import { AppTokenAuthProvider } from "@twurple/auth" +import { ApiClient } from "@twurple/api" +import { ReverseProxyAdapter, EventSubHttpListener } from "@twurple/eventsub-http" +import { NgrokAdapter } from "@twurple/eventsub-ngrok" +import type { EventSubStreamOnlineEvent, EventSubStreamOfflineEvent } from "@twurple/eventsub-base" +import { EmbedBuilder, ChannelType, ComponentType, ButtonBuilder, ButtonStyle, Locale } from "discord.js" +import type { Client, Guild } from "discord.js" +import chalk from "chalk" +import discordClient from "@/index" +import type { GuildTwitch } from "@/types/schemas" +import dbGuild from "@/schemas/guild" +import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" + +// Twurple API client setup +const authProvider = new AppTokenAuthProvider(clientId, clientSecret) +export const twitchClient = new ApiClient({ authProvider }) + +let adapter +if (process.env.NODE_ENV === "development") { + const authtoken = process.env.NGROK_AUTHTOKEN + + logConsole('twitch', 'starting_listener_ngrok') + adapter = new NgrokAdapter({ ngrokConfig: { authtoken } }) +} else { + const hostName = process.env.TWURPLE_HOSTNAME ?? "localhost" + const port = process.env.TWURPLE_PORT ?? "3000" + + console.log(chalk.magenta(`[Twitch] Starting listener with port ${port}...`)) + adapter = new ReverseProxyAdapter({ hostName, port: parseInt(port) }) +} + +const secret = process.env.TWURPLE_SECRET ?? "VeryUnsecureSecretPleaseChangeMe" +export const listener = new EventSubHttpListener({ apiClient: twitchClient, adapter, secret }) +listener.start() + +// Twurple subscriptions callback functions +export const onlineSub = async (event: EventSubStreamOnlineEvent) => { + console.log(chalk.magenta(`[Twitch] Stream from ${event.broadcasterName} (ID ${event.broadcasterId}) is now online, sending Discord messages...`)) + + const results = await Promise.allSettled(discordClient.guilds.cache.map(async guild => { + try { + console.log(chalk.magenta(`[Twitch] Processing guild: ${guild.name} (ID: ${guild.id}) for streamer ${event.broadcasterName}`)) + + const notification = await generateNotification(guild, event.broadcasterId, event.broadcasterName) + if (notification.status !== "ok") { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Notification generation failed with status: ${notification.status}`)); return } + + const { guildProfile, dbData, channel, content, embed } = notification + if (!dbData) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} No dbData found`)); return } + + const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === event.broadcasterId) + if (streamerIndex === -1) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer ${event.broadcasterName} not found in this guild`)); return } + + if (dbData.streamers[streamerIndex].messageId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message already exists for ${event.broadcasterName}, skipping`)); return } + + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Sending notification for ${event.broadcasterName}`)) + const message = await channel.send({ content, embeds: [embed] }) + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message sent with ID: ${message.id}`)) + + dbData.streamers[streamerIndex].messageId = message.id + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) + + startStreamWatching(guild.id, event.broadcasterId, event.broadcasterName, message.id) + } catch (error) { + console.log(chalk.magenta(`[Twitch] Error processing guild ${guild.name}`)) + console.error(error) + } + })) + + results.forEach((result, index) => { + if (result.status === "rejected") console.log(chalk.magenta(`[Twitch] Guild ${index} failed:`), result.reason) + }) +} + +export const offlineSub = async (event: EventSubStreamOfflineEvent) => { + console.log(chalk.magenta(`[Twitch] Stream from ${event.broadcasterName} (ID ${event.broadcasterId}) is now offline, editing Discord messages...`)) + + await Promise.all(discordClient.guilds.cache.map(async guild => { + await stopStreamWatching(guild.id, event.broadcasterId, event.broadcasterName) + })) +} + +// Stream upadting intervals +const streamIntervals = new Map() + +export function startStreamWatching(guildId: string, streamerId: string, streamerName: string, messageId: string) { + console.log(chalk.magenta(`[Twitch] StreamWatching - Démarrage du visionnage de ${streamerName} (ID ${streamerId}) sur ${guildId}`)) + + const key = `${guildId}-${streamerId}` + if (streamIntervals.has(key)) { + clearInterval(streamIntervals.get(key)) + streamIntervals.delete(key) + } + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const interval = setInterval(async () => { + try { + const guild = await discordClient.guilds.fetch(guildId) + const notification = await generateNotification(guild, streamerId, streamerName) + if (notification.status !== "ok") { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Notification generation failed with status: ${notification.status}`)); return } + + const { channel, content, embed } = notification + if (!embed) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Embed is missing`)); return } + + try { + const message = await channel.messages.fetch(messageId) + await message.edit({ content, embeds: [embed] }) + } catch (error) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Error editing message for ${streamerName} (ID ${streamerId})`)) + console.error(error) + } + } catch (error) { + console.log(chalk.magenta(`[Twitch] StreamWatching - Erreur lors du visionnage de ${streamerName} (ID ${streamerId}) sur ${guildId}`)) + console.error(error) + await stopStreamWatching(guildId, streamerId, streamerName) + } + }, 60000) + + streamIntervals.set(key, interval) +} + +export async function stopStreamWatching(guildId: string, streamerId: string, streamerName: string) { + console.log(chalk.magenta(`[Twitch] StreamWatching - Arrêt du visionnage de ${streamerName} (ID ${streamerId})`)) + + const key = `${guildId}-${streamerId}` + if (streamIntervals.has(key)) { + clearInterval(streamIntervals.get(key)) + streamIntervals.delete(key) + } + + const guild = await discordClient.guilds.fetch(guildId) + const guildProfile = await dbGuild.findOne({ guildId: guild.id }) + if (!guildProfile) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)); return } + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbData.enabled || !dbData.channelId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)); return } + + const channel = await guild.channels.fetch(dbData.channelId) + if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Channel with ID ${dbData.channelId} not found for Twitch notifications`)) + return + } + + const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) + if (!streamer) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer not found in guild for ${streamerName} (ID ${streamerId})`)); return } + + const messageId = streamer.messageId + if (!messageId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message ID not found for ${streamerName} (ID ${streamerId})`)); return } + + const user = await twitchClient.users.getUserById(streamerId) + if (!user) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} User data not found for ${streamerName} (ID ${streamerId})`)) + + let duration_string = "" + const stream = await user?.getStream() + if (!stream) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Stream data not found for ${streamerName} (ID ${streamerId})`)) + duration_string = t(guild.preferredLocale, "twitch.notification.offline.duration_unknown") + } else { + const duration = new Date().getTime() - stream.startDate.getTime() + const seconds = Math.floor(duration / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + duration_string = `${hours ? hours + "H " : ""}${minutes % 60 ? (minutes % 60) + "M " : ""}${seconds % 60 ? (seconds % 60) + "S" : ""}` + } + + let content = "" + if (!streamer.discordUserId) content = t(guild.preferredLocale, "twitch.notification.offline.everyone", { streamer: streamerName }) + else content = t(guild.preferredLocale, "twitch.notification.offline.everyone_with_mention", { discordId: streamer.discordUserId }) + + const embed = new EmbedBuilder() + .setColor("#6441a5") + .setAuthor({ + name: t(guild.preferredLocale, "twitch.notification.offline.author", { duration: duration_string }), + iconURL: user?.profilePictureUrl ?? "https://static-cdn.jtvnw.net/emoticons/v2/58765/static/light/3.0" + }) + .setTimestamp() + + try { + const message = await channel.messages.fetch(messageId) + await message.edit({ content, embeds: [embed] }) + } catch (error) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Error editing message for ${streamerName} (ID ${streamerId})`)) + console.error(error) + } + + const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === streamerId) + if (streamerIndex === -1) return + + dbData.streamers[streamerIndex].messageId = "" + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) +} + +async function generateNotification(guild: Guild, streamerId: string, streamerName: string) { + const guildProfile = await dbGuild.findOne({ guildId: guild.id }) + if (!guildProfile) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)) + return { status: "noProfile" } + } + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbData.enabled || !dbData.channelId) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Twitch module is not enabled or channel ID is missing`)) + return { status: "disabled" } + } + + const channel = await guild.channels.fetch(dbData.channelId) + if ((channel?.type !== ChannelType.GuildText && channel?.type !== ChannelType.GuildAnnouncement)) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Channel with ID ${dbData.channelId} not found for Twitch notifications`)) + return { status: "noChannel" } + } + + const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) + if (!streamer) { + console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer not found in guild for ${streamerName} (ID ${streamerId})`)) + return { status: "noStreamer" } + } + + const user = await twitchClient.users.getUserById(streamerId) + if (!user) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} User data not found for ${streamerName} (ID ${streamerId})`)) + + const stream = await user?.getStream() + if (!stream) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Stream data not found for ${streamerName} (ID ${streamerId})`)) + + let content = "" + if (!streamer.discordUserId) content = t(guild.preferredLocale, "twitch.notification.online.everyone", { streamer: user?.displayName ?? streamerName }) + else content = t(guild.preferredLocale, "twitch.notification.online.everyone_with_mention", { discordId: streamer.discordUserId }) + + const embed = new EmbedBuilder() + .setColor("#6441a5") + .setTitle(stream?.title ?? t(guild.preferredLocale, "twitch.notification.online.title_unknown")) + .setURL(`https://twitch.tv/${streamerName}`) + .setAuthor({ + name: t(guild.preferredLocale, "twitch.notification.online.author", { streamer: (user?.displayName ?? streamerName).toUpperCase() }), + iconURL: user?.profilePictureUrl ?? "https://static-cdn.jtvnw.net/emoticons/v2/58765/static/light/3.0" + }) + .setDescription(t(guild.preferredLocale, "twitch.notification.online.description", { + game: stream?.gameName ?? "?", + viewers: stream?.viewers.toString() ?? "?" + })) + .setImage(stream?.thumbnailUrl.replace("{width}", "1920").replace("{height}", "1080") ?? "https://assets.help.twitch.tv/article/img/000002222-01a.png") + .setTimestamp() + + return { status: "ok", guildProfile, dbData, channel, content, embed } +} + +export function generateTwitchEmbed(dbData: GuildTwitch, client: Client, guildId: string, locale: Locale) { + // Récupérer les informations du canal + let channelInfo = t(locale, "twitch.channel_not_configured") + if (dbData.channelId) { + const guild = client.guilds.cache.get(guildId) + const channel = guild?.channels.cache.get(dbData.channelId) + channelInfo = channel ? `<#${channel.id}>` : t(locale, "twitch.common.channel_not_found") + } + + // Créer l'embed principal + const embed = new EmbedBuilder() + .setTitle(t(locale, "twitch.title")) + .setColor(dbData.enabled ? 0x9146FF : 0x808080) + .addFields( + { name: t(locale, "common.status"), value: dbData.enabled ? t(locale, "twitch.common.enabled") : t(locale, "twitch.common.disabled"), inline: true }, + { name: t(locale, "common.channel"), value: channelInfo, inline: true }, + { name: "👥 Streamers", value: t(locale, "twitch.streamers_count", { count: dbData.streamers.length.toString() }), inline: true } + ) + .setFooter({ text: t(locale, "twitch.managed_by", { bot: client.user?.displayName ?? "Bot" }) }) + .setTimestamp() + + // Boutons première ligne - Toggle et configuration + const toggleButton = new ButtonBuilder() + .setCustomId(dbData.enabled ? "twitch_disable" : "twitch_enable") + .setLabel(dbData.enabled ? t(locale, "common.disable") : t(locale, "common.enable")) + .setStyle(dbData.enabled ? ButtonStyle.Danger : ButtonStyle.Success) + .setEmoji(dbData.enabled ? "❌" : "✅") + + const channelButton = new ButtonBuilder() + .setCustomId("twitch_channel") + .setLabel(t(locale, "twitch.common.configure_channel")) + .setStyle(ButtonStyle.Secondary) + .setEmoji("📺") + + // Boutons seconde ligne - Gestion des streamers + const listButton = new ButtonBuilder() + .setCustomId("twitch_streamer_list") + .setLabel(t(locale, "common.list")) + .setStyle(ButtonStyle.Secondary) + .setEmoji("📋") + + const addButton = new ButtonBuilder() + .setCustomId("twitch_streamer_add") + .setLabel(t(locale, "common.add")) + .setStyle(ButtonStyle.Primary) + .setEmoji("➕") + + const removeButton = new ButtonBuilder() + .setCustomId("twitch_streamer_remove") + .setLabel(t(locale, "common.remove")) + .setStyle(ButtonStyle.Danger) + .setEmoji("🗑️") + + const components = [ + { + type: ComponentType.ActionRow, + components: [toggleButton, channelButton] + }, + { + type: ComponentType.ActionRow, + components: [listButton, addButton, removeButton] + } + ] + + return { embed, components } +} diff --git a/src/utils/uptime.ts b/src/utils/uptime.ts new file mode 100644 index 0000000..51ab152 --- /dev/null +++ b/src/utils/uptime.ts @@ -0,0 +1,10 @@ +import type { Client } from "discord.js" + +export default function (uptime: Client["uptime"]) { + if (!uptime) return "0J, 0H, 0M et 0S" + const days = Math.floor(uptime / 86400000) + const hours = Math.floor(uptime / 3600000) % 24 + const minutes = Math.floor(uptime / 60000) % 60 + const seconds = Math.floor(uptime / 1000) % 60 + return `${days}J, ${hours}H, ${minutes}M et ${seconds}S` +} diff --git a/tsconfig.json b/tsconfig.json index 588136d..cfbc196 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,23 @@ { "compilerOptions": { - "target": "ES2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "module": "CommonJS", /* Specify what module code is generated. */ + "lib": ["ES2022"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "module": "commonjs", /* Specify what module code is generated. */ "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ "outDir": "./dist", /* Specify an output folder for all emitted files. */ "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "removeComments": true, /* Disable emitting comments. */ "strict": true, /* Enable all strict type-checking options. */ + "strictNullChecks": true, /* Ensure strict null checks. */ "skipLibCheck": true, /* Skip type checking all .d.ts files. */ - "resolveJsonModule": true + "resolveJsonModule": true, /* Include modules imported with .json extension. */ + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + "@/*": ["./src/*"] + } }, - "include": ["./src/**/*"], - "exclude": ["./src/utilsAMP/**/*", "./src/utilsCrack/**/*"], - - "ts-node": { - "transpileOnly": true, - "transpiler": "@swc/core" - } -} \ No newline at end of file + "include": ["./src/**/*"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..e9519ce --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs'], + target: 'node22', + outDir: 'dist', + clean: true, + minify: true, + sourcemap: false, + splitting: false, + treeshake: true, + bundle: true, + platform: 'node', + external: [ + 'bufferutil', + 'zlib-sync' + ], + esbuildOptions(options) { + options.banner = { + js: '#!/usr/bin/env node' + } + options.alias = { + '@': './src' + } + options.target = 'es2022' + } +}) From 5e7c1842a4f75a5bd11dd895f54f5617955a16db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Mon, 9 Jun 2025 16:55:41 +0200 Subject: [PATCH 07/11] Fix path + ffmpeg --- .gitea/workflows/build-and-push.yml | 1 + build/node.dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index 04750bb..b69fccb 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -65,6 +65,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . + file: build/node.dockerfile platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/build/node.dockerfile b/build/node.dockerfile index 53d6a63..479c63e 100644 --- a/build/node.dockerfile +++ b/build/node.dockerfile @@ -5,7 +5,7 @@ ENV NODE_ENV=production WORKDIR /app -RUN apk add --no-cache python3 make g++ +RUN apk add --no-cache ffmpeg python3 make g++ # Copy package files and install only production dependencies COPY package.json package-lock.json* . From 60d0c01212e7cdc24cc282eeab52d4f4308b836d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Mon, 9 Jun 2025 19:10:35 +0200 Subject: [PATCH 08/11] Fix chmod --- README.md | 0 build/node.dockerfile | 1 - src/commands/global/amp.ts | 0 src/commands/global/database.ts | 0 src/commands/global/ping.ts | 0 src/commands/player/loop.ts | 0 src/commands/player/lyrics.ts | 0 src/commands/player/panel.ts | 0 src/commands/player/pause.ts | 0 src/commands/player/play.ts | 0 src/commands/player/previous.ts | 0 src/commands/player/queue.ts | 0 src/commands/player/resume.ts | 0 src/commands/player/shuffle.ts | 0 src/commands/player/skip.ts | 0 src/commands/player/stop.ts | 0 src/commands/player/volume.ts | 0 src/commands/salonpostam/crack.ts | 0 src/commands/salonpostam/papa.ts | 0 src/commands/salonpostam/parle.ts | 0 src/commands/salonpostam/spam.ts | 0 src/commands/salonpostam/update.ts | 0 src/events/client/error.ts | 0 src/events/client/guildCreate.ts | 0 src/events/client/guildMemberAdd.ts | 0 src/events/client/guildMemberRemove.ts | 0 src/events/client/guildUpdate.ts | 0 src/events/client/interactionCreate.ts | 0 src/events/client/ready.ts | 0 src/events/player/audioTrackAdd.ts | 0 src/events/player/audioTracksAdd.ts | 0 src/events/player/debug.ts | 0 src/events/player/disconnect.ts | 0 src/events/player/emptyChannel.ts | 0 src/events/player/emptyQueue.ts | 0 src/events/player/error.ts | 0 src/events/player/playerError.ts | 0 src/events/player/playerSkip.ts | 0 src/events/player/playerStart.ts | 0 src/index.ts | 0 src/static/parle.mp3 | Bin src/static/stronger_shorter.mp3 | Bin tsconfig.json | 0 43 files changed, 1 deletion(-) mode change 100755 => 100644 README.md mode change 100755 => 100644 src/commands/global/amp.ts mode change 100755 => 100644 src/commands/global/database.ts mode change 100755 => 100644 src/commands/global/ping.ts mode change 100755 => 100644 src/commands/player/loop.ts mode change 100755 => 100644 src/commands/player/lyrics.ts mode change 100755 => 100644 src/commands/player/panel.ts mode change 100755 => 100644 src/commands/player/pause.ts mode change 100755 => 100644 src/commands/player/play.ts mode change 100755 => 100644 src/commands/player/previous.ts mode change 100755 => 100644 src/commands/player/queue.ts mode change 100755 => 100644 src/commands/player/resume.ts mode change 100755 => 100644 src/commands/player/shuffle.ts mode change 100755 => 100644 src/commands/player/skip.ts mode change 100755 => 100644 src/commands/player/stop.ts mode change 100755 => 100644 src/commands/player/volume.ts mode change 100755 => 100644 src/commands/salonpostam/crack.ts mode change 100755 => 100644 src/commands/salonpostam/papa.ts mode change 100755 => 100644 src/commands/salonpostam/parle.ts mode change 100755 => 100644 src/commands/salonpostam/spam.ts mode change 100755 => 100644 src/commands/salonpostam/update.ts mode change 100755 => 100644 src/events/client/error.ts mode change 100755 => 100644 src/events/client/guildCreate.ts mode change 100755 => 100644 src/events/client/guildMemberAdd.ts mode change 100755 => 100644 src/events/client/guildMemberRemove.ts mode change 100755 => 100644 src/events/client/guildUpdate.ts mode change 100755 => 100644 src/events/client/interactionCreate.ts mode change 100755 => 100644 src/events/client/ready.ts mode change 100755 => 100644 src/events/player/audioTrackAdd.ts mode change 100755 => 100644 src/events/player/audioTracksAdd.ts mode change 100755 => 100644 src/events/player/debug.ts mode change 100755 => 100644 src/events/player/disconnect.ts mode change 100755 => 100644 src/events/player/emptyChannel.ts mode change 100755 => 100644 src/events/player/emptyQueue.ts mode change 100755 => 100644 src/events/player/error.ts mode change 100755 => 100644 src/events/player/playerError.ts mode change 100755 => 100644 src/events/player/playerSkip.ts mode change 100755 => 100644 src/events/player/playerStart.ts mode change 100755 => 100644 src/index.ts mode change 100755 => 100644 src/static/parle.mp3 mode change 100755 => 100644 src/static/stronger_shorter.mp3 mode change 100755 => 100644 tsconfig.json diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/build/node.dockerfile b/build/node.dockerfile index 479c63e..5e089f2 100644 --- a/build/node.dockerfile +++ b/build/node.dockerfile @@ -12,7 +12,6 @@ COPY package.json package-lock.json* . RUN npm ci --only=production --ignore-scripts && \ npm install bufferutil zlib-sync - # Copy the builded files and the charts COPY ./dist/* . diff --git a/src/commands/global/amp.ts b/src/commands/global/amp.ts old mode 100755 new mode 100644 diff --git a/src/commands/global/database.ts b/src/commands/global/database.ts old mode 100755 new mode 100644 diff --git a/src/commands/global/ping.ts b/src/commands/global/ping.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/loop.ts b/src/commands/player/loop.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/lyrics.ts b/src/commands/player/lyrics.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/panel.ts b/src/commands/player/panel.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/pause.ts b/src/commands/player/pause.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/play.ts b/src/commands/player/play.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/previous.ts b/src/commands/player/previous.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/queue.ts b/src/commands/player/queue.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/resume.ts b/src/commands/player/resume.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/shuffle.ts b/src/commands/player/shuffle.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/skip.ts b/src/commands/player/skip.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/stop.ts b/src/commands/player/stop.ts old mode 100755 new mode 100644 diff --git a/src/commands/player/volume.ts b/src/commands/player/volume.ts old mode 100755 new mode 100644 diff --git a/src/commands/salonpostam/crack.ts b/src/commands/salonpostam/crack.ts old mode 100755 new mode 100644 diff --git a/src/commands/salonpostam/papa.ts b/src/commands/salonpostam/papa.ts old mode 100755 new mode 100644 diff --git a/src/commands/salonpostam/parle.ts b/src/commands/salonpostam/parle.ts old mode 100755 new mode 100644 diff --git a/src/commands/salonpostam/spam.ts b/src/commands/salonpostam/spam.ts old mode 100755 new mode 100644 diff --git a/src/commands/salonpostam/update.ts b/src/commands/salonpostam/update.ts old mode 100755 new mode 100644 diff --git a/src/events/client/error.ts b/src/events/client/error.ts old mode 100755 new mode 100644 diff --git a/src/events/client/guildCreate.ts b/src/events/client/guildCreate.ts old mode 100755 new mode 100644 diff --git a/src/events/client/guildMemberAdd.ts b/src/events/client/guildMemberAdd.ts old mode 100755 new mode 100644 diff --git a/src/events/client/guildMemberRemove.ts b/src/events/client/guildMemberRemove.ts old mode 100755 new mode 100644 diff --git a/src/events/client/guildUpdate.ts b/src/events/client/guildUpdate.ts old mode 100755 new mode 100644 diff --git a/src/events/client/interactionCreate.ts b/src/events/client/interactionCreate.ts old mode 100755 new mode 100644 diff --git a/src/events/client/ready.ts b/src/events/client/ready.ts old mode 100755 new mode 100644 diff --git a/src/events/player/audioTrackAdd.ts b/src/events/player/audioTrackAdd.ts old mode 100755 new mode 100644 diff --git a/src/events/player/audioTracksAdd.ts b/src/events/player/audioTracksAdd.ts old mode 100755 new mode 100644 diff --git a/src/events/player/debug.ts b/src/events/player/debug.ts old mode 100755 new mode 100644 diff --git a/src/events/player/disconnect.ts b/src/events/player/disconnect.ts old mode 100755 new mode 100644 diff --git a/src/events/player/emptyChannel.ts b/src/events/player/emptyChannel.ts old mode 100755 new mode 100644 diff --git a/src/events/player/emptyQueue.ts b/src/events/player/emptyQueue.ts old mode 100755 new mode 100644 diff --git a/src/events/player/error.ts b/src/events/player/error.ts old mode 100755 new mode 100644 diff --git a/src/events/player/playerError.ts b/src/events/player/playerError.ts old mode 100755 new mode 100644 diff --git a/src/events/player/playerSkip.ts b/src/events/player/playerSkip.ts old mode 100755 new mode 100644 diff --git a/src/events/player/playerStart.ts b/src/events/player/playerStart.ts old mode 100755 new mode 100644 diff --git a/src/index.ts b/src/index.ts old mode 100755 new mode 100644 diff --git a/src/static/parle.mp3 b/src/static/parle.mp3 old mode 100755 new mode 100644 diff --git a/src/static/stronger_shorter.mp3 b/src/static/stronger_shorter.mp3 old mode 100755 new mode 100644 diff --git a/tsconfig.json b/tsconfig.json old mode 100755 new mode 100644 From d06df32bab831ef43f8d205300ae0f74f686f2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Mon, 9 Jun 2025 23:42:02 +0200 Subject: [PATCH 09/11] Fix workflow tags and remove attestation --- .gitea/workflows/build-and-push.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index b69fccb..4b1bf93 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -54,12 +54,17 @@ jobs: tags: | # Tag avec le nom du tag Git type=ref,event=tag + # Tag 'latest' pour la branche master + type=raw,value=latest,enable={{is_default_branch}} + # Tag avec le SHA pour les autres branches + type=sha,prefix={{branch}}- labels: | org.opencontainers.image.title=${{ env.IMAGE_NAME }} - org.opencontainers.image.description=Bot Discord - org.opencontainers.image.url=https://gitea.zac.ovh/zachary/bot_Tamiseur - org.opencontainers.image.source=https://gitea.zac.ovh/zachary/bot_Tamiseur + org.opencontainers.image.description=Bot Discord de moi + org.opencontainers.image.url=https://git.zac.ovh/zachary/bot_Tamiseur + org.opencontainers.image.source=https://git.zac.ovh/zachary/bot_Tamiseur org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.created={{date 'RFC3339'}} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -72,10 +77,3 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }} - subject-digest: ${{ steps.build.outputs.digest }} - push-to-registry: true From 9a4902291e35565cfbabfd7a237e5daea2fa3cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Tue, 10 Jun 2025 01:49:29 +0200 Subject: [PATCH 10/11] Fix workflow sha tag --- .gitea/workflows/build-and-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml index 4b1bf93..d9ae4e2 100644 --- a/.gitea/workflows/build-and-push.yml +++ b/.gitea/workflows/build-and-push.yml @@ -57,7 +57,7 @@ jobs: # Tag 'latest' pour la branche master type=raw,value=latest,enable={{is_default_branch}} # Tag avec le SHA pour les autres branches - type=sha,prefix={{branch}}- + type=sha,prefix=sha- labels: | org.opencontainers.image.title=${{ env.IMAGE_NAME }} org.opencontainers.image.description=Bot Discord de moi From 066a3864dd9afa4acea349d9b0da8d1b8f036c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Tue, 10 Jun 2025 01:53:08 +0200 Subject: [PATCH 11/11] Push build version --- deploy/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/values.yaml b/deploy/values.yaml index 6ee8790..2039b78 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -3,7 +3,7 @@ deployment: strategy: RollingUpdate image: repository: "rgy.angels-dev.fr/prod/bot_tamiseur" - tag: "4.0.0" + tag: "build_2025-06-10_01h49" pullPolicy: IfNotPresent env: NODE_ENV: "production"