Compare commits
	
		
			25 Commits
		
	
	
		
			build_2025
			...
			build_2025
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 18a293690e | |||
| fb7ba5d145 | |||
| f94a3852e8 | |||
| 462ad2e9d6 | |||
| f1b5592045 | |||
| af4e6e2e69 | |||
| 6d0c0145ee | |||
| e714e94f85 | |||
| 0cc81d6430 | |||
| 1dcb8c6826 | |||
| 2b6870b861 | |||
| ceb7a74b11 | |||
| fd4e17a754 | |||
| 4ed73f7c72 | |||
| f1a488d362 | |||
| 066a3864dd | |||
| 9a4902291e | |||
| d06df32bab | |||
| 60d0c01212 | |||
| 5e7c1842a4 | |||
| ddd617317c | |||
| f2c6388da6 | |||
| 66c5891510 | |||
| eb6c40c2f5 | |||
| 19119e5c77 | 
| @@ -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 | ||||||
| @@ -2,17 +2,12 @@ name: Build and Push Docker Image | |||||||
|  |  | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: |  | ||||||
|       - master |  | ||||||
|     tags: |     tags: | ||||||
|       - 'build_*' |       - 'build_*' | ||||||
|   pull_request: |  | ||||||
|     branches: |  | ||||||
|       - master |  | ||||||
|  |  | ||||||
| env: | env: | ||||||
|   REGISTRY: rgy.angels-dev.fr |   REGISTRY: rgy.angels-dev.fr | ||||||
|   PATH: prod |   IMAGE_PATH: prod | ||||||
|   IMAGE_NAME: bot_tamiseur |   IMAGE_NAME: bot_tamiseur | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
| @@ -26,6 +21,16 @@ jobs: | |||||||
|     - name: Checkout repository |     - name: Checkout repository | ||||||
|       uses: actions/checkout@v4 |       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 |     - name: Set up Docker Buildx | ||||||
|       uses: docker/setup-buildx-action@v3 |       uses: docker/setup-buildx-action@v3 | ||||||
|  |  | ||||||
| @@ -36,41 +41,34 @@ jobs: | |||||||
|         username: ${{ secrets.REGISTRY_USERNAME }} |         username: ${{ secrets.REGISTRY_USERNAME }} | ||||||
|         password: ${{ secrets.REGISTRY_PASSWORD }} |         password: ${{ secrets.REGISTRY_PASSWORD }} | ||||||
|  |  | ||||||
|     - name: Extract metadata |     - name: Extract metadata for Docker | ||||||
|       id: meta |       id: meta | ||||||
|       uses: docker/metadata-action@v5 |       uses: docker/metadata-action@v5 | ||||||
|       with: |       with: | ||||||
|         images: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} |         images: ${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}/${{ env.IMAGE_NAME }} | ||||||
|         tags: | |         tags: | | ||||||
|           # Tag avec le nom de la branche |  | ||||||
|           type=ref,event=branch |  | ||||||
|           # Tag avec le nom du tag Git |           # Tag avec le nom du tag Git | ||||||
|           type=ref,event=tag |           type=ref,event=tag | ||||||
|           # Tag avec le SHA du commit |           # Tag 'latest' pour la branche master | ||||||
|           type=sha,prefix={{branch}}- |  | ||||||
|           # Tag latest pour la branche master |  | ||||||
|           type=raw,value=latest,enable={{is_default_branch}} |           type=raw,value=latest,enable={{is_default_branch}} | ||||||
|  |           # Tag avec le SHA pour les autres branches | ||||||
|  |           type=sha,prefix=sha- | ||||||
|         labels: | |         labels: | | ||||||
|           org.opencontainers.image.title=${{ env.IMAGE_NAME }} |           org.opencontainers.image.title=${{ env.IMAGE_NAME }} | ||||||
|           org.opencontainers.image.description=Bot Discord |           org.opencontainers.image.description=Bot Discord de moi | ||||||
|           org.opencontainers.image.url=https://gitea.zac.ovh/zachary/bot_Tamiseur |           org.opencontainers.image.url=https://git.zac.ovh/zachary/bot_Tamiseur | ||||||
|           org.opencontainers.image.source=https://gitea.zac.ovh/zachary/bot_Tamiseur |           org.opencontainers.image.source=https://git.zac.ovh/zachary/bot_Tamiseur | ||||||
|           org.opencontainers.image.revision=${{ github.sha }} |           org.opencontainers.image.revision=${{ github.sha }} | ||||||
|  |           org.opencontainers.image.created={{date 'RFC3339'}} | ||||||
|  |  | ||||||
|     - name: Build and push Docker image |     - name: Build and push Docker image | ||||||
|       uses: docker/build-push-action@v5 |       uses: docker/build-push-action@v5 | ||||||
|       with: |       with: | ||||||
|         context: . |         context: . | ||||||
|         platforms: linux/amd64,linux/arm64  # Multi-architecture si nécessaire |         file: build/node.dockerfile | ||||||
|  |         platforms: linux/amd64,linux/arm64 | ||||||
|         push: true |         push: true | ||||||
|         tags: ${{ steps.meta.outputs.tags }} |         tags: ${{ steps.meta.outputs.tags }} | ||||||
|         labels: ${{ steps.meta.outputs.labels }} |         labels: ${{ steps.meta.outputs.labels }} | ||||||
|         cache-from: type=gha |         cache-from: type=gha | ||||||
|         cache-to: type=gha,mode=max |         cache-to: type=gha,mode=max | ||||||
|  |  | ||||||
|     - name: Generate artifact attestation |  | ||||||
|       uses: actions/attest-build-provenance@v1 |  | ||||||
|       with: |  | ||||||
|         subject-name: ${{ env.REGISTRY }}/${{ env.PATH }}/${{ env.IMAGE_NAME }} |  | ||||||
|         subject-digest: ${{ steps.build.outputs.digest }} |  | ||||||
|         push-to-registry: true |  | ||||||
							
								
								
									
										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/ | dist/ | ||||||
| node_modules/ | node_modules/ | ||||||
| public/cracks/* | public/cracks/ | ||||||
| .env* |  | ||||||
| .ncurc.json |  | ||||||
							
								
								
									
										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
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										128
									
								
								README.md
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -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 | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								build/node.dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								build/node.dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | # Starting from node | ||||||
|  | FROM node:22-slim | ||||||
|  |  | ||||||
|  | # Install build dependencies | ||||||
|  | RUN apt-get update && \ | ||||||
|  |     apt-get install -y ffmpeg python3 make g++ | ||||||
|  |  | ||||||
|  | # Set the working directory | ||||||
|  | WORKDIR /app | ||||||
|  | RUN chown node:node ./ | ||||||
|  | USER node | ||||||
|  |  | ||||||
|  | # Copy package files first | ||||||
|  | COPY --chown=node:node package.json package-lock.json* . | ||||||
|  |  | ||||||
|  | # Install app dependencies | ||||||
|  | ENV NODE_ENV=production | ||||||
|  | RUN npm ci --only=production --ignore-scripts && \ | ||||||
|  |     npm install bufferutil zlib-sync && \ | ||||||
|  |     npm cache clean --force | ||||||
|  |  | ||||||
|  | # Copy the builded files | ||||||
|  | COPY --chown=node:node ./dist/* . | ||||||
|  |  | ||||||
|  | # Return to root user to remove build dependencies | ||||||
|  | USER root | ||||||
|  | RUN apt-get remove -y python3 make g++ && \ | ||||||
|  |     apt-get autoremove -y && \ | ||||||
|  |     rm -rf /var/lib/apt/lists/* | ||||||
|  |  | ||||||
|  | # Go back to node user | ||||||
|  | USER node | ||||||
|  |  | ||||||
|  | # Start the application | ||||||
|  | CMD ["npm", "start"] | ||||||
							
								
								
									
										32
									
								
								deploy/templates/ingress.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								deploy/templates/ingress.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | {{- if .Values.ingress.enabled }} | ||||||
|  | apiVersion: networking.k8s.io/v1 | ||||||
|  | kind: Ingress | ||||||
|  | metadata: | ||||||
|  |   name: {{ .Release.Name }} | ||||||
|  |   annotations: | ||||||
|  |     external-dns.alpha.kubernetes.io/target: omegamaestro.{{ .Values.ingress.domain }} | ||||||
|  |     cert-manager.io/cluster-issuer: {{ .Values.ingress.issuer }} | ||||||
|  |     nginx.ingress.kubernetes.io/backend-protocol: "HTTP" | ||||||
|  |     {{- if .Values.ingress.geoip }} | ||||||
|  |     nginx.ingress.kubernetes.io/server-snippet: | | ||||||
|  |       if ($lan = yes) { set $allowed_country yes; } | ||||||
|  |       if ($allowed_country = no) { return 451; } | ||||||
|  |     {{- end }} | ||||||
|  | spec: | ||||||
|  |   ingressClassName: {{ .Values.ingress.class }} | ||||||
|  |   tls: | ||||||
|  |   - hosts: | ||||||
|  |     - {{ .Values.ingress.subdomain }}.{{ .Values.ingress.domain }} | ||||||
|  |     secretName: {{ .Release.Name }}-tls | ||||||
|  |   rules: | ||||||
|  |   - host: "{{ .Values.ingress.subdomain }}.{{ .Values.ingress.domain }}" | ||||||
|  |     http: | ||||||
|  |       paths: | ||||||
|  |       - path: / | ||||||
|  |         pathType: Prefix | ||||||
|  |         backend: | ||||||
|  |           service: | ||||||
|  |             name: "{{ .Release.Name }}-{{ .Values.service.name }}" | ||||||
|  |             port: | ||||||
|  |               name: {{ .Values.service.name }} | ||||||
|  | {{- end }} | ||||||
							
								
								
									
										15
									
								
								deploy/templates/service.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								deploy/templates/service.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | {{- if .Values.service.enabled }} | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: Service | ||||||
|  | metadata: | ||||||
|  |   name: "{{ .Release.Name }}-{{ .Values.service.name }}" | ||||||
|  | spec: | ||||||
|  |   selector: | ||||||
|  |     pod: {{ .Release.Name }} | ||||||
|  |   ports: | ||||||
|  |     - name: {{ .Values.service.name }} | ||||||
|  |       port: {{ .Values.deployment.env.TWURPLE_PORT | default .Values.service.port }} | ||||||
|  |       targetPort: {{ .Values.deployment.env.TWURPLE_PORT | default .Values.service.port }} | ||||||
|  |       protocol: TCP | ||||||
|  |   type: {{ .Values.service.type }} | ||||||
|  | {{- end }} | ||||||
| @@ -3,7 +3,7 @@ deployment: | |||||||
|   strategy: RollingUpdate |   strategy: RollingUpdate | ||||||
|   image: |   image: | ||||||
|     repository: "rgy.angels-dev.fr/prod/bot_tamiseur" |     repository: "rgy.angels-dev.fr/prod/bot_tamiseur" | ||||||
|     tag: "3.0.4" |     tag: "build_2025-08-19_22h46" | ||||||
|     pullPolicy: IfNotPresent |     pullPolicy: IfNotPresent | ||||||
|   env: |   env: | ||||||
|     NODE_ENV: "production" |     NODE_ENV: "production" | ||||||
| @@ -16,3 +16,16 @@ deployment: | |||||||
|     requests: |     requests: | ||||||
|       Cpu: "0.1" |       Cpu: "0.1" | ||||||
|       Memory: "50Mi" |       Memory: "50Mi" | ||||||
|  |  | ||||||
|  | service: | ||||||
|  |   enabled: true | ||||||
|  |   type: ClusterIP | ||||||
|  |   name: twurple | ||||||
|  |  | ||||||
|  | ingress: | ||||||
|  |   enabled: true | ||||||
|  |   class: nginx | ||||||
|  |   subdomain: dcb-chantier.prd | ||||||
|  |   domain: angels-dev.fr | ||||||
|  |   issuer: letsencrypt-prod | ||||||
|  |   geoip: false | ||||||
| @@ -1,23 +1,22 @@ | |||||||
| import typescriptEslint from "@typescript-eslint/eslint-plugin" | import eslint from "@eslint/js" | ||||||
| import tsParser from "@typescript-eslint/parser" | import tseslint from "typescript-eslint" | ||||||
| import { FlatCompat } from "@eslint/eslintrc" |  | ||||||
| import { fileURLToPath } from "node:url" |  | ||||||
| import path from "node:path" |  | ||||||
| import js from "@eslint/js" |  | ||||||
|  |  | ||||||
| const __filename = fileURLToPath(import.meta.url) | export default tseslint.config( | ||||||
| const __dirname = path.dirname(__filename) | 	eslint.configs.recommended, | ||||||
| const compat = new FlatCompat({ | 	tseslint.configs.recommendedTypeChecked, | ||||||
| 	baseDirectory: __dirname, |  | ||||||
| 	recommendedConfig: js.configs.recommended, |  | ||||||
| 	allConfig: js.configs.all |  | ||||||
| }) |  | ||||||
|  |  | ||||||
| export default [ |  | ||||||
| 	...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), |  | ||||||
| 	{ | 	{ | ||||||
| 		plugins: { "@typescript-eslint": typescriptEslint }, | 		languageOptions: { | ||||||
| 		languageOptions: { parser: tsParser }, | 			parserOptions: { | ||||||
| 		rules: { "prefer-const": "off" } | 				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", | 	"name": "bot_tamiseur", | ||||||
| 	"description": "Listen to music and use fun commands with your friends!", | 	"description": "Listen to music and use fun commands with your friends!", | ||||||
| 	"version": "3.0.4", | 	"version": "4.0.1", | ||||||
| 	"author": { | 	"author": { | ||||||
| 		"name": "Zachary Guénot" | 		"name": "Zachary Guénot" | ||||||
| 	}, | 	}, | ||||||
| 	"main": "src/index.ts", | 	"main": "src/index.ts", | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"format": "prettier --write .", | 		"start": "node index.js", | ||||||
| 		"start": "npx tsx src/index.ts", | 		"start:prod": "NODE_ENV=production node dist/index.js", | ||||||
| 		"dev": "nodemon -e ts src/index.ts", | 		"start:dev": "NODE_ENV=development tsx src/index.ts", | ||||||
| 		"build": "tsc", | 		"dev": "NODE_ENV=development tsx watch src/index.ts", | ||||||
| 		"lint": "eslint src/**/*.ts", | 		"lint": "eslint .", | ||||||
| 		"prod": "node dist/index.js" | 		"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": { | 	"dependencies": { | ||||||
| 		"@discord-player/equalizer": "^7.0.0", | 		"@discord-player/extractor": "^7.1.0", | ||||||
| 		"@discord-player/extractor": "^7.0.0", |  | ||||||
| 		"@discordjs/voice": "^0.18.0", | 		"@discordjs/voice": "^0.18.0", | ||||||
| 		"@evan/opus": "^1.0.3", | 		"@twurple/api": "^7.3.0", | ||||||
| 		"axios": "^1.7.9", | 		"@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", | 		"bufferutil": "^4.0.9", | ||||||
| 		"chalk": "^4.1.2", | 		"chalk": "^5.4.1", | ||||||
| 		"discord-player": "^7.0.0", | 		"discord-player": "^7.1.0", | ||||||
| 		"discord-player-youtubei": "^1.3.7", | 		"discord-player-youtubei": "^1.4.6", | ||||||
| 		"discord.js": "^14.17.2", | 		"discord.js": "^14.19.3", | ||||||
| 		"dotenv": "^16.4.7", |  | ||||||
| 		"iconv-lite": "^0.6.3", | 		"iconv-lite": "^0.6.3", | ||||||
| 		"jsdom": "^25.0.1", |  | ||||||
| 		"libsodium-wrappers": "^0.7.15", |  | ||||||
| 		"mediaplex": "^1.0.0", | 		"mediaplex": "^1.0.0", | ||||||
| 		"mongoose": "^8.9.3", | 		"mongoose": "^8.15.1", | ||||||
| 		"parse-torrent": "^9.1.5", | 		"parse-torrent": "^9.1.5", | ||||||
| 		"require-all": "^3.0.0", | 		"zlib-sync": "^0.1.10" | ||||||
| 		"rss-parser": "^3.13.0", |  | ||||||
| 		"utf-8-validate": "^6.0.5", |  | ||||||
| 		"websocket": "^1.0.35" |  | ||||||
| 	}, | 	}, | ||||||
| 	"devDependencies": { | 	"devDependencies": { | ||||||
| 		"@eslint/eslintrc": "^3.2.0", | 		"@eslint/js": "^9.28.0", | ||||||
| 		"@eslint/js": "^9.17.0", | 		"@types/node": "^22.15.30", | ||||||
| 		"@swc/core": "^1.10.4", |  | ||||||
| 		"@types/node": "^22.10.5", |  | ||||||
| 		"@types/parse-torrent": "^5.8.7", | 		"@types/parse-torrent": "^5.8.7", | ||||||
| 		"@types/websocket": "^1.0.10", | 		"dotenv": "^16.5.0", | ||||||
| 		"@typescript-eslint/eslint-plugin": "^8.19.0", | 		"eslint": "^9.28.0", | ||||||
| 		"@typescript-eslint/parser": "^8.19.0", | 		"tsup": "^8.5.0", | ||||||
| 		"eslint": "^9.17.0", | 		"tsx": "^4.19.4", | ||||||
| 		"nodemon": "^3.1.9", | 		"typescript": "^5.8.3", | ||||||
| 		"prettier": "^3.4.2", | 		"typescript-eslint": "^8.33.1" | ||||||
| 		"tsx": "^4.19.2" |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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[] | ||||||
							
								
								
									
										89
									
								
								src/buttons/freebox/lcd_status.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/buttons/freebox/lcd_status.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | 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" | ||||||
|  | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
|  | 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) { | ||||||
|  | 		logConsoleError('freebox', 'lcd_status_error', undefined, error as 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] }) | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								src/buttons/freebox/test_connection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/buttons/freebox/test_connection.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | 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" | ||||||
|  | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
|  | 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) { | ||||||
|  | 		logConsoleError('freebox', 'test_connection_error', undefined, error as 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 | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								src/buttons/twitch/streamer_list.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/buttons/twitch/streamer_list.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | 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 { logConsoleError } 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) { | ||||||
|  | 			logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as 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 }) | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								src/buttons/twitch/streamer_remove.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/buttons/twitch/streamer_remove.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | 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 { logConsoleError } 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) { | ||||||
|  | 			logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as 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 }) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										407
									
								
								src/commands/global/amp.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										407
									
								
								src/commands/global/amp.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,210 +1,241 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, EmbedBuilder, inlineCode, PermissionFlagsBits } from 'discord.js' | import { SlashCommandBuilder, EmbedBuilder, inlineCode, PermissionFlagsBits, MessageFlags } from "discord.js" | ||||||
