Compare commits
	
		
			19 Commits
		
	
	
		
			build_2025
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 87272f4b45 | |||
| 18a293690e | |||
| fb7ba5d145 | |||
| f94a3852e8 | |||
| 462ad2e9d6 | |||
| f1b5592045 | |||
| af4e6e2e69 | |||
| 6d0c0145ee | |||
| e714e94f85 | |||
| 0cc81d6430 | |||
| 1dcb8c6826 | |||
| 2b6870b861 | |||
| ceb7a74b11 | |||
| fd4e17a754 | |||
| 4ed73f7c72 | |||
| f1a488d362 | |||
| 066a3864dd | |||
| 9a4902291e | |||
| d06df32bab | 
| @@ -2,13 +2,8 @@ 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 | ||||||
| @@ -54,12 +49,17 @@ jobs: | |||||||
|         tags: | |         tags: | | ||||||
|           # Tag avec le nom du tag Git |           # Tag avec le nom du tag Git | ||||||
|           type=ref,event=tag |           type=ref,event=tag | ||||||
|  |           # Tag 'latest' pour la branche master | ||||||
|  |           type=raw,value=latest,enable={{is_default_branch}} | ||||||
|  |           # Tag avec le SHA pour les autres branches | ||||||
|  |           type=sha,prefix=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 | ||||||
| @@ -72,10 +72,3 @@ jobs: | |||||||
|         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.IMAGE_PATH }}/${{ env.IMAGE_NAME }} |  | ||||||
|         subject-digest: ${{ steps.build.outputs.digest }} |  | ||||||
|         push-to-registry: true |  | ||||||
|   | |||||||
| @@ -1,22 +1,34 @@ | |||||||
| # Starting from node | # Starting from node | ||||||
| FROM node:22-alpine | FROM node:22-slim | ||||||
|  |  | ||||||
| ENV NODE_ENV=production | # Install build dependencies | ||||||
|  | RUN apt-get update && \ | ||||||
|  |     apt-get install -y ffmpeg python3 make g++ | ||||||
|  |  | ||||||
|  | # Set the working directory | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
|  | RUN chown node:node ./ | ||||||
|  | USER node | ||||||
|  |  | ||||||
| RUN apk add --no-cache ffmpeg python3 make g++ | # Copy package files first | ||||||
|  | COPY --chown=node:node package.json package-lock.json* . | ||||||
|  |  | ||||||
| # Copy package files and install only production dependencies | # Install app dependencies | ||||||
| COPY package.json package-lock.json* . | ENV NODE_ENV=production | ||||||
| RUN npm ci --only=production --ignore-scripts && \ | RUN npm ci --only=production --ignore-scripts && \ | ||||||
|     npm install bufferutil zlib-sync |     npm install bufferutil zlib-sync && \ | ||||||
|  |     npm cache clean --force | ||||||
|  |  | ||||||
| # Copy the builded files and the charts | # Copy the builded files | ||||||
| COPY ./dist/* . | COPY --chown=node:node ./dist/* . | ||||||
|  |  | ||||||
| # Set the permissions | # Return to root user to remove build dependencies | ||||||
| RUN chown -R node:node /app | 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 | USER node | ||||||
|  |  | ||||||
| # Start the application | # Start the application | ||||||
|   | |||||||
							
								
								
									
										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: "4.0.0" |     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,103 +0,0 @@ | |||||||
| # Système de Contrôle des LEDs Freebox |  | ||||||
|  |  | ||||||
| Ce module permet au bot de contrôler automatiquement les LEDs de l'écran LCD de la Freebox avec des timers programmables. |  | ||||||
|  |  | ||||||
| ## Fonctionnalités |  | ||||||
|  |  | ||||||
| ### Contrôle Manuel des LEDs |  | ||||||
| - Allumer/éteindre les LEDs instantanément via commande Discord |  | ||||||
| - Récupérer la configuration actuelle de l'écran LCD |  | ||||||
|  |  | ||||||
| ### Timer Automatique |  | ||||||
| - Programmation d'extinction automatique la nuit |  | ||||||
| - Programmation d'allumage automatique le matin |  | ||||||
| - Gestion par bot unique par serveur (évite les conflits) |  | ||||||
| - Persistance des paramètres en base de données |  | ||||||
|  |  | ||||||
| ## Configuration Prérequise |  | ||||||
|  |  | ||||||
| 1. **Module Freebox activé** : `/database edit guildFbx.enabled true` |  | ||||||
| 2. **Hôte configuré** : `/database edit guildFbx.host <ip_freebox>` |  | ||||||
| 3. **Version API configurée** : `/database edit guildFbx.version <version>` |  | ||||||
| 4. **Authentification** : `/freebox init` (suivre le processus d'autorisation) |  | ||||||
|  |  | ||||||
| ## Commandes Disponibles |  | ||||||
|  |  | ||||||
| ### Récupération de configuration |  | ||||||
| ``` |  | ||||||
| /freebox get lcd |  | ||||||
| ``` |  | ||||||
| Récupère et affiche la configuration actuelle de l'écran LCD. |  | ||||||
|  |  | ||||||
| ### Contrôle manuel des LEDs |  | ||||||
| ``` |  | ||||||
| /freebox lcd leds enabled:true   # Allumer les LEDs |  | ||||||
| /freebox lcd leds enabled:false  # Éteindre les LEDs |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Gestion du timer |  | ||||||
| ``` |  | ||||||
| # Activer le timer avec horaires |  | ||||||
| /freebox lcd timer action:enable morning_time:08:00 night_time:22:30 |  | ||||||
|  |  | ||||||
| # Vérifier le statut du timer |  | ||||||
| /freebox lcd timer action:status |  | ||||||
|  |  | ||||||
| # Désactiver le timer |  | ||||||
| /freebox lcd timer action:disable |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Fonctionnement du Timer |  | ||||||
|  |  | ||||||
| ### Programmation |  | ||||||
| - Le timer est programmé automatiquement au démarrage du bot |  | ||||||
| - Seul le bot configuré comme "gestionnaire" peut contrôler les LEDs d'un serveur |  | ||||||
| - Les horaires sont vérifiés et formatés (HH:MM, 24h) |  | ||||||
|  |  | ||||||
| ### Exécution |  | ||||||
| - **Matin** : Les LEDs s'allument à l'heure programmée |  | ||||||
| - **Soir** : Les LEDs s'éteignent à l'heure programmée |  | ||||||
| - **Reprogrammation** : Le cycle se répète automatiquement chaque jour |  | ||||||
|  |  | ||||||
| ### Logs |  | ||||||
| Les opérations sont loggées dans la console : |  | ||||||
| - Programmation des timers |  | ||||||
| - Exécution des commandes d'allumage/extinction |  | ||||||
| - Erreurs de connexion ou d'authentification |  | ||||||
|  |  | ||||||
| ## Gestion Multi-Bot |  | ||||||
|  |  | ||||||
| ### Système de Verrouillage |  | ||||||
| - Un seul bot peut gérer les LEDs par serveur Discord |  | ||||||
| - L'ID du bot gestionnaire est stocké en base de données |  | ||||||
| - Les autres bots reçoivent un message d'erreur s'ils tentent d'utiliser les commandes |  | ||||||
|  |  | ||||||
| ### Changement de Bot Gestionnaire |  | ||||||
| Pour changer de bot gestionnaire : |  | ||||||
| 1. Désactiver le timer sur le bot actuel : `/freebox lcd timer action:disable` |  | ||||||
| 2. Activer le timer sur le nouveau bot : `/freebox lcd timer action:enable` |  | ||||||
|  |  | ||||||
| ## Dépannage |  | ||||||
|  |  | ||||||
| ### Erreurs Communes |  | ||||||
| - **"Module Freebox désactivé"** : Activer avec `/database edit guildFbx.enabled true` |  | ||||||
| - **"Hôte non configuré"** : Définir avec `/database edit guildFbx.host <ip>` |  | ||||||
| - **"Token d'app manquant"** : Refaire l'initialisation avec `/freebox init` |  | ||||||
| - **"Géré par un autre bot"** : Désactiver sur l'autre bot d'abord |  | ||||||
|  |  | ||||||
| ### Vérification de Configuration |  | ||||||
| 1. Vérifier que la Freebox est accessible sur le réseau |  | ||||||
| 2. S'assurer que l'application est autorisée dans l'interface Freebox |  | ||||||
| 3. Vérifier les logs de la console pour les erreurs détaillées |  | ||||||
|  |  | ||||||
| ## API Freebox Utilisée |  | ||||||
|  |  | ||||||
| - `GET /api/v8/lcd/config/` : Récupération de la configuration LCD |  | ||||||
| - `PUT /api/v8/lcd/config/` : Modification de la configuration LCD |  | ||||||
| - Propriété `led_strip_enabled` : Contrôle de l'état des LEDs |  | ||||||
|  |  | ||||||
| ## Sécurité |  | ||||||
|  |  | ||||||
| - Les tokens d'authentification sont gérés automatiquement |  | ||||||
| - Les sessions sont créées à la demande |  | ||||||
| - Les erreurs d'authentification sont loggées mais les tokens ne sont pas exposés |  | ||||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
| 	"name": "bot_tamiseur", | 	"name": "bot_tamiseur", | ||||||
| 	"version": "4.0.0", | 	"version": "4.0.1", | ||||||
| 	"lockfileVersion": 3, | 	"lockfileVersion": 3, | ||||||
| 	"requires": true, | 	"requires": true, | ||||||
| 	"packages": { | 	"packages": { | ||||||
| 		"": { | 		"": { | ||||||
| 			"name": "bot_tamiseur", | 			"name": "bot_tamiseur", | ||||||
| 			"version": "4.0.0", | 			"version": "4.0.1", | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"@discord-player/extractor": "^7.1.0", | 				"@discord-player/extractor": "^7.1.0", | ||||||
| 				"@discordjs/voice": "^0.18.0", | 				"@discordjs/voice": "^0.18.0", | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
| 	"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": "4.0.0", | 	"version": "4.0.1", | ||||||
| 	"author": { | 	"author": { | ||||||
| 		"name": "Zachary Guénot" | 		"name": "Zachary Guénot" | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import type { APIResponseData, APIResponseDataError, GetChallenge, LcdConfig, Op | |||||||
| import type { GuildFbx } from "@/types/schemas" | import type { GuildFbx } from "@/types/schemas" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
|  | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export const id = "freebox_lcd_status" | export const id = "freebox_lcd_status" | ||||||
| export async function execute(interaction: ButtonInteraction) { | export async function execute(interaction: ButtonInteraction) { | ||||||
| @@ -82,7 +83,7 @@ export async function execute(interaction: ButtonInteraction) { | |||||||
|  |  | ||||||
| 		return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) | 		return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) | ||||||
| 	} catch (error) { | 	} catch (error) { | ||||||
| 		console.error("Erreur lors de la récupération de l'état LCD:", error) | 		logConsoleError('freebox', 'lcd_status_error', undefined, error as Error) | ||||||
| 		return interaction.followUp({ content: t(interaction.locale, "freebox.lcd.unexpected_error"), flags: MessageFlags.Ephemeral }) | 		return interaction.followUp({ content: t(interaction.locale, "freebox.lcd.unexpected_error"), flags: MessageFlags.Ephemeral }) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import type { APIResponseData, APIResponseDataError, APIResponseDataVersion, Con | |||||||
| import type { GuildFbx } from "@/types/schemas" | import type { GuildFbx } from "@/types/schemas" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
|  | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export const id = "freebox_test_connection" | export const id = "freebox_test_connection" | ||||||
| export async function execute(interaction: ButtonInteraction) { | export async function execute(interaction: ButtonInteraction) { | ||||||
| @@ -65,7 +66,7 @@ export async function execute(interaction: ButtonInteraction) { | |||||||
|  |  | ||||||
| 		return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) | 		return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) | ||||||
| 	} catch (error) { | 	} catch (error) { | ||||||
| 		console.error("Erreur lors du test de connexion Freebox:", error) | 		logConsoleError('freebox', 'test_connection_error', undefined, error as Error) | ||||||
| 		return interaction.followUp({ content: t(interaction.locale, "freebox.test.connection_error"), flags: MessageFlags.Ephemeral }) | 		return interaction.followUp({ content: t(interaction.locale, "freebox.test.connection_error"), flags: MessageFlags.Ephemeral }) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import type { GuildTwitch } from "@/types/schemas" | |||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { twitchClient } from "@/utils/twitch" | import { twitchClient } from "@/utils/twitch" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
| import { logConsole } from "@/utils/console" | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export const id = "twitch_streamer_list" | export const id = "twitch_streamer_list" | ||||||
| export async function execute(interaction: ButtonInteraction) { | export async function execute(interaction: ButtonInteraction) { | ||||||
| @@ -34,8 +34,7 @@ export async function execute(interaction: ButtonInteraction) { | |||||||
| 				streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.user_not_found")}\n└ ID: \`${streamer.twitchUserId}\``) | 				streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.user_not_found")}\n└ ID: \`${streamer.twitchUserId}\``) | ||||||
| 			} | 			} | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId }) | 			logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as Error) | ||||||
| 			console.error(error) |  | ||||||
| 			streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.fetch_error")}\n└ ID: \`${streamer.twitchUserId}\``) | 			streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.fetch_error")}\n└ ID: \`${streamer.twitchUserId}\``) | ||||||
| 		} | 		} | ||||||
| 	})) | 	})) | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { twitchClient } from "@/utils/twitch" | |||||||
| import type { GuildTwitch } from "@/types/schemas" | import type { GuildTwitch } from "@/types/schemas" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
| import { logConsole } from "@/utils/console" | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export const id = "twitch_streamer_remove" | export const id = "twitch_streamer_remove" | ||||||
| export async function execute(interaction: ButtonInteraction) { | export async function execute(interaction: ButtonInteraction) { | ||||||
| @@ -25,8 +25,7 @@ export async function execute(interaction: ButtonInteraction) { | |||||||
| 				description: user ? `@${user.name}` : t(interaction.locale, "twitch.user_not_found") | 				description: user ? `@${user.name}` : t(interaction.locale, "twitch.user_not_found") | ||||||
| 			} | 			} | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId }) | 			logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as Error) | ||||||
| 			console.error(error) |  | ||||||
| 			return { | 			return { | ||||||
| 				label: `ID: ${streamer.twitchUserId}`, | 				label: `ID: ${streamer.twitchUserId}`, | ||||||
| 				value: streamer.twitchUserId, | 				value: streamer.twitchUserId, | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import type { | |||||||
| import type { GuildFbx } from "@/types/schemas" | import type { GuildFbx } from "@/types/schemas" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
|  | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export const data = new SlashCommandBuilder() | export const data = new SlashCommandBuilder() | ||||||
| 	.setName("freebox") | 	.setName("freebox") | ||||||
| @@ -238,7 +239,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { | |||||||
| 				clearInterval(initCheck) | 				clearInterval(initCheck) | ||||||
|  |  | ||||||
| 				return interaction.followUp({ content: t(interaction.locale, "freebox.auth.user_denied_access"), flags: MessageFlags.Ephemeral }) | 				return interaction.followUp({ content: t(interaction.locale, "freebox.auth.user_denied_access"), flags: MessageFlags.Ephemeral }) | ||||||
| 			} else if (status === "pending") { console.log("Freebox authorization pending...") } | 			} else if (status === "pending") logConsole('freebox', 'authorization_pending') | ||||||
| 		}, 2000) | 		}, 2000) | ||||||
| 	} | 	} | ||||||
| 	else { | 	else { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import * as amp from "./amp" | |||||||
| import * as boost from "./boost" | import * as boost from "./boost" | ||||||
| import * as database from "./database" | import * as database from "./database" | ||||||
| import * as freebox from "./freebox" | import * as freebox from "./freebox" | ||||||
|  | import * as locale from "./locale" | ||||||
| import * as ping from "./ping" | import * as ping from "./ping" | ||||||
| import * as twitch from "./twitch" | import * as twitch from "./twitch" | ||||||
|  |  | ||||||
| @@ -12,6 +13,7 @@ export default [ | |||||||
| 	boost, | 	boost, | ||||||
| 	database, | 	database, | ||||||
| 	freebox, | 	freebox, | ||||||
|  | 	locale, | ||||||
| 	ping, | 	ping, | ||||||
| 	twitch | 	twitch | ||||||
| ] as Command[] | ] 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  | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| import { SlashCommandBuilder, ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js" | import { SlashCommandBuilder, ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js" | ||||||
| import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData } from "discord.js" | import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData } from "discord.js" | ||||||
| import chalk from "chalk" |  | ||||||
| import { twitchClient, listener, onlineSub, offlineSub, generateTwitchEmbed } from "@/utils/twitch" | import { twitchClient, listener, onlineSub, offlineSub, generateTwitchEmbed } from "@/utils/twitch" | ||||||
| import type { GuildTwitch } from "@/types/schemas" | import type { GuildTwitch } from "@/types/schemas" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
|  | import { logConsole, logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export const data = new SlashCommandBuilder() | export const data = new SlashCommandBuilder() | ||||||
| 	.setName("twitch") | 	.setName("twitch") | ||||||
| @@ -120,8 +120,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { | |||||||
| 					if (user) streamers.push(`- ${user.displayName} (${streamer.twitchUserId})`) | 					if (user) streamers.push(`- ${user.displayName} (${streamer.twitchUserId})`) | ||||||
| 					else streamers.push(`- ${t(interaction.locale, "twitch.user_not_found_id", { id: streamer.twitchUserId })}`) | 					else streamers.push(`- ${t(interaction.locale, "twitch.user_not_found_id", { id: streamer.twitchUserId })}`) | ||||||
| 				} catch (error) { | 				} catch (error) { | ||||||
| 					console.log(chalk.magenta(`[Twitch] Error fetching user for ID ${streamer.twitchUserId}`)) | 					logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as Error) | ||||||
| 					console.error(error) |  | ||||||
| 				} | 				} | ||||||
| 			})) | 			})) | ||||||
| 			const streamerList = streamers.length > 0 ? streamers.join("\n") : t(interaction.locale, "twitch.no_streamers") | 			const streamerList = streamers.length > 0 ? streamers.join("\n") : t(interaction.locale, "twitch.no_streamers") | ||||||
| @@ -167,7 +166,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { | |||||||
| 			if (!await dbGuild.exists({ "guildTwitch.streamers.twitchUserId": user.id })) { | 			if (!await dbGuild.exists({ "guildTwitch.streamers.twitchUserId": user.id })) { | ||||||
| 				const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(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() })) | 				await Promise.all(userSubs.data.map(async sub => { if (sub.transportMethod === "webhook" && (sub.type === "stream.online" || sub.type === "stream.offline")) await sub.unsubscribe() })) | ||||||
| 				console.log(chalk.magenta(`[Twitch] Listener removed for ${user.displayName} (ID ${user.id})`)) | 				logConsole('twitch', 'listener_removed', { name: user.displayName, id: user.id }) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			return interaction.reply({ content: t(interaction.locale, "twitch.streamer_removed", { username }), flags: MessageFlags.Ephemeral }) | 			return interaction.reply({ content: t(interaction.locale, "twitch.streamer_removed", { username }), flags: MessageFlags.Ephemeral }) | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import type { TrackSearchResult } from "@/types/player" | |||||||
| import type { GuildPlayer } from "@/types/schemas" | import type { GuildPlayer } from "@/types/schemas" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
|  | import { logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| export const data = new SlashCommandBuilder() | export const data = new SlashCommandBuilder() | ||||||
| 	.setName("play") | 	.setName("play") | ||||||
| @@ -55,7 +56,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	try { if (!queue.connection) await queue.connect(voiceChannel) } | 	try { if (!queue.connection) await queue.connect(voiceChannel) } | ||||||
| 	catch (error) { console.error(error) } | 	catch (error) { logConsoleError('discord_player', 'play.connect_error', {}, error as Error) } | ||||||
|  |  | ||||||
| 	const guildProfile = await dbGuild.findOne({ guildId: queue.guild.id }) | 	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 }) | 	if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral }) | ||||||
| @@ -82,6 +83,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { | |||||||
| 	const result = await player.search(query, { requestedBy: interaction.user }) | 	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 }) | 	if (!result.hasTracks()) return interaction.followUp({ content: t(interaction.locale, "player.no_track_found", { query }), flags: MessageFlags.Ephemeral }) | ||||||
| 	const track = result.tracks[0] | 	const track = result.tracks[0] | ||||||
|  | 	if (process.env.NODE_ENV === "development") console.log(query, result, track) | ||||||
|  |  | ||||||
| 	const entry = queue.tasksQueue.acquire() | 	const entry = queue.tasksQueue.acquire() | ||||||
| 	await entry.getTask() | 	await entry.getTask() | ||||||
| @@ -93,7 +95,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { | |||||||
| 		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") | 		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 })) | 		return await interaction.followUp(t(interaction.locale, "player.loading_track", { title: track.title, author: track.author, source: track_source })) | ||||||
| 	} | 	} | ||||||
| 	catch (error) { console.error(error) } | 	catch (error) { logConsoleError('discord_player', 'play.execution_error', {}, error as Error) } | ||||||
| 	finally { queue.tasksQueue.release() } | 	finally { queue.tasksQueue.release() } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -105,6 +107,7 @@ export async function autocompleteRun(interaction: AutocompleteInteraction) { | |||||||
|  |  | ||||||
| 	const resultsSpotify = await player.search(query, { searchEngine: `ext:${SpotifyExtractor.identifier}` }) | 	const resultsSpotify = await player.search(query, { searchEngine: `ext:${SpotifyExtractor.identifier}` }) | ||||||
| 	const resultsYouTube = await player.search(query, { searchEngine: `ext:${YoutubeiExtractor.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 => ({ | 	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})`}`, | 		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})`}`, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Events, EmbedBuilder, ChannelType } from "discord.js" | import { Events, EmbedBuilder, ChannelType } from "discord.js" | ||||||
| import type { GuildMember } from "discord.js" | import type { GuildMember } from "discord.js" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
| import { logConsole } from "@/utils/console" | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export const name = Events.GuildMemberAdd | export const name = Events.GuildMemberAdd | ||||||
| @@ -30,10 +30,11 @@ export async function execute(member: GuildMember) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		const guildLocale = await getGuildLocale(guild.id) | ||||||
| 		const embed = new EmbedBuilder() | 		const embed = new EmbedBuilder() | ||||||
| 			.setColor(guild.members.me.displayHexColor) | 			.setColor(guild.members.me.displayHexColor) | ||||||
| 			.setTitle(t(guild.preferredLocale, "welcome.title", { username: member.user.username })) | 			.setTitle(t(guildLocale, "welcome.title", { username: member.user.username })) | ||||||
| 			.setDescription(t(guild.preferredLocale, "welcome.description", { memberCount: guild.memberCount.toString() })) | 			.setDescription(t(guildLocale, "welcome.description", { memberCount: guild.memberCount.toString() })) | ||||||
| 			.setThumbnail(member.user.avatarURL()) | 			.setThumbnail(member.user.avatarURL()) | ||||||
| 			.setTimestamp(new Date()) | 			.setTimestamp(new Date()) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Events } from "discord.js" | import { Events } from "discord.js" | ||||||
| import type { GuildMember } from "discord.js" | import type { GuildMember } from "discord.js" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = Events.GuildMemberRemove | export const name = Events.GuildMemberRemove | ||||||
| export function execute(member: GuildMember) { | export function execute(member: GuildMember) { | ||||||
| @@ -15,8 +15,9 @@ export function execute(member: GuildMember) { | |||||||
| 			const channel = guild.channels.cache.get("1091140609139560508") | 			const channel = guild.channels.cache.get("1091140609139560508") | ||||||
| 			if (!channel) return | 			if (!channel) return | ||||||
|  |  | ||||||
| 			await channel.setName(t(guild.preferredLocale, "salonpostam.update.loading")) | 			const guildLocale = await getGuildLocale(guild.id) | ||||||
| 			await channel.setName(t(guild.preferredLocale, "salonpostam.update.members_updated", { count: i.toString() })) | 			await channel.setName(t(guildLocale, "salonpostam.update.loading")) | ||||||
|  | 			await channel.setName(t(guildLocale, "salonpostam.update.members_updated", { count: i.toString() })) | ||||||
| 		}).catch(console.error) | 		}).catch(console.error) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Events, EmbedBuilder, ChannelType } from "discord.js" | import { Events, EmbedBuilder, ChannelType } from "discord.js" | ||||||
| import type { GuildMember } from "discord.js" | import type { GuildMember } from "discord.js" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
| import { logConsole } from "@/utils/console" | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| export const name = Events.GuildMemberUpdate | export const name = Events.GuildMemberUpdate | ||||||
| @@ -24,10 +24,11 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) { | |||||||
| 		if (!hadRole && hasRole) { | 		if (!hadRole && hasRole) { | ||||||
| 			if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return } | 			if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return } | ||||||
|  |  | ||||||
|  | 			const guildLocale = await getGuildLocale(guild.id) | ||||||
| 			const embed = new EmbedBuilder() | 			const embed = new EmbedBuilder() | ||||||
| 				.setColor(guild.members.me.displayHexColor) | 				.setColor(guild.members.me.displayHexColor) | ||||||
| 				.setTitle(t(guild.preferredLocale, "boost.new_boost_title", { username: newMember.user.username })) | 				.setTitle(t(guildLocale, "boost.new_boost_title", { username: newMember.user.username })) | ||||||
| 				.setDescription(t(guild.preferredLocale, "boost.new_boost_description", { count: guild.premiumSubscriptionCount?.toString() ?? "0" })) | 				.setDescription(t(guildLocale, "boost.new_boost_description", { count: guild.premiumSubscriptionCount?.toString() ?? "0" })) | ||||||
| 				.setThumbnail(newMember.user.avatarURL()) | 				.setThumbnail(newMember.user.avatarURL()) | ||||||
| 				.setTimestamp(new Date()) | 				.setTimestamp(new Date()) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import { connect } from "mongoose" | |||||||
| import type { Document } from "mongoose" | import type { Document } from "mongoose" | ||||||
| import { playerDisco, playerReplay } from "@/utils/player" | import { playerDisco, playerReplay } from "@/utils/player" | ||||||
| import { twitchClient, listener, onlineSub, offlineSub, startStreamWatching } from "@/utils/twitch" | import { twitchClient, listener, onlineSub, offlineSub, startStreamWatching } from "@/utils/twitch" | ||||||
| import { logConsole } from "@/utils/console" | import { logConsole, logConsoleError } from "@/utils/console" | ||||||
| import type { GuildPlayer, Disco, GuildTwitch, GuildFbx } from "@/types/schemas" | import type { GuildPlayer, Disco, GuildTwitch, GuildFbx } from "@/types/schemas" | ||||||
| import * as Freebox from "@/utils/freebox" | import * as Freebox from "@/utils/freebox" | ||||||
| import dbGuildInit from "@/utils/dbGuildInit" | import dbGuildInit from "@/utils/dbGuildInit" | ||||||
| @@ -19,13 +19,14 @@ export async function execute(client: Client) { | |||||||
| 	logConsole('discordjs', 'ready', { tag: client.user?.tag ?? "unknown" }) | 	logConsole('discordjs', 'ready', { tag: client.user?.tag ?? "unknown" }) | ||||||
| 	client.user?.setActivity("some bangers...", { type: ActivityType.Listening }) | 	client.user?.setActivity("some bangers...", { type: ActivityType.Listening }) | ||||||
|  |  | ||||||
| 	await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Spotify' }) }).catch(console.error) | 	const player = useMainPlayer() | ||||||
| 	await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Youtube' }) }).catch(console.error) | 	await player.extractors.register(SpotifyExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Spotify' }) }).catch(console.error) | ||||||
|  | 	await player.extractors.register(YoutubeiExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Youtube' }) }).catch(console.error) | ||||||
|  | 	if (process.env.NODE_ENV === "development") console.log(player.scanDeps()) | ||||||
|  |  | ||||||
| 	const mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}` | 	const mongo_url = `mongodb://${process.env.MONGOOSE_USER}:${process.env.MONGOOSE_PASSWORD}@${process.env.MONGOOSE_HOST}/${process.env.MONGOOSE_DATABASE}` | ||||||
| 	await connect(mongo_url).catch(console.error) | 	await connect(mongo_url).catch(console.error) | ||||||
|  |  | ||||||
| 	if (process.env.NODE_ENV === "development") await twitchClient.eventSub.deleteAllSubscriptions() |  | ||||||
| 	const streamerIds: string[] = [] | 	const streamerIds: string[] = [] | ||||||
|  |  | ||||||
| 	await Promise.all(client.guilds.cache.map(async guild => { | 	await Promise.all(client.guilds.cache.map(async guild => { | ||||||
| @@ -69,6 +70,10 @@ export async function execute(client: Client) { | |||||||
| 			if (!user) { logConsole('twitch', 'ready.user_not_found', { guild: guild.name, userId: streamer.twitchUserId }); return } | 			if (!user) { logConsole('twitch', 'ready.user_not_found', { guild: guild.name, userId: streamer.twitchUserId }); return } | ||||||
|  |  | ||||||
| 			const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(streamer.twitchUserId) | 			const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(streamer.twitchUserId) | ||||||
|  | 			if (process.env.NODE_ENV === "development") { | ||||||
|  | 				console.log(userSubs) | ||||||
|  | 				userSubs.data.forEach(sub => { console.log(sub) }) | ||||||
|  | 			} | ||||||
| 			if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) { | 			if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) { | ||||||
| 				// eslint-disable-next-line @typescript-eslint/no-misused-promises | 				// eslint-disable-next-line @typescript-eslint/no-misused-promises | ||||||
| 				listener.onStreamOnline(streamer.twitchUserId, onlineSub) | 				listener.onStreamOnline(streamer.twitchUserId, onlineSub) | ||||||
| @@ -95,8 +100,7 @@ export async function execute(client: Client) { | |||||||
| 						startStreamWatching(guild.id, streamer.twitchUserId, user.name, streamer.messageId) | 						startStreamWatching(guild.id, streamer.twitchUserId, user.name, streamer.messageId) | ||||||
| 						logConsole('twitch', 'ready.monitoring_restored', { guild: guild.name, userName: user.name }) | 						logConsole('twitch', 'ready.monitoring_restored', { guild: guild.name, userName: user.name }) | ||||||
| 					} catch (error) { | 					} catch (error) { | ||||||
| 						logConsole('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name }) | 						logConsoleError('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name }, error as Error) | ||||||
| 						console.error(error) |  | ||||||
| 						await cleanupMessageId(guildProfile, streamer.twitchUserId) | 						await cleanupMessageId(guildProfile, streamer.twitchUserId) | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| @@ -105,8 +109,6 @@ export async function execute(client: Client) { | |||||||
| 				logConsole('twitch', 'ready.stream_offline_cleanup', { guild: guild.name, userName: user.name }) | 				logConsole('twitch', 'ready.stream_offline_cleanup', { guild: guild.name, userName: user.name }) | ||||||
| 				await cleanupMessageId(guildProfile, streamer.twitchUserId) | 				await cleanupMessageId(guildProfile, streamer.twitchUserId) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			logConsole('twitch', 'user_operational', { name: user.name, id: streamer.twitchUserId }) |  | ||||||
| 		})) | 		})) | ||||||
| 	})) | 	})) | ||||||
|  |  | ||||||
| @@ -131,9 +133,8 @@ async function cleanupMessageId(guildProfile: Document, twitchUserId: string) { | |||||||
| 				 | 				 | ||||||
| 		guildProfile.set("guildTwitch", dbData) | 		guildProfile.set("guildTwitch", dbData) | ||||||
| 		guildProfile.markModified("guildTwitch") | 		guildProfile.markModified("guildTwitch") | ||||||
| 		await guildProfile.save() | 		await guildProfile.save().catch(console.error) | ||||||
| 	} catch (error) { | 	} catch (error) { | ||||||
| 		logConsole('twitch', 'ready.cleanup_error', { userId: twitchUserId }) | 		logConsoleError('twitch', 'ready.cleanup_error', { userId: twitchUserId }, error as Error) | ||||||
| 		console.error(error) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,14 @@ | |||||||
| import type { GuildQueue, Track } from "discord-player" | import type { GuildQueue, Track } from "discord-player" | ||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = "audioTrackAdd" | export const name = "audioTrackAdd" | ||||||
| export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) { | export async function 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 | 	if (!queue.metadata.channel) return | ||||||
| 	 | 	 | ||||||
| 	if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added", { title: track.title }) }) | 	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 }) }) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| import type { GuildQueue, Track } from "discord-player" | import type { GuildQueue, Track } from "discord-player" | ||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = "audioTracksAdd" | export const name = "audioTracksAdd" | ||||||
| export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track[]) { | export async function execute(queue: GuildQueue<PlayerMetadata>, track: 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 | 	if (!queue.metadata.channel) return | ||||||
| 	if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added_playlist", { count: track.length.toString() }) }) |  | ||||||
|  | 	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() }) }) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import type { GuildQueue } from "discord-player" | import type { GuildQueue } from "discord-player" | ||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import { stopProgressSaving } from "@/utils/player" | import { stopProgressSaving } from "@/utils/player" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = "disconnect" | export const name = "disconnect" | ||||||
| export async function execute(queue: GuildQueue<PlayerMetadata>) { | export async function execute(queue: GuildQueue<PlayerMetadata>) { | ||||||
| @@ -9,5 +9,9 @@ export async function execute(queue: GuildQueue<PlayerMetadata>) { | |||||||
| 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | ||||||
|  |  | ||||||
| 	if (!queue.metadata.channel) return | 	if (!queue.metadata.channel) return | ||||||
| 	if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.disconnect") }) |  | ||||||
|  | 	if ("send" in queue.metadata.channel) { | ||||||
|  | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
|  | 		return queue.metadata.channel.send({ content: t(guildLocale, "player.disconnect") }) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import type { GuildQueue } from "discord-player" | import type { GuildQueue } from "discord-player" | ||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import { stopProgressSaving } from "@/utils/player" | import { stopProgressSaving } from "@/utils/player" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = "emptyChannel" | export const name = "emptyChannel" | ||||||
| export async function execute(queue: GuildQueue<PlayerMetadata>) { | export async function execute(queue: GuildQueue<PlayerMetadata>) { | ||||||
| @@ -10,5 +10,9 @@ export async function execute(queue: GuildQueue<PlayerMetadata>) { | |||||||
| 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | ||||||
|  |  | ||||||
| 	if (!queue.metadata.channel) return | 	if (!queue.metadata.channel) return | ||||||
| 	if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.leaving_empty_channel") }) |  | ||||||
|  | 	if ("send" in queue.metadata.channel) { | ||||||
|  | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
|  | 		return queue.metadata.channel.send({ content: t(guildLocale, "player.leaving_empty_channel") }) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import type { GuildQueue } from "discord-player" | import type { GuildQueue } from "discord-player" | ||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import { stopProgressSaving } from "@/utils/player" | import { stopProgressSaving } from "@/utils/player" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = "emptyQueue" | export const name = "emptyQueue" | ||||||
| export async function execute(queue: GuildQueue<PlayerMetadata>) { | export async function execute(queue: GuildQueue<PlayerMetadata>) { | ||||||
| @@ -9,5 +9,9 @@ export async function execute(queue: GuildQueue<PlayerMetadata>) { | |||||||
| 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | 	await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") | ||||||
| 	 | 	 | ||||||
| 	if (!queue.metadata.channel) return | 	if (!queue.metadata.channel) return | ||||||
| 	if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.queue_empty") }) |  | ||||||
|  | 	if ("send" in queue.metadata.channel) { | ||||||
|  | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
|  | 		return queue.metadata.channel.send({ content: t(guildLocale, "player.queue_empty") }) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,14 @@ | |||||||
| import type { GuildQueue, Track } from "discord-player" | import type { GuildQueue, Track } from "discord-player" | ||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = "playerSkip" | export const name = "playerSkip" | ||||||
| export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) { | export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) { | ||||||
| 	// Emitted when the audio player fails to load the stream for a song | 	// Emitted when the audio player fails to load the stream for a song | ||||||
| 	if (!queue.metadata.channel) return | 	if (!queue.metadata.channel) return | ||||||
| 	if ("send" in queue.metadata.channel) return queue.metadata.channel.send({  |  | ||||||
| 		content: t(queue.guild.preferredLocale, "player.track_skipped", { title: track.title, author: track.author })  | 	if ("send" in queue.metadata.channel) { | ||||||
| 	}) | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
|  | 		return queue.metadata.channel.send({ content: t(guildLocale, "player.track_skipped", { title: track.title, author: track.author }) }) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| import type { GuildQueue, Track } from "discord-player" | import type { GuildQueue, Track } from "discord-player" | ||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
|  |  | ||||||
| export const name = "playerStart" | export const name = "playerStart" | ||||||
| export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) { | export async function execute(queue: GuildQueue<PlayerMetadata>, track: Track) { | ||||||
| 	// Emitted when the player starts to play a song | 	// Emitted when the player starts to play a song | ||||||
| 	if (!queue.metadata.channel) return | 	if (!queue.metadata.channel) return | ||||||
| 	if ("send" in queue.metadata.channel) await queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.now_playing", { title: track.title, author: track.author }) }) |  | ||||||
|  | 	if ("send" in queue.metadata.channel) { | ||||||
|  | 		const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
|  | 		await queue.metadata.channel.send({ content: t(guildLocale, "player.now_playing", { title: track.title, author: track.author }) }) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -103,10 +103,6 @@ | |||||||
|       "description_enabled": "Disco mode is enabled! Visual and audio effects will be applied during music playback.", |       "description_enabled": "Disco mode is enabled! Visual and audio effects will be applied during music playback.", | ||||||
|       "description_disabled": "Disco mode is disabled. Enable it to enjoy visual and audio effects during music playback.", |       "description_disabled": "Disco mode is disabled. Enable it to enjoy visual and audio effects during music playback.", | ||||||
|       "channel_not_configured": "No channel configured", |       "channel_not_configured": "No channel configured", | ||||||
|       "channel_not_found": "Channel not found", |  | ||||||
|       "enabled": "✅ Enabled", |  | ||||||
|       "disabled": "❌ Disabled", |  | ||||||
|       "configure_channel": "Configure Channel", |  | ||||||
|       "configure_channel_first": "❌ Cannot enable Disco mode! Please first configure a channel with the **Configure Channel** button.", |       "configure_channel_first": "❌ Cannot enable Disco mode! Please first configure a channel with the **Configure Channel** button.", | ||||||
|       "effects_applied": "Disco effects will be applied in {channel}.", |       "effects_applied": "Disco effects will be applied in {channel}.", | ||||||
|       "select_channel": "Please select the channel where to apply Disco effects:", |       "select_channel": "Please select the channel where to apply Disco effects:", | ||||||
| @@ -187,11 +183,6 @@ | |||||||
|       "message_not_found": "Message not found for {userName}, cleaning up messageId", |       "message_not_found": "Message not found for {userName}, cleaning up messageId", | ||||||
|       "stream_offline_cleanup": "Offline stream detected for {userName}, cleaning up messageId", |       "stream_offline_cleanup": "Offline stream detected for {userName}, cleaning up messageId", | ||||||
|       "cleanup_error": "Error while cleaning up messageId for {userId}" |       "cleanup_error": "Error while cleaning up messageId for {userId}" | ||||||
|     }, |  | ||||||
|     "logs": { |  | ||||||
|       "user_fetch_error": "Error while fetching user for ID {userId}", |  | ||||||
|       "listener_removed": "Listener removed for {streamerName} (ID {userId})", |  | ||||||
|       "listener_removal_error": "Error while removing listener for {streamerName}" |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "amp": { |   "amp": { | ||||||
| @@ -422,6 +413,9 @@ | |||||||
|       "select_streamer_remove": "Select a streamer to remove" |       "select_streamer_remove": "Select a streamer to remove" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |   "locale": { | ||||||
|  |     "updated": "✅ Server language updated from **{oldLanguage}** to **{newLanguage}**!" | ||||||
|  |   }, | ||||||
|   "database": { |   "database": { | ||||||
|     "owner_only": "This command can only be used by the bot owner!", |     "owner_only": "This command can only be used by the bot owner!", | ||||||
|     "server_only": "This command must be used in a server!", |     "server_only": "This command must be used in a server!", | ||||||
| @@ -477,7 +471,8 @@ | |||||||
|       "debug": "[Discord-Player] Debug - Player debug event: {message}", |       "debug": "[Discord-Player] Debug - Player debug event: {message}", | ||||||
|       "disco": { |       "disco": { | ||||||
|         "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Channel is not configured!", |         "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Channel is not configured!", | ||||||
|         "channel_not_found": "[Discord-Player] PlayerDisco - {guild} No channel found with id {channelId}" |         "channel_not_found": "[Discord-Player] PlayerDisco - {guild} No channel found with id {channelId}", | ||||||
|  |         "general_error": "[Discord-Player] Disco - General disco module error" | ||||||
|       }, |       }, | ||||||
|       "progress_saving": { |       "progress_saving": { | ||||||
|         "missing_ids": "[Discord-Player] ProgressSaving - GuildId or BotId is missing!", |         "missing_ids": "[Discord-Player] ProgressSaving - GuildId or BotId is missing!", | ||||||
| @@ -485,6 +480,14 @@ | |||||||
|         "stop": "[Discord-Player] ProgressSaving - Stopping save for server {guildId} (bot {botId})", |         "stop": "[Discord-Player] ProgressSaving - Stopping save for server {guildId} (bot {botId})", | ||||||
|         "error": "[Discord-Player] ProgressSaving - Error saving progress for guild {guildId} (bot {botId})", |         "error": "[Discord-Player] ProgressSaving - Error saving progress for guild {guildId} (bot {botId})", | ||||||
|         "database_not_exist": "[Discord-Player] ProgressSaving - Database data does not exist!" |         "database_not_exist": "[Discord-Player] ProgressSaving - Database data does not exist!" | ||||||
|  |       }, | ||||||
|  |       "replay": { | ||||||
|  |         "connect_error": "[Discord-Player] Replay - Error connecting to voice channel", | ||||||
|  |         "play_error": "[Discord-Player] Replay - Error playing track" | ||||||
|  |       }, | ||||||
|  |       "play": { | ||||||
|  |         "connect_error": "[Discord-Player] Play - Error connecting to voice channel", | ||||||
|  |         "execution_error": "[Discord-Player] Play - Error executing track" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "mongoose": { |     "mongoose": { | ||||||
| @@ -494,7 +497,8 @@ | |||||||
|       "error": "[Mongoose] An error occurred with the database connection: {message}", |       "error": "[Mongoose] An error occurred with the database connection: {message}", | ||||||
|       "event_triggered": "[Mongoose] Event {event} triggered", |       "event_triggered": "[Mongoose] Event {event} triggered", | ||||||
|       "guild_init": "[Mongoose] Initializing guild profile for {name} ({id})", |       "guild_init": "[Mongoose] Initializing guild profile for {name} ({id})", | ||||||
|       "guild_create": "[Mongoose] GuildCreate - Database data for new guild \"{name}\" successfully initialized!" |       "guild_create": "[Mongoose] GuildCreate - Database data for new guild \"{name}\" successfully initialized!", | ||||||
|  |       "locale_fetch_error": "[Mongoose] Error fetching guild locale for {guildId}" | ||||||
|     }, |     }, | ||||||
|     "twitch": { |     "twitch": { | ||||||
|       "starting_listener": "[Twitch] Starting listener with {adapter}...", |       "starting_listener": "[Twitch] Starting listener with {adapter}...", | ||||||
| @@ -525,13 +529,29 @@ | |||||||
|       "stream_data_not_found": "[Twitch] StreamWatching - {guild} Stream data not found for {streamer} (ID {id})", |       "stream_data_not_found": "[Twitch] StreamWatching - {guild} Stream data not found for {streamer} (ID {id})", | ||||||
|       "message_id_not_found": "[Twitch] StreamWatching - {guild} Message ID not found for {streamer} (ID {id})", |       "message_id_not_found": "[Twitch] StreamWatching - {guild} Message ID not found for {streamer} (ID {id})", | ||||||
|       "user_fetch_error": "[Twitch] Error fetching user for ID {id}", |       "user_fetch_error": "[Twitch] Error fetching user for ID {id}", | ||||||
|       "user_fetch_error_detailed": "[Twitch] Error while fetching user for ID {id}", |  | ||||||
|       "starting_listener_ngrok": "[Twitch] Starting listener with ngrok...", |       "starting_listener_ngrok": "[Twitch] Starting listener with ngrok...", | ||||||
|       "user_fetch_error_buttons": "[Twitch] Error fetching user for ID {id} in buttons/selectmenu", |       "listener_removal_error": "[Twitch] Error removing listener for {streamerName}", | ||||||
|       "listener_removal_error": "[Twitch] Error removing listener for {streamerName}" |       "missing_credentials": "[Twitch] Missing TWITCH_APP_ID or TWITCH_APP_SECRET in environment variables!", | ||||||
|  |       "starting_listener_port": "[Twitch] Starting listener with port {port}...", | ||||||
|  |       "streamer_already_processing": "[Twitch] StreamWatching - {{{guildName}}} Streamer {broadcasterName} already being processed, skipping", | ||||||
|  |       "stop_watching_error": "[Twitch] Error stopping watching for {streamer} (ID {id}) on {guildId}" | ||||||
|     }, |     }, | ||||||
|     "freebox": { |     "freebox": { | ||||||
|       "lcd_timer_restored": "Timers restored successfully for {guild}!" |       "lcd_timer_restored": "Timers restored successfully for {guild}!", | ||||||
|  |       "authorization_pending": "[Freebox] Authorization pending...", | ||||||
|  |       "timer_scheduled": "[Freebox] Timer scheduled for {guildId} - Turn on: {nextMorning}, Turn off: {nextNight}", | ||||||
|  |       "timers_cleaned": "[Freebox] Timers cleaned for {guildId}", | ||||||
|  |       "all_timers_cleaned": "[Freebox] All timers have been cleaned", | ||||||
|  |       "missing_configuration": "[Freebox] Missing configuration for server {guildId}", | ||||||
|  |       "challenge_error": "[Freebox] Error retrieving challenge for {guildId}", | ||||||
|  |       "challenge_not_found": "[Freebox] Challenge not found for {guildId}", | ||||||
|  |       "session_error": "[Freebox] Error creating session for {guildId}", | ||||||
|  |       "session_token_not_found": "[Freebox] Session token not found for {guildId}", | ||||||
|  |       "leds_control_error": "[Freebox] Error controlling LEDs for {guildId}", | ||||||
|  |       "leds_success": "[Freebox] LEDs {status} successfully for {guildId}", | ||||||
|  |       "leds_error": "[Freebox] Error controlling LEDs for {guildId}", | ||||||
|  |       "lcd_status_error": "[Freebox] Error retrieving LCD status", | ||||||
|  |       "test_connection_error": "[Freebox] Error testing connection" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -183,11 +183,6 @@ | |||||||
|       "message_not_found": "Message introuvable pour {userName}, nettoyage du messageId", |       "message_not_found": "Message introuvable pour {userName}, nettoyage du messageId", | ||||||
|       "stream_offline_cleanup": "Stream hors ligne détecté pour {userName}, nettoyage du messageId", |       "stream_offline_cleanup": "Stream hors ligne détecté pour {userName}, nettoyage du messageId", | ||||||
|       "cleanup_error": "Erreur lors du nettoyage du messageId pour {userId}" |       "cleanup_error": "Erreur lors du nettoyage du messageId pour {userId}" | ||||||
|     }, |  | ||||||
|     "logs": { |  | ||||||
|       "user_fetch_error": "Erreur lors de la récupération de l'utilisateur pour l'ID {userId}", |  | ||||||
|       "listener_removed": "Listener supprimé pour {streamerName} (ID {userId})", |  | ||||||
|       "listener_removal_error": "Erreur lors de la suppression du listener pour {streamerName}" |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "amp": { |   "amp": { | ||||||
| @@ -418,6 +413,9 @@ | |||||||
|       "select_streamer_remove": "Sélectionner un streamer à supprimer" |       "select_streamer_remove": "Sélectionner un streamer à supprimer" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |   "locale": { | ||||||
|  |     "updated": "✅ Langue du serveur mise à jour de **{oldLanguage}** vers **{newLanguage}** !" | ||||||
|  |   }, | ||||||
|   "database": { |   "database": { | ||||||
|     "owner_only": "Cette commande ne peut être utilisée que par le propriétaire du bot !", |     "owner_only": "Cette commande ne peut être utilisée que par le propriétaire du bot !", | ||||||
|     "server_only": "Cette commande doit être utilisée sur un serveur !", |     "server_only": "Cette commande doit être utilisée sur un serveur !", | ||||||
| @@ -448,8 +446,7 @@ | |||||||
|         "button_error": "[DiscordJS] InteractionCreate - Erreur lors du clic sur {id}", |         "button_error": "[DiscordJS] InteractionCreate - Erreur lors du clic sur {id}", | ||||||
|         "selectmenu_not_found": "[DiscordJS] InteractionCreate - Aucun SelectMenu avec l'id {id} trouvé.", |         "selectmenu_not_found": "[DiscordJS] InteractionCreate - Aucun SelectMenu avec l'id {id} trouvé.", | ||||||
|         "selectmenu_used": "[DiscordJS] InteractionCreate - SelectMenu '{id}' utilisé par {user}", |         "selectmenu_used": "[DiscordJS] InteractionCreate - SelectMenu '{id}' utilisé par {user}", | ||||||
|         "selectmenu_error": "[DiscordJS] InteractionCreate - Erreur lors de l'utilisation de {id}", |         "selectmenu_error": "[DiscordJS] InteractionCreate - Erreur lors de l'utilisation de {id}" | ||||||
|         "selectmenu_invalid_type": "[DiscordJS] InteractionCreate - Type de SelectMenu invalide pour {id} reçu '{type}'" |  | ||||||
|       }, |       }, | ||||||
|       "error": "[DiscordJS] Error - Une erreur s'est produite : {message}", |       "error": "[DiscordJS] Error - Une erreur s'est produite : {message}", | ||||||
|       "boost": { |       "boost": { | ||||||
| @@ -474,7 +471,8 @@ | |||||||
|       "debug": "[Discord-Player] Debug - Événement de débogage du lecteur : {message}", |       "debug": "[Discord-Player] Debug - Événement de débogage du lecteur : {message}", | ||||||
|       "disco": { |       "disco": { | ||||||
|         "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Le canal n'est pas configuré !", |         "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Le canal n'est pas configuré !", | ||||||
|         "channel_not_found": "[Discord-Player] PlayerDisco - {guild} Aucun canal trouvé avec l'id {channelId}" |         "channel_not_found": "[Discord-Player] PlayerDisco - {guild} Aucun canal trouvé avec l'id {channelId}", | ||||||
|  |         "general_error": "[Discord-Player] Disco - Erreur générale du module Disco" | ||||||
|       }, |       }, | ||||||
|       "progress_saving": { |       "progress_saving": { | ||||||
|         "missing_ids": "[Discord-Player] ProgressSaving - GuildId ou BotId manquant !", |         "missing_ids": "[Discord-Player] ProgressSaving - GuildId ou BotId manquant !", | ||||||
| @@ -482,6 +480,14 @@ | |||||||
|         "stop": "[Discord-Player] ProgressSaving - Arrêt de la sauvegarde pour le serveur {guildId} (bot {botId})", |         "stop": "[Discord-Player] ProgressSaving - Arrêt de la sauvegarde pour le serveur {guildId} (bot {botId})", | ||||||
|         "error": "[Discord-Player] ProgressSaving - Erreur lors de la sauvegarde pour le serveur {guildId} (bot {botId})", |         "error": "[Discord-Player] ProgressSaving - Erreur lors de la sauvegarde pour le serveur {guildId} (bot {botId})", | ||||||
|         "database_not_exist": "[Discord-Player] ProgressSaving - Les données de base n'existent pas !" |         "database_not_exist": "[Discord-Player] ProgressSaving - Les données de base n'existent pas !" | ||||||
|  |       }, | ||||||
|  |       "replay": { | ||||||
|  |         "connect_error": "[Discord-Player] Replay - Erreur lors de la connexion au canal vocal", | ||||||
|  |         "play_error": "[Discord-Player] Replay - Erreur lors de la lecture de la piste" | ||||||
|  |       }, | ||||||
|  |       "play": { | ||||||
|  |         "connect_error": "[Discord-Player] Play - Erreur lors de la connexion au canal vocal", | ||||||
|  |         "execution_error": "[Discord-Player] Play - Erreur lors de l'exécution de la piste" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "mongoose": { |     "mongoose": { | ||||||
| @@ -491,7 +497,8 @@ | |||||||
|       "error": "[Mongoose] Une erreur s'est produite avec la connexion à la base de données : {message}", |       "error": "[Mongoose] Une erreur s'est produite avec la connexion à la base de données : {message}", | ||||||
|       "event_triggered": "[Mongoose] Événement {event} déclenché", |       "event_triggered": "[Mongoose] Événement {event} déclenché", | ||||||
|       "guild_init": "[Mongoose] Initialisation du profil de serveur pour {name} ({id})", |       "guild_init": "[Mongoose] Initialisation du profil de serveur pour {name} ({id})", | ||||||
|       "guild_create": "[Mongoose] GuildCreate - Données de base pour le nouveau serveur \"{name}\" initialisées avec succès !" |       "guild_create": "[Mongoose] GuildCreate - Données de base pour le nouveau serveur \"{name}\" initialisées avec succès !", | ||||||
|  |       "locale_fetch_error": "[Mongoose] Erreur lors de la récupération de la locale du serveur {guildId}" | ||||||
|     }, |     }, | ||||||
|     "twitch": { |     "twitch": { | ||||||
|       "starting_listener": "[Twitch] Démarrage du listener avec {adapter}...", |       "starting_listener": "[Twitch] Démarrage du listener avec {adapter}...", | ||||||
| @@ -522,13 +529,29 @@ | |||||||
|       "stream_data_not_found": "[Twitch] StreamWatching - {guild} Données de stream non trouvées pour {streamer} (ID {id})", |       "stream_data_not_found": "[Twitch] StreamWatching - {guild} Données de stream non trouvées pour {streamer} (ID {id})", | ||||||
|       "message_id_not_found": "[Twitch] StreamWatching - {guild} ID de message non trouvé pour {streamer} (ID {id})", |       "message_id_not_found": "[Twitch] StreamWatching - {guild} ID de message non trouvé pour {streamer} (ID {id})", | ||||||
|       "user_fetch_error": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id}", |       "user_fetch_error": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id}", | ||||||
|       "user_fetch_error_detailed": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id}", |  | ||||||
|       "starting_listener_ngrok": "[Twitch] Démarrage du listener avec ngrok...", |       "starting_listener_ngrok": "[Twitch] Démarrage du listener avec ngrok...", | ||||||
|       "user_fetch_error_buttons": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id} dans buttons/selectmenu", |       "listener_removal_error": "[Twitch] Erreur lors de la suppression du listener pour {streamerName}", | ||||||
|       "listener_removal_error": "[Twitch] Erreur lors de la suppression du listener pour {streamerName}" |       "missing_credentials": "[Twitch] TWITCH_APP_ID ou TWITCH_APP_SECRET manquant dans les variables d'environnement !", | ||||||
|  |       "starting_listener_port": "[Twitch] Démarrage du listener avec le port {port}...", | ||||||
|  |       "streamer_already_processing": "[Twitch] StreamWatching - {{{guildName}}} Streamer {broadcasterName} déjà en cours de traitement, ignoré", | ||||||
|  |       "stop_watching_error": "[Twitch] Erreur lors de l'arrêt du watching pour {streamer} (ID {id}) sur {guildId}" | ||||||
|     }, |     }, | ||||||
|     "freebox": { |     "freebox": { | ||||||
|       "lcd_timer_restored": "Minuteurs restaurés avec succès pour {guild} !" |       "lcd_timer_restored": "Minuteurs restaurés avec succès pour {guild} !", | ||||||
|  |       "authorization_pending": "[Freebox] Autorisation en attente...", | ||||||
|  |       "timer_scheduled": "[Freebox] Timer programmé pour {guildId} - Allumage: {nextMorning}, Extinction: {nextNight}", | ||||||
|  |       "timers_cleaned": "[Freebox] Timers nettoyés pour {guildId}", | ||||||
|  |       "all_timers_cleaned": "[Freebox] Tous les timers ont été nettoyés", | ||||||
|  |       "missing_configuration": "[Freebox] Configuration manquante pour le serveur {guildId}", | ||||||
|  |       "challenge_error": "[Freebox] Erreur lors de la récupération du challenge pour {guildId}", | ||||||
|  |       "challenge_not_found": "[Freebox] Challenge introuvable pour {guildId}", | ||||||
|  |       "session_error": "[Freebox] Erreur lors de la création de la session pour {guildId}", | ||||||
|  |       "session_token_not_found": "[Freebox] Token de session introuvable pour {guildId}", | ||||||
|  |       "leds_control_error": "[Freebox] Erreur lors du contrôle des LEDs pour {guildId}", | ||||||
|  |       "leds_success": "[Freebox] LEDs {status} avec succès pour {guildId}", | ||||||
|  |       "leds_error": "[Freebox] Erreur lors du contrôle des LEDs pour {guildId}", | ||||||
|  |       "lcd_status_error": "[Freebox] Erreur lors de la récupération de l'état LCD", | ||||||
|  |       "test_connection_error": "[Freebox] Erreur lors du test de connexion" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ const guildSchema = new Schema({ | |||||||
| 	guildId: { type: String, required: true }, | 	guildId: { type: String, required: true }, | ||||||
| 	guildName: { type: String, required: true }, | 	guildName: { type: String, required: true }, | ||||||
| 	guildIcon: { type: String, required: true }, | 	guildIcon: { type: String, required: true }, | ||||||
|  | 	guildLocale: { type: String, required: true }, | ||||||
| 	guildPlayer: { | 	guildPlayer: { | ||||||
| 		instances: [{ | 		instances: [{ | ||||||
| 			botId: { type: String, required: true }, | 			botId: { type: String, required: true }, | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ export async function execute(interaction: StringSelectMenuInteraction) { | |||||||
| 		const user = await twitchClient.users.getUserById(twitchUserId) | 		const user = await twitchClient.users.getUserById(twitchUserId) | ||||||
| 		if (user) streamerName = user.displayName | 		if (user) streamerName = user.displayName | ||||||
| 	} catch { | 	} catch { | ||||||
| 		logConsole('twitch', 'user_fetch_error_buttons', { id: twitchUserId }) | 		logConsole('twitch', 'user_fetch_error', { id: twitchUserId }) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Supprimer le streamer | 	// Supprimer le streamer | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ export interface GuildSchema { | |||||||
| 	guildId: string | 	guildId: string | ||||||
| 	guildName: string | 	guildName: string | ||||||
| 	guildIcon: string | 	guildIcon: string | ||||||
|  | 	guildLocale: string | ||||||
| 	guildPlayer: GuildPlayer | 	guildPlayer: GuildPlayer | ||||||
| 	guildAmp: GuildAmp | 	guildAmp: GuildAmp | ||||||
| 	guildFbx: GuildFbx | 	guildFbx: GuildFbx | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ export default async (guild: Guild) => { | |||||||
| 		guildId: guild.id, | 		guildId: guild.id, | ||||||
| 		guildName: guild.name, | 		guildName: guild.name, | ||||||
| 		guildIcon: guild.iconURL() ?? "None", | 		guildIcon: guild.iconURL() ?? "None", | ||||||
|  | 		guildLocale: 'fr', | ||||||
| 		guildPlayer: { | 		guildPlayer: { | ||||||
| 			disco: { enabled: false } | 			disco: { enabled: false } | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import type { | |||||||
| } from "@/types/freebox" | } from "@/types/freebox" | ||||||
| import type { GuildFbx } from "@/types/schemas" | import type { GuildFbx } from "@/types/schemas" | ||||||
| import { t } from "@/utils/i18n" | import { t } from "@/utils/i18n" | ||||||
|  | import { logConsole } from "@/utils/console" | ||||||
|  |  | ||||||
| const app: TokenRequest = { | const app: TokenRequest = { | ||||||
| 	app_id: "fr.angels-dev.tamiseur", | 	app_id: "fr.angels-dev.tamiseur", | ||||||
| @@ -123,7 +124,7 @@ export const Timer = { | |||||||
| 		// Stocker les références des timers | 		// Stocker les références des timers | ||||||
| 		activeTimers.set(guildId, { morning: morningTimer, night: nightTimer }) | 		activeTimers.set(guildId, { morning: morningTimer, night: nightTimer }) | ||||||
|  |  | ||||||
| 		console.log(`[Freebox LCD] Timer programmé pour ${guildId} - Allumage: ${nextMorning.toLocaleString()}, Extinction: ${nextNight.toLocaleString()}`) | 		logConsole('freebox', 'timer_scheduled', { guildId, nextMorning: nextMorning.toLocaleString(), nextNight: nextNight.toLocaleString() }) | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	// Fonction utilitaire pour calculer la prochaine occurrence d'une heure donnée | 	// Fonction utilitaire pour calculer la prochaine occurrence d'une heure donnée | ||||||
| @@ -131,10 +132,7 @@ export const Timer = { | |||||||
| 		const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0) | 		const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0) | ||||||
| 		 | 		 | ||||||
| 		// Si l'heure cible est déjà passée aujourd'hui, programmer pour demain | 		// Si l'heure cible est déjà passée aujourd'hui, programmer pour demain | ||||||
| 		if (target <= now) { | 		if (target <= now) target.setDate(target.getDate() + 1) | ||||||
| 			target.setDate(target.getDate() + 1) |  | ||||||
| 		} |  | ||||||
| 		 |  | ||||||
| 		return target | 		return target | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -145,7 +143,7 @@ export const Timer = { | |||||||
| 			if (timers.morning) clearTimeout(timers.morning) | 			if (timers.morning) clearTimeout(timers.morning) | ||||||
| 			if (timers.night) clearTimeout(timers.night) | 			if (timers.night) clearTimeout(timers.night) | ||||||
| 			activeTimers.delete(guildId) | 			activeTimers.delete(guildId) | ||||||
| 			console.log(`[Freebox LCD] Timers nettoyés pour ${guildId}`) | 			logConsole('freebox', 'timers_cleaned', { guildId }) | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -154,39 +152,36 @@ export const Timer = { | |||||||
| 		for (const [guildId] of activeTimers) { | 		for (const [guildId] of activeTimers) { | ||||||
| 			Timer.clear(guildId) | 			Timer.clear(guildId) | ||||||
| 		} | 		} | ||||||
| 		console.log(`[Freebox LCD] Tous les timers ont été nettoyés`) | 		logConsole('freebox', 'all_timers_cleaned') | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	// Fonction pour contrôler les LEDs | 	// Fonction pour contrôler les LEDs | ||||||
| 	async controlLeds(guildId: string, dbDataFbx: GuildFbx, enabled: boolean) { | 	async controlLeds(guildId: string, dbDataFbx: GuildFbx, enabled: boolean) { | ||||||
| 		if (!dbDataFbx.host || !dbDataFbx.appToken) { | 		if (!dbDataFbx.host || !dbDataFbx.appToken) { logConsole('freebox', 'missing_configuration', { guildId }); return } | ||||||
| 			console.error(`[Freebox LCD] Configuration manquante pour le serveur ${guildId}`) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		try { | 		try { | ||||||
| 			// Obtenir le challenge | 			// Obtenir le challenge | ||||||
| 			const challengeData = await Login.Challenge(dbDataFbx.host) as APIResponseData<GetChallenge> | 			const challengeData = await Login.Challenge(dbDataFbx.host) as APIResponseData<GetChallenge> | ||||||
| 			if (!challengeData.success) { console.error(`[Freebox LCD] Erreur lors de la récupération du challenge pour ${guildId}`); return } | 			if (!challengeData.success) { logConsole('freebox', 'challenge_error', { guildId }); return } | ||||||
|  |  | ||||||
| 			const challenge = challengeData.result.challenge | 			const challenge = challengeData.result.challenge | ||||||
| 			if (!challenge) { console.error(`[Freebox LCD] Challenge introuvable pour ${guildId}`); return } | 			if (!challenge) { logConsole('freebox', 'challenge_not_found', { guildId }); return } | ||||||
|  |  | ||||||
| 			// Créer la session | 			// Créer la session | ||||||
| 			const password = crypto.createHmac("sha1", dbDataFbx.appToken).update(challenge).digest("hex") | 			const password = crypto.createHmac("sha1", dbDataFbx.appToken).update(challenge).digest("hex") | ||||||
| 			const sessionData = await Login.Session(dbDataFbx.host, password) as APIResponseData<OpenSession> | 			const sessionData = await Login.Session(dbDataFbx.host, password) as APIResponseData<OpenSession> | ||||||
| 			if (!sessionData.success) { console.error(`[Freebox LCD] Erreur lors de la création de la session pour ${guildId}`); return } | 			if (!sessionData.success) { logConsole('freebox', 'session_error', { guildId }); return } | ||||||
|  |  | ||||||
| 			const sessionToken = sessionData.result.session_token | 			const sessionToken = sessionData.result.session_token | ||||||
| 			if (!sessionToken) { console.error(`[Freebox LCD] Token de session introuvable pour ${guildId}`); return } | 			if (!sessionToken) { logConsole('freebox', 'session_token_not_found', { guildId }); return } | ||||||
|  |  | ||||||
| 			// Contrôler les LEDs | 			// Contrôler les LEDs | ||||||
| 			const lcdData = await Set.LcdConfig(dbDataFbx.host, sessionToken, { led_strip_enabled: enabled }) as APIResponseData<LcdConfig> | 			const lcdData = await Set.LcdConfig(dbDataFbx.host, sessionToken, { led_strip_enabled: enabled }) as APIResponseData<LcdConfig> | ||||||
| 			if (!lcdData.success) { console.error(`[Freebox LCD] Erreur lors du contrôle des LEDs pour ${guildId}:`, lcdData); return } | 			if (!lcdData.success) { logConsole('freebox', 'leds_control_error', { guildId }); return } | ||||||
|  |  | ||||||
| 			console.log(`[Freebox LCD] LEDs ${enabled ? 'allumées' : 'éteintes'} avec succès pour ${guildId}`) | 			logConsole('freebox', 'leds_success', { status: enabled ? 'allumées' : 'éteintes', guildId }) | ||||||
| 		} catch (error) { | 		} catch { | ||||||
| 			console.error(`[Freebox LCD] Erreur lors du contrôle des LEDs pour ${guildId}:`, error) | 			logConsole('freebox', 'leds_error', { guildId }) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import type { Locale } from "discord.js" | import type { Locale } from "discord.js" | ||||||
| import frLocale from "@/locales/fr.json" | import frLocale from "@/locales/fr.json" | ||||||
| import enLocale from "@/locales/en.json" | import enLocale from "@/locales/en.json" | ||||||
|  | import dbGuild from "@/schemas/guild" | ||||||
|  | import { logConsoleError } from "./console" | ||||||
|  |  | ||||||
| // Variables d'environnement pour les locales avec valeurs par défaut | // Variables d'environnement pour les locales avec valeurs par défaut | ||||||
| const DEFAULT_LOCALE = process.env.DEFAULT_LOCALE ?? 'fr' | const DEFAULT_LOCALE = process.env.DEFAULT_LOCALE ?? 'fr' | ||||||
| @@ -11,6 +13,21 @@ type LocaleData = Record<string, unknown> | |||||||
| type ReplacementParams = Record<string, string | number> | type ReplacementParams = Record<string, string | number> | ||||||
| type TranslationKey = string | type TranslationKey = string | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Récupère la locale configurée pour un serveur | ||||||
|  |  * @param guildId - L'ID du serveur Discord | ||||||
|  |  * @returns La locale configurée ou 'fr' par défaut | ||||||
|  |  */ | ||||||
|  | export async function getGuildLocale(guildId: string): Promise<string> { | ||||||
|  | 	try { | ||||||
|  | 		const guildProfile = await dbGuild.findOne({ guildId }) | ||||||
|  | 		return guildProfile?.guildLocale ?? 'fr' | ||||||
|  | 	} catch (error) { | ||||||
|  | 		logConsoleError('mongoose', 'locale.fetch_error', { guildId }, error as Error) | ||||||
|  | 		return 'fr' | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| // Conversion des imports en type LocaleData | // Conversion des imports en type LocaleData | ||||||
| const frLocaleData = frLocale as unknown as LocaleData | const frLocaleData = frLocale as unknown as LocaleData | ||||||
| const enLocaleData = enLocale as unknown as LocaleData | const enLocaleData = enLocale as unknown as LocaleData | ||||||
| @@ -104,6 +121,40 @@ export function getCommandLocalizations(baseKey: string) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fonction de localisation utilisant la locale du serveur | ||||||
|  |  * @param guildLocale - La locale configurée du serveur | ||||||
|  |  * @param key - La clé de traduction (ex: "player.not_in_voice") | ||||||
|  |  * @param params - Les paramètres à remplacer dans la traduction | ||||||
|  |  * @returns La chaîne traduite | ||||||
|  |  */ | ||||||
|  | export function tGuild(guildLocale: string, key: TranslationKey, params: ReplacementParams = {}): string { | ||||||
|  | 	return t(guildLocale, key, params) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fonction helper pour obtenir la locale appropriée (serveur ou utilisateur) | ||||||
|  |  * @param guildId - L'ID du serveur (optionnel) | ||||||
|  |  * @param userLocale - La locale de l'utilisateur | ||||||
|  |  * @returns La locale à utiliser | ||||||
|  |  */ | ||||||
|  | export async function getLocaleForContext(guildId: string | null, userLocale: string): Promise<string> { | ||||||
|  | 	if (guildId) return await getGuildLocale(guildId) | ||||||
|  | 	return userLocale | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fonction de traduction intelligente qui utilise automatiquement la locale du serveur | ||||||
|  |  * @param interaction - L'interaction Discord | ||||||
|  |  * @param key - La clé de traduction | ||||||
|  |  * @param params - Les paramètres de remplacement | ||||||
|  |  * @returns La chaîne traduite | ||||||
|  |  */ | ||||||
|  | export async function tSmart(interaction: { guild: { id: string } | null; locale: string }, key: TranslationKey, params: ReplacementParams = {}): Promise<string> { | ||||||
|  | 	const locale = await getLocaleForContext(interaction.guild?.id ?? null, interaction.locale) | ||||||
|  | 	return t(locale, key, params) | ||||||
|  | } | ||||||
|  |  | ||||||
| // Export des constantes de locale | // Export des constantes de locale | ||||||
| export { DEFAULT_LOCALE, CONSOLE_LOCALE } | export { DEFAULT_LOCALE, CONSOLE_LOCALE } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,8 +5,8 @@ import type { GuildPlayer, Disco } from "@/types/schemas" | |||||||
| import type { PlayerMetadata } from "@/types/player" | import type { PlayerMetadata } from "@/types/player" | ||||||
| import uptime from "./uptime" | import uptime from "./uptime" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "./i18n" | import { t, getGuildLocale } from "./i18n" | ||||||
| import { logConsole } from "./console" | import { logConsole, logConsoleError } from "./console" | ||||||
|  |  | ||||||
| const progressIntervals = new Map<string, NodeJS.Timeout>() | const progressIntervals = new Map<string, NodeJS.Timeout>() | ||||||
|  |  | ||||||
| @@ -25,7 +25,7 @@ export function startProgressSaving(guildId: string, botId: string) { | |||||||
| 	const interval = setInterval(async () => { | 	const interval = setInterval(async () => { | ||||||
| 		try { | 		try { | ||||||
| 			const queue = useQueue(guildId) | 			const queue = useQueue(guildId) | ||||||
| 			if (!queue || !queue.isPlaying() || !queue.currentTrack) { startProgressSaving(guildId, botId); return } | 			if (!queue || !queue.isPlaying() || !queue.currentTrack) { await stopProgressSaving(guildId, botId); return } | ||||||
|  |  | ||||||
| 			const guildProfile = await dbGuild.findOne({ guildId }) | 			const guildProfile = await dbGuild.findOne({ guildId }) | ||||||
| 			if (!guildProfile) { await stopProgressSaving(guildId, botId); return } | 			if (!guildProfile) { await stopProgressSaving(guildId, botId); return } | ||||||
| @@ -50,8 +50,7 @@ export function startProgressSaving(guildId: string, botId: string) { | |||||||
| 			guildProfile.markModified("guildPlayer") | 			guildProfile.markModified("guildPlayer") | ||||||
| 			await guildProfile.save().catch(console.error) | 			await guildProfile.save().catch(console.error) | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			logConsole('discord_player', 'progress_saving.error', { guildId, botId }) | 			logConsoleError('discord_player', 'progress_saving.error', { guildId, botId }, error as Error) | ||||||
| 			console.error(error) |  | ||||||
| 			await stopProgressSaving(guildId, botId) | 			await stopProgressSaving(guildId, botId) | ||||||
| 		} | 		} | ||||||
| 	}, 3000) | 	}, 3000) | ||||||
| @@ -127,12 +126,13 @@ export async function playerReplay(client: Client, dbData: GuildPlayer) { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	try { if (!queue.connection) await queue.connect(voiceChannel) } | 	try { if (!queue.connection) await queue.connect(voiceChannel) } | ||||||
| 	catch (error) { console.error(error) } | 	catch (error) { logConsoleError('discord_player', 'replay.connect_error', {}, error as Error) } | ||||||
|  |  | ||||||
| 	if (!instance.replay.trackUrl) return | 	if (!instance.replay.trackUrl) return | ||||||
| 	 | 	 | ||||||
|  | 	const guildLocale = await getGuildLocale(queue.guild.id) | ||||||
| 	const result = await player.search(instance.replay.trackUrl, { requestedBy: client.user ?? undefined }) | 	const result = await player.search(instance.replay.trackUrl, { requestedBy: client.user ?? undefined }) | ||||||
| 	if (!result.hasTracks()) await textChannel.send(t(queue.guild.preferredLocale, "player.no_track_found", { url: instance.replay.trackUrl })) | 	if (!result.hasTracks()) await textChannel.send(t(guildLocale, "player.no_track_found", { url: instance.replay.trackUrl })) | ||||||
| 	const track = result.tracks[0] | 	const track = result.tracks[0] | ||||||
|  |  | ||||||
| 	const entry = queue.tasksQueue.acquire() | 	const entry = queue.tasksQueue.acquire() | ||||||
| @@ -143,9 +143,9 @@ export async function playerReplay(client: Client, dbData: GuildPlayer) { | |||||||
| 		if (!queue.isPlaying()) await queue.node.play() | 		if (!queue.isPlaying()) await queue.node.play() | ||||||
| 		if (instance.replay.progress) await queue.node.seek(instance.replay.progress) | 		if (instance.replay.progress) await queue.node.seek(instance.replay.progress) | ||||||
| 		startProgressSaving(queue.guild.id, botId) | 		startProgressSaving(queue.guild.id, botId) | ||||||
| 		await textChannel.send(t(queue.guild.preferredLocale, "player.music_restarted")) | 		await textChannel.send(t(guildLocale, "player.music_restarted")) | ||||||
| 	} | 	} | ||||||
| 	catch (error) { console.error(error) } | 	catch (error) { logConsoleError('discord_player', 'replay.play_error', {}, error as Error) } | ||||||
| 	finally { queue.tasksQueue.release() } | 	finally { queue.tasksQueue.release() } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -164,7 +164,8 @@ export async function playerDisco(client: Client, guild: Guild, dbData: Disco) { | |||||||
| 			return "clear" | 			return "clear" | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		const { embed, components } = generatePlayerEmbed(guild, guild.preferredLocale) | 		const guildLocale = await getGuildLocale(guild.id) | ||||||
|  | 		const { embed, components } = generatePlayerEmbed(guild, guildLocale) | ||||||
| 		if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${uptime(client.uptime)} \n ${embed.data.footer.text}` }) | 		if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${uptime(client.uptime)} \n ${embed.data.footer.text}` }) | ||||||
| 		else embed.setFooter({ text: `Uptime: ${uptime(client.uptime)}` }) | 		else embed.setFooter({ text: `Uptime: ${uptime(client.uptime)}` }) | ||||||
|  |  | ||||||
| @@ -183,7 +184,7 @@ export async function playerDisco(client: Client, guild: Guild, dbData: Disco) { | |||||||
| 		} | 		} | ||||||
| 		else return await channel.send({ embeds: [embed] }) | 		else return await channel.send({ embeds: [embed] }) | ||||||
| 	} catch (error) { | 	} catch (error) { | ||||||
| 		console.error(error) | 		logConsoleError('discord_player', 'disco.general_error', {}, error as Error) | ||||||
| 		return "clear" | 		return "clear" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -199,7 +200,7 @@ export async function playerEdit(interaction: ButtonInteraction) { | |||||||
| 	await interaction.update({ components }) | 	await interaction.update({ components }) | ||||||
| } | } | ||||||
|  |  | ||||||
| export function generatePlayerEmbed(guild: Guild, locale: Locale) { | export function generatePlayerEmbed(guild: Guild, locale: Locale | string) { | ||||||
| 	const embed = new EmbedBuilder().setColor("#ffc370") | 	const embed = new EmbedBuilder().setColor("#ffc370") | ||||||
|  |  | ||||||
| 	const queue = useQueue(guild.id) | 	const queue = useQueue(guild.id) | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| const clientId = process.env.TWITCH_APP_ID | const clientId = process.env.TWITCH_APP_ID | ||||||
| const clientSecret = process.env.TWITCH_APP_SECRET | const clientSecret = process.env.TWITCH_APP_SECRET | ||||||
| if (!clientId || !clientSecret) { | if (!clientId || !clientSecret) { | ||||||
| 	console.warn(chalk.red("[Twitch] Missing TWITCH_APP_ID or TWITCH_APP_SECRET in environment variables!")) | 	logConsole('twitch', 'missing_credentials') | ||||||
| 	process.exit(1) | 	process.exit(1) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -14,12 +14,11 @@ import { NgrokAdapter } from "@twurple/eventsub-ngrok" | |||||||
| import type { EventSubStreamOnlineEvent, EventSubStreamOfflineEvent } from "@twurple/eventsub-base" | import type { EventSubStreamOnlineEvent, EventSubStreamOfflineEvent } from "@twurple/eventsub-base" | ||||||
| import { EmbedBuilder, ChannelType, ComponentType, ButtonBuilder, ButtonStyle, Locale } from "discord.js" | import { EmbedBuilder, ChannelType, ComponentType, ButtonBuilder, ButtonStyle, Locale } from "discord.js" | ||||||
| import type { Client, Guild } from "discord.js" | import type { Client, Guild } from "discord.js" | ||||||
| import chalk from "chalk" |  | ||||||
| import discordClient from "@/index" | import discordClient from "@/index" | ||||||
| import type { GuildTwitch } from "@/types/schemas" | import type { GuildTwitch } from "@/types/schemas" | ||||||
| import dbGuild from "@/schemas/guild" | import dbGuild from "@/schemas/guild" | ||||||
| import { t } from "@/utils/i18n" | import { t, getGuildLocale } from "@/utils/i18n" | ||||||
| import { logConsole } from "@/utils/console" | import { logConsole, logConsoleError } from "@/utils/console" | ||||||
|  |  | ||||||
| // Twurple API client setup | // Twurple API client setup | ||||||
| const authProvider = new AppTokenAuthProvider(clientId, clientSecret) | const authProvider = new AppTokenAuthProvider(clientId, clientSecret) | ||||||
| @@ -35,7 +34,7 @@ if (process.env.NODE_ENV === "development") { | |||||||
| 	const hostName = process.env.TWURPLE_HOSTNAME ?? "localhost" | 	const hostName = process.env.TWURPLE_HOSTNAME ?? "localhost" | ||||||
| 	const port = process.env.TWURPLE_PORT ?? "3000" | 	const port = process.env.TWURPLE_PORT ?? "3000" | ||||||
|  |  | ||||||
| 	console.log(chalk.magenta(`[Twitch] Starting listener with port ${port}...`)) | 	logConsole('twitch', 'starting_listener_port', { port }) | ||||||
| 	adapter = new ReverseProxyAdapter({ hostName, port: parseInt(port) }) | 	adapter = new ReverseProxyAdapter({ hostName, port: parseInt(port) }) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -45,26 +44,30 @@ listener.start() | |||||||
|  |  | ||||||
| // Twurple subscriptions callback functions | // Twurple subscriptions callback functions | ||||||
| export const onlineSub = async (event: EventSubStreamOnlineEvent) => { | export const onlineSub = async (event: EventSubStreamOnlineEvent) => { | ||||||
| 	console.log(chalk.magenta(`[Twitch] Stream from ${event.broadcasterName} (ID ${event.broadcasterId}) is now online, sending Discord messages...`)) | 	logConsole('twitch', 'stream_online', { streamer: event.broadcasterName, id: event.broadcasterId }) | ||||||
|  |  | ||||||
| 	const results = await Promise.allSettled(discordClient.guilds.cache.map(async guild => { | 	const results = await Promise.allSettled(discordClient.guilds.cache.map(async guild => { | ||||||
|  | 		const processingKey = `${guild.id}-${event.broadcasterId}` | ||||||
| 		try { | 		try { | ||||||
| 			console.log(chalk.magenta(`[Twitch] Processing guild: ${guild.name} (ID: ${guild.id}) for streamer ${event.broadcasterName}`)) | 			if (processingStreamers.has(processingKey)) { logConsole('twitch', 'streamer_already_processing', { guildName: guild.name, broadcasterName: event.broadcasterName }); return } | ||||||
|  | 			processingStreamers.add(processingKey) | ||||||
|  |  | ||||||
|  | 			logConsole('twitch', 'processing_guild', { name: guild.name, id: guild.id, streamer: event.broadcasterName }) | ||||||
|  |  | ||||||
| 			const notification = await generateNotification(guild, event.broadcasterId, event.broadcasterName) | 			const notification = await generateNotification(guild, event.broadcasterId, event.broadcasterName) | ||||||
| 			if (notification.status !== "ok") { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Notification generation failed with status: ${notification.status}`)); return } | 			if (notification.status !== "ok") { logConsole('twitch', 'notification_failed', { guild: guild.name, status: notification.status }); return } | ||||||
|  |  | ||||||
| 			const { guildProfile, dbData, channel, content, embed } = notification | 			const { guildProfile, dbData, channel, content, embed } = notification | ||||||
| 			if (!dbData) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} No dbData found`)); return } | 			if (!dbData) { logConsole('twitch', 'no_db_data', { guild: guild.name }); return } | ||||||
|  |  | ||||||
| 			const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === event.broadcasterId) | 			const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === event.broadcasterId) | ||||||
| 			if (streamerIndex === -1) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer ${event.broadcasterName} not found in this guild`)); return } | 			if (streamerIndex === -1) { logConsole('twitch', 'streamer_not_found', { guild: guild.name, streamer: event.broadcasterName }); return } | ||||||
|  |  | ||||||
| 			if (dbData.streamers[streamerIndex].messageId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message already exists for ${event.broadcasterName}, skipping`)); return } | 			if (dbData.streamers[streamerIndex].messageId) { logConsole('twitch', 'message_exists', { guild: guild.name, streamer: event.broadcasterName }); return } | ||||||
|  |  | ||||||
| 			console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Sending notification for ${event.broadcasterName}`)) | 			logConsole('twitch', 'sending_notification', { guild: guild.name, streamer: event.broadcasterName }) | ||||||
| 			const message = await channel.send({ content, embeds: [embed] }) | 			const message = await channel.send({ content, embeds: [embed] }) | ||||||
| 			console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message sent with ID: ${message.id}`)) | 			logConsole('twitch', 'message_sent', { guild: guild.name, id: message.id }) | ||||||
|  |  | ||||||
| 			dbData.streamers[streamerIndex].messageId = message.id | 			dbData.streamers[streamerIndex].messageId = message.id | ||||||
|  |  | ||||||
| @@ -74,18 +77,20 @@ export const onlineSub = async (event: EventSubStreamOnlineEvent) => { | |||||||
|  |  | ||||||
| 			startStreamWatching(guild.id, event.broadcasterId, event.broadcasterName, message.id) | 			startStreamWatching(guild.id, event.broadcasterId, event.broadcasterName, message.id) | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			console.log(chalk.magenta(`[Twitch] Error processing guild ${guild.name}`)) | 			processingStreamers.delete(processingKey) | ||||||
| 			console.error(error) | 			logConsoleError('twitch', 'error_processing_guild', { name: guild.name }, error as Error) | ||||||
|  | 		} finally { | ||||||
|  | 			processingStreamers.delete(processingKey) | ||||||
| 		} | 		} | ||||||
| 	})) | 	})) | ||||||
|  |  | ||||||
| 	results.forEach((result, index) => { | 	results.forEach((result, index) => { | ||||||
| 		if (result.status === "rejected") console.log(chalk.magenta(`[Twitch] Guild ${index} failed:`), result.reason) | 		if (result.status === "rejected") logConsoleError('twitch', 'guild_failed', { index: index.toString() }, result.reason instanceof Error ? result.reason : new Error(String(result.reason))) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| export const offlineSub = async (event: EventSubStreamOfflineEvent) => { | export const offlineSub = async (event: EventSubStreamOfflineEvent) => { | ||||||
| 	console.log(chalk.magenta(`[Twitch] Stream from ${event.broadcasterName} (ID ${event.broadcasterId}) is now offline, editing Discord messages...`)) | 	logConsole('twitch', 'stream_offline', { streamer: event.broadcasterName, id: event.broadcasterId }) | ||||||
|  |  | ||||||
| 	await Promise.all(discordClient.guilds.cache.map(async guild => { | 	await Promise.all(discordClient.guilds.cache.map(async guild => { | ||||||
| 		await stopStreamWatching(guild.id, event.broadcasterId, event.broadcasterName) | 		await stopStreamWatching(guild.id, event.broadcasterId, event.broadcasterName) | ||||||
| @@ -95,8 +100,11 @@ export const offlineSub = async (event: EventSubStreamOfflineEvent) => { | |||||||
| // Stream upadting intervals | // Stream upadting intervals | ||||||
| const streamIntervals = new Map<string, NodeJS.Timeout>() | const streamIntervals = new Map<string, NodeJS.Timeout>() | ||||||
|  |  | ||||||
|  | // Tracking des streamers en cours de traitement pour éviter les doublons | ||||||
|  | const processingStreamers = new Set<string>() | ||||||
|  |  | ||||||
| export function startStreamWatching(guildId: string, streamerId: string, streamerName: string, messageId: string) { | export function startStreamWatching(guildId: string, streamerId: string, streamerName: string, messageId: string) { | ||||||
| 	console.log(chalk.magenta(`[Twitch] StreamWatching - Démarrage du visionnage de ${streamerName} (ID ${streamerId}) sur ${guildId}`)) | 	logConsole('twitch', 'start_watching', { streamer: streamerName, id: streamerId, guildId }) | ||||||
|  |  | ||||||
| 	const key = `${guildId}-${streamerId}` | 	const key = `${guildId}-${streamerId}` | ||||||
| 	if (streamIntervals.has(key)) { | 	if (streamIntervals.has(key)) { | ||||||
| @@ -109,21 +117,19 @@ export function startStreamWatching(guildId: string, streamerId: string, streame | |||||||
| 		try { | 		try { | ||||||
| 			const guild = await discordClient.guilds.fetch(guildId) | 			const guild = await discordClient.guilds.fetch(guildId) | ||||||
| 			const notification = await generateNotification(guild, streamerId, streamerName) | 			const notification = await generateNotification(guild, streamerId, streamerName) | ||||||
| 			if (notification.status !== "ok") { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Notification generation failed with status: ${notification.status}`)); return } | 			if (notification.status !== "ok") { logConsole('twitch', 'notification_failed', { guild: guild.name, status: notification.status }); return } | ||||||
|  |  | ||||||
| 			const { channel, content, embed } = notification | 			const { channel, content, embed } = notification | ||||||
| 			if (!embed) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Embed is missing`)); return } | 			if (!embed) { logConsole('twitch', 'embed_missing', { guild: guild.name }); return } | ||||||
|  |  | ||||||
| 			try { | 			try { | ||||||
| 				const message = await channel.messages.fetch(messageId) | 				const message = await channel.messages.fetch(messageId) | ||||||
| 				await message.edit({ content, embeds: [embed] }) | 				await message.edit({ content, embeds: [embed] }) | ||||||
| 			} catch (error) { | 			} catch (error) { | ||||||
| 				console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Error editing message for ${streamerName} (ID ${streamerId})`)) | 				logConsoleError('twitch', 'error_editing_message', { guild: guild.name, streamer: streamerName, id: streamerId }, error as Error) | ||||||
| 				console.error(error) |  | ||||||
| 			} | 			} | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 			console.log(chalk.magenta(`[Twitch] StreamWatching - Erreur lors du visionnage de ${streamerName} (ID ${streamerId}) sur ${guildId}`)) | 			logConsoleError('twitch', 'error_watching', { streamer: streamerName, id: streamerId, guildId }, error as Error) | ||||||
| 			console.error(error) |  | ||||||
| 			await stopStreamWatching(guildId, streamerId, streamerName) | 			await stopStreamWatching(guildId, streamerId, streamerName) | ||||||
| 		} | 		} | ||||||
| 	}, 60000) | 	}, 60000) | ||||||
| @@ -132,7 +138,7 @@ export function startStreamWatching(guildId: string, streamerId: string, streame | |||||||
| } | } | ||||||
|  |  | ||||||
| export async function stopStreamWatching(guildId: string, streamerId: string, streamerName: string) { | export async function stopStreamWatching(guildId: string, streamerId: string, streamerName: string) { | ||||||
| 	console.log(chalk.magenta(`[Twitch] StreamWatching - Arrêt du visionnage de ${streamerName} (ID ${streamerId})`)) | 	logConsole('twitch', 'stop_watching', { streamer: streamerName, id: streamerId }) | ||||||
|  |  | ||||||
| 	const key = `${guildId}-${streamerId}` | 	const key = `${guildId}-${streamerId}` | ||||||
| 	if (streamIntervals.has(key)) { | 	if (streamIntervals.has(key)) { | ||||||
| @@ -140,33 +146,35 @@ export async function stopStreamWatching(guildId: string, streamerId: string, st | |||||||
| 		streamIntervals.delete(key) | 		streamIntervals.delete(key) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	try { | ||||||
| 		const guild = await discordClient.guilds.fetch(guildId) | 		const guild = await discordClient.guilds.fetch(guildId) | ||||||
| 		const guildProfile = await dbGuild.findOne({ guildId: guild.id }) | 		const guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||||
| 	if (!guildProfile) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)); return } | 		if (!guildProfile) { logConsole('twitch', 'database_not_exist', { guild: guild.name }); return } | ||||||
|  |  | ||||||
| 		const dbData = guildProfile.get("guildTwitch") as GuildTwitch | 		const dbData = guildProfile.get("guildTwitch") as GuildTwitch | ||||||
| 	if (!dbData.enabled || !dbData.channelId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)); return } | 		if (!dbData.enabled || !dbData.channelId) { logConsole('twitch', 'database_not_exist', { guild: guild.name }); return } | ||||||
|  |  | ||||||
| 		const channel = await guild.channels.fetch(dbData.channelId) | 		const channel = await guild.channels.fetch(dbData.channelId) | ||||||
| 		if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { | 		if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { | ||||||
| 		console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Channel with ID ${dbData.channelId} not found for Twitch notifications`)) | 			logConsole('twitch', 'channel_not_found', { guild: guild.name, channelId: dbData.channelId }) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) | 		const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) | ||||||
| 	if (!streamer) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer not found in guild for ${streamerName} (ID ${streamerId})`)); return } | 		if (!streamer) { logConsole('twitch', 'streamer_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }); return } | ||||||
|  |  | ||||||
| 		const messageId = streamer.messageId | 		const messageId = streamer.messageId | ||||||
| 	if (!messageId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message ID not found for ${streamerName} (ID ${streamerId})`)); return } | 		if (!messageId) { logConsole('twitch', 'message_id_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }); return } | ||||||
|  |  | ||||||
| 		const user = await twitchClient.users.getUserById(streamerId) | 		const user = await twitchClient.users.getUserById(streamerId) | ||||||
| 	if (!user) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} User data not found for ${streamerName} (ID ${streamerId})`)) | 		if (!user) logConsole('twitch', 'user_data_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) | ||||||
|  |  | ||||||
|  | 		const guildLocale = await getGuildLocale(guild.id) | ||||||
| 		let duration_string = "" | 		let duration_string = "" | ||||||
| 		const stream = await user?.getStream() | 		const stream = await user?.getStream() | ||||||
| 		if (!stream) { | 		if (!stream) { | ||||||
| 		console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Stream data not found for ${streamerName} (ID ${streamerId})`)) | 			logConsole('twitch', 'stream_data_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) | ||||||
| 		duration_string = t(guild.preferredLocale, "twitch.notification.offline.duration_unknown") | 			duration_string = t(guildLocale, "twitch.notification.offline.duration_unknown") | ||||||
| 		} else { | 		} else { | ||||||
| 			const duration = new Date().getTime() - stream.startDate.getTime() | 			const duration = new Date().getTime() - stream.startDate.getTime() | ||||||
| 			const seconds = Math.floor(duration / 1000) | 			const seconds = Math.floor(duration / 1000) | ||||||
| @@ -177,13 +185,13 @@ export async function stopStreamWatching(guildId: string, streamerId: string, st | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		let content = "" | 		let content = "" | ||||||
| 	if (!streamer.discordUserId) content = t(guild.preferredLocale, "twitch.notification.offline.everyone", { streamer: streamerName }) | 		if (!streamer.discordUserId) content = t(guildLocale, "twitch.notification.offline.everyone", { streamer: streamerName }) | ||||||
| 	else content = t(guild.preferredLocale, "twitch.notification.offline.everyone_with_mention", { discordId: streamer.discordUserId }) | 		else content = t(guildLocale, "twitch.notification.offline.everyone_with_mention", { discordId: streamer.discordUserId }) | ||||||
|  |  | ||||||
| 		const embed = new EmbedBuilder() | 		const embed = new EmbedBuilder() | ||||||
| 			.setColor("#6441a5") | 			.setColor("#6441a5") | ||||||
| 			.setAuthor({ | 			.setAuthor({ | ||||||
| 			name: t(guild.preferredLocale, "twitch.notification.offline.author", { duration: duration_string }), | 				name: t(guildLocale, "twitch.notification.offline.author", { duration: duration_string }), | ||||||
| 				iconURL: user?.profilePictureUrl ?? "https://static-cdn.jtvnw.net/emoticons/v2/58765/static/light/3.0" | 				iconURL: user?.profilePictureUrl ?? "https://static-cdn.jtvnw.net/emoticons/v2/58765/static/light/3.0" | ||||||
| 			}) | 			}) | ||||||
| 			.setTimestamp() | 			.setTimestamp() | ||||||
| @@ -192,8 +200,7 @@ export async function stopStreamWatching(guildId: string, streamerId: string, st | |||||||
| 			const message = await channel.messages.fetch(messageId) | 			const message = await channel.messages.fetch(messageId) | ||||||
| 			await message.edit({ content, embeds: [embed] }) | 			await message.edit({ content, embeds: [embed] }) | ||||||
| 		} catch (error) { | 		} catch (error) { | ||||||
| 		console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Error editing message for ${streamerName} (ID ${streamerId})`)) | 			logConsoleError('twitch', 'error_editing_message', { guild: guild.name, streamer: streamerName, id: streamerId }, error as Error) | ||||||
| 		console.error(error) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === streamerId) | 		const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === streamerId) | ||||||
| @@ -204,52 +211,56 @@ export async function stopStreamWatching(guildId: string, streamerId: string, st | |||||||
| 		guildProfile.set("guildTwitch", dbData) | 		guildProfile.set("guildTwitch", dbData) | ||||||
| 		guildProfile.markModified("guildTwitch") | 		guildProfile.markModified("guildTwitch") | ||||||
| 		await guildProfile.save().catch(console.error) | 		await guildProfile.save().catch(console.error) | ||||||
|  | 	} catch (error) { | ||||||
|  | 		logConsoleError('twitch', 'stop_watching_error', { streamer: streamerName, id: streamerId, guildId }, error as Error) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| async function generateNotification(guild: Guild, streamerId: string, streamerName: string) { | async function generateNotification(guild: Guild, streamerId: string, streamerName: string) { | ||||||
| 	const guildProfile = await dbGuild.findOne({ guildId: guild.id }) | 	const guildProfile = await dbGuild.findOne({ guildId: guild.id }) | ||||||
| 	if (!guildProfile) { | 	if (!guildProfile) { | ||||||
| 		console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)) | 		logConsole('twitch', 'database_not_exist', { guild: guild.name }) | ||||||
| 		return { status: "noProfile" } | 		return { status: "noProfile" } | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const dbData = guildProfile.get("guildTwitch") as GuildTwitch | 	const dbData = guildProfile.get("guildTwitch") as GuildTwitch | ||||||
| 	if (!dbData.enabled || !dbData.channelId) { | 	if (!dbData.enabled || !dbData.channelId) { | ||||||
| 		console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Twitch module is not enabled or channel ID is missing`)) | 		logConsole('twitch', 'module_disabled', { guild: guild.name }) | ||||||
| 		return { status: "disabled" } | 		return { status: "disabled" } | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const channel = await guild.channels.fetch(dbData.channelId) | 	const channel = await guild.channels.fetch(dbData.channelId) | ||||||
| 	if ((channel?.type !== ChannelType.GuildText && channel?.type !== ChannelType.GuildAnnouncement)) { | 	if ((channel?.type !== ChannelType.GuildText && channel?.type !== ChannelType.GuildAnnouncement)) { | ||||||
| 		console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Channel with ID ${dbData.channelId} not found for Twitch notifications`)) | 		logConsole('twitch', 'channel_not_found', { guild: guild.name, channelId: dbData.channelId }) | ||||||
| 		return { status: "noChannel" } | 		return { status: "noChannel" } | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) | 	const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) | ||||||
| 	if (!streamer) { | 	if (!streamer) { | ||||||
| 		console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer not found in guild for ${streamerName} (ID ${streamerId})`)) | 		logConsole('twitch', 'streamer_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) | ||||||
| 		return { status: "noStreamer" } | 		return { status: "noStreamer" } | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const user = await twitchClient.users.getUserById(streamerId) | 	const user = await twitchClient.users.getUserById(streamerId) | ||||||
| 	if (!user) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} User data not found for ${streamerName} (ID ${streamerId})`)) | 	if (!user) logConsole('twitch', 'user_data_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) | ||||||
|  |  | ||||||
| 	const stream = await user?.getStream() | 	const stream = await user?.getStream() | ||||||
| 	if (!stream) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Stream data not found for ${streamerName} (ID ${streamerId})`)) | 	if (!stream) logConsole('twitch', 'stream_data_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) | ||||||
|  |  | ||||||
|  | 	const guildLocale = await getGuildLocale(guild.id) | ||||||
| 	let content = "" | 	let content = "" | ||||||
| 	if (!streamer.discordUserId) content = t(guild.preferredLocale, "twitch.notification.online.everyone", { streamer: user?.displayName ?? streamerName }) | 	if (!streamer.discordUserId) content = t(guildLocale, "twitch.notification.online.everyone", { streamer: user?.displayName ?? streamerName }) | ||||||
| 	else content = t(guild.preferredLocale, "twitch.notification.online.everyone_with_mention", { discordId: streamer.discordUserId }) | 	else content = t(guildLocale, "twitch.notification.online.everyone_with_mention", { discordId: streamer.discordUserId }) | ||||||
|  |  | ||||||
| 	const embed = new EmbedBuilder() | 	const embed = new EmbedBuilder() | ||||||
| 		.setColor("#6441a5") | 		.setColor("#6441a5") | ||||||
| 		.setTitle(stream?.title ?? t(guild.preferredLocale, "twitch.notification.online.title_unknown")) | 		.setTitle(stream?.title ?? t(guildLocale, "twitch.notification.online.title_unknown")) | ||||||
| 		.setURL(`https://twitch.tv/${streamerName}`) | 		.setURL(`https://twitch.tv/${streamerName}`) | ||||||
| 		.setAuthor({ | 		.setAuthor({ | ||||||
| 			name: t(guild.preferredLocale, "twitch.notification.online.author", { streamer: (user?.displayName ?? streamerName).toUpperCase() }), | 			name: t(guildLocale, "twitch.notification.online.author", { streamer: (user?.displayName ?? streamerName).toUpperCase() }), | ||||||
| 			iconURL: user?.profilePictureUrl ?? "https://static-cdn.jtvnw.net/emoticons/v2/58765/static/light/3.0" | 			iconURL: user?.profilePictureUrl ?? "https://static-cdn.jtvnw.net/emoticons/v2/58765/static/light/3.0" | ||||||
| 		}) | 		}) | ||||||
| 		.setDescription(t(guild.preferredLocale, "twitch.notification.online.description", {  | 		.setDescription(t(guildLocale, "twitch.notification.online.description", {  | ||||||
| 			game: stream?.gameName ?? "?",  | 			game: stream?.gameName ?? "?",  | ||||||
| 			viewers: stream?.viewers.toString() ?? "?"  | 			viewers: stream?.viewers.toString() ?? "?"  | ||||||
| 		})) | 		})) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user