Compare commits
	
		
			7 Commits
		
	
	
		
			5af815d99f
			...
			build_2025
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5e7c1842a4 | |||
| ddd617317c | |||
| f2c6388da6 | |||
| 66c5891510 | |||
| eb6c40c2f5 | |||
| 19119e5c77 | |||
| 1e3f62d3c4 | 
| @@ -1,12 +0,0 @@ | ||||
| .git | ||||
| .vscode | ||||
| node_modules | ||||
| public/cracks/* | ||||
| .dockerignore | ||||
| .env | ||||
| .gitignore | ||||
| .ncurc.json | ||||
| Dockerfile | ||||
| eslint.config.mjs | ||||
| README.md | ||||
| tsconfig.json | ||||
							
								
								
									
										29
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										81
									
								
								.gitea/workflows/build-and-push.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								.gitea/workflows/build-and-push.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| name: Build and Push Docker Image | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|     tags: | ||||
|       - 'build_*' | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - master | ||||
|  | ||||
| env: | ||||
|   REGISTRY: rgy.angels-dev.fr | ||||
|   IMAGE_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: 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 | ||||
|  | ||||
|     - 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 for Docker | ||||
|       id: meta | ||||
|       uses: docker/metadata-action@v5 | ||||
|       with: | ||||
|         images: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }} | ||||
|         tags: | | ||||
|           # Tag avec le nom du tag Git | ||||
|           type=ref,event=tag | ||||
|         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: . | ||||
|         file: build/node.dockerfile | ||||
|         platforms: linux/amd64,linux/arm64 | ||||
|         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.IMAGE_PATH }}/${{ env.IMAGE_NAME }} | ||||
|         subject-digest: ${{ steps.build.outputs.digest }} | ||||
|         push-to-registry: true | ||||
							
								
								
									
										22
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/copilot-instructions.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,4 @@ | ||||
| .env | ||||
| dist/ | ||||
| node_modules/ | ||||
| public/cracks/* | ||||
| .env* | ||||
| .ncurc.json | ||||
| public/cracks/ | ||||
							
								
								
									
										18
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -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": ["<node_internals>/**"], | ||||
| 			"runtimeExecutable": "nodemon", | ||||
| 			"console": "integratedTerminal", | ||||
| 			"internalConsoleOptions": "neverOpen", | ||||
| 			"restart": true | ||||
| 		} | ||||
| 	] | ||||
| } | ||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -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" | ||||
| } | ||||
							
								
								
									
										19
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -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"] | ||||
							
								
								
									
										21
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| # ===================== | ||||
| # 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; | ||||
							
								
								
									
										128
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								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 <url_repository> | ||||
| 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 | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										24
									
								
								build/node.dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								build/node.dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| # Starting from node | ||||
| FROM node:22-alpine | ||||
|  | ||||
| ENV NODE_ENV=production | ||||
|  | ||||
| WORKDIR /app | ||||
|  | ||||
| RUN apk add --no-cache ffmpeg 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"] | ||||
							
								
								
									
										12
									
								
								deploy/Chart.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								deploy/Chart.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										34
									
								
								deploy/templates/deployment.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								deploy/templates/deployment.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }} | ||||
							
								
								
									
										18
									
								
								deploy/values.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								deploy/values.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| deployment: | ||||
|   replica: 1 | ||||
|   strategy: RollingUpdate | ||||
|   image: | ||||
|     repository: "rgy.angels-dev.fr/prod/bot_tamiseur" | ||||
|     tag: "4.0.0" | ||||
|     pullPolicy: IfNotPresent | ||||
|   env: | ||||
|     NODE_ENV: "production" | ||||
|  | ||||
|   ## Pas de limite CPU pour éviter latence | ||||
|   resources: | ||||
|     limits: | ||||
|       # cpu: "" | ||||
|       # Memory: "500Mi" | ||||
|     requests: | ||||
|       Cpu: "0.1" | ||||
|       Memory: "50Mi" | ||||
							
								
								
									
										103
									
								
								docs/FREEBOX_LCD.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								docs/FREEBOX_LCD.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <ip_freebox>` | ||||
| 3. **Version API configurée** : `/database edit guildFbx.version <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 <ip>` | ||||
| - **"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 | ||||
| @@ -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"] | ||||
| 	} | ||||
| ] | ||||
| ) | ||||
|   | ||||
							
								
								
									
										3664
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3664
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										65
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								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" | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										11
									
								
								src/buttons/freebox/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/buttons/freebox/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
							
								
								
									
										88
									
								
								src/buttons/freebox/lcd_status.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/buttons/freebox/lcd_status.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<GetChallenge> | ||||
| 		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<OpenSession> | ||||
| 		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<LcdConfig> | ||||
| 		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 }) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										76
									
								
								src/buttons/freebox/refresh_status.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/buttons/freebox/refresh_status.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ButtonBuilder>() | ||||
| 		.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] }) | ||||
| } | ||||
							
								
								
									
										71
									
								
								src/buttons/freebox/test_connection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/buttons/freebox/test_connection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<GetChallenge> | ||||
| 		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<OpenSession> | ||||
| 		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<ConnectionStatus> | ||||
| 		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 }) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/buttons/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/buttons/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/buttons/player/disco_channel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/buttons/player/disco_channel.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ChannelSelectMenuBuilder>().addComponents(channelSelect) | ||||
|  | ||||
| 	return interaction.reply({ content: t(interaction.locale, "player.disco.select_channel"), components: [row], flags: MessageFlags.Ephemeral }) | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/buttons/player/disco_disable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/buttons/player/disco_disable.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/buttons/player/disco_enable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/buttons/player/disco_enable.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										29
									
								
								src/buttons/player/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/buttons/player/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
							
								
								
									
										22
									
								
								src/buttons/player/loop.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/buttons/player/loop.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/buttons/player/pause.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/buttons/player/pause.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/buttons/player/previous.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/buttons/player/previous.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/buttons/player/resume.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/buttons/player/resume.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/buttons/player/shuffle.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/buttons/player/shuffle.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/buttons/player/skip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/buttons/player/skip.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/buttons/player/stop.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/buttons/player/stop.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/buttons/player/volume_down.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/buttons/player/volume_down.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/buttons/player/volume_up.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/buttons/player/volume_up.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/buttons/twitch/channel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/buttons/twitch/channel.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ChannelSelectMenuBuilder>().addComponents(channelSelect) | ||||
|  | ||||
| 	return interaction.reply({ | ||||
| 		content: t(interaction.locale, "twitch.select_notification_channel"), | ||||
| 		components: [row], | ||||
| 		flags: MessageFlags.Ephemeral | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/buttons/twitch/disable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/buttons/twitch/disable.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/buttons/twitch/enable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/buttons/twitch/enable.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/buttons/twitch/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/buttons/twitch/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
							
								
								
									
										20
									
								
								src/buttons/twitch/streamer_add.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/buttons/twitch/streamer_add.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										51
									
								
								src/buttons/twitch/streamer_list.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/buttons/twitch/streamer_list.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/buttons/twitch/streamer_remove.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/buttons/twitch/streamer_remove.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<StringSelectMenuBuilder>().addComponents(selectMenu) | ||||
|  | ||||
| 	return interaction.reply({ content: t(interaction.locale, "twitch.select_streamer_prompt"), components: [row], flags: MessageFlags.Ephemeral }) | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
| @@ -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' | ||||
| 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" | ||||
|  | ||||
| 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 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}`)}` | ||||
| } | ||||
|  | ||||
| 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 !') | ||||
| 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("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) | ||||
| 	.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) | ||||
| 			) | ||||
| 		) | ||||
| 	) | ||||
|  | ||||
| 		let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id }) | ||||
| 		if (!guildProfile) return await interaction.respond([]) | ||||
| 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 }) | ||||
|  | ||||
| 		let dbData = guildProfile.get('guildAmp') | ||||
| 		if (!dbData?.enabled) return await interaction.respond([]) | ||||
| 	const dbData = guildProfile.get("guildAmp") as GuildAmp | ||||
| 	if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "amp.module_disabled"), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 		let host = dbData.host as string | ||||
| 		let username = dbData.username as string | ||||
| 		let sessionID = dbData.sessionID as string | ||||
| 		let rememberMeToken = dbData.rememberMeToken as string | ||||
| 	const host = dbData.host | ||||
| 	if (!host) return interaction.reply({ content: t(interaction.locale, "amp.host_not_configured"), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 		// 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) | ||||
| 	let username = dbData.username | ||||
| 	let sessionID = dbData.sessionID | ||||
| 	let rememberMeToken = dbData.rememberMeToken | ||||
|  | ||||
| 				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([]) | ||||
| 	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) | ||||
| 		} | ||||
| 		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 }) | ||||
| 		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 (result.status === 'fail') return interaction.respond([]) | ||||
| 		else if (result.status === 'error') return interaction.respond([]) | ||||
| 		else if (subcommand == "manage") { | ||||
| 			const instanceID = interaction.options.getString("instance", true) | ||||
|  | ||||
| 		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\` !` }) | ||||
| 			const manageResult = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID) | ||||
| 			if (manageResult.status !== "success") return interaction.followUp({ content: returnMsg(manageResult, interaction.locale), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 		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\` !` }) | ||||
| 			const serversResult = await AMP.ADSModule.Servers(host, sessionID, instanceID) | ||||
| 			if (serversResult.status !== "success") return interaction.followUp({ content: returnMsg(serversResult, interaction.locale), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 		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)) | ||||
| 			return interaction.followUp({ content: t(interaction.locale, "amp.manage_success") }) | ||||
| 		} | ||||
| 		await interaction.deferReply() | ||||
| 		else if (subcommand == "restart") { | ||||
| 			const query = interaction.options.getString("name", true) | ||||
|  | ||||
| 		// 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) | ||||
| 			const restartResult = await AMP.ADSModule.RestartInstance(host, sessionID, query) | ||||
| 			if (restartResult.status !== "success") return interaction.followUp({ content: returnMsg(restartResult, interaction.locale), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 				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)) | ||||
| 			} | ||||
| 			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) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|  | ||||
| 	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" }) }) | ||||
| } | ||||
| @@ -1,76 +1,108 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, APIEmbedField, PermissionFlagsBits } from 'discord.js' | ||||
| 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" | ||||
|  | ||||
| import dbGuildInit from '../../utils/dbGuildInit' | ||||
| import dbGuild from '../../schemas/guild' | ||||
| const parseObject = (obj: object, prefix = ""): { name: string, value: object | string | boolean }[] => { | ||||
| 	const fields: { name: string, value: object | string | boolean }[] = [] | ||||
|  | ||||
| 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}.`)) | ||||
| 	for (const [key, value] of Object.entries(obj)) { | ||||
| 		if (value !== null && typeof value === "object") fields.push(...parseObject(value as object, `${prefix}${key}.`)) | ||||
| 		else { | ||||
| 			if (typeof value === 'boolean') value = value ? 'True' : 'False' | ||||
| 			else if (!value) value = 'None' | ||||
| 			else value = value.toString() | ||||
| 			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 }) | ||||
| 			fields.push({ name: `${prefix}${key}`, value: newValue }) | ||||
| 		} | ||||
| 	} | ||||
| 	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 }) | ||||
| 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) | ||||
| 		) | ||||
| 	) | ||||
|  | ||||
| 		let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||
| 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 }) | ||||
|  | ||||
| 		if (interaction.options.getSubcommand() === 'info') { | ||||
| 			if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist !` }) | ||||
| 	const guild = interaction.guild | ||||
| 	if (!guild) return interaction.reply({ content: t(interaction.locale, "database.server_only"), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 			let fields = parseObject(guildProfile.toObject()) | ||||
| 	let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||
|  | ||||
| 			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] }) | ||||
| 	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 }) | ||||
|  | ||||
| 		} else if (interaction.options.getSubcommand() === 'init') { | ||||
| 			if (guildProfile) return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** already exists !` }) | ||||
| 		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[]) | ||||
|  | ||||
| 			guildProfile = await dbGuildInit(guild) | ||||
| 			if (!guildProfile) return await interaction.reply({ content: `An error occured while initializing database data for **${guild.name}** !` }) | ||||
| 		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 }) | ||||
|  | ||||
| 			return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully initialized !` }) | ||||
| 		guildProfile = await dbGuildInit(guild) | ||||
|  | ||||
| 		} 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\` !` }) | ||||
| 		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 }) | ||||
|  | ||||
| 			let key = interaction.options.getString('key', true) | ||||
| 			let value = interaction.options.getString('value', true) | ||||
| 		const key = interaction.options.getString("key", true) | ||||
| 		const value = interaction.options.getString("value", true) | ||||
|  | ||||
| 			let oldValue = guildProfile.get(key) | ||||
| 			if (!oldValue) oldValue = 'None' | ||||
| 		let oldValue: string = guildProfile.get(key) as string | ||||
| 		if (!oldValue) oldValue = t(interaction.locale, "common.none") | ||||
|  | ||||
| 			guildProfile.set(key, value) | ||||
| 			await guildProfile.save().catch(console.error) | ||||
| 		guildProfile.set(key, value) | ||||
| 		guildProfile.markModified(key) | ||||
| 		await guildProfile.save().catch(console.error) | ||||
|  | ||||
| 			return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully updated !\n**${key}**: ${oldValue} -> ${value}` }) | ||||
| 		} | ||||
| 		return interaction.reply({ content: t(interaction.locale, "database.updated", { name: guildProfile.guildName, key, oldValue, value }), flags: MessageFlags.Ephemeral }) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										353
									
								
								src/commands/global/freebox.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										353
									
								
								src/commands/global/freebox.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ButtonBuilder>().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<RequestAuthorization> | ||||
| 		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<TrackAuthorizationProgress> | ||||
| 			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<GetChallenge> | ||||
| 		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<OpenSession> | ||||
| 		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<ConnectionStatus> | ||||
| 				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<LcdConfig> | ||||
| 				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<LcdConfig> | ||||
| 				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 }) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/commands/global/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/commands/global/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
| @@ -1,11 +1,17 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { SlashCommandBuilder } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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`) | ||||
| 	} | ||||
| 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() | ||||
| 	})) | ||||
| } | ||||
							
								
								
									
										198
									
								
								src/commands/global/twitch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/commands/global/twitch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/commands/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/commands/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
							
								
								
									
										60
									
								
								src/commands/player/disco.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/commands/player/disco.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/commands/player/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/commands/player/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
| @@ -1,21 +1,29 @@ | ||||
| import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js' | ||||
| import { useQueue } from'discord-player' | ||||
| 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 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 !' }) | ||||
| 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) | ||||
| 	) | ||||
|  | ||||
| 		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'}.`) | ||||
| 	} | ||||
| 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")) | ||||
| } | ||||
| @@ -1,45 +1,61 @@ | ||||
| import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| import { lyricsExtractor } from '@discord-player/extractor' | ||||
| 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 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() | ||||
| 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" }) | ||||
| 	) | ||||
|  | ||||
| 		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 !' }) | ||||
| export async function execute(interaction: ChatInputCommandInteraction) { | ||||
| 	await interaction.deferReply() | ||||
|  | ||||
| 			if (track.raw.source === 'spotify') query = `${track.author} ${track.title}` | ||||
| 			else query = track.title | ||||
| 		} | ||||
| 	const player = useMainPlayer() | ||||
| 	const embed = new EmbedBuilder().setColor("#ffff64").setFooter({ text: "Powered by Genius" }) | ||||
| 	let lyrics = [] as LrcSearchResult[] | ||||
|  | ||||
| 		let lyricsFinder = lyricsExtractor() | ||||
| 	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 }) | ||||
|  | ||||
| 		let lyrics = await lyricsFinder.search(query).catch(() => null) | ||||
| 		if (!lyrics) return interaction.followUp({ content: 'Pas de paroles trouvées !' }) | ||||
| 		const track = queue.currentTrack | ||||
| 		if (!track) return interaction.followUp({ content: t(interaction.locale, "player.no_current_track"), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 		let trimmedLyrics = lyrics.lyrics.substring(0, 1997) | ||||
| 		lyrics = await player.lyrics.search({ trackName: track.title, artistName: track.author }) | ||||
|  | ||||
| 		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 | ||||
| 			}) | ||||
| 		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) | ||||
|  | ||||
| 		return interaction.followUp({ embeds: [embed] }) | ||||
| 			.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] }) | ||||
| } | ||||
| @@ -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' | ||||
| 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 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 !' }) | ||||
| 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" }) | ||||
|  | ||||
| 		let guild = interaction.guild | ||||
| 		if (!guild) return await interaction.reply({ content: 'Cette commande n\'est pas disponible en message privé.', ephemeral: true }) | ||||
| 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 }) | ||||
|  | ||||
| 		let client = guild.client | ||||
| 	const guild = interaction.guild | ||||
| 	if (!guild) return interaction.reply({ content: t(interaction.locale, "common.private_message_not_available"), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 		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)}` }) | ||||
| 	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] }) | ||||
| 	} | ||||
| 	return interaction.reply({ embeds: [embed] }) | ||||
| } | ||||
| @@ -1,15 +1,17 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { useQueue } from "discord-player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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 !' }) | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("pause") | ||||
| 	.setDescription("Pause the music") | ||||
| 	.setDescriptionLocalizations({ fr: "Met en pause la musique" }) | ||||
|  | ||||
| 		queue.node.setPaused(!queue.node.isPaused()) | ||||
| 		return await interaction.reply('Musique mise en pause !') | ||||
| 	} | ||||
| 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")) | ||||
| } | ||||
| @@ -1,96 +1,123 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from 'discord.js' | ||||
| import { useMainPlayer, useQueue, QueryType } from 'discord-player' | ||||
| import dbGuild from '../../schemas/guild' | ||||
| 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" | ||||
|  | ||||
| interface TrackSearchResult { name: string, value: string } | ||||
| 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 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([]) | ||||
| 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 }) | ||||
|  | ||||
| 		let player = useMainPlayer() | ||||
| 	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 }) | ||||
|  | ||||
| 		const resultsYouTube = await player.search(query, { searchEngine: QueryType.YOUTUBE }) | ||||
| 		const resultsSpotify = await player.search(query, { searchEngine: QueryType.SPOTIFY_SEARCH }) | ||||
| 	await interaction.deferReply() | ||||
|  | ||||
| 		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 query = interaction.options.getString("search", true) | ||||
| 	const player = useMainPlayer() | ||||
| 	let queue = useQueue(interaction.guild?.id ?? "") | ||||
|  | ||||
| 		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() } | ||||
| 	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) | ||||
| } | ||||
| @@ -1,15 +1,18 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useHistory } from 'discord-player' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { useHistory } from "discord-player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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 !') | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("previous") | ||||
| 	.setDescription("Play the previous song") | ||||
| 	.setNameLocalizations({ fr: "precedent" }) | ||||
| 	.setDescriptionLocalizations({ fr: "Joue la musique précédente" }) | ||||
|  | ||||
| 		await history.previous() | ||||
| 		return await interaction.reply('Musique précédente jouée !') | ||||
| 	} | ||||
| 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 }) | ||||
| } | ||||
| @@ -1,19 +1,22 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { useQueue } from "discord-player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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.' }) | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("queue") | ||||
| 	.setDescription("Get the queue") | ||||
| 	.setNameLocalizations({ fr: "file" }) | ||||
| 	.setDescriptionLocalizations({ fr: "Récupérer la file d'attente." }) | ||||
|  | ||||
| 		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.` }) | ||||
| 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}) | ||||
|  | ||||
| 		return interaction.reply({ content: `Lecture en cours : ${track} \nFile d'attente actuelle : \n${tracks.join('\n')}` }) | ||||
| 	} | ||||
| 	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") }) }) | ||||
| } | ||||
| @@ -1,15 +1,18 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { useQueue } from "discord-player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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 !' }) | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("resume") | ||||
| 	.setDescription("Resume the music") | ||||
| 	.setNameLocalizations({ fr: "reprendre" }) | ||||
| 	.setDescriptionLocalizations({ fr: "Reprendre la musique" }) | ||||
|  | ||||
| 		queue.node.setPaused(!queue.node.isPaused()) | ||||
| 		return await interaction.reply('Musique reprise !') | ||||
| 	} | ||||
| 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")) | ||||
| } | ||||
| @@ -1,15 +1,18 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { useQueue } from "discord-player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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 !' }) | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("shuffle") | ||||
| 	.setDescription("Shuffle the queue") | ||||
| 	.setNameLocalizations({ fr: "melanger" }) | ||||
| 	.setDescriptionLocalizations({ fr: "Mélange la file d'attente" }) | ||||
|  | ||||
| 		queue.tracks.shuffle() | ||||
| 		return await interaction.reply('File d\'attente mélangée !') | ||||
| 	} | ||||
| 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")) | ||||
| } | ||||
| @@ -1,15 +1,18 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { useQueue } from "discord-player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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 !' }) | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("skip") | ||||
| 	.setDescription("Skip the current song") | ||||
| 	.setNameLocalizations({ fr: "passer" }) | ||||
| 	.setDescriptionLocalizations({ fr: "Passer la musique en cours" }) | ||||
|  | ||||
| 		queue.node.skip() | ||||
| 		return await interaction.reply('Musique passée !') | ||||
| 	} | ||||
| 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")) | ||||
| } | ||||
| @@ -1,15 +1,21 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| 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 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 !' }) | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("stop") | ||||
| 	.setDescription("Stop the music") | ||||
| 	.setNameLocalizations({ fr: "arreter" }) | ||||
| 	.setDescriptionLocalizations({ fr: "Arrêter la musique" }) | ||||
|  | ||||
| 		queue.delete() | ||||
| 		return await interaction.reply('Musique arrêtée !') | ||||
| 	} | ||||
| 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")) | ||||
| } | ||||
| @@ -1,21 +1,26 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { useQueue } from 'discord-player' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { useQueue } from "discord-player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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 !' }) | ||||
| 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) | ||||
| 	) | ||||
|  | ||||
| 		queue.node.setVolume(volume as number) | ||||
| 		return await interaction.reply(`Volume modifié à ${volume}% !`) | ||||
| 	} | ||||
| 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() })) | ||||
| } | ||||
| @@ -1,58 +1,70 @@ | ||||
| import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, MessageReaction, User }from 'discord.js' | ||||
| import * as crack from '../../utils/crack' | ||||
| 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 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() | ||||
| 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) | ||||
| 	) | ||||
|  | ||||
| 		let query = interaction.options.getString('jeu') | ||||
| 		if (!query) return | ||||
| export async function execute(interaction: ChatInputCommandInteraction) { | ||||
| 	await interaction.deferReply() | ||||
|  | ||||
| 		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}" !` }) | ||||
| 	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 ?? "") | ||||
| 			 | ||||
| 		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}` }) | ||||
| 			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] | ||||
|  | ||||
| 			let emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'] | ||||
| 			for (let i = 0; i < games.length; i++) await message.react(emojis[i]) | ||||
| 	const url = await repo(game) | ||||
| 	if (!url) return | ||||
| 	 | ||||
| 			// 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] | ||||
| 	const file = await torrent(url) | ||||
| 	if (!file) return | ||||
| 	 | ||||
| 		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) | ||||
| 	const filePath = await download(url, file) | ||||
| 	if (!filePath) return | ||||
| 	 | ||||
| 		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 !`) | ||||
| 	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] }) | ||||
| 	} | ||||
| 	await interaction.followUp({ embeds: [embed], files: [filePath] }) | ||||
| } | ||||
| @@ -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 <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 <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))}` }) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/commands/salonpostam/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/commands/salonpostam/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
| @@ -1,34 +1,34 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js' | ||||
| import { getVoiceConnection, joinVoiceChannel } from '@discordjs/voice' | ||||
| import { SlashCommandBuilder } from "discord.js" | ||||
| import type { ChatInputCommandInteraction, GuildMember } from "discord.js" | ||||
| import { getVoiceConnection, joinVoiceChannel } from "@discordjs/voice" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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 !' }) | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("papa") | ||||
| 	.setDescription("If daddy calls me, I join him") | ||||
| 	.setDescriptionLocalizations({ fr: "Si papa m'appelle, je le rejoins" }) | ||||
|  | ||||
| 		let guild = interaction.guild | ||||
| 		if (!guild) return interaction.reply({ content: 'Je ne peux pas rejoindre ton vocal en message privé, papa !' }) | ||||
| export async function execute(interaction: ChatInputCommandInteraction) { | ||||
| 	if (interaction.user.id !== "223831938346123275") return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.not_your_father") }) | ||||
|  | ||||
| 		let member = interaction.member as GuildMember | ||||
| 	const guild = interaction.guild | ||||
| 	if (!guild) return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.no_dm") }) | ||||
|  | ||||
| 		let botChannel = guild.members.me?.voice.channel | ||||
| 		let papaChannel = member.voice.channel | ||||
| 	const member = interaction.member as GuildMember | ||||
|  | ||||
| 		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 !' }) | ||||
| 	} | ||||
| 	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") }) | ||||
| } | ||||
| @@ -1,79 +1,84 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js' | ||||
| import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, EndBehaviorType } from '@discordjs/voice' | ||||
| 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 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 }) | ||||
| 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) | ||||
| 	) | ||||
|  | ||||
| 		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 | ||||
| export async function execute(interaction: ChatInputCommandInteraction) { | ||||
| 	const guild = interaction.guild | ||||
| 	if (!guild) 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 }) | ||||
| 	const user = interaction.options.getUser("user", true) | ||||
| 	const member = await guild.members.fetch(user.id) | ||||
| 	const caller = interaction.member as GuildMember | ||||
|  | ||||
| 		await interaction.reply({ content: 'Je vais parler par dessus cette personne !', ephemeral: true }) | ||||
| 	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 }) | ||||
|  | ||||
| 		/* | ||||
| 		// 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) | ||||
| 	await interaction.reply({ content: t(interaction.locale, "salonpostam.parle.will_speak_over"), flags: MessageFlags.Ephemeral }) | ||||
|  | ||||
| 		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 }) | ||||
| 	/* | ||||
| 	// 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) | ||||
|  | ||||
| 		// Limit the number of files to the last 10 | ||||
| 		//files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10) | ||||
| 	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 }) | ||||
|  | ||||
| 		// 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]) | ||||
| 		*/ | ||||
| 	// Limit the number of files to the last 10 | ||||
| 	//files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10) | ||||
|  | ||||
| 		let playing = false | ||||
| 		let player = createAudioPlayer() | ||||
| 		player.on(AudioPlayerStatus.Idle, () => { playing = false }) | ||||
| 	// 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 connection = joinVoiceChannel({ | ||||
| 			channelId: caller.voice.channelId as string, | ||||
| 			guildId: interaction.guildId as string, | ||||
| 			adapterCreator: guild.voiceAdapterCreator, | ||||
| 			selfDeaf: false | ||||
| 		}) | ||||
| 		connection.subscribe(player) | ||||
| 	let playing = false | ||||
| 	const player = createAudioPlayer() | ||||
| 	player.on(AudioPlayerStatus.Idle, () => { playing = false }) | ||||
|  | ||||
| 		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) | ||||
| 			} | ||||
| 		}) | ||||
| 	const connection = joinVoiceChannel({ | ||||
| 		channelId: caller.voice.channelId ?? "", | ||||
| 		guildId: interaction.guildId ?? "", | ||||
| 		adapterCreator: guild.voiceAdapterCreator, | ||||
| 		selfDeaf: false | ||||
| 	}) | ||||
| 	connection.subscribe(player) | ||||
|  | ||||
| 		interaction.client.on('voiceStateUpdate', (oldState, newState) => { | ||||
| 			if (oldState.id === member.id && newState.channelId !== caller.voice.channelId) { | ||||
| 				stream.destroy() | ||||
| 				connection.disconnect() | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| 	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() | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
| @@ -1,29 +1,47 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| 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') | ||||
| 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) | ||||
| 	) | ||||
|  | ||||
| 		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() | ||||
| 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() | ||||
| } | ||||
| @@ -1,19 +1,22 @@ | ||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, Guild } from 'discord.js' | ||||
| import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||
| import type { ChatInputCommandInteraction } from "discord.js" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| export default { | ||||
| 	data: new SlashCommandBuilder() | ||||
| 		.setName('update') | ||||
| 		.setDescription('Update the member count channel.'), | ||||
| 	async execute(interaction: ChatInputCommandInteraction) { | ||||
| 		let guild = interaction.guild as Guild | ||||
| export const data = new SlashCommandBuilder() | ||||
| 	.setName("update") | ||||
| 	.setDescription("Update the member count channel") | ||||
| 	.setDescriptionLocalizations({ fr: "Mettre à jour le canal de nombre de membres" }) | ||||
|  | ||||
| 		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) | ||||
| 	} | ||||
| 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) | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
| @@ -1,14 +1,12 @@ | ||||
| import { Events, Guild } from 'discord.js' | ||||
| import dbGuildInit from '../../utils/dbGuildInit' | ||||
| import { Events, Guild } from "discord.js" | ||||
| import dbGuildInit from "@/utils/dbGuildInit" | ||||
| import { logConsole } from "@/utils/console" | ||||
|  | ||||
| export default { | ||||
| 	name: Events.GuildCreate, | ||||
| 	async execute(guild: Guild) { | ||||
| 		console.log(`Joined "${guild.name}" with ${guild.memberCount} members`) | ||||
| export const name = Events.GuildCreate | ||||
| export async function execute(guild: Guild) { | ||||
| 	logConsole('discordjs', 'guild_create', { name: guild.name, count: guild.memberCount.toString() }) | ||||
|  | ||||
| 		let guildProfile = await dbGuildInit(guild) | ||||
| 		if (!guildProfile) return console.log(`An error occured while initializing database data for "${guild.name}" !`) | ||||
| 	const guildProfile = await dbGuildInit(guild) | ||||
|  | ||||
| 		console.log(`Database data for new guild "${guildProfile.guildName}" successfully initialized !`) | ||||
| 	} | ||||
| 	logConsole('mongoose', 'guild_create', { name: guildProfile.guildName }) | ||||
| } | ||||
| @@ -1,44 +1,42 @@ | ||||
| import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js' | ||||
| import { Events, EmbedBuilder, ChannelType } from "discord.js" | ||||
| import type { GuildMember } from "discord.js" | ||||
| import { t } from "@/utils/i18n" | ||||
| import { logConsole } from "@/utils/console" | ||||
|  | ||||
| export default { | ||||
| 	name: Events.GuildMemberAdd, | ||||
| 	async execute(member: GuildMember) { | ||||
| 		if (member.guild.id === '1086577543651524699') { // Salon posé tamisé | ||||
| 			let guild = member.guild | ||||
| 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(() => { | ||||
| 				let i = 0 | ||||
| 				guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) | ||||
| 		guild.members.fetch().then(async () => { | ||||
| 			let i = 0 | ||||
| 			guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) | ||||
|  | ||||
| 				let channel = guild.channels.cache.get('1091140609139560508') | ||||
| 				if (!channel) return | ||||
| 			const 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 | ||||
| 			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 | ||||
|  | ||||
| 			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] }) | ||||
| 		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] }) | ||||
| 	} | ||||
| } | ||||
| @@ -1,21 +1,22 @@ | ||||
| import { Events, GuildMember } from 'discord.js' | ||||
| import { Events } from "discord.js" | ||||
| import type { GuildMember } from "discord.js" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| export default { | ||||
| 	name: Events.GuildMemberRemove, | ||||
| 	async execute(member: GuildMember) { | ||||
| 		if (member.guild.id === '1086577543651524699') { // Salon posé tamisé | ||||
| 			let guild = member.guild | ||||
| 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(() => { | ||||
| 				let i = 0 | ||||
| 				guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) | ||||
| 		guild.members.fetch().then(async () => { | ||||
| 			let i = 0 | ||||
| 			guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) | ||||
|  | ||||
| 				let channel = guild.channels.cache.get('1091140609139560508') | ||||
| 				if (!channel) return | ||||
| 			const channel = guild.channels.cache.get("1091140609139560508") | ||||
| 			if (!channel) return | ||||
|  | ||||
| 				channel.setName('Changement...') | ||||
| 				channel.setName(`${i} Gens Posés`) | ||||
| 			}).catch(console.error) | ||||
| 		} | ||||
| 			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) | ||||
| 	} | ||||
| } | ||||
| @@ -1,35 +1,37 @@ | ||||
| import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js' | ||||
| import { Events, EmbedBuilder, ChannelType } from "discord.js" | ||||
| import type { GuildMember } from "discord.js" | ||||
| import { t } from "@/utils/i18n" | ||||
| import { logConsole } from "@/utils/console" | ||||
|  | ||||
| export default { | ||||
| 	name: Events.GuildMemberUpdate, | ||||
| 	async execute(oldMember: GuildMember, newMember: GuildMember) { | ||||
| 		if (newMember.guild.id === '796327643783626782') { // Jujul Community | ||||
| 			let guild = newMember.guild | ||||
| export const name = Events.GuildMemberUpdate | ||||
| export async function execute(oldMember: GuildMember, newMember: GuildMember) { | ||||
| 	if (newMember.guild.id === "796327643783626782") { | ||||
| 		// Jujul Community | ||||
| 		const 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" !`) | ||||
| 		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 | ||||
| 		} | ||||
|  | ||||
| 			let boostRole = guild.roles.premiumSubscriberRole | ||||
| 			if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`) | ||||
| 		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) | ||||
| 		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 !`) | ||||
| 		if (!hadRole && hasRole) { | ||||
| 			if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return } | ||||
|  | ||||
| 				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()) | ||||
| 			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()) | ||||
|  | ||||
| 				await channel.send({ embeds: [embed] }) | ||||
| 			} | ||||
| 			return channel.send({ embeds: [embed] }) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,18 +1,18 @@ | ||||
| import { Events, Guild } from 'discord.js' | ||||
| import dbGuildInit from '../../utils/dbGuildInit' | ||||
| import dbGuild from '../../schemas/guild' | ||||
| 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 default { | ||||
| 	name: Events.GuildUpdate, | ||||
| 	async execute(oldGuild: Guild, newGuild: Guild) { | ||||
| 		console.log(`Guild ${oldGuild.name} updated`) | ||||
| 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) | ||||
| 		} | ||||
| 	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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/events/client/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/events/client/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
| @@ -1,45 +1,49 @@ | ||||
| import { Events, Interaction, ChatInputCommandInteraction, AutocompleteInteraction, ButtonInteraction } from 'discord.js' | ||||
| import { playerButtons, playerEdit } from '../../utils/player' | ||||
| 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 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.`) | ||||
| 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 } | ||||
|  | ||||
| 		if (interaction.isChatInputCommand()) { | ||||
| 			interaction = interaction as ChatInputCommandInteraction | ||||
| 		logConsole('discordjs', 'interaction_create.command_launched', { command: interaction.commandName, user: interaction.user.tag }) | ||||
|  | ||||
| 			let chatInputCommand = interaction.client.commands.get(interaction.commandName) | ||||
| 			if (!chatInputCommand) return console.error(`No chat input command matching ${interaction.commandName} was found.`) | ||||
| 		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 } | ||||
|  | ||||
| 			console.log(`Command '${interaction.commandName}' launched by ${interaction.user.tag}`) | ||||
| 		logConsole('discordjs', 'interaction_create.autocomplete_launched', { command: interaction.commandName, user: 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 | ||||
| 		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 } | ||||
|  | ||||
| 			let autoCompleteRun = interaction.client.commands.get(interaction.commandName) | ||||
| 			if (!autoCompleteRun) return console.error(`No autoCompleteRun matching ${interaction.commandName} was found.`) | ||||
| 		logConsole('discordjs', 'interaction_create.button_clicked', { id: interaction.customId, user: interaction.user.tag }) | ||||
|  | ||||
| 			console.log(`AutoCompleteRun '${interaction.commandName}' launched by ${interaction.user.tag}`) | ||||
| 		try { await button.execute(interaction) } | ||||
| 		catch (error) { logConsoleError('discordjs', 'interaction_create.button_error', { id: interaction.customId }, error as Error) } | ||||
|  | ||||
| 			try { await autoCompleteRun.autocompleteRun(interaction) } | ||||
| 			catch (error) { console.error(`Error autocompleting ${interaction.commandName}:`, error) } | ||||
| 		} | ||||
| 		else if (interaction.isButton()) { | ||||
| 			interaction = interaction as ButtonInteraction | ||||
| 		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 } | ||||
|  | ||||
| 			let button = interaction.client.buttons.get(interaction.customId) | ||||
| 			if (!button) return console.error(`No button id matching ${interaction.customId} was found.`) | ||||
| 		logConsole('discordjs', 'interaction_create.selectmenu_used', { id: interaction.customId, user: interaction.user.tag }) | ||||
|  | ||||
| 			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) } | ||||
| 		} | ||||
| 		try { await selectMenu.execute(interaction) } | ||||
| 		catch (error) { logConsoleError('discordjs', 'interaction_create.selectmenu_error', { id: interaction.customId }, error as Error) } | ||||
| 	} | ||||
| } | ||||
| @@ -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 { 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" | ||||
|  | ||||
| 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 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 }) | ||||
|  | ||||
| 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(() => { 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) | ||||
|  | ||||
| 		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) | ||||
| 	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) | ||||
|  | ||||
| 		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) | ||||
| 	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) | ||||
|  | ||||
| 		let guilds = client.guilds.cache | ||||
| 		guilds.forEach(async guild => { | ||||
| 			let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||
| 		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) | ||||
|  | ||||
| 			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 } | ||||
| 		// 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 | ||||
|  | ||||
| 			client.disco = { interval: {} as NodeJS.Timeout } | ||||
| 			client.disco.interval = setInterval(async () => { | ||||
| 				let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||
| 			if (dbDataDisco.enabled) { | ||||
| 				const state = await playerDisco(client, guild, dbDataDisco) | ||||
| 				if (state === "clear") clearInterval(client.disco.interval) | ||||
| 			} | ||||
| 		}, 3000) | ||||
|  | ||||
| 				if (guildProfile?.guildPlayer?.disco?.enabled) { | ||||
| 					let state = await playerDisco(client, guildProfile) | ||||
| 					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) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 			client.rss = { interval: {} as NodeJS.Timeout } | ||||
| 			client.rss.interval = setInterval(async () => { | ||||
| 				let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||
| 		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 } | ||||
|  | ||||
| 				if (guildProfile?.guildRss?.enabled) { | ||||
| 					let state = await rss(client, guildProfile) | ||||
| 					if (state === 'clear') clearInterval(client.rss.interval) | ||||
| 				} | ||||
| 			}, 30000) | ||||
| 		await Promise.all(dbDataTwitch.streamers.map(async streamer => { | ||||
| 			if (streamerIds.includes(streamer.twitchUserId)) return | ||||
| 			streamerIds.push(streamer.twitchUserId) | ||||
|  | ||||
| 			// 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' | ||||
| 			const user = await twitchClient.users.getUserById(streamer.twitchUserId) | ||||
| 			if (!user) { logConsole('twitch', 'ready.user_not_found', { guild: guild.name, userId: streamer.twitchUserId }); return } | ||||
|  | ||||
| 			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 !`)) | ||||
| 			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 }) | ||||
| 			} | ||||
|  | ||||
| 			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\` !`)) | ||||
| 			logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) | ||||
|  | ||||
| 			let twitch = new WebSocket.client().on('connect', async connection => { | ||||
| 				console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub WebSocket Connected !`)) | ||||
| 			const stream = await user.getStream() | ||||
| 			if (stream && streamer.messageId) { | ||||
| 				logConsole('twitch', 'ready.stream_restoration', { guild: guild.name, userName: user.name, userId: streamer.twitchUserId }) | ||||
|  | ||||
| 				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}`)) | ||||
| 						} | ||||
| 				// 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) | ||||
| 			} | ||||
|  | ||||
| 					// Handle notification messages | ||||
| 					else if (data.metadata.message_type === 'notification') Twitch.notification(client_id, client_secret, channel_access_token, data, guild) | ||||
| 			logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) | ||||
| 		})) | ||||
| 	})) | ||||
|  | ||||
| 				} 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))) | ||||
| 	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 | ||||
|  | ||||
| 			twitch.connect('wss://eventsub.wss.twitch.tv/ws') | ||||
| 		}) | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
| @@ -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  | ||||
| 		} | ||||
| 		*/ | ||||
| 	} | ||||
| } | ||||
| @@ -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 !')) | ||||
| 	} | ||||
| export const name = "connected" | ||||
| export function execute() { | ||||
| 	logConsole('mongoose', 'connected') | ||||
| } | ||||
| @@ -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...')) | ||||
| 	} | ||||
| export const name = "connecting" | ||||
| export function execute() { | ||||
| 	logConsole('mongoose', 'connecting') | ||||
| } | ||||
| @@ -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 !')) | ||||
| 	} | ||||
| export const name = "disconnected" | ||||
| export function execute() { | ||||
| 	logConsole('mongoose', 'disconnected') | ||||
| } | ||||
| @@ -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)) | ||||
| 	} | ||||
| export const name = "error" | ||||
| export function execute(error: Error) { | ||||
| 	logConsoleError('mongoose', 'error', { message: error.message }, error) | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/events/mongo/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/events/mongo/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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[] | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { GuildQueue, Track } from 'discord-player' | ||||
| import { PlayerMetadata } from '../../utils/player' | ||||
| import type { GuildQueue, Track } from "discord-player" | ||||
| import type { PlayerMetadata } from "@/types/player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| export default { | ||||
| 	name: 'audioTrackAdd', | ||||
| 	async execute(queue: GuildQueue<PlayerMetadata>, 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 !`) | ||||
| 	} | ||||
| export const name = "audioTrackAdd" | ||||
| export async function execute(queue: GuildQueue<PlayerMetadata>, 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 }) }) | ||||
| } | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { GuildQueue, Track } from 'discord-player' | ||||
| import { PlayerMetadata } from '../../utils/player' | ||||
| import type { GuildQueue, Track } from "discord-player" | ||||
| import type { PlayerMetadata } from "@/types/player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| export default { | ||||
| 	name: 'audioTracksAdd', | ||||
| 	async execute(queue: GuildQueue<PlayerMetadata>, track: Array<Track>) { | ||||
| 		// Emitted when the player adds multiple songs to its queue | ||||
| 		queue.metadata.channel.send(`Ajout de ${track.length} musiques à la file d'attente !`) | ||||
| 	} | ||||
| export const name = "audioTracksAdd" | ||||
| export async function execute(queue: GuildQueue<PlayerMetadata>, 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() }) }) | ||||
| } | ||||
| @@ -1,10 +1,9 @@ | ||||
| import { GuildQueue } from 'discord-player' | ||||
| import type { GuildQueue } from "discord-player" | ||||
| import { logConsoleDev } from "@/utils/console" | ||||
|  | ||||
| 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}`) | ||||
| 	} | ||||
| 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 }) | ||||
| } | ||||
| @@ -1,24 +1,13 @@ | ||||
| import { GuildQueue } from 'discord-player' | ||||
| import { PlayerMetadata } from '../../utils/player' | ||||
| import dbGuild from '../../schemas/guild' | ||||
| import type { GuildQueue } from "discord-player" | ||||
| import type { PlayerMetadata } from "@/types/player" | ||||
| import { stopProgressSaving } from "@/utils/player" | ||||
| import { t } from "@/utils/i18n" | ||||
|  | ||||
| export default { | ||||
| 	name: 'disconnect', | ||||
| 	async execute(queue: GuildQueue<PlayerMetadata>) { | ||||
| 		// Emitted when the bot leaves the voice channel | ||||
| 		queue.metadata.channel.send("J'ai quitté le vocal !") | ||||
| export const name = "disconnect" | ||||
| export async function execute(queue: GuildQueue<PlayerMetadata>) { | ||||
| 	// Emitted when the bot leaves the voice channel | ||||
| 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | ||||
|  | ||||
| 		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) | ||||
| 	} | ||||
| 	if (!queue.metadata.channel) return | ||||
| 	if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.disconnect") }) | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user