| import dbGuild from '../../schemas/guild' | import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, Locale } from "discord.js" | ||||||
| import * as AMP from '../../utils/amp' | 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 { | function returnMsg(result: ReturnMsgData, locale: Locale) { | ||||||
| 	name: string | 	if (result.status === "fail") return `${t(locale, "common.failed")}\n${inlineCode(`${result.Title}: ${result.Message}`)}` | ||||||
| 	value: string | 	if (result.status === "error") return `${t(locale, "common.error_occurred")}\n${inlineCode(`${result.error_code}`)}` | ||||||
| 	inline: boolean |  | ||||||
| } |  | ||||||
| interface InstanceResult { |  | ||||||
| 	status: string |  | ||||||
| 	data:  [ |  | ||||||
| 		Host |  | ||||||
| 	] |  | ||||||
| } |  | ||||||
| interface Host { |  | ||||||
| 	AvailableInstances: Instance[] |  | ||||||
| 	FriendlyName: string |  | ||||||
| } |  | ||||||
| interface Instance { |  | ||||||
| 	InstanceID: string |  | ||||||
| 	FriendlyName: string |  | ||||||
| 	Running: boolean |  | ||||||
| 	Module: string |  | ||||||
| 	Port: number |  | ||||||
| } |  | ||||||
| interface FailMsgData { |  | ||||||
|     Title: string |  | ||||||
|     Message: string |  | ||||||
| } |  | ||||||
| interface ErrorMsgData { |  | ||||||
|     error_code: string |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function failMsg(data: FailMsgData) { return `La commande a échouée !\n${inlineCode(`${data.Title}: ${data.Message}`)}` } | export const data = new SlashCommandBuilder() | ||||||
| function errorMsg(data: ErrorMsgData) { return `Y'a eu une erreur !\n${inlineCode(`${data.error_code}`)}` } | 	.setName("amp") | ||||||
|  | 	.setDescription("Access my AMP gaming panel") | ||||||
| export default { | 	.setDescriptionLocalizations({ fr: "Accède à mon panel de jeu AMP" }) | ||||||
| 	data: new SlashCommandBuilder() |  | ||||||
| 	.setName('amp') |  | ||||||
| 	.setDescription('Accède à mon panel de jeu AMP !') |  | ||||||
| 	.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) | 	.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) | ||||||
| 	.addSubcommand(subcommand => subcommand.setName('login').setDescription("Connectez-vous avant d'effectuer une autre commande !") | 	.addSubcommand(subcommand => subcommand | ||||||
| 		.addStringOption(option => option.setName('username').setDescription("Nom d'Utilisateur").setRequired(true)) | 		.setName("login") | ||||||
| 		.addStringOption(option => option.setName('password').setDescription('Mot de Passe').setRequired(true)) | 		.setDescription("Log in before performing another command") | ||||||
| 		.addBooleanOption(option => option.setName('remember').setDescription('Mémoriser les identifiants').setRequired(true)) | 		.setNameLocalizations({ fr: "connexion" }) | ||||||
| 		.addStringOption(option => option.setName('otp').setDescription('Code de double authentification')))		 | 		.setDescriptionLocalizations({ fr: "Connectez-vous avant d'effectuer une autre commande" }) | ||||||
| 	.addSubcommandGroup(subcommandgroup => subcommandgroup.setName('instances').setDescription('Intéragir avec les instances AMP.') | 		.addStringOption(option => option | ||||||
| 		.addSubcommand(subcommand => subcommand.setName('list').setDescription('Liste toutes les instances disponibles.')) | 			.setName("username") | ||||||
| 		.addSubcommand(subcommand => subcommand.setName('manage').setDescription('Gérer une instance.') | 			.setDescription("Username") | ||||||
| 			.addStringOption(option => option.setName('instance').setDescription("Nom de l'instance").setRequired(true).setAutocomplete(true))) | 			.setNameLocalizations({ fr: "nom_utilisateur" }) | ||||||
| 		.addSubcommand(subcommand => subcommand.setName('restart').setDescription('Redémarre une instance.') | 			.setDescriptionLocalizations({ fr: "Nom d'utilisateur" }) | ||||||
| 			.addStringOption(option => option.setName('name').setDescription("Nom de l'instance").setRequired(true))) | 			.setRequired(true) | ||||||
| 	), | 		) | ||||||
| 	async autocompleteRun(interaction: AutocompleteInteraction) { | 		.addStringOption(option => option | ||||||
| 		let query = interaction.options.getString('instance', true) | 			.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 }) | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		if (!guildProfile) return await interaction.respond([]) | 	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') | 	const dbData = guildProfile.get("guildAmp") as GuildAmp | ||||||
| 		if (!dbData?.enabled) return await interaction.respond([]) | 	if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "amp.module_disabled"), flags: MessageFlags.Ephemeral }) | ||||||
|  |  | ||||||
| 		let host = dbData.host as string | 	const host = dbData.host | ||||||
| 		let username = dbData.username as string | 	if (!host) return interaction.reply({ content: t(interaction.locale, "amp.host_not_configured"), flags: MessageFlags.Ephemeral }) | ||||||
| 		let sessionID = dbData.sessionID as string |  | ||||||
| 		let rememberMeToken = dbData.rememberMeToken as string |  | ||||||
|  |  | ||||||
| 		// Check if the SessionID is still valid | 	let username = dbData.username | ||||||
| 		let session = await AMP.CheckSession(host, sessionID) | 	let sessionID = dbData.sessionID | ||||||
| 		if (session.status === 'fail') { | 	let rememberMeToken = dbData.rememberMeToken | ||||||
| 			if (rememberMeToken) { |  | ||||||
| 				// Refresh the SessionID if the RememberMeToken is available |  | ||||||
| 				let details = { username, password: '', token: rememberMeToken, rememberMe: true } |  | ||||||
| 				let result = await AMP.Core.Login(host, details) |  | ||||||
|  |  | ||||||
| 				if (result.status === 'success') sessionID = result.data.sessionID | 	const subcommandGroup = interaction.options.getSubcommandGroup(false) | ||||||
| 				else if (result.status === 'fail') return interaction.respond([]) | 	const subcommand = interaction.options.getSubcommand(true) | ||||||
| 				else if (result.status === 'error') return interaction.respond([]) | 	if (subcommand == "login") { | ||||||
| 			} else return await interaction.respond([]) | 		// 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[] = [] | 		const result = await AMP.Core.Login(host, details) | ||||||
| 		let result = await AMP.ADSModule.GetInstances(host, sessionID) | 		if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral }) | ||||||
| 		if (result.status === 'success') { |  | ||||||
| 			let hosts = result.data.result as Host[] | 		const loginData = result.data as LoginSuccessData | ||||||
| 			hosts.forEach(host => { | 		username = dbData.username = loginData.userInfo.Username | ||||||
| 				let instances = host.AvailableInstances as Instance[] | 		sessionID = dbData.sessionID = loginData.sessionID | ||||||
| 				instances.forEach(instance => { | 		rememberMeToken = dbData.rememberMeToken = loginData.rememberMeToken | ||||||
| 					if (instance.FriendlyName.includes(query)) choices.push({ name: `${host.FriendlyName} - ${instance.FriendlyName}`, value: instance.InstanceID }) |  | ||||||
|  | 		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 (subcommand == "manage") { | ||||||
| 		else if (result.status === 'error') return interaction.respond([]) | 			const instanceID = interaction.options.getString("instance", true) | ||||||
|  |  | ||||||
| 		return interaction.respond(choices) | 			const manageResult = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID) | ||||||
| 	}, | 			if (manageResult.status !== "success") return interaction.followUp({ content: returnMsg(manageResult, interaction.locale), flags: MessageFlags.Ephemeral }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { |  | ||||||
| 		let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id }) |  | ||||||
| 		if (!guildProfile) return interaction.reply({ content: `Database data for **${interaction.guild?.name}** does not exist, please initialize with \`/database init\` !` }) |  | ||||||
|  |  | ||||||
| 		let dbData = guildProfile.get('guildAmp') | 			const serversResult = await AMP.ADSModule.Servers(host, sessionID, instanceID) | ||||||
| 		if (!dbData?.enabled) return interaction.reply({ content: `AMP module is disabled for **${interaction.guild?.name}**, please activate with \`/database edit guildAmp.enabled True\` !` }) | 			if (serversResult.status !== "success") return interaction.followUp({ content: returnMsg(serversResult, interaction.locale), flags: MessageFlags.Ephemeral }) | ||||||
|  |  | ||||||
| 		let host = dbData.host as string | 			return interaction.followUp({ content: t(interaction.locale, "amp.manage_success") }) | ||||||
| 		let username = dbData.username as string |  | ||||||
| 		let sessionID = dbData.sessionID as string |  | ||||||
| 		let rememberMeToken = dbData.rememberMeToken as string |  | ||||||
|  |  | ||||||
| 		// Let the user login |  | ||||||
| 		if (interaction.options.getSubcommand() == 'login') { |  | ||||||
| 			// Get a SessionID and a RememberMeToken if wanted |  | ||||||
| 			await interaction.deferReply({ ephemeral: true }) |  | ||||||
|  |  | ||||||
| 			let details = { |  | ||||||
| 				username: interaction.options.getString('username') || '', |  | ||||||
| 				password: interaction.options.getString('password') || '', |  | ||||||
| 				rememberMe: interaction.options.getBoolean('remember') || '', |  | ||||||
| 				token: interaction.options.getString('otp') || '' |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			let result = await AMP.Core.Login(host, details) |  | ||||||
| 			if (result.status === 'success') { |  | ||||||
| 				username = dbData['username'] = result.data.userInfo.Username |  | ||||||
| 				sessionID = dbData['sessionID'] = result.data.sessionID |  | ||||||
| 				rememberMeToken = dbData['rememberMeToken'] = result.data.rememberMeToken |  | ||||||
|  |  | ||||||
| 				guildProfile.set('guildAmp', dbData) |  | ||||||
| 				guildProfile.markModified('guildAmp') |  | ||||||
| 				await guildProfile.save().catch(console.error) |  | ||||||
|  |  | ||||||
| 				return await interaction.followUp(`Tu es connecté au panel sous **${username}** !`) |  | ||||||
| 			} |  | ||||||
| 			else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data)) |  | ||||||
| 			else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data)) |  | ||||||
| 		} | 		} | ||||||
| 		await interaction.deferReply() | 		else if (subcommand == "restart") { | ||||||
|  | 			const query = interaction.options.getString("name", true) | ||||||
|  |  | ||||||
| 		// Check if the SessionID is still valid | 			const restartResult = await AMP.ADSModule.RestartInstance(host, sessionID, query) | ||||||
| 		let session = await AMP.CheckSession(host, sessionID) | 			if (restartResult.status !== "success") return interaction.followUp({ content: returnMsg(restartResult, interaction.locale), flags: MessageFlags.Ephemeral }) | ||||||
| 		if (session.status === 'fail') { |  | ||||||
| 			if (rememberMeToken) { |  | ||||||
| 				// Refresh the SessionID if the RememberMeToken is available |  | ||||||
| 				let details = { username, password: '', token: rememberMeToken, rememberMe: true } |  | ||||||
| 				let result = await AMP.Core.Login(host, details) |  | ||||||
|  |  | ||||||
| 				if (result.status === 'success') sessionID = result.data.sessionID | 			return interaction.followUp({ content: t(interaction.locale, "amp.restart_success") }) | ||||||
| 				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)) |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 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 = { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("boost") | ||||||
| 		.setName('boost') | 	.setDescription("Test the server boost") | ||||||
| 		.setDescription('Tester le boost du serveur !') | 	.setDescriptionLocalizations({ fr: "Tester le boost du serveur" }) | ||||||
| 		.setDefaultMemberPermissions(PermissionFlagsBits.Administrator), | 	.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é !`) |  | ||||||
|  |  | ||||||
| 		let guild = interaction.guild | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		if (!guild) return console.log(`\u001b[1;31m Aucun serveur trouvé !`) | 	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 | 	const member = interaction.member | ||||||
| 		if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`) | 	if (!member) { logConsole('discordjs', 'boost.no_member'); return } | ||||||
|  |  | ||||||
| 		let boostRole = guild.roles.premiumSubscriberRole | 	const guild = interaction.guild | ||||||
| 		if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`) | 	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 !`) | 	const channel = await guild.channels.fetch("924353449930412153") | ||||||
|  | 	if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { | ||||||
| 		let embed = new EmbedBuilder() | 		logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" }) | ||||||
| 			.setColor(guild.members.me.displayHexColor) | 		return | ||||||
| 			.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 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" }) }) | ||||||
| } | } | ||||||
							
								
								
									
										138
									
								
								src/commands/global/database.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										138
									
								
								src/commands/global/database.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -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' | const parseObject = (obj: object, prefix = ""): { name: string, value: object | string | boolean }[] => { | ||||||
| import dbGuild from '../../schemas/guild' | 	const fields: { name: string, value: object | string | boolean }[] = [] | ||||||
|  |  | ||||||
| const parseObject = (obj: object, prefix = ''): { name: string, value: object | string | boolean }[] => { | 	for (const [key, value] of Object.entries(obj)) { | ||||||
| 	let fields: { name: string, value: object | string | boolean }[] = [] | 		if (value !== null && typeof value === "object") fields.push(...parseObject(value as object, `${prefix}${key}.`)) | ||||||
| 	 |  | ||||||
| 	for (let [key, value] of Object.entries(obj)) { |  | ||||||
| 		if (typeof value === 'object') fields.push(...parseObject(value, `${prefix}${key}.`)) |  | ||||||
| 		else { | 		else { | ||||||
| 			if (typeof value === 'boolean') value = value ? 'True' : 'False' | 			let newValue: string | ||||||
| 			else if (!value) value = 'None' | 			if (typeof value === "boolean") newValue = value ? "True" : "False" | ||||||
| 			else value = value.toString() | 			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 | 	return fields | ||||||
| } | } | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("database") | ||||||
| 		.setName('database') | 	.setDescription("Communicate with the database") | ||||||
| 		.setDescription('Communicate with the database') | 	.setDescriptionLocalizations({ fr: "Communiquer avec la base de données" }) | ||||||
| 		.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) | 	.setDefaultMemberPermissions(PermissionFlagsBits.Administrator) | ||||||
| 		.addSubcommand(subcommand => subcommand.setName('info').setDescription('Returns information about the current guild')) | 	.addSubcommand(subcommand => subcommand | ||||||
| 		.addSubcommand(subcommand => subcommand.setName('init').setDescription('Force initialize an entry for the current guild in the database')) | 		.setName("info") | ||||||
| 		.addSubcommand(subcommand => subcommand.setName('edit').setDescription('Modify parameters for the current guild') | 		.setDescription("Returns information about the current guild") | ||||||
| 			.addStringOption(option => option.setName('key').setDescription('Key to modify').setRequired(true)) | 		.setNameLocalizations({ fr: "info" }) | ||||||
| 			.addStringOption(option => option.setName('value').setDescription('Value to set').setRequired(true))), | 		.setDescriptionLocalizations({ fr: "Retourne les informations sur le serveur actuel" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	) | ||||||
| 		let guild = interaction.guild | 	.addSubcommand(subcommand => subcommand | ||||||
| 		if (!guild) return await interaction.reply({ content: 'This command must be used in a server.', ephemeral: true }) | 		.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') { | 	const guild = interaction.guild | ||||||
| 			if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist !` }) | 	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() | 	const subcommand = interaction.options.getSubcommand(true) | ||||||
| 				.setTitle('Database Information') | 	if (subcommand === "info") { | ||||||
| 				.setDescription(`Guild **${guildProfile.guildName}** (ID: ${guildProfile.guildId})`) | 		if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) | ||||||
| 				.setThumbnail(guildProfile.guildIcon as string) |  | ||||||
| 				.setTimestamp() |  | ||||||
| 				//.addFields(fields as APIEmbedField[]) |  | ||||||
| 				// Limit the number of fields to 25 |  | ||||||
| 				.addFields(fields.slice(0, 25) as APIEmbedField[]) |  | ||||||
| 			return await interaction.reply({ embeds: [embed] }) |  | ||||||
|  |  | ||||||
| 		} else if (interaction.options.getSubcommand() === 'init') { | 		const fields = parseObject(guildProfile.toObject()) | ||||||
| 			if (guildProfile) return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** already exists !` }) | 		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) | 		return interaction.reply({ embeds: [embed] }) | ||||||
| 			if (!guildProfile) return await interaction.reply({ content: `An error occured while initializing database data for **${guild.name}** !` }) | 	} | ||||||
|  | 	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') { | 		return interaction.reply({ content: t(interaction.locale, "database.initialized", { name: guildProfile.guildName }), flags: MessageFlags.Ephemeral }) | ||||||
| 			if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist, please init with \`/database init\` !` }) | 	} | ||||||
|  | 	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) | 		const key = interaction.options.getString("key", true) | ||||||
| 			let value = interaction.options.getString('value', true) | 		const value = interaction.options.getString("value", true) | ||||||
|  |  | ||||||
| 			let oldValue = guildProfile.get(key) | 		let oldValue: string = guildProfile.get(key) as string | ||||||
| 			if (!oldValue) oldValue = 'None' | 		if (!oldValue) oldValue = t(interaction.locale, "common.none") | ||||||
|  |  | ||||||
| 			guildProfile.set(key, value) | 		guildProfile.set(key, value) | ||||||
| 			await guildProfile.save().catch(console.error) | 		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 }) | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
							
								
								
									
										354
									
								
								src/commands/global/freebox.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								src/commands/global/freebox.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,354 @@ | |||||||
|  | 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" | ||||||
|  | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
|  | 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") logConsole('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 }) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								src/commands/global/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/commands/global/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | import * as amp from "./amp" | ||||||
|  | import * as boost from "./boost" | ||||||
|  | import * as database from "./database" | ||||||
|  | import * as freebox from "./freebox" | ||||||
|  | import * as locale from "./locale" | ||||||
|  | import * as ping from "./ping" | ||||||
|  | import * as twitch from "./twitch" | ||||||
|  |  | ||||||
|  | import type { Command } from "@/types" | ||||||
|  |  | ||||||
|  | export default [ | ||||||
|  | 	amp, | ||||||
|  | 	boost, | ||||||
|  | 	database, | ||||||
|  | 	freebox, | ||||||
|  | 	locale, | ||||||
|  | 	ping, | ||||||
|  | 	twitch | ||||||
|  | ] as Command[] | ||||||
							
								
								
									
										56
									
								
								src/commands/global/locale.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/commands/global/locale.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
|  | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import dbGuild from "@/schemas/guild" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
|  | export const data = new SlashCommandBuilder() | ||||||
|  | 	.setName("locale") | ||||||
|  | 	.setDescription("Manage server language") | ||||||
|  | 	.setDescriptionLocalizations({ fr: "Gérer la langue du serveur" }) | ||||||
|  | 	.addStringOption(option => option | ||||||
|  | 		.setName("language") | ||||||
|  | 		.setDescription("Select the server language") | ||||||
|  | 		.setNameLocalizations({ fr: "langue" }) | ||||||
|  | 		.setDescriptionLocalizations({ fr: "Sélectionner la langue du serveur" }) | ||||||
|  | 		.setRequired(true) | ||||||
|  | 		.addChoices( | ||||||
|  | 			{ name: "Français", value: "fr" }, | ||||||
|  | 			{ name: "English", value: "en-US" } | ||||||
|  | 		) | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 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 }) | ||||||
|  |  | ||||||
|  | 	const language = interaction.options.getString("language", true) | ||||||
|  |  | ||||||
|  | 	// Récupération du profil du serveur | ||||||
|  | 	const guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||||
|  | 	if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) | ||||||
|  |  | ||||||
|  | 	// Sauvegarde de l'ancienne langue pour le message de confirmation | ||||||
|  | 	const oldLocale = guildProfile.guildLocale | ||||||
|  |  | ||||||
|  | 	// Mise à jour de la langue | ||||||
|  | 	guildProfile.guildLocale = language | ||||||
|  | 	guildProfile.markModified("guildLocale") | ||||||
|  | 	await guildProfile.save().catch(console.error) | ||||||
|  |  | ||||||
|  | 	// Utilisation de la nouvelle langue pour la réponse | ||||||
|  | 	const languageNames = { | ||||||
|  | 		'fr': 'Français', | ||||||
|  | 		'en-US': 'English' | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const oldLanguageName = languageNames[oldLocale as keyof typeof languageNames] || oldLocale | ||||||
|  | 	const newLanguageName = languageNames[language as keyof typeof languageNames] || language | ||||||
|  |  | ||||||
|  | 	return interaction.reply({  | ||||||
|  | 		content: t(language, "locale.updated", {  | ||||||
|  | 			oldLanguage: oldLanguageName,  | ||||||
|  | 			newLanguage: newLanguageName  | ||||||
|  | 		}),  | ||||||
|  | 		flags: MessageFlags.Ephemeral  | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								src/commands/global/ping.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										24
									
								
								src/commands/global/ping.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -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 { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("ping") | ||||||
| 		.setName('ping') | 	.setDescription("Check the latency of the bot") | ||||||
| 		.setDescription('Check the latency of the bot'), | 	.setDescriptionLocalizations({ fr: "Vérifier la latence du bot" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { |  | ||||||
| 		let sent = await interaction.reply({ content: 'Pinging...', fetchReply: true }) | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		interaction.editReply(`Websocket heartbeat: ${interaction.client.ws.ping}ms.\nRoundtrip latency: ${sent.createdTimestamp - interaction.createdTimestamp}ms`) | 	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() | ||||||
|  | 	})) | ||||||
| } | } | ||||||
							
								
								
									
										197
									
								
								src/commands/global/twitch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								src/commands/global/twitch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,197 @@ | |||||||
|  | import { SlashCommandBuilder, ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js" | ||||||
|  | import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData } from "discord.js" | ||||||
|  | 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" | ||||||
|  | import { logConsole, logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
|  | 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) { | ||||||
|  | 					logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as 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() })) | ||||||
|  | 				logConsole('twitch', 'listener_removed', { name: 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[] | ||||||
							
								
								
									
										44
									
								
								src/commands/player/loop.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										44
									
								
								src/commands/player/loop.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,21 +1,29 @@ | |||||||
| import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js' | import { SlashCommandBuilder } from "discord.js" | ||||||
| import { useQueue } from'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import type { QueueRepeatMode } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("loop") | ||||||
| 		.setName('loop') | 	.setDescription("Loop the current music") | ||||||
| 		.setDescription('Boucler la musique en cours de lecture.') | 	.setNameLocalizations({ fr: "boucle" }) | ||||||
| 		.addIntegerOption(option => option.setName('loop') | 	.setDescriptionLocalizations({ fr: "Boucler la musique en cours de lecture" }) | ||||||
| 			.setDescription('Mode de boucle (0 = Off, 1 = Titre, 2 = File d\'Attente; 3 = Autoplay)') | 	.addIntegerOption(option => option | ||||||
| 			.setRequired(true) | 		.setName("mode") | ||||||
| 			.setMinValue(0) | 		.setDescription("Loop mode (0 = Off | 1 = Track | 2 = Queue | 3 = Autoplay)") | ||||||
| 			.setMaxValue(3)), | 		.setDescriptionLocalizations({ fr: "Mode de boucle (0 = Arrêt | 1 = Titre | 2 = File d'Attente | 3 = Autoplay)" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 		.setRequired(true) | ||||||
| 		let loop = interaction.options.getInteger('loop') | 		.setMinValue(0) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') | 		.setMaxValue(3) | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) | 	) | ||||||
|  |  | ||||||
| 		queue.setRepeatMode(loop as number) | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		return await interaction.reply(`Boucle ${loop === 0 ? 'désactivée' : loop === 1 ? 'en mode Titre' : loop === 2 ? 'en mode File d\'Attente' : 'en autoplay'}.`) | 	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")) | ||||||
| } | } | ||||||
							
								
								
									
										86
									
								
								src/commands/player/lyrics.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										86
									
								
								src/commands/player/lyrics.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,45 +1,61 @@ | |||||||
| import { ChatInputCommandInteraction, SlashCommandBuilder, EmbedBuilder } from 'discord.js' | import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
| import { lyricsExtractor } from '@discord-player/extractor' | import { useQueue, useMainPlayer } from "discord-player" | ||||||
|  | import type { LrcSearchResult } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("lyrics") | ||||||
| 		.setName('lyrics') | 	.setDescription("Search for song lyrics") | ||||||
| 		.setDescription('Rechercher les paroles d\'une musique.') | 	.setNameLocalizations({ fr: "paroles" }) | ||||||
| 		.addStringOption(option => option.setName('recherche').setDescription('Chercher une musique spécifique')), | 	.setDescriptionLocalizations({ fr: "Rechercher les paroles d'une musique" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.addStringOption(option => option | ||||||
| 		await interaction.deferReply() | 		.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) | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		if (!query) { | 	await interaction.deferReply() | ||||||
| 			let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 			if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
| 			let track = queue.currentTrack |  | ||||||
| 			if (!track) return interaction.followUp({ content: 'Aucune musique en cours, recherche en une plutôt !' }) |  | ||||||
|  |  | ||||||
| 			if (track.raw.source === 'spotify') query = `${track.author} ${track.title}` | 	const player = useMainPlayer() | ||||||
| 			else query = track.title | 	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) | 		const track = queue.currentTrack | ||||||
| 		if (!lyrics) return interaction.followUp({ content: 'Pas de paroles trouvées !' }) | 		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() | 		if (!lyrics.length) return interaction.followUp({ content: t(interaction.locale, "player.no_lyrics_found"), flags: MessageFlags.Ephemeral }) | ||||||
| 			.setColor('#ffc370') | 		const trimmedLyrics = lyrics[0].plainLyrics.substring(0, 1997) | ||||||
| 			.setTitle(lyrics.title) |  | ||||||
| 			.setURL(lyrics.url) | 		embed | ||||||
| 			.setThumbnail(lyrics.thumbnail) | 			.setTitle(track.title) | ||||||
| 			.setAuthor({ | 			.setURL(track.url) | ||||||
| 				name: lyrics.artist.name, |  | ||||||
| 				iconURL: lyrics.artist.image, |  | ||||||
| 				url: lyrics.artist.url |  | ||||||
| 			}) |  | ||||||
| 			.setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics) | 			.setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics) | ||||||
|  | 			.setThumbnail(track.thumbnail) | ||||||
| 		return interaction.followUp({ embeds: [embed] }) | 			.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] }) | ||||||
| } | } | ||||||
							
								
								
									
										39
									
								
								src/commands/player/panel.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										39
									
								
								src/commands/player/panel.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,25 +1,26 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { playerGenerate } from '../../utils/player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
| import getUptime from '../../utils/getUptime' | import { useQueue } from "discord-player" | ||||||
| import { useQueue } from 'discord-player' | import { generatePlayerEmbed } from "@/utils/player" | ||||||
|  | import uptime from "@/utils/uptime" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("panel") | ||||||
| 		.setName('panel') | 	.setDescription("Generate current playback info") | ||||||
| 		.setDescription('Générer les infos de la lecture en cours.'), | 	.setNameLocalizations({ fr: "panneau" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Générer les infos de la lecture en cours" }) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
|  |  | ||||||
| 		let guild = interaction.guild | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		if (!guild) return await interaction.reply({ content: 'Cette commande n\'est pas disponible en message privé.', ephemeral: true }) | 	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) | 	const { embed, components } = generatePlayerEmbed(guild, interaction.locale) | ||||||
| 		if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)} \n ${embed.data.footer.text}` }) | 	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: `Uptime: ${getUptime(client.uptime)}` }) | 	else embed.setFooter({ text: `${t(interaction.locale, "player.uptime")}: ${uptime(guild.client.uptime)}` }) | ||||||
|  |  | ||||||
| 		return interaction.reply({ embeds: [embed] }) | 	return interaction.reply({ embeds: [embed] }) | ||||||
| 	} |  | ||||||
| } | } | ||||||
							
								
								
									
										26
									
								
								src/commands/player/pause.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										26
									
								
								src/commands/player/pause.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,15 +1,17 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("pause") | ||||||
| 		.setName('pause') | 	.setDescription("Pause the music") | ||||||
| 		.setDescription('Met en pause la musique.'), | 	.setDescriptionLocalizations({ fr: "Met en pause la musique" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { |  | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
|  |  | ||||||
| 		queue.node.setPaused(!queue.node.isPaused()) | export const execute = async (interaction: ChatInputCommandInteraction) => { | ||||||
| 		return await interaction.reply('Musique mise en pause !') | 	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")) | ||||||
| } | } | ||||||
							
								
								
									
										206
									
								
								src/commands/player/play.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										206
									
								
								src/commands/player/play.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,96 +1,126 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useMainPlayer, useQueue, QueryType } from 'discord-player' | import type { ChatInputCommandInteraction, AutocompleteInteraction, GuildMember } from "discord.js" | ||||||
| import dbGuild from '../../schemas/guild' | 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" | ||||||
|  | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| 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 { | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 	data: new SlashCommandBuilder() | 	const member = interaction.member as GuildMember | ||||||
| 		.setName('play') | 	const voiceChannel = member.voice.channel | ||||||
| 		.setDescription('Jouer une musique.') | 	if (!voiceChannel) return interaction.reply({ content: t(interaction.locale, "player.not_in_voice"), flags: MessageFlags.Ephemeral }) | ||||||
| 		.addStringOption(option => option.setName('recherche').setDescription('Titre de la musique à chercher').setRequired(true).setAutocomplete(true)), |  | ||||||
| 	async autocompleteRun(interaction: AutocompleteInteraction) { |  | ||||||
| 		let query = interaction.options.getString('recherche', true) |  | ||||||
| 		if (!query) return interaction.respond([]) |  | ||||||
|  |  | ||||||
| 		let player = useMainPlayer() | 	const 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 }) | 	await interaction.deferReply() | ||||||
| 		const resultsSpotify = await player.search(query, { searchEngine: QueryType.SPOTIFY_SEARCH }) |  | ||||||
|  |  | ||||||
| 		const tracksYouTube = resultsYouTube.tracks.slice(0, 5).map((t) => ({ | 	const query = interaction.options.getString("search", true) | ||||||
| 			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})`}`, | 	const player = useMainPlayer() | ||||||
| 			value: t.url | 	let queue = useQueue(interaction.guild?.id ?? "") | ||||||
|         })) |  | ||||||
| 		const tracksSpotify = resultsSpotify.tracks.slice(0, 5).map((t) => ({ |  | ||||||
| 			name: `Spotify: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`, |  | ||||||
| 			value: t.url |  | ||||||
| 		})) |  | ||||||
|  |  | ||||||
| 		const tracks: TrackSearchResult[] = [] | 	if (!queue) { | ||||||
| 		tracksYouTube.forEach((t) => tracks.push({ name: t.name, value: t.value })) | 		if (interaction.guild) queue = player.nodes.create(interaction.guild, { | ||||||
| 		tracksSpotify.forEach((t) => tracks.push({ name: t.name, value: t.value })) | 			metadata: { | ||||||
|  | 				channel: interaction.channel, | ||||||
| 		return interaction.respond(tracks) | 				client: interaction.guild.members.me, | ||||||
| 	}, | 				requestedBy: interaction.user | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 			}, | ||||||
| 		let member = interaction.member as GuildMember | 			selfDeaf: true, | ||||||
| 		let voiceChannel = member.voice.channel | 			volume: 20, | ||||||
| 		if (!voiceChannel) return await interaction.reply({ content: 'T\'es pas dans un vocal, idiot !', ephemeral: true }) | 			leaveOnEmpty: true, | ||||||
|  | 			leaveOnEmptyCooldown: 30000, | ||||||
| 		let botChannel = interaction.guild?.members.me?.voice.channel | 			leaveOnEnd: true, | ||||||
| 		if (botChannel && voiceChannel.id !== botChannel.id) return await interaction.reply({ content: 'T\'es pas dans mon vocal !', ephemeral: true }) | 			leaveOnEndCooldown: 300000 | ||||||
|   | 		}) | ||||||
| 		await interaction.deferReply() | 		else return | ||||||
| 		 |  | ||||||
| 		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() } |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	try { if (!queue.connection) await queue.connect(voiceChannel) } | ||||||
|  | 	catch (error) { logConsoleError('discord_player', 'play.connect_error', {}, error as 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] | ||||||
|  | 	if (process.env.NODE_ENV === "development") console.log(query, result, track) | ||||||
|  |  | ||||||
|  | 	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) { logConsoleError('discord_player', 'play.execution_error', {}, error as 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}` }) | ||||||
|  | 	if (process.env.NODE_ENV === "development") console.log(resultsSpotify, resultsYouTube) | ||||||
|  |  | ||||||
|  | 	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) | ||||||
| } | } | ||||||
							
								
								
									
										27
									
								
								src/commands/player/previous.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										27
									
								
								src/commands/player/previous.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,15 +1,18 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useHistory } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useHistory } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("previous") | ||||||
| 		.setName('previous') | 	.setDescription("Play the previous song") | ||||||
| 		.setDescription('Joue la musique précédente.'), | 	.setNameLocalizations({ fr: "precedent" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Joue la musique précédente" }) | ||||||
| 		let history = useHistory(interaction.guild?.id ?? '') |  | ||||||
| 		if (!history) return await interaction.reply('Il n\'y a pas d\'historique de musique !') |  | ||||||
|  |  | ||||||
| 		await history.previous() | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		return await interaction.reply('Musique précédente jouée !') | 	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 }) | ||||||
| } | } | ||||||
							
								
								
									
										33
									
								
								src/commands/player/queue.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										33
									
								
								src/commands/player/queue.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,19 +1,22 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("queue") | ||||||
| 		.setName('queue') | 	.setDescription("Get the queue") | ||||||
| 		.setDescription("Récupérer la file d'attente."), | 	.setNameLocalizations({ fr: "file" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Récupérer la file d'attente." }) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 		if (!queue) return interaction.reply({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
| 		if (!queue.currentTrack) return interaction.reply({ content: 'Aucune musique en cours de lecture.' }) |  | ||||||
|  |  | ||||||
| 		let track = `[${queue.currentTrack.title}](${queue.currentTrack.url})` | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		let tracks = queue.tracks.map((track, index) => { return `${index + 1}. [${track.title}](${track.url})` }) | 	const queue = useQueue(interaction.guild?.id ?? "") | ||||||
| 		if (tracks.length === 0) return interaction.reply({ content: `Lecture en cours : ${track} \nAucune musique dans la file d'attente.` }) | 	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") }) }) | ||||||
| } | } | ||||||
							
								
								
									
										27
									
								
								src/commands/player/resume.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										27
									
								
								src/commands/player/resume.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,15 +1,18 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("resume") | ||||||
| 		.setName('resume') | 	.setDescription("Resume the music") | ||||||
| 		.setDescription('Reprendre la musique.'), | 	.setNameLocalizations({ fr: "reprendre" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Reprendre la musique" }) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
|  |  | ||||||
| 		queue.node.setPaused(!queue.node.isPaused()) | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		return await interaction.reply('Musique reprise !') | 	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")) | ||||||
| } | } | ||||||
							
								
								
									
										27
									
								
								src/commands/player/shuffle.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										27
									
								
								src/commands/player/shuffle.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,15 +1,18 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("shuffle") | ||||||
| 		.setName('shuffle') | 	.setDescription("Shuffle the queue") | ||||||
| 		.setDescription('Mélange la file d\'attente.'), | 	.setNameLocalizations({ fr: "melanger" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Mélange la file d'attente" }) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
|  |  | ||||||
| 		queue.tracks.shuffle() | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		return await interaction.reply('File d\'attente mélangée !') | 	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")) | ||||||
| } | } | ||||||
							
								
								
									
										27
									
								
								src/commands/player/skip.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										27
									
								
								src/commands/player/skip.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,15 +1,18 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("skip") | ||||||
| 		.setName('skip') | 	.setDescription("Skip the current song") | ||||||
| 		.setDescription('Passer la musique en cours.'), | 	.setNameLocalizations({ fr: "passer" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Passer la musique en cours" }) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
|  |  | ||||||
| 		queue.node.skip() | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		return await interaction.reply('Musique passée !') | 	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")) | ||||||
| } | } | ||||||
							
								
								
									
										30
									
								
								src/commands/player/stop.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										30
									
								
								src/commands/player/stop.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,15 +1,21 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import { stopProgressSaving } from "@/utils/player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("stop") | ||||||
| 		.setName('stop') | 	.setDescription("Stop the music") | ||||||
| 		.setDescription('Arrêter la musique.'), | 	.setNameLocalizations({ fr: "arreter" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Arrêter la musique" }) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') |  | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
|  |  | ||||||
| 		queue.delete() | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		return await interaction.reply('Musique arrêtée !') | 	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")) | ||||||
| } | } | ||||||
							
								
								
									
										41
									
								
								src/commands/player/volume.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										41
									
								
								src/commands/player/volume.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,21 +1,26 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { useQueue } from 'discord-player' | import type { ChatInputCommandInteraction } from "discord.js" | ||||||
|  | import { useQueue } from "discord-player" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("volume") | ||||||
| 		.setName('volume') | 	.setDescription("Change the music volume") | ||||||
| 		.setDescription('Modifie le volume de la musique.') | 	.setDescriptionLocalizations({ fr: "Modifie le volume de la musique" }) | ||||||
| 		.addIntegerOption(option => option.setName('volume') | 	.addIntegerOption(option => option | ||||||
| 			.setDescription('Le volume à mettre (%)') | 		.setName("volume") | ||||||
| 			.setRequired(true) | 		.setDescription("The volume to set (%)") | ||||||
| 			.setMinValue(1) | 		.setDescriptionLocalizations({ fr: "Le volume à mettre (%)" }) | ||||||
| 			.setMaxValue(100)), | 		.setRequired(true) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 		.setMinValue(0) | ||||||
| 		let volume = interaction.options.getInteger('volume') | 		.setMaxValue(100) | ||||||
| 		let queue = useQueue(interaction.guild?.id ?? '') | 	) | ||||||
| 		if (!queue) return interaction.followUp({ content: 'Aucune file d\'attente en cours, recherche une musique plutôt !' }) |  | ||||||
|  |  | ||||||
| 		queue.node.setVolume(volume as number) | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		return await interaction.reply(`Volume modifié à ${volume}% !`) | 	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() })) | ||||||
| } | } | ||||||
							
								
								
									
										106
									
								
								src/commands/salonpostam/crack.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										106
									
								
								src/commands/salonpostam/crack.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,58 +1,70 @@ | |||||||
| import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, MessageReaction, User }from 'discord.js' | import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js" | ||||||
| import * as crack from '../../utils/crack' | 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 { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder().setName('crack').setDescription('Télécharge un crack sur le site online-fix.me !') | 	.setName("crack") | ||||||
| 		.addStringOption(option => option.setName('jeu').setDescription('Quel jeu tu veux DL ?').setRequired(true)), | 	.setDescription("Download a crack from online-fix.me") | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.setDescriptionLocalizations({ fr: "Télécharge un crack sur online-fix.me" }) | ||||||
| 		await interaction.deferReply() | 	.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') | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		if (!query) return | 	await interaction.deferReply() | ||||||
|  |  | ||||||
| 		let games = await crack.search(query) as crack.Game[] | 	const query = interaction.options.getString("game", true) | ||||||
| 		if (!Array.isArray(games)) { | 	let games = await search(query) | ||||||
| 			//if (games.toString() == "TypeError: Cannot read properties of undefined (reading 'split')") return interaction.followUp({ content: `J'ai rien trouvé pour "${query}" !` }) | 	if (!Array.isArray(games)) return interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.no_games_found", { query }), flags: MessageFlags.Ephemeral }) | ||||||
| 			//else return interaction.followUp({ content: "Une erreur s'est produite ! ```" + games + "```" }) |  | ||||||
| 			return interaction.followUp({ content: `J'ai rien trouvé pour "${query}" !` }) | 	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) return | ||||||
| 		if (games.length > 1) { | 			game = games[index] | ||||||
| 			games = games.slice(0, 9) | 		}) | ||||||
| 			let list = '' | 		.catch(() => { return interaction.followUp({ content: t(interaction.locale, "salonpostam.crack.selection_timeout"), flags: MessageFlags.Ephemeral }) }) | ||||||
| 			for (let i = 0; i < games.length; i++) list += `\n${i + 1}. ${games[i].name} (${games[i].link})` | 	} else game = games[0] | ||||||
| 			let message = await interaction.followUp({ content: `J'ai trouvé plusieurs jeux pour "${query}" ! ${list}` }) |  | ||||||
|  |  | ||||||
| 			let emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'] | 	const url = await repo(game) | ||||||
| 			for (let i = 0; i < games.length; i++) await message.react(emojis[i]) | 	if (!url) return | ||||||
| 	 | 	 | ||||||
| 			// Wait for a reaction to be added by the interaction author. | 	const file = await torrent(url) | ||||||
| 			const filter = (reaction: MessageReaction, user: User) => { if (reaction.emoji.name) { return emojis.includes(reaction.emoji.name) && user.id === interaction.user.id } return false } | 	if (!file) return | ||||||
| 			await message.awaitReactions({ filter, max: 1, time: 5000, errors: ['time'] }).then(collected => { |  | ||||||
| 				console.log(collected) |  | ||||||
| 				if (!collected.first) return |  | ||||||
| 				let reaction = collected.first() |  | ||||||
| 				let index = emojis.indexOf(reaction?.emoji.name ?? '') |  | ||||||
| 				game = games[index] |  | ||||||
| 			}).catch(() => { return interaction.followUp({ content: "T'as mis trop de temps à choisir !" }) }) |  | ||||||
| 		} |  | ||||||
| 		else game = games[0] |  | ||||||
| 	 | 	 | ||||||
| 		let url = await crack.repo(game) | 	const filePath = await download(url, file) | ||||||
| 		if (!url) return | 	if (!filePath) return | ||||||
| 		let file = await crack.torrent(url) |  | ||||||
| 		if (!file) return |  | ||||||
| 		let filePath = await crack.download(url, file) |  | ||||||
| 		if (!filePath) return |  | ||||||
| 		let link = await crack.magnet(filePath) |  | ||||||
| 	 | 	 | ||||||
| 		let embed = new EmbedBuilder() | 	const link = magnet(filePath) | ||||||
| 			.setColor('#ffc370') | 	const embed = new EmbedBuilder() | ||||||
| 			.setTitle(game.name) | 		.setColor("#ffc370") | ||||||
| 			.setURL(game.link) | 		.setTitle(game.name) | ||||||
| 			.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 !`) | 		.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[] | ||||||
							
								
								
									
										56
									
								
								src/commands/salonpostam/papa.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										56
									
								
								src/commands/salonpostam/papa.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,34 +1,34 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js' | import { SlashCommandBuilder } from "discord.js" | ||||||
| import { getVoiceConnection, joinVoiceChannel } from '@discordjs/voice' | import type { ChatInputCommandInteraction, GuildMember } from "discord.js" | ||||||
|  | import { getVoiceConnection, joinVoiceChannel } from "@discordjs/voice" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("papa") | ||||||
| 		.setName('papa') | 	.setDescription("If daddy calls me, I join him") | ||||||
| 		.setDescription('Si papa m\'appelle, je le rejoins !'), | 	.setDescriptionLocalizations({ fr: "Si papa m'appelle, je le rejoins" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { |  | ||||||
| 		if (interaction.user.id !== '223831938346123275') return interaction.reply({ content: 'T\'es pas mon père, dégage !' }) |  | ||||||
|  |  | ||||||
| 		let guild = interaction.guild | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		if (!guild) return interaction.reply({ content: 'Je ne peux pas rejoindre ton vocal en message privé, papa !' }) | 	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 | 	const member = interaction.member as GuildMember | ||||||
| 		let papaChannel = member.voice.channel |  | ||||||
|  |  | ||||||
| 		if (!papaChannel && botChannel) { | 	const botChannel = guild.members.me?.voice.channel | ||||||
| 			const voiceConnection = getVoiceConnection(guild.id); | 	const papaChannel = member.voice.channel | ||||||
| 			if (voiceConnection) voiceConnection.destroy() |  | ||||||
| 			return interaction.reply({ content: 'Je quitte le vocal, papa !' }) | 	if (!papaChannel && botChannel) { | ||||||
| 		} | 		const voiceConnection = getVoiceConnection(guild.id) | ||||||
| 		else if (papaChannel && (!botChannel || botChannel.id !== papaChannel.id)) { | 		if (voiceConnection) voiceConnection.destroy() | ||||||
| 			joinVoiceChannel({ | 		return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.leaving_voice") }) | ||||||
| 				channelId: papaChannel.id, | 	} else if (papaChannel && (!botChannel || botChannel.id !== papaChannel.id)) { | ||||||
| 				guildId: papaChannel.guild.id, | 		joinVoiceChannel({ | ||||||
| 				adapterCreator: papaChannel.guild.voiceAdapterCreator, | 			channelId: papaChannel.id, | ||||||
| 			}) | 			guildId: papaChannel.guild.id, | ||||||
| 			return interaction.reply({ content: 'Je rejoins ton vocal, papa !' }) | 			adapterCreator: papaChannel.guild.voiceAdapterCreator, | ||||||
| 		} | 		}) | ||||||
| 		else return interaction.reply({ content: 'Je suis déjà dans ton vocal, papa !' }) | 		return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.joining_voice") }) | ||||||
| 	} | 	} else return interaction.reply({ content: t(interaction.locale, "salonpostam.papa.already_connected") }) | ||||||
| } | } | ||||||
							
								
								
									
										137
									
								
								src/commands/salonpostam/parle.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										137
									
								
								src/commands/salonpostam/parle.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,79 +1,84 @@ | |||||||
| import { SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from 'discord.js' | import { SlashCommandBuilder, MessageFlags } from "discord.js" | ||||||
| import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, EndBehaviorType } from '@discordjs/voice' | import type { ChatInputCommandInteraction, GuildMember } from "discord.js" | ||||||
|  | import { joinVoiceChannel, createAudioPlayer, createAudioResource, AudioPlayerStatus, EndBehaviorType } from "@discordjs/voice" | ||||||
|  | import { t } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("speak") | ||||||
| 		.setName('parle') | 	.setDescription("Make me talk over someone annoying in voice chat") | ||||||
| 		.setDescription('Fais moi parler par dessus quelqu\'un de chiant dans le vocal') | 	.setNameLocalizations({ fr: "parle" }) | ||||||
| 		.addUserOption(option => option.setName('user').setDescription('La personne en question').setRequired(true)), | 	.setDescriptionLocalizations({ fr: "Fais moi parler par dessus quelqu'un d'ennuyant dans le vocal" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 	.addUserOption(option => option | ||||||
| 		if (interaction.user.id !== '223831938346123275') return await interaction.reply({ content: 'Tu n\'as pas le droit d\'utiliser cette commande !', ephemeral: true }) | 		.setName("user") | ||||||
|  | 		.setDescription("The person in question") | ||||||
|  | 		.setNameLocalizations({ fr: "utilisateur" }) | ||||||
|  | 		.setDescriptionLocalizations({ fr: "La personne en question" }) | ||||||
|  | 		.setRequired(true) | ||||||
|  | 	) | ||||||
|  |  | ||||||
| 		let user = interaction.options.getUser('user') | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		if (!user) return | 	const guild = interaction.guild | ||||||
| 		let guild = interaction.guild | 	if (!guild) return | ||||||
| 		if (!guild) return |  | ||||||
| 		let member = guild.members.cache.get(user.id) as GuildMember |  | ||||||
| 		if (!member) return |  | ||||||
| 		let caller = interaction.member as GuildMember |  | ||||||
| 		if (!caller) return |  | ||||||
|  |  | ||||||
| 		if (!caller.voice.channel) return await interaction.reply({ content: 'You must be in a voice channel to use this command.', ephemeral: true }) | 	const user = interaction.options.getUser("user", true) | ||||||
| 		if (!member.voice.channel) return await interaction.reply({ content: 'The member must be in a voice channel to use this command.', ephemeral: true }) | 	const member = await guild.members.fetch(user.id) | ||||||
| 		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 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 }) | ||||||
|  |  | ||||||
| 		/* | 	await interaction.reply({ content: t(interaction.locale, "salonpostam.parle.will_speak_over"), 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) |  | ||||||
|  |  | ||||||
| 		let files = [] | 	/* | ||||||
| 		await messages.forEach(m => m.attachments.forEach(a => { | 	// Searches for audio files uploaded in the channel | ||||||
| 			if (a.contentType === 'audio/mpeg') files.push(a) | 	const messages = await interaction.channel.messages.fetch({ limit: 10, cache: false }).filter(m => m.attachments.size > 0) | ||||||
| 		})) |  | ||||||
| 		if (files.size === 0) return await interaction.editReply({ content: 'Aucun fichier audio trouvé dans ce channel.', ephemeral: true }) |  | ||||||
|  |  | ||||||
| 		// Limit the number of files to the last 10 | 	const files = [] | ||||||
| 		//files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10) | 	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 | 	// Limit the number of files to the last 10 | ||||||
| 		let file = await interaction.channel.send({ content: 'Choisissez un fichier audio :', files: files }) | 	//files = files.sort((a, b) => b.createdTimestamp - a.createdTimestamp).first(10) | ||||||
| 		let filter = m => m.author.id === interaction.user.id && !isNaN(m.content) && parseInt(m.content) > 0 && parseInt(m.content) <= files.size |  | ||||||
| 		let response = await interaction.channel.awaitMessages({ filter, max: 1, time: 30000, errors: ['time'] }) |  | ||||||
| 		file = files.get(files.keyArray()[response.first().content - 1]) |  | ||||||
| 		*/ |  | ||||||
|  |  | ||||||
| 		let playing = false | 	// Ask the user to choose a file | ||||||
| 		let player = createAudioPlayer() | 	let file = await interaction.channel.send({ content: 'Choisissez un fichier audio :', files }) | ||||||
| 		player.on(AudioPlayerStatus.Idle, () => { playing = false }) | 	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({ | 	let playing = false | ||||||
| 			channelId: caller.voice.channelId as string, | 	const player = createAudioPlayer() | ||||||
| 			guildId: interaction.guildId as string, | 	player.on(AudioPlayerStatus.Idle, () => { playing = false }) | ||||||
| 			adapterCreator: guild.voiceAdapterCreator, |  | ||||||
| 			selfDeaf: false |  | ||||||
| 		}) |  | ||||||
| 		connection.subscribe(player) |  | ||||||
|  |  | ||||||
| 		let stream = connection.receiver.subscribe(user.id, { end: { behavior: EndBehaviorType.Manual } }) | 	const connection = joinVoiceChannel({ | ||||||
| 		stream.on('data', () => { | 		channelId: caller.voice.channelId ?? "", | ||||||
| 			if (!user) return | 		guildId: interaction.guildId ?? "", | ||||||
| 			if (connection.receiver.speaking.users.has(user.id) && !playing) { | 		adapterCreator: guild.voiceAdapterCreator, | ||||||
| 				playing = true | 		selfDeaf: false | ||||||
| 				let resource = createAudioResource('../../static/parle.mp3', { inlineVolume: true }) | 	}) | ||||||
| 				//let resource = createAudioResource(file.attachments.first().url, { inlineVolume: true }) | 	connection.subscribe(player) | ||||||
| 				if (resource.volume) resource.volume.setVolume(0.2) |  | ||||||
| 				player.play(resource) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 		interaction.client.on('voiceStateUpdate', (oldState, newState) => { | 	const stream = connection.receiver.subscribe(user.id, { | ||||||
| 			if (oldState.id === member.id && newState.channelId !== caller.voice.channelId) { | 		end: { behavior: EndBehaviorType.Manual } | ||||||
| 				stream.destroy() | 	}) | ||||||
| 				connection.disconnect() | 	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() | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
| } | } | ||||||
							
								
								
									
										68
									
								
								src/commands/salonpostam/spam.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										68
									
								
								src/commands/salonpostam/spam.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -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 { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("spam") | ||||||
| 		.setName('spam') | 	.setDescription("Spam a user with a message") | ||||||
| 		.setDescription('Spam') | 	.setDescriptionLocalizations({ fr: "Spammer un utilisateur avec un message" }) | ||||||
| 		.addUserOption(option => option.setName('user').setDescription('Spam').setRequired(true)) | 	.addUserOption(option => option | ||||||
| 		.addStringOption(option => option.setName('string').setDescription('Spam').setRequired(true)) | 		.setName("user") | ||||||
| 		.addIntegerOption(option => option.setName('integer').setDescription('Spam').setRequired(true)), | 		.setDescription("Target user") | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { | 		.setNameLocalizations({ fr: "utilisateur" }) | ||||||
| 		let user = interaction.options.getUser('user') | 		.setDescriptionLocalizations({ fr: "Utilisateur cible" }) | ||||||
| 		let string = interaction.options.getString('string') | 		.setRequired(true) | ||||||
| 		let integer = interaction.options.getInteger('integer') | 	) | ||||||
|  | 	.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 }) | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 		let i = 0 | 	const user = interaction.options.getUser("user", true) | ||||||
| 		function myLoop() { | 	const string = interaction.options.getString("message", true) | ||||||
| 			setTimeout(function () { | 	const integer = interaction.options.getInteger("count", true) | ||||||
| 				if (!user) return |  | ||||||
| 				if (!string) return | 	await interaction.reply({ content: t(interaction.locale, "salonpostam.spam.started"), flags: MessageFlags.Ephemeral }) | ||||||
| 				if (!integer) return | 	let i = 0 | ||||||
| 				user.send(string).catch(error => console.error(error)) | 	function myLoop() { | ||||||
| 				i++ | 		setTimeout(() => { | ||||||
| 				if (i < integer) myLoop() | 			user.send(string).catch(console.error) | ||||||
| 			}, 1000) | 			i++ | ||||||
| 		} | 			if (i < integer) myLoop() | ||||||
| 		myLoop() | 		}, 1000) | ||||||
| 	} | 	} | ||||||
|  | 	myLoop() | ||||||
| } | } | ||||||
							
								
								
									
										35
									
								
								src/commands/salonpostam/update.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										35
									
								
								src/commands/salonpostam/update.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -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 { | export const data = new SlashCommandBuilder() | ||||||
| 	data: new SlashCommandBuilder() | 	.setName("update") | ||||||
| 		.setName('update') | 	.setDescription("Update the member count channel") | ||||||
| 		.setDescription('Update the member count channel.'), | 	.setDescriptionLocalizations({ fr: "Mettre à jour le canal de nombre de membres" }) | ||||||
| 	async execute(interaction: ChatInputCommandInteraction) { |  | ||||||
| 		let guild = interaction.guild as Guild |  | ||||||
|  |  | ||||||
| 		guild.members.fetch().then(() => { | export async function execute(interaction: ChatInputCommandInteraction) { | ||||||
| 			let i = 0 | 	const guild = interaction.guild | ||||||
| 			guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) | 	if (!guild) return interaction.reply({ content: t(interaction.locale, "common.command_server_only"), flags: MessageFlags.Ephemeral }) | ||||||
| 			let channel = guild.channels.cache.get('1091140609139560508') |  | ||||||
| 			if (!channel) return | 	guild.members.fetch().then(async () => { | ||||||
| 			channel.setName(`${i} Gens Posés`) | 		let i = 0 | ||||||
| 			interaction.reply(`${i} Gens Posés !`) | 		guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) | ||||||
| 		}).catch(console.error) | 		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) | ||||||
| } | } | ||||||
							
								
								
									
										11
									
								
								src/events/client/error.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										11
									
								
								src/events/client/error.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,8 +1,7 @@ | |||||||
| import { Events } from 'discord.js' | import { Events } from "discord.js" | ||||||
|  | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = Events.Error | ||||||
| 	name: Events.Error, | export function execute(error: Error) { | ||||||
| 	execute(error: Error) { | 	logConsoleError('discordjs', 'error', { message: error.message }, error) | ||||||
| 		console.error(error) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
							
								
								
									
										18
									
								
								src/events/client/guildCreate.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										18
									
								
								src/events/client/guildCreate.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,14 +1,12 @@ | |||||||
| import { Events, Guild } from 'discord.js' | import { Events, Guild } from "discord.js" | ||||||
| import dbGuildInit from '../../utils/dbGuildInit' | import dbGuildInit from "@/utils/dbGuildInit" | ||||||
|  | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = Events.GuildCreate | ||||||
| 	name: Events.GuildCreate, | export async function execute(guild: Guild) { | ||||||
| 	async execute(guild: Guild) { | 	logConsole('discordjs', 'guild_create', { name: guild.name, count: guild.memberCount.toString() }) | ||||||
| 		console.log(`Joined "${guild.name}" with ${guild.memberCount} members`) |  | ||||||
|  |  | ||||||
| 		let guildProfile = await dbGuildInit(guild) | 	const guildProfile = await dbGuildInit(guild) | ||||||
| 		if (!guildProfile) return console.log(`An error occured while initializing database data for "${guild.name}" !`) |  | ||||||
|  |  | ||||||
| 		console.log(`Database data for new guild "${guildProfile.guildName}" successfully initialized !`) | 	logConsole('mongoose', 'guild_create', { name: guildProfile.guildName }) | ||||||
| 	} |  | ||||||
| } | } | ||||||
							
								
								
									
										71
									
								
								src/events/client/guildMemberAdd.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										71
									
								
								src/events/client/guildMemberAdd.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,44 +1,43 @@ | |||||||
| import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js' | import { Events, EmbedBuilder, ChannelType } from "discord.js" | ||||||
|  | import type { GuildMember } from "discord.js" | ||||||
|  | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = Events.GuildMemberAdd | ||||||
| 	name: Events.GuildMemberAdd, | export async function execute(member: GuildMember) { | ||||||
| 	async execute(member: GuildMember) { | 	if (member.guild.id === "1086577543651524699") { | ||||||
| 		if (member.guild.id === '1086577543651524699') { // Salon posé tamisé | 		// Salon posé tamisé | ||||||
| 			let guild = member.guild | 		const guild = member.guild | ||||||
|  |  | ||||||
| 			guild.members.fetch().then(() => { | 		guild.members.fetch().then(async () => { | ||||||
| 				let i = 0 | 			let i = 0 | ||||||
| 				guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) | 			guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) | ||||||
|  |  | ||||||
| 				let channel = guild.channels.cache.get('1091140609139560508') | 			const channel = guild.channels.cache.get("1091140609139560508") | ||||||
| 				if (!channel) return | 			if (!channel) return | ||||||
|  |  | ||||||
| 				channel.setName('Changement...') | 			await channel.setName("Changement...") | ||||||
| 				channel.setName(`${i} Gens Posés`) | 			await channel.setName(`${i} Gens Posés`) | ||||||
| 			}).catch(console.error) | 		}).catch(console.error) | ||||||
| 		} else if (member.guild.id === '796327643783626782') { // Jujul Community | 	} else if (member.guild.id === "796327643783626782") { | ||||||
| 			let guild = member.guild | 		// Jujul Community | ||||||
|  | 		const guild = member.guild | ||||||
|  | 		if (!guild.members.me) return | ||||||
|  |  | ||||||
| 			let channel = guild.channels.cache.get('837248593609097237') as TextChannel | 		const channel = guild.channels.cache.get("837248593609097237") | ||||||
| 			if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "837248593609097237" !`) | 		if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { | ||||||
| 			 | 			logConsole('discordjs', 'guild_member_add', { channelId: '837248593609097237' }) | ||||||
| 			if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`) | 			return | ||||||
|  |  | ||||||
| 			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 guildLocale = await getGuildLocale(guild.id) | ||||||
|  | 		const embed = new EmbedBuilder() | ||||||
|  | 			.setColor(guild.members.me.displayHexColor) | ||||||
|  | 			.setTitle(t(guildLocale, "welcome.title", { username: member.user.username })) | ||||||
|  | 			.setDescription(t(guildLocale, "welcome.description", { memberCount: guild.memberCount.toString() })) | ||||||
|  | 			.setThumbnail(member.user.avatarURL()) | ||||||
|  | 			.setTimestamp(new Date()) | ||||||
|  |  | ||||||
|  | 		return channel.send({ embeds: [embed] }) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
							
								
								
									
										32
									
								
								src/events/client/guildMemberRemove.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										32
									
								
								src/events/client/guildMemberRemove.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,21 +1,23 @@ | |||||||
| import { Events, GuildMember } from 'discord.js' | import { Events } from "discord.js" | ||||||
|  | import type { GuildMember } from "discord.js" | ||||||
|  | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const name = Events.GuildMemberRemove | ||||||
| 	name: Events.GuildMemberRemove, | export function execute(member: GuildMember) { | ||||||
| 	async execute(member: GuildMember) { | 	if (member.guild.id === "1086577543651524699") { | ||||||
| 		if (member.guild.id === '1086577543651524699') { // Salon posé tamisé | 		// Salon posé tamisé | ||||||
| 			let guild = member.guild | 		const guild = member.guild | ||||||
|  |  | ||||||
| 			guild.members.fetch().then(() => { | 		guild.members.fetch().then(async () => { | ||||||
| 				let i = 0 | 			let i = 0 | ||||||
| 				guild.members.cache.forEach(async member => { if (!member.user.bot) i++ }) | 			guild.members.cache.forEach(member => { if (!member.user.bot) i++ }) | ||||||
|  |  | ||||||
| 				let channel = guild.channels.cache.get('1091140609139560508') | 			const channel = guild.channels.cache.get("1091140609139560508") | ||||||
| 				if (!channel) return | 			if (!channel) return | ||||||
|  |  | ||||||
| 				channel.setName('Changement...') | 			const guildLocale = await getGuildLocale(guild.id) | ||||||
| 				channel.setName(`${i} Gens Posés`) | 			await channel.setName(t(guildLocale, "salonpostam.update.loading")) | ||||||
| 			}).catch(console.error) | 			await channel.setName(t(guildLocale, "salonpostam.update.members_updated", { count: i.toString() })) | ||||||
| 		} | 		}).catch(console.error) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -1,35 +1,38 @@ | |||||||
| import { Events, GuildMember, EmbedBuilder, TextChannel } from 'discord.js' | import { Events, EmbedBuilder, ChannelType } from "discord.js" | ||||||
|  | import type { GuildMember } from "discord.js" | ||||||
|  | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = Events.GuildMemberUpdate | ||||||
| 	name: Events.GuildMemberUpdate, | export async function execute(oldMember: GuildMember, newMember: GuildMember) { | ||||||
| 	async execute(oldMember: GuildMember, newMember: GuildMember) { | 	if (newMember.guild.id === "796327643783626782") { | ||||||
| 		if (newMember.guild.id === '796327643783626782') { // Jujul Community | 		// Jujul Community | ||||||
| 			let guild = newMember.guild | 		const guild = newMember.guild | ||||||
|  |  | ||||||
| 			let channel = guild.channels.cache.get('924353449930412153') as TextChannel | 		const channel = await guild.channels.fetch("924353449930412153") | ||||||
| 			if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`) | 		if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { | ||||||
|  | 			logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" }) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 			let boostRole = guild.roles.premiumSubscriberRole | 		const boostRole = guild.roles.premiumSubscriberRole | ||||||
| 			if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`) | 		if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return } | ||||||
|  |  | ||||||
| 			const hadRole = oldMember.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) | 		const hasRole = newMember.roles.cache.find(role => role.id === boostRole.id) | ||||||
|  |  | ||||||
| 			if (!hadRole && hasRole) { | 		if (!hadRole && hasRole) { | ||||||
| 				if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`) | 			if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return } | ||||||
|  |  | ||||||
| 				let embed = new EmbedBuilder() | 			const guildLocale = await getGuildLocale(guild.id) | ||||||
| 					.setColor(guild.members.me.displayHexColor) | 			const embed = new EmbedBuilder() | ||||||
| 					.setTitle(`Nouveau boost de ${newMember.user.username} !`) | 				.setColor(guild.members.me.displayHexColor) | ||||||
| 					.setDescription(` | 				.setTitle(t(guildLocale, "boost.new_boost_title", { username: newMember.user.username })) | ||||||
| 						Merci à toi pour ce boost.\n | 				.setDescription(t(guildLocale, "boost.new_boost_description", { count: guild.premiumSubscriptionCount?.toString() ?? "0" })) | ||||||
| 						Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts ! | 				.setThumbnail(newMember.user.avatarURL()) | ||||||
| 					`) | 				.setTimestamp(new Date()) | ||||||
| 					.setThumbnail(newMember.user.avatarURL()) |  | ||||||
| 					.setTimestamp(new Date()) |  | ||||||
|  |  | ||||||
| 				await channel.send({ embeds: [embed] }) | 			return channel.send({ embeds: [embed] }) | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
							
								
								
									
										28
									
								
								src/events/client/guildUpdate.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										28
									
								
								src/events/client/guildUpdate.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,18 +1,18 @@ | |||||||
| import { Events, Guild } from 'discord.js' | import { Events } from "discord.js" | ||||||
| import dbGuildInit from '../../utils/dbGuildInit' | import type { Guild } from "discord.js" | ||||||
| import dbGuild from '../../schemas/guild' | import dbGuildInit from "@/utils/dbGuildInit" | ||||||
|  | import dbGuild from "@/schemas/guild" | ||||||
|  | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = Events.GuildUpdate | ||||||
| 	name: Events.GuildUpdate, | export async function execute(oldGuild: Guild, newGuild: Guild) { | ||||||
| 	async execute(oldGuild: Guild, newGuild: Guild) { | 	logConsole('discordjs', 'guild_update', { name: oldGuild.name }) | ||||||
| 		console.log(`Guild ${oldGuild.name} updated`) |  | ||||||
|  |  | ||||||
| 		let guildProfile = await dbGuild.findOne({ guildId: newGuild.id }) | 	let guildProfile = await dbGuild.findOne({ guildId: newGuild.id }) | ||||||
| 		if (!guildProfile) guildProfile = await dbGuildInit(newGuild) | 	if (!guildProfile) guildProfile = await dbGuildInit(newGuild) | ||||||
| 		else { | 	else { | ||||||
| 			guildProfile.guildName = newGuild.name | 		guildProfile.guildName = newGuild.name | ||||||
| 			guildProfile.guildIcon = newGuild.iconURL() ?? 'None' | 		guildProfile.guildIcon = newGuild.iconURL() ?? "None" | ||||||
| 			await guildProfile.save().catch(console.error) | 		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[] | ||||||
							
								
								
									
										70
									
								
								src/events/client/interactionCreate.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										70
									
								
								src/events/client/interactionCreate.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,45 +1,49 @@ | |||||||
| import { Events, Interaction, ChatInputCommandInteraction, AutocompleteInteraction, ButtonInteraction } from 'discord.js' | import { Events } from "discord.js" | ||||||
| import { playerButtons, playerEdit } from '../../utils/player' | 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 { | export const name = Events.InteractionCreate | ||||||
| 	name: Events.InteractionCreate, | export async function execute(interaction: Interaction) { | ||||||
| 	async execute(interaction: Interaction) { | 	if (interaction.isChatInputCommand()) { | ||||||
| 		//if (!interaction.isAutocomplete() && !interaction.isChatInputCommand() && !interaction.isButton()) return console.error(`Interaction ${interaction.commandName} is not a command.`) | 		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()) { | 		logConsole('discordjs', 'interaction_create.command_launched', { command: interaction.commandName, user: interaction.user.tag }) | ||||||
| 			interaction = interaction as ChatInputCommandInteraction |  | ||||||
|  |  | ||||||
| 			let chatInputCommand = interaction.client.commands.get(interaction.commandName) | 		try { await chatInputCommand.execute(interaction) } | ||||||
| 			if (!chatInputCommand) return console.error(`No chat input command matching ${interaction.commandName} was found.`) | 		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) } | 		try { await autocompleteRun.autocompleteRun(interaction) } | ||||||
| 			catch (error) { console.error(`Error executing ${interaction.commandName}:`, error) } | 		catch (error) { logConsoleError('discordjs', 'interaction_create.autocomplete_error', { command: interaction.commandName }, error as Error) } | ||||||
| 		} | 	} | ||||||
| 		else if (interaction.isAutocomplete()) { | 	else if (interaction.isButton()) { | ||||||
| 			interaction = interaction as AutocompleteInteraction | 		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) | 		logConsole('discordjs', 'interaction_create.button_clicked', { id: interaction.customId, user: interaction.user.tag }) | ||||||
| 			if (!autoCompleteRun) return console.error(`No autoCompleteRun matching ${interaction.commandName} was found.`) |  | ||||||
|  |  | ||||||
| 			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) } | 		if (buttonFolders.find(folder => folder.name === "player" ? folder.commands.some(cmd => cmd.id === interaction.customId) : false)) await playerEdit(interaction) | ||||||
| 			catch (error) { console.error(`Error autocompleting ${interaction.commandName}:`, error) } | 	} | ||||||
| 		} | 	else if (interaction.isAnySelectMenu()) { | ||||||
| 		else if (interaction.isButton()) { | 		const selectMenu = selectMenus.find(menu => menu.id === interaction.customId) | ||||||
| 			interaction = interaction as ButtonInteraction | 		if (!selectMenu) { logConsole('discordjs', 'interaction_create.selectmenu_not_found', { id: interaction.customId }); return } | ||||||
|  |  | ||||||
| 			let button = interaction.client.buttons.get(interaction.customId) | 		logConsole('discordjs', 'interaction_create.selectmenu_used', { id: interaction.customId, user: interaction.user.tag }) | ||||||
| 			if (!button) return console.error(`No button id matching ${interaction.customId} was found.`) |  | ||||||
|  |  | ||||||
| 			console.log(`Button '${interaction.customId}' clicked by ${interaction.user.tag}`) | 		try { await selectMenu.execute(interaction) } | ||||||
|  | 		catch (error) { logConsoleError('discordjs', 'interaction_create.selectmenu_error', { id: interaction.customId }, error as Error) } | ||||||
| 			if (playerButtons.includes(interaction.customId)) { await playerEdit(interaction) } |  | ||||||
|  |  | ||||||
| 			try { await button.execute(interaction) } |  | ||||||
| 			catch (error) { console.error(`Error clicking ${interaction.customId}:`, error) } |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
							
								
								
									
										213
									
								
								src/events/client/ready.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										213
									
								
								src/events/client/ready.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,117 +1,140 @@ | |||||||
| import { Events, Client, ActivityType } from 'discord.js' | import { Events, ActivityType, ChannelType } from "discord.js" | ||||||
| import { SpotifyExtractor } from '@discord-player/extractor' | import type { Client } from "discord.js" | ||||||
| import { YoutubeiExtractor } from 'discord-player-youtubei' | import { useMainPlayer } from "discord-player" | ||||||
| import { useMainPlayer } from 'discord-player' | import { SpotifyExtractor } from "@discord-player/extractor" | ||||||
| import { connect } from 'mongoose' | import { YoutubeiExtractor } from "discord-player-youtubei" | ||||||
| import WebSocket from 'websocket' | import { connect } from "mongoose" | ||||||
| import chalk from 'chalk' | import type { Document } from "mongoose" | ||||||
| import 'dotenv/config' | import { playerDisco, playerReplay } from "@/utils/player" | ||||||
|  | import { twitchClient, listener, onlineSub, offlineSub, startStreamWatching } from "@/utils/twitch" | ||||||
|  | import { logConsole, logConsoleError } 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' | export const name = Events.ClientReady | ||||||
| import dbGuild from '../../schemas/guild' | export const once = true | ||||||
| import { playerDisco, playerReplay } from '../../utils/player' | export async function execute(client: Client) { | ||||||
| import * as Twitch from '../../utils/twitch' | 	logConsole('discordjs', 'ready', { tag: client.user?.tag ?? "unknown" }) | ||||||
| import rss from '../../utils/rss' | 	client.user?.setActivity("some bangers...", { type: ActivityType.Listening }) | ||||||
|  |  | ||||||
| export default { | 	const player = useMainPlayer() | ||||||
| 	name: Events.ClientReady, | 	await player.extractors.register(SpotifyExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Spotify' }) }).catch(console.error) | ||||||
| 	once: true, | 	await player.extractors.register(YoutubeiExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Youtube' }) }).catch(console.error) | ||||||
| 	async execute(client: Client) { | 	if (process.env.NODE_ENV === "development") console.log(player.scanDeps()) | ||||||
| 		console.log(chalk.blue(`[DiscordJS] Connected to Discord ! Logged in as ${client.user?.tag ?? 'unknown'}`)) |  | ||||||
| 		client.user?.setActivity('some bangers...', { type: ActivityType.Listening }) |  | ||||||
|  |  | ||||||
| 		await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => console.log(chalk.blue('[Discord-Player] Spotify extractor loaded.'))).catch(console.error) | 	const mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}` | ||||||
| 		await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => console.log(chalk.blue('[Discord-Player] Youtube extractor loaded.'))).catch(console.error) | 	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}` | 	const streamerIds: string[] = [] | ||||||
| 		await connect(mongo_url).catch(console.error) |  | ||||||
|  |  | ||||||
|  | 	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 | 		const dbDataPlayer = guildProfile.get("guildPlayer") as GuildPlayer | ||||||
| 		guilds.forEach(async guild => { | 		const botInstance = dbDataPlayer.instances?.find(instance => instance.botId === client.user?.id) | ||||||
| 			let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | 		if (botInstance?.replay.trackUrl) await playerReplay(client, dbDataPlayer) | ||||||
|  |  | ||||||
| 			if (!guildProfile) guildProfile = await dbGuildInit(guild) | 		client.disco = { interval: {} as NodeJS.Timeout } | ||||||
| 			if (guildProfile.guildPlayer?.replay?.enabled && guildProfile.guildPlayer?.replay?.textChannelId) await playerReplay(client, guildProfile) | 		// 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 } | 			if (dbDataDisco.enabled) { | ||||||
| 			client.disco.interval = setInterval(async () => { | 				const state = await playerDisco(client, guild, dbDataDisco) | ||||||
| 				let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | 				if (state === "clear") clearInterval(client.disco.interval) | ||||||
|  | 			} | ||||||
|  | 		}, 3000) | ||||||
|  |  | ||||||
| 				if (guildProfile?.guildPlayer?.disco?.enabled) { | 		// Gestion du timer LCD Freebox | ||||||
| 					let state = await playerDisco(client, guildProfile) | 		const dbDataFbx = guildProfile.get("guildFbx") as GuildFbx | ||||||
| 					if (state === 'clear') clearInterval(client.disco.interval) | 		if (dbDataFbx.enabled && dbDataFbx.lcd) { | ||||||
| 				} | 			if (dbDataFbx.lcd.enabled && dbDataFbx.lcd.botId === client.user?.id) { | ||||||
| 			}, 3000) | 				logConsole('freebox', 'lcd_timer_restored', { guild: guild.name }) | ||||||
|  | 				Freebox.Timer.schedule(client, guild.id, dbDataFbx) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 			client.rss = { interval: {} as NodeJS.Timeout } | 		const dbDataTwitch = guildProfile.get("guildTwitch") as GuildTwitch | ||||||
| 			client.rss.interval = setInterval(async () => { | 		if (!dbDataTwitch.enabled) return | ||||||
| 				let guildProfile = await dbGuild.findOne({ guildId: guild.id }) | 		if (!dbDataTwitch.streamers.length) { logConsole('twitch', 'ready.no_streamers_configured', { guild: guild.name }); return } | ||||||
|  |  | ||||||
| 				if (guildProfile?.guildRss?.enabled) { | 		await Promise.all(dbDataTwitch.streamers.map(async streamer => { | ||||||
| 					let state = await rss(client, guildProfile) | 			if (streamerIds.includes(streamer.twitchUserId)) return | ||||||
| 					if (state === 'clear') clearInterval(client.rss.interval) | 			streamerIds.push(streamer.twitchUserId) | ||||||
| 				} |  | ||||||
| 			}, 30000) |  | ||||||
|  |  | ||||||
| 			// TWITCH EVENTSUB | 			const user = await twitchClient.users.getUserById(streamer.twitchUserId) | ||||||
| 			if (process.env['TWITCH_RUNNING_' + guild.id]) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Already running...`)) | 			if (!user) { logConsole('twitch', 'ready.user_not_found', { guild: guild.name, userId: streamer.twitchUserId }); return } | ||||||
| 			console.log(chalk.magenta(`[Twitch] {${guild.name}} Not running, starting...`)) |  | ||||||
| 			process.env['TWITCH_RUNNING_' + guild.id] = 'true' |  | ||||||
|  |  | ||||||
| 			let client_id = process.env.TWITCH_APP_ID as string | 			const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(streamer.twitchUserId) | ||||||
| 			let client_secret = process.env.TWITCH_APP_SECRET as string | 			if (process.env.NODE_ENV === "development") { | ||||||
| 			if (!client_id || !client_secret) return console.log(chalk.magenta(`[Twitch] {${guild.name}} App ID or Secret is not defined !`)) | 				console.log(userSubs) | ||||||
|  | 				userSubs.data.forEach(sub => { console.log(sub) }) | ||||||
|  | 			} | ||||||
|  | 			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') | 			logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) | ||||||
| 			if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Module is disabled, please activate with \`/database edit guildTwitch.enabled True\` !`)) |  | ||||||
|  |  | ||||||
| 			let twitch = new WebSocket.client().on('connect', async connection => { | 			const stream = await user.getStream() | ||||||
| 				console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub WebSocket Connected !`)) | 			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 { | 				// Vérifier que le message existe encore | ||||||
| 					let data = JSON.parse(message.utf8Data) | 				if (!dbDataTwitch.channelId) return | ||||||
| 					let channel_access_token = guildProfile.get('guildTwitch')?.channelAccessToken as string | 				const channel = await guild.channels.fetch(dbDataTwitch.channelId) | ||||||
|  | 				if (channel && (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement)) { | ||||||
| 					// Check when Twitch asks to login | 					try { | ||||||
| 					if (data.metadata.message_type === 'session_welcome') { | 						await channel.messages.fetch(streamer.messageId) | ||||||
|  | 						startStreamWatching(guild.id, streamer.twitchUserId, user.name, streamer.messageId) | ||||||
| 						// Check if the channel access token is still valid before connecting | 						logConsole('twitch', 'ready.monitoring_restored', { guild: guild.name, userName: user.name }) | ||||||
| 						channel_access_token = await Twitch.checkChannel(client_id, client_secret, channel_access_token, guild) as string | 					} catch (error) { | ||||||
| 						if (!channel_access_token) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't refresh channel access token !`)) | 						logConsoleError('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name }, error as Error) | ||||||
|  | 						await cleanupMessageId(guildProfile, streamer.twitchUserId) | ||||||
| 						// 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}`)) |  | ||||||
| 						} |  | ||||||
| 					} | 					} | ||||||
|  | 				} | ||||||
|  | 			} 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 | 	const subs = await twitchClient.eventSub.getSubscriptions() | ||||||
| 					else if (data.metadata.message_type === 'notification') Twitch.notification(client_id, client_secret, channel_access_token, data, guild) | 	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 | ||||||
|  |  | ||||||
| 				} catch (error) { console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error)) }  }  }) | 		await sub.unsubscribe().catch(console.error) | ||||||
| 				.on('error', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error))) | 		logConsole('twitch', 'unsubscribed', { type: sub.type, id: sub.condition.broadcaster_user_id as string }) | ||||||
| 				.on('close', () => { | 	})) | ||||||
| 					console.log(chalk.magenta(`[Twitch] {${guild.name}} EventSub Connection Closed !`)) | } | ||||||
| 					twitch.connect('wss://eventsub.wss.twitch.tv/ws') |  | ||||||
| 				}) |  | ||||||
| 			}).on('connectFailed', error => console.error(chalk.magenta(`[Twitch] {${guild.name}} ` + error))) |  | ||||||
|  |  | ||||||
| 			twitch.connect('wss://eventsub.wss.twitch.tv/ws') | 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(console.error) | ||||||
|  | 	} catch (error) { | ||||||
|  | 		logConsoleError('twitch', 'ready.cleanup_error', { userId: twitchUserId }, error as 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 { | export const name = "connected" | ||||||
| 	name: 'connected', | export function execute() { | ||||||
| 	async execute() { | 	logConsole('mongoose', 'connected') | ||||||
| 		console.log(chalk.green('[Mongoose] Connected to MongoDB !')) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| import chalk from 'chalk' | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = "connecting" | ||||||
| 	name: 'connecting', | export function execute() { | ||||||
| 	async execute() { | 	logConsole('mongoose', 'connecting') | ||||||
| 		console.log(chalk.green('[Mongoose] Connecting to MongoDB...')) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| import chalk from 'chalk' | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = "disconnected" | ||||||
| 	name: 'disconnected', | export function execute() { | ||||||
| 	async execute() { | 	logConsole('mongoose', 'disconnected') | ||||||
| 		console.log(chalk.green('[Mongoose] Disconnected from MongoDB !')) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| @@ -1,8 +1,6 @@ | |||||||
| import chalk from 'chalk' | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = "error" | ||||||
| 	name: 'error', | export function execute(error: Error) { | ||||||
| 	async execute(error: Error) { | 	logConsoleError('mongoose', 'error', { message: error.message }, error) | ||||||
| 		console.log(chalk.red('[Mongoose] An error occured with the database conenction :\n' + 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[] | ||||||
							
								
								
									
										18
									
								
								src/events/player/audioTrackAdd.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										18
									
								
								src/events/player/audioTrackAdd.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,10 +1,14 @@ | |||||||
| import { GuildQueue, Track } from 'discord-player' | import type { GuildQueue, Track } from "discord-player" | ||||||
| import { PlayerMetadata } from '../../utils/player' | import type { PlayerMetadata } from "@/types/player" | ||||||
|  | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const name = "audioTrackAdd" | ||||||
| 	name: 'audioTrackAdd', | export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) { | ||||||
| 	async execute(queue: GuildQueue<PlayerMetadata>, track: Track) { | 	// Emitted when the player adds a single song to its queue | ||||||
| 		// Emitted when the player adds a single song to its queue | 	if (!queue.metadata.channel) return | ||||||
| 		queue.metadata.channel.send(`Musique **${track.title}** de **${track.author}** ajoutée à la file d'attente !`) | 	 | ||||||
|  | 	if ("send" in queue.metadata.channel) { | ||||||
|  | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
|  | 		return queue.metadata.channel.send({ content: t(guildLocale, "player.track_added", { title: track.title }) }) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
							
								
								
									
										18
									
								
								src/events/player/audioTracksAdd.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										18
									
								
								src/events/player/audioTracksAdd.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,10 +1,14 @@ | |||||||
| import { GuildQueue, Track } from 'discord-player' | import type { GuildQueue, Track } from "discord-player" | ||||||
| import { PlayerMetadata } from '../../utils/player' | import type { PlayerMetadata } from "@/types/player" | ||||||
|  | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const name = "audioTracksAdd" | ||||||
| 	name: 'audioTracksAdd', | export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track[]) { | ||||||
| 	async execute(queue: GuildQueue<PlayerMetadata>, track: Array<Track>) { | 	// Emitted when the player adds multiple songs to its queue | ||||||
| 		// Emitted when the player adds multiple songs to its queue | 	if (!queue.metadata.channel) return | ||||||
| 		queue.metadata.channel.send(`Ajout de ${track.length} musiques à la file d'attente !`) |  | ||||||
|  | 	if ("send" in queue.metadata.channel) { | ||||||
|  | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
|  | 		return queue.metadata.channel.send({ content: t(guildLocale, "player.track_added_playlist", { count: track.length.toString() }) }) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
							
								
								
									
										15
									
								
								src/events/player/debug.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										15
									
								
								src/events/player/debug.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,10 +1,9 @@ | |||||||
| import { GuildQueue } from 'discord-player' | import type { GuildQueue } from "discord-player" | ||||||
|  | import { logConsoleDev } from "@/utils/console" | ||||||
|  |  | ||||||
| export default { | export const name = "debug" | ||||||
| 	name: 'debug', | export function execute(queue: GuildQueue, message: string) { | ||||||
| 	async execute(queue: GuildQueue, message: string) { | 	// Emitted when the player queue sends debug info | ||||||
| 		// Emitted when the player queue sends debug info | 	// Useful for seeing what state the current queue is at | ||||||
| 		// Useful for seeing what state the current queue is at | 	logConsoleDev('discord_player', 'debug', { message }) | ||||||
| 		console.log(`Player debug event: ${message}`) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
							
								
								
									
										31
									
								
								src/events/player/disconnect.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										31
									
								
								src/events/player/disconnect.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,24 +1,17 @@ | |||||||
| import { GuildQueue } from 'discord-player' | import type { GuildQueue } from "discord-player" | ||||||
| import { PlayerMetadata } from '../../utils/player' | import type { PlayerMetadata } from "@/types/player" | ||||||
| import dbGuild from '../../schemas/guild' | import { stopProgressSaving } from "@/utils/player" | ||||||
|  | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export default { | export const name = "disconnect" | ||||||
| 	name: 'disconnect', | export async function execute(queue: GuildQueue<PlayerMetadata>) { | ||||||
| 	async execute(queue: GuildQueue<PlayerMetadata>) { | 	// Emitted when the bot leaves the voice channel | ||||||
| 		// Emitted when the bot leaves the voice channel | 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | ||||||
| 		queue.metadata.channel.send("J'ai quitté le vocal !") |  | ||||||
|  |  | ||||||
| 		let guildProfile = await dbGuild.findOne({ guildId: queue.guild.id }) | 	if (!queue.metadata.channel) return | ||||||
| 		if (!guildProfile) return console.log(`Database data for **${queue.guild.name}** does not exist !`) |  | ||||||
|  |  | ||||||
| 		let dbData = guildProfile.get('guildPlayer.replay') | 	if ("send" in queue.metadata.channel) { | ||||||
| 		dbData['textChannelId'] = '' | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
| 		dbData['voiceChannelId'] = '' | 		return queue.metadata.channel.send({ content: t(guildLocale, "player.disconnect") }) | ||||||
| 		dbData['trackUrl'] = '' |  | ||||||
| 		dbData['progress'] = '' |  | ||||||
|  |  | ||||||
| 		guildProfile.set('guildPlayer.replay', dbData) |  | ||||||
| 		guildProfile.markModified('guildPlayer.replay') |  | ||||||
| 		return await guildProfile.save().catch(console.error) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user