diff --git a/build/node.dockerfile b/build/node.dockerfile index 5e089f2..0d71649 100644 --- a/build/node.dockerfile +++ b/build/node.dockerfile @@ -1,22 +1,34 @@ # 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 +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 -COPY package.json package-lock.json* . +# Install app dependencies +ENV NODE_ENV=production RUN npm ci --only=production --ignore-scripts && \ - npm install bufferutil zlib-sync + npm install bufferutil zlib-sync && \ + npm cache clean --force -# Copy the builded files and the charts -COPY ./dist/* . +# Copy the builded files +COPY --chown=node:node ./dist/* . -# Set the permissions -RUN chown -R node:node /app +# Return to root user to remove build dependencies +USER root +RUN apt-get remove -y python3 make g++ && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +# Go back to node user USER node # Start the application diff --git a/deploy/templates/ingress.yaml b/deploy/templates/ingress.yaml new file mode 100644 index 0000000..3809a82 --- /dev/null +++ b/deploy/templates/ingress.yaml @@ -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 }} \ No newline at end of file diff --git a/deploy/templates/service.yaml b/deploy/templates/service.yaml new file mode 100644 index 0000000..6c5a92b --- /dev/null +++ b/deploy/templates/service.yaml @@ -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 }} \ No newline at end of file diff --git a/deploy/values.yaml b/deploy/values.yaml index 2039b78..a678e1c 100644 --- a/deploy/values.yaml +++ b/deploy/values.yaml @@ -15,4 +15,17 @@ deployment: # Memory: "500Mi" requests: Cpu: "0.1" - Memory: "50Mi" \ No newline at end of file + 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 \ No newline at end of file diff --git a/docs/FREEBOX_LCD.md b/docs/FREEBOX_LCD.md deleted file mode 100644 index 77ba092..0000000 --- a/docs/FREEBOX_LCD.md +++ /dev/null @@ -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 ` -3. **Version API configurée** : `/database edit guildFbx.version ` -4. **Authentification** : `/freebox init` (suivre le processus d'autorisation) - -## Commandes Disponibles - -### Récupération de configuration -``` -/freebox get lcd -``` -Récupère et affiche la configuration actuelle de l'écran LCD. - -### Contrôle manuel des LEDs -``` -/freebox lcd leds enabled:true # Allumer les LEDs -/freebox lcd leds enabled:false # Éteindre les LEDs -``` - -### Gestion du timer -``` -# Activer le timer avec horaires -/freebox lcd timer action:enable morning_time:08:00 night_time:22:30 - -# Vérifier le statut du timer -/freebox lcd timer action:status - -# Désactiver le timer -/freebox lcd timer action:disable -``` - -## Fonctionnement du Timer - -### Programmation -- Le timer est programmé automatiquement au démarrage du bot -- Seul le bot configuré comme "gestionnaire" peut contrôler les LEDs d'un serveur -- Les horaires sont vérifiés et formatés (HH:MM, 24h) - -### Exécution -- **Matin** : Les LEDs s'allument à l'heure programmée -- **Soir** : Les LEDs s'éteignent à l'heure programmée -- **Reprogrammation** : Le cycle se répète automatiquement chaque jour - -### Logs -Les opérations sont loggées dans la console : -- Programmation des timers -- Exécution des commandes d'allumage/extinction -- Erreurs de connexion ou d'authentification - -## Gestion Multi-Bot - -### Système de Verrouillage -- Un seul bot peut gérer les LEDs par serveur Discord -- L'ID du bot gestionnaire est stocké en base de données -- Les autres bots reçoivent un message d'erreur s'ils tentent d'utiliser les commandes - -### Changement de Bot Gestionnaire -Pour changer de bot gestionnaire : -1. Désactiver le timer sur le bot actuel : `/freebox lcd timer action:disable` -2. Activer le timer sur le nouveau bot : `/freebox lcd timer action:enable` - -## Dépannage - -### Erreurs Communes -- **"Module Freebox désactivé"** : Activer avec `/database edit guildFbx.enabled true` -- **"Hôte non configuré"** : Définir avec `/database edit guildFbx.host ` -- **"Token d'app manquant"** : Refaire l'initialisation avec `/freebox init` -- **"Géré par un autre bot"** : Désactiver sur l'autre bot d'abord - -### Vérification de Configuration -1. Vérifier que la Freebox est accessible sur le réseau -2. S'assurer que l'application est autorisée dans l'interface Freebox -3. Vérifier les logs de la console pour les erreurs détaillées - -## API Freebox Utilisée - -- `GET /api/v8/lcd/config/` : Récupération de la configuration LCD -- `PUT /api/v8/lcd/config/` : Modification de la configuration LCD -- Propriété `led_strip_enabled` : Contrôle de l'état des LEDs - -## Sécurité - -- Les tokens d'authentification sont gérés automatiquement -- Les sessions sont créées à la demande -- Les erreurs d'authentification sont loggées mais les tokens ne sont pas exposés diff --git a/src/buttons/freebox/lcd_status.ts b/src/buttons/freebox/lcd_status.ts index 3b38664..7690714 100644 --- a/src/buttons/freebox/lcd_status.ts +++ b/src/buttons/freebox/lcd_status.ts @@ -6,6 +6,7 @@ import type { APIResponseData, APIResponseDataError, GetChallenge, LcdConfig, Op import type { GuildFbx } from "@/types/schemas" import dbGuild from "@/schemas/guild" import { t } from "@/utils/i18n" +import { logConsoleError } from "@/utils/console" export const id = "freebox_lcd_status" export async function execute(interaction: ButtonInteraction) { @@ -82,7 +83,7 @@ export async function execute(interaction: ButtonInteraction) { return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) } catch (error) { - console.error("Erreur lors de la récupération de l'état LCD:", error) + logConsoleError('freebox', 'lcd_status_error', undefined, error as Error) return interaction.followUp({ content: t(interaction.locale, "freebox.lcd.unexpected_error"), flags: MessageFlags.Ephemeral }) } } diff --git a/src/buttons/freebox/test_connection.ts b/src/buttons/freebox/test_connection.ts index cd78347..6189a75 100644 --- a/src/buttons/freebox/test_connection.ts +++ b/src/buttons/freebox/test_connection.ts @@ -6,6 +6,7 @@ import type { APIResponseData, APIResponseDataError, APIResponseDataVersion, Con import type { GuildFbx } from "@/types/schemas" import dbGuild from "@/schemas/guild" import { t } from "@/utils/i18n" +import { logConsoleError } from "@/utils/console" export const id = "freebox_test_connection" export async function execute(interaction: ButtonInteraction) { @@ -65,7 +66,7 @@ export async function execute(interaction: ButtonInteraction) { return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }) } 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 }) } } diff --git a/src/buttons/twitch/streamer_list.ts b/src/buttons/twitch/streamer_list.ts index b2267f1..b4dfcb8 100644 --- a/src/buttons/twitch/streamer_list.ts +++ b/src/buttons/twitch/streamer_list.ts @@ -4,7 +4,7 @@ import type { GuildTwitch } from "@/types/schemas" import dbGuild from "@/schemas/guild" import { twitchClient } from "@/utils/twitch" import { t } from "@/utils/i18n" -import { logConsole } from "@/utils/console" +import { logConsoleError } from "@/utils/console" export const id = "twitch_streamer_list" 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}\``) } } catch (error) { - logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId }) - console.error(error) + logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as Error) streamers.push(`**${index + 1}.** ${t(interaction.locale, "twitch.list.fetch_error")}\n└ ID: \`${streamer.twitchUserId}\``) } })) diff --git a/src/buttons/twitch/streamer_remove.ts b/src/buttons/twitch/streamer_remove.ts index e0be364..a8339a1 100644 --- a/src/buttons/twitch/streamer_remove.ts +++ b/src/buttons/twitch/streamer_remove.ts @@ -4,7 +4,7 @@ import { twitchClient } from "@/utils/twitch" import type { GuildTwitch } from "@/types/schemas" import dbGuild from "@/schemas/guild" import { t } from "@/utils/i18n" -import { logConsole } from "@/utils/console" +import { logConsoleError } from "@/utils/console" export const id = "twitch_streamer_remove" 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") } } catch (error) { - logConsole('twitch', 'user_fetch_error_buttons', { id: streamer.twitchUserId }) - console.error(error) + logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as Error) return { label: `ID: ${streamer.twitchUserId}`, value: streamer.twitchUserId, diff --git a/src/commands/global/freebox.ts b/src/commands/global/freebox.ts index cc6abb9..b90067b 100644 --- a/src/commands/global/freebox.ts +++ b/src/commands/global/freebox.ts @@ -9,6 +9,7 @@ import type { import type { GuildFbx } from "@/types/schemas" import dbGuild from "@/schemas/guild" import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" export const data = new SlashCommandBuilder() .setName("freebox") @@ -238,7 +239,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { clearInterval(initCheck) return interaction.followUp({ content: t(interaction.locale, "freebox.auth.user_denied_access"), flags: MessageFlags.Ephemeral }) - } else if (status === "pending") { console.log("Freebox authorization pending...") } + } else if (status === "pending") logConsole('freebox', 'authorization_pending') }, 2000) } else { diff --git a/src/commands/global/index.ts b/src/commands/global/index.ts index bc1fd0f..1949659 100644 --- a/src/commands/global/index.ts +++ b/src/commands/global/index.ts @@ -2,6 +2,7 @@ import * as amp from "./amp" import * as boost from "./boost" import * as database from "./database" import * as freebox from "./freebox" +import * as locale from "./locale" import * as ping from "./ping" import * as twitch from "./twitch" @@ -12,6 +13,7 @@ export default [ boost, database, freebox, + locale, ping, twitch ] as Command[] diff --git a/src/commands/global/locale.ts b/src/commands/global/locale.ts new file mode 100644 index 0000000..4687a90 --- /dev/null +++ b/src/commands/global/locale.ts @@ -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 + }) +} diff --git a/src/commands/global/twitch.ts b/src/commands/global/twitch.ts index fcbaf1d..0f9c6bf 100644 --- a/src/commands/global/twitch.ts +++ b/src/commands/global/twitch.ts @@ -1,10 +1,10 @@ import { SlashCommandBuilder, ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js" import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData } from "discord.js" -import chalk from "chalk" import { twitchClient, listener, onlineSub, offlineSub, generateTwitchEmbed } from "@/utils/twitch" import type { GuildTwitch } from "@/types/schemas" import dbGuild from "@/schemas/guild" import { t } from "@/utils/i18n" +import { logConsole, logConsoleError } from "@/utils/console" export const data = new SlashCommandBuilder() .setName("twitch") @@ -120,8 +120,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { if (user) streamers.push(`- ${user.displayName} (${streamer.twitchUserId})`) else streamers.push(`- ${t(interaction.locale, "twitch.user_not_found_id", { id: streamer.twitchUserId })}`) } catch (error) { - console.log(chalk.magenta(`[Twitch] Error fetching user for ID ${streamer.twitchUserId}`)) - console.error(error) + logConsoleError('twitch', 'user_fetch_error', { id: streamer.twitchUserId }, error as Error) } })) const streamerList = streamers.length > 0 ? streamers.join("\n") : t(interaction.locale, "twitch.no_streamers") @@ -167,7 +166,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { if (!await dbGuild.exists({ "guildTwitch.streamers.twitchUserId": user.id })) { const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(user.id) await Promise.all(userSubs.data.map(async sub => { if (sub.transportMethod === "webhook" && (sub.type === "stream.online" || sub.type === "stream.offline")) await sub.unsubscribe() })) - console.log(chalk.magenta(`[Twitch] Listener removed for ${user.displayName} (ID ${user.id})`)) + logConsole('twitch', 'listener_removed', { name: user.displayName, id: user.id }) } return interaction.reply({ content: t(interaction.locale, "twitch.streamer_removed", { username }), flags: MessageFlags.Ephemeral }) diff --git a/src/commands/player/play.ts b/src/commands/player/play.ts index 0ee67e3..47989df 100644 --- a/src/commands/player/play.ts +++ b/src/commands/player/play.ts @@ -8,6 +8,7 @@ import type { TrackSearchResult } from "@/types/player" import type { GuildPlayer } from "@/types/schemas" import dbGuild from "@/schemas/guild" import { t } from "@/utils/i18n" +import { logConsoleError } from "@/utils/console" export const data = new SlashCommandBuilder() .setName("play") @@ -55,7 +56,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { } 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 }) 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 }) if (!result.hasTracks()) return interaction.followUp({ content: t(interaction.locale, "player.no_track_found", { query }), flags: MessageFlags.Ephemeral }) const track = result.tracks[0] + if (process.env.NODE_ENV === "development") console.log(query, result, track) const entry = queue.tasksQueue.acquire() await entry.getTask() @@ -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") 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() } } @@ -105,6 +107,7 @@ export async function autocompleteRun(interaction: AutocompleteInteraction) { const resultsSpotify = await player.search(query, { searchEngine: `ext:${SpotifyExtractor.identifier}` }) const resultsYouTube = await player.search(query, { searchEngine: `ext:${YoutubeiExtractor.identifier}` }) + if (process.env.NODE_ENV === "development") console.log(resultsSpotify, resultsYouTube) const tracksSpotify = resultsSpotify.tracks.slice(0, 5).map(t => ({ name: `Spotify: ${`${t.title} - ${t.author} (${t.duration})`.length > 75 ? `${`${t.title} - ${t.author}`.substring(0, 75)}... (${t.duration})` : `${t.title} - ${t.author} (${t.duration})`}`, diff --git a/src/events/client/guildMemberAdd.ts b/src/events/client/guildMemberAdd.ts index c8da743..d142d76 100644 --- a/src/events/client/guildMemberAdd.ts +++ b/src/events/client/guildMemberAdd.ts @@ -1,6 +1,6 @@ import { Events, EmbedBuilder, ChannelType } 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" export const name = Events.GuildMemberAdd @@ -30,10 +30,11 @@ export async function execute(member: GuildMember) { return } + const guildLocale = await getGuildLocale(guild.id) const embed = new EmbedBuilder() .setColor(guild.members.me.displayHexColor) - .setTitle(t(guild.preferredLocale, "welcome.title", { username: member.user.username })) - .setDescription(t(guild.preferredLocale, "welcome.description", { memberCount: guild.memberCount.toString() })) + .setTitle(t(guildLocale, "welcome.title", { username: member.user.username })) + .setDescription(t(guildLocale, "welcome.description", { memberCount: guild.memberCount.toString() })) .setThumbnail(member.user.avatarURL()) .setTimestamp(new Date()) diff --git a/src/events/client/guildMemberRemove.ts b/src/events/client/guildMemberRemove.ts index ff703c7..edec633 100644 --- a/src/events/client/guildMemberRemove.ts +++ b/src/events/client/guildMemberRemove.ts @@ -1,6 +1,6 @@ import { Events } 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 function execute(member: GuildMember) { @@ -15,8 +15,9 @@ export function execute(member: GuildMember) { const channel = guild.channels.cache.get("1091140609139560508") if (!channel) return - await channel.setName(t(guild.preferredLocale, "salonpostam.update.loading")) - await channel.setName(t(guild.preferredLocale, "salonpostam.update.members_updated", { count: i.toString() })) + const guildLocale = await getGuildLocale(guild.id) + await channel.setName(t(guildLocale, "salonpostam.update.loading")) + await channel.setName(t(guildLocale, "salonpostam.update.members_updated", { count: i.toString() })) }).catch(console.error) } } diff --git a/src/events/client/guildMemberUpdate.ts b/src/events/client/guildMemberUpdate.ts index b953313..9110704 100644 --- a/src/events/client/guildMemberUpdate.ts +++ b/src/events/client/guildMemberUpdate.ts @@ -1,6 +1,6 @@ import { Events, EmbedBuilder, ChannelType } 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" export const name = Events.GuildMemberUpdate @@ -24,10 +24,11 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) { if (!hadRole && hasRole) { if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return } + const guildLocale = await getGuildLocale(guild.id) const embed = new EmbedBuilder() .setColor(guild.members.me.displayHexColor) - .setTitle(t(guild.preferredLocale, "boost.new_boost_title", { username: newMember.user.username })) - .setDescription(t(guild.preferredLocale, "boost.new_boost_description", { count: guild.premiumSubscriptionCount?.toString() ?? "0" })) + .setTitle(t(guildLocale, "boost.new_boost_title", { username: newMember.user.username })) + .setDescription(t(guildLocale, "boost.new_boost_description", { count: guild.premiumSubscriptionCount?.toString() ?? "0" })) .setThumbnail(newMember.user.avatarURL()) .setTimestamp(new Date()) diff --git a/src/events/client/ready.ts b/src/events/client/ready.ts index 6a3f1e6..2895c4d 100644 --- a/src/events/client/ready.ts +++ b/src/events/client/ready.ts @@ -7,7 +7,7 @@ import { connect } from "mongoose" import type { Document } from "mongoose" import { playerDisco, playerReplay } from "@/utils/player" import { twitchClient, listener, onlineSub, offlineSub, startStreamWatching } from "@/utils/twitch" -import { logConsole } from "@/utils/console" +import { logConsole, logConsoleError } from "@/utils/console" import type { GuildPlayer, Disco, GuildTwitch, GuildFbx } from "@/types/schemas" import * as Freebox from "@/utils/freebox" import dbGuildInit from "@/utils/dbGuildInit" @@ -19,8 +19,10 @@ export async function execute(client: Client) { logConsole('discordjs', 'ready', { tag: client.user?.tag ?? "unknown" }) client.user?.setActivity("some bangers...", { type: ActivityType.Listening }) - await useMainPlayer().extractors.register(SpotifyExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Spotify' }) }).catch(console.error) - await useMainPlayer().extractors.register(YoutubeiExtractor, {}).then(() => { logConsole('discord_player', 'extractor_loaded', { extractor: 'Youtube' }) }).catch(console.error) + const player = useMainPlayer() + 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}` await connect(mongo_url).catch(console.error) @@ -95,8 +97,7 @@ export async function execute(client: Client) { startStreamWatching(guild.id, streamer.twitchUserId, user.name, streamer.messageId) logConsole('twitch', 'ready.monitoring_restored', { guild: guild.name, userName: user.name }) } catch (error) { - logConsole('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name }) - console.error(error) + logConsoleError('twitch', 'ready.message_not_found', { guild: guild.name, userName: user.name }, error as Error) await cleanupMessageId(guildProfile, streamer.twitchUserId) } } @@ -131,9 +132,8 @@ async function cleanupMessageId(guildProfile: Document, twitchUserId: string) { guildProfile.set("guildTwitch", dbData) guildProfile.markModified("guildTwitch") - await guildProfile.save() + await guildProfile.save().catch(console.error) } catch (error) { - logConsole('twitch', 'ready.cleanup_error', { userId: twitchUserId }) - console.error(error) + logConsoleError('twitch', 'ready.cleanup_error', { userId: twitchUserId }, error as Error) } } diff --git a/src/events/player/audioTrackAdd.ts b/src/events/player/audioTrackAdd.ts index 6477e3c..768ea9e 100644 --- a/src/events/player/audioTrackAdd.ts +++ b/src/events/player/audioTrackAdd.ts @@ -1,11 +1,14 @@ import type { GuildQueue, Track } from "discord-player" import type { PlayerMetadata } from "@/types/player" -import { t } from "@/utils/i18n" +import { t, getGuildLocale } from "@/utils/i18n" export const name = "audioTrackAdd" export async function execute(queue: GuildQueue, track: Track) { // Emitted when the player adds a single song to its queue if (!queue.metadata.channel) return - if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added", { title: track.title }) }) + 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 }) }) + } } diff --git a/src/events/player/audioTracksAdd.ts b/src/events/player/audioTracksAdd.ts index 3bd0da7..16f2881 100644 --- a/src/events/player/audioTracksAdd.ts +++ b/src/events/player/audioTracksAdd.ts @@ -1,10 +1,14 @@ import type { GuildQueue, Track } from "discord-player" import type { PlayerMetadata } from "@/types/player" -import { t } from "@/utils/i18n" +import { t, getGuildLocale } from "@/utils/i18n" export const name = "audioTracksAdd" export async function execute(queue: GuildQueue, track: Track[]) { // Emitted when the player adds multiple songs to its queue if (!queue.metadata.channel) return - if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added_playlist", { count: track.length.toString() }) }) + + 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() }) }) + } } diff --git a/src/events/player/disconnect.ts b/src/events/player/disconnect.ts index d6d5153..2c2514d 100644 --- a/src/events/player/disconnect.ts +++ b/src/events/player/disconnect.ts @@ -1,7 +1,7 @@ import type { GuildQueue } from "discord-player" import type { PlayerMetadata } from "@/types/player" import { stopProgressSaving } from "@/utils/player" -import { t } from "@/utils/i18n" +import { t, getGuildLocale } from "@/utils/i18n" export const name = "disconnect" export async function execute(queue: GuildQueue) { @@ -9,5 +9,9 @@ export async function execute(queue: GuildQueue) { await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") if (!queue.metadata.channel) return - if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.disconnect") }) + + if ("send" in queue.metadata.channel) { + const guildLocale = await getGuildLocale(queue.guild.id) + return queue.metadata.channel.send({ content: t(guildLocale, "player.disconnect") }) + } } diff --git a/src/events/player/emptyChannel.ts b/src/events/player/emptyChannel.ts index 7dd8bb7..676f83c 100644 --- a/src/events/player/emptyChannel.ts +++ b/src/events/player/emptyChannel.ts @@ -1,7 +1,7 @@ import type { GuildQueue } from "discord-player" import type { PlayerMetadata } from "@/types/player" import { stopProgressSaving } from "@/utils/player" -import { t } from "@/utils/i18n" +import { t, getGuildLocale } from "@/utils/i18n" export const name = "emptyChannel" export async function execute(queue: GuildQueue) { @@ -10,5 +10,9 @@ export async function execute(queue: GuildQueue) { await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") if (!queue.metadata.channel) return - if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.leaving_empty_channel") }) + + 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") }) + } } diff --git a/src/events/player/emptyQueue.ts b/src/events/player/emptyQueue.ts index 5ffb8e7..d515809 100644 --- a/src/events/player/emptyQueue.ts +++ b/src/events/player/emptyQueue.ts @@ -1,7 +1,7 @@ import type { GuildQueue } from "discord-player" import type { PlayerMetadata } from "@/types/player" import { stopProgressSaving } from "@/utils/player" -import { t } from "@/utils/i18n" +import { t, getGuildLocale } from "@/utils/i18n" export const name = "emptyQueue" export async function execute(queue: GuildQueue) { @@ -9,5 +9,9 @@ export async function execute(queue: GuildQueue) { await stopProgressSaving(queue.guild.id, queue.player.client.user?.id ?? "") if (!queue.metadata.channel) return - if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.queue_empty") }) + + if ("send" in queue.metadata.channel) { + const guildLocale = await getGuildLocale(queue.guild.id) + return queue.metadata.channel.send({ content: t(guildLocale, "player.queue_empty") }) + } } diff --git a/src/events/player/playerSkip.ts b/src/events/player/playerSkip.ts index eb58b8a..d28db15 100644 --- a/src/events/player/playerSkip.ts +++ b/src/events/player/playerSkip.ts @@ -1,12 +1,14 @@ import type { GuildQueue, Track } from "discord-player" import type { PlayerMetadata } from "@/types/player" -import { t } from "@/utils/i18n" +import { t, getGuildLocale } from "@/utils/i18n" export const name = "playerSkip" export async function execute(queue: GuildQueue, track: Track) { // Emitted when the audio player fails to load the stream for a song if (!queue.metadata.channel) return - if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ - content: t(queue.guild.preferredLocale, "player.track_skipped", { title: track.title, author: track.author }) - }) + + 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 }) }) + } } diff --git a/src/events/player/playerStart.ts b/src/events/player/playerStart.ts index f8a7d65..3b91068 100644 --- a/src/events/player/playerStart.ts +++ b/src/events/player/playerStart.ts @@ -1,10 +1,14 @@ import type { GuildQueue, Track } from "discord-player" import type { PlayerMetadata } from "@/types/player" -import { t } from "@/utils/i18n" +import { t, getGuildLocale } from "@/utils/i18n" export const name = "playerStart" export async function execute(queue: GuildQueue, track: Track) { // Emitted when the player starts to play a song if (!queue.metadata.channel) return - if ("send" in queue.metadata.channel) await queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.now_playing", { title: track.title, author: track.author }) }) + + 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 }) }) + } } diff --git a/src/locales/en.json b/src/locales/en.json index 0437c5e..5386fbd 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -103,10 +103,6 @@ "description_enabled": "Disco mode is enabled! Visual and audio effects will be applied during music playback.", "description_disabled": "Disco mode is disabled. Enable it to enjoy visual and audio effects during music playback.", "channel_not_configured": "No channel configured", - "channel_not_found": "Channel not found", - "enabled": "✅ Enabled", - "disabled": "❌ Disabled", - "configure_channel": "Configure Channel", "configure_channel_first": "❌ Cannot enable Disco mode! Please first configure a channel with the **Configure Channel** button.", "effects_applied": "Disco effects will be applied in {channel}.", "select_channel": "Please select the channel where to apply Disco effects:", @@ -187,11 +183,6 @@ "message_not_found": "Message not found for {userName}, cleaning up messageId", "stream_offline_cleanup": "Offline stream detected for {userName}, cleaning up messageId", "cleanup_error": "Error while cleaning up messageId for {userId}" - }, - "logs": { - "user_fetch_error": "Error while fetching user for ID {userId}", - "listener_removed": "Listener removed for {streamerName} (ID {userId})", - "listener_removal_error": "Error while removing listener for {streamerName}" } }, "amp": { @@ -422,6 +413,9 @@ "select_streamer_remove": "Select a streamer to remove" } }, + "locale": { + "updated": "✅ Server language updated from **{oldLanguage}** to **{newLanguage}**!" + }, "database": { "owner_only": "This command can only be used by the bot owner!", "server_only": "This command must be used in a server!", @@ -477,7 +471,8 @@ "debug": "[Discord-Player] Debug - Player debug event: {message}", "disco": { "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Channel is not configured!", - "channel_not_found": "[Discord-Player] PlayerDisco - {guild} No channel found with id {channelId}" + "channel_not_found": "[Discord-Player] PlayerDisco - {guild} No channel found with id {channelId}", + "general_error": "[Discord-Player] Disco - General disco module error" }, "progress_saving": { "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})", "error": "[Discord-Player] ProgressSaving - Error saving progress for guild {guildId} (bot {botId})", "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": { @@ -494,7 +497,8 @@ "error": "[Mongoose] An error occurred with the database connection: {message}", "event_triggered": "[Mongoose] Event {event} triggered", "guild_init": "[Mongoose] Initializing guild profile for {name} ({id})", - "guild_create": "[Mongoose] GuildCreate - Database data for new guild \"{name}\" successfully initialized!" + "guild_create": "[Mongoose] GuildCreate - Database data for new guild \"{name}\" successfully initialized!", + "locale_fetch_error": "[Mongoose] Error fetching guild locale for {guildId}" }, "twitch": { "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})", "message_id_not_found": "[Twitch] StreamWatching - {guild} Message ID not found for {streamer} (ID {id})", "user_fetch_error": "[Twitch] Error fetching user for ID {id}", - "user_fetch_error_detailed": "[Twitch] Error while fetching user for ID {id}", "starting_listener_ngrok": "[Twitch] Starting listener with ngrok...", - "user_fetch_error_buttons": "[Twitch] Error fetching user for ID {id} in buttons/selectmenu", - "listener_removal_error": "[Twitch] Error removing listener for {streamerName}" + "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": { - "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" } } } diff --git a/src/locales/fr.json b/src/locales/fr.json index c720ac9..e470b35 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -183,11 +183,6 @@ "message_not_found": "Message introuvable pour {userName}, nettoyage du messageId", "stream_offline_cleanup": "Stream hors ligne détecté pour {userName}, nettoyage du messageId", "cleanup_error": "Erreur lors du nettoyage du messageId pour {userId}" - }, - "logs": { - "user_fetch_error": "Erreur lors de la récupération de l'utilisateur pour l'ID {userId}", - "listener_removed": "Listener supprimé pour {streamerName} (ID {userId})", - "listener_removal_error": "Erreur lors de la suppression du listener pour {streamerName}" } }, "amp": { @@ -418,6 +413,9 @@ "select_streamer_remove": "Sélectionner un streamer à supprimer" } }, + "locale": { + "updated": "✅ Langue du serveur mise à jour de **{oldLanguage}** vers **{newLanguage}** !" + }, "database": { "owner_only": "Cette commande ne peut être utilisée que par le propriétaire du bot !", "server_only": "Cette commande doit être utilisée sur un serveur !", @@ -448,8 +446,7 @@ "button_error": "[DiscordJS] InteractionCreate - Erreur lors du clic sur {id}", "selectmenu_not_found": "[DiscordJS] InteractionCreate - Aucun SelectMenu avec l'id {id} trouvé.", "selectmenu_used": "[DiscordJS] InteractionCreate - SelectMenu '{id}' utilisé par {user}", - "selectmenu_error": "[DiscordJS] InteractionCreate - Erreur lors de l'utilisation de {id}", - "selectmenu_invalid_type": "[DiscordJS] InteractionCreate - Type de SelectMenu invalide pour {id} reçu '{type}'" + "selectmenu_error": "[DiscordJS] InteractionCreate - Erreur lors de l'utilisation de {id}" }, "error": "[DiscordJS] Error - Une erreur s'est produite : {message}", "boost": { @@ -474,7 +471,8 @@ "debug": "[Discord-Player] Debug - Événement de débogage du lecteur : {message}", "disco": { "channel_not_configured": "[Discord-Player] PlayerDisco - {guild} Le canal n'est pas configuré !", - "channel_not_found": "[Discord-Player] PlayerDisco - {guild} Aucun canal trouvé avec l'id {channelId}" + "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": { "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})", "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 !" + }, + "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": { @@ -491,7 +497,8 @@ "error": "[Mongoose] Une erreur s'est produite avec la connexion à la base de données : {message}", "event_triggered": "[Mongoose] Événement {event} déclenché", "guild_init": "[Mongoose] Initialisation du profil de serveur pour {name} ({id})", - "guild_create": "[Mongoose] GuildCreate - Données de base pour le nouveau serveur \"{name}\" initialisées avec succès !" + "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": { "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})", "message_id_not_found": "[Twitch] StreamWatching - {guild} ID de message non trouvé pour {streamer} (ID {id})", "user_fetch_error": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id}", - "user_fetch_error_detailed": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id}", "starting_listener_ngrok": "[Twitch] Démarrage du listener avec ngrok...", - "user_fetch_error_buttons": "[Twitch] Erreur lors de la récupération de l'utilisateur pour l'ID {id} dans buttons/selectmenu", - "listener_removal_error": "[Twitch] Erreur lors de la suppression du listener pour {streamerName}" + "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": { - "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" } } } diff --git a/src/schemas/guild.ts b/src/schemas/guild.ts index 51ab4f4..62e5eda 100644 --- a/src/schemas/guild.ts +++ b/src/schemas/guild.ts @@ -5,6 +5,7 @@ const guildSchema = new Schema({ guildId: { type: String, required: true }, guildName: { type: String, required: true }, guildIcon: { type: String, required: true }, + guildLocale: { type: String, required: true }, guildPlayer: { instances: [{ botId: { type: String, required: true }, diff --git a/src/selectmenus/twitch/streamer_remove.ts b/src/selectmenus/twitch/streamer_remove.ts index 4e22138..50246da 100644 --- a/src/selectmenus/twitch/streamer_remove.ts +++ b/src/selectmenus/twitch/streamer_remove.ts @@ -26,7 +26,7 @@ export async function execute(interaction: StringSelectMenuInteraction) { const user = await twitchClient.users.getUserById(twitchUserId) if (user) streamerName = user.displayName } catch { - logConsole('twitch', 'user_fetch_error_buttons', { id: twitchUserId }) + logConsole('twitch', 'user_fetch_error', { id: twitchUserId }) } // Supprimer le streamer diff --git a/src/types/schemas.ts b/src/types/schemas.ts index 6c53587..05793e8 100644 --- a/src/types/schemas.ts +++ b/src/types/schemas.ts @@ -5,6 +5,7 @@ export interface GuildSchema { guildId: string guildName: string guildIcon: string + guildLocale: string guildPlayer: GuildPlayer guildAmp: GuildAmp guildFbx: GuildFbx diff --git a/src/utils/dbGuildInit.ts b/src/utils/dbGuildInit.ts index 9bf0004..69b941c 100644 --- a/src/utils/dbGuildInit.ts +++ b/src/utils/dbGuildInit.ts @@ -11,6 +11,7 @@ export default async (guild: Guild) => { guildId: guild.id, guildName: guild.name, guildIcon: guild.iconURL() ?? "None", + guildLocale: 'fr', guildPlayer: { disco: { enabled: false } }, diff --git a/src/utils/freebox.ts b/src/utils/freebox.ts index c5a6a55..0dfc93b 100644 --- a/src/utils/freebox.ts +++ b/src/utils/freebox.ts @@ -9,6 +9,7 @@ import type { } from "@/types/freebox" import type { GuildFbx } from "@/types/schemas" import { t } from "@/utils/i18n" +import { logConsole } from "@/utils/console" const app: TokenRequest = { app_id: "fr.angels-dev.tamiseur", @@ -123,7 +124,7 @@ export const Timer = { // Stocker les références des timers activeTimers.set(guildId, { morning: morningTimer, night: nightTimer }) - console.log(`[Freebox LCD] Timer programmé pour ${guildId} - Allumage: ${nextMorning.toLocaleString()}, Extinction: ${nextNight.toLocaleString()}`) + logConsole('freebox', 'timer_scheduled', { guildId, nextMorning: nextMorning.toLocaleString(), nextNight: nextNight.toLocaleString() }) }, // 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) // Si l'heure cible est déjà passée aujourd'hui, programmer pour demain - if (target <= now) { - target.setDate(target.getDate() + 1) - } - + if (target <= now) target.setDate(target.getDate() + 1) return target }, @@ -145,7 +143,7 @@ export const Timer = { if (timers.morning) clearTimeout(timers.morning) if (timers.night) clearTimeout(timers.night) 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) { 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 async controlLeds(guildId: string, dbDataFbx: GuildFbx, enabled: boolean) { - if (!dbDataFbx.host || !dbDataFbx.appToken) { - console.error(`[Freebox LCD] Configuration manquante pour le serveur ${guildId}`) - return - } + if (!dbDataFbx.host || !dbDataFbx.appToken) { logConsole('freebox', 'missing_configuration', { guildId }); return } try { // Obtenir le challenge const challengeData = await Login.Challenge(dbDataFbx.host) as APIResponseData - if (!challengeData.success) { console.error(`[Freebox LCD] Erreur lors de la récupération du challenge pour ${guildId}`); return } + if (!challengeData.success) { logConsole('freebox', 'challenge_error', { guildId }); return } 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 const password = crypto.createHmac("sha1", dbDataFbx.appToken).update(challenge).digest("hex") const sessionData = await Login.Session(dbDataFbx.host, password) as APIResponseData - if (!sessionData.success) { console.error(`[Freebox LCD] Erreur lors de la création de la session pour ${guildId}`); return } + if (!sessionData.success) { logConsole('freebox', 'session_error', { guildId }); return } 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 const lcdData = await Set.LcdConfig(dbDataFbx.host, sessionToken, { led_strip_enabled: enabled }) as APIResponseData - if (!lcdData.success) { console.error(`[Freebox LCD] Erreur lors du contrôle des LEDs pour ${guildId}:`, lcdData); return } + if (!lcdData.success) { logConsole('freebox', 'leds_control_error', { guildId }); return } - console.log(`[Freebox LCD] LEDs ${enabled ? 'allumées' : 'éteintes'} avec succès pour ${guildId}`) - } catch (error) { - console.error(`[Freebox LCD] Erreur lors du contrôle des LEDs pour ${guildId}:`, error) + logConsole('freebox', 'leds_success', { status: enabled ? 'allumées' : 'éteintes', guildId }) + } catch { + logConsole('freebox', 'leds_error', { guildId }) } } } diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index f079343..50c6d5a 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -1,6 +1,8 @@ import type { Locale } from "discord.js" import frLocale from "@/locales/fr.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 const DEFAULT_LOCALE = process.env.DEFAULT_LOCALE ?? 'fr' @@ -11,6 +13,21 @@ type LocaleData = Record type ReplacementParams = Record 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 { + 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 const frLocaleData = frLocale 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 { + 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 { + const locale = await getLocaleForContext(interaction.guild?.id ?? null, interaction.locale) + return t(locale, key, params) +} + // Export des constantes de locale export { DEFAULT_LOCALE, CONSOLE_LOCALE } diff --git a/src/utils/player.ts b/src/utils/player.ts index 2359124..01214d2 100644 --- a/src/utils/player.ts +++ b/src/utils/player.ts @@ -5,8 +5,8 @@ import type { GuildPlayer, Disco } from "@/types/schemas" import type { PlayerMetadata } from "@/types/player" import uptime from "./uptime" import dbGuild from "@/schemas/guild" -import { t } from "./i18n" -import { logConsole } from "./console" +import { t, getGuildLocale } from "./i18n" +import { logConsole, logConsoleError } from "./console" const progressIntervals = new Map() @@ -25,7 +25,7 @@ export function startProgressSaving(guildId: string, botId: string) { const interval = setInterval(async () => { try { 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 }) if (!guildProfile) { await stopProgressSaving(guildId, botId); return } @@ -50,8 +50,7 @@ export function startProgressSaving(guildId: string, botId: string) { guildProfile.markModified("guildPlayer") await guildProfile.save().catch(console.error) } catch (error) { - logConsole('discord_player', 'progress_saving.error', { guildId, botId }) - console.error(error) + logConsoleError('discord_player', 'progress_saving.error', { guildId, botId }, error as Error) await stopProgressSaving(guildId, botId) } }, 3000) @@ -127,12 +126,13 @@ export async function playerReplay(client: Client, dbData: GuildPlayer) { }) 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 + const guildLocale = await getGuildLocale(queue.guild.id) 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 entry = queue.tasksQueue.acquire() @@ -143,9 +143,9 @@ export async function playerReplay(client: Client, dbData: GuildPlayer) { if (!queue.isPlaying()) await queue.node.play() if (instance.replay.progress) await queue.node.seek(instance.replay.progress) startProgressSaving(queue.guild.id, botId) - await textChannel.send(t(queue.guild.preferredLocale, "player.music_restarted")) + 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() } } @@ -164,7 +164,8 @@ export async function playerDisco(client: Client, guild: Guild, dbData: Disco) { 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}` }) 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] }) } catch (error) { - console.error(error) + logConsoleError('discord_player', 'disco.general_error', {}, error as Error) return "clear" } } @@ -199,7 +200,7 @@ export async function playerEdit(interaction: ButtonInteraction) { 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 queue = useQueue(guild.id) diff --git a/src/utils/twitch.ts b/src/utils/twitch.ts index e08ae5b..1982b5e 100644 --- a/src/utils/twitch.ts +++ b/src/utils/twitch.ts @@ -2,7 +2,7 @@ const clientId = process.env.TWITCH_APP_ID const clientSecret = process.env.TWITCH_APP_SECRET if (!clientId || !clientSecret) { - console.warn(chalk.red("[Twitch] Missing TWITCH_APP_ID or TWITCH_APP_SECRET in environment variables!")) + logConsole('twitch', 'missing_credentials') process.exit(1) } @@ -14,12 +14,11 @@ import { NgrokAdapter } from "@twurple/eventsub-ngrok" import type { EventSubStreamOnlineEvent, EventSubStreamOfflineEvent } from "@twurple/eventsub-base" import { EmbedBuilder, ChannelType, ComponentType, ButtonBuilder, ButtonStyle, Locale } from "discord.js" import type { Client, Guild } from "discord.js" -import chalk from "chalk" import discordClient from "@/index" import type { GuildTwitch } from "@/types/schemas" import dbGuild from "@/schemas/guild" -import { t } from "@/utils/i18n" -import { logConsole } from "@/utils/console" +import { t, getGuildLocale } from "@/utils/i18n" +import { logConsole, logConsoleError } from "@/utils/console" // Twurple API client setup const authProvider = new AppTokenAuthProvider(clientId, clientSecret) @@ -35,7 +34,7 @@ if (process.env.NODE_ENV === "development") { const hostName = process.env.TWURPLE_HOSTNAME ?? "localhost" 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) }) } @@ -45,26 +44,30 @@ listener.start() // Twurple subscriptions callback functions export const onlineSub = async (event: EventSubStreamOnlineEvent) => { - console.log(chalk.magenta(`[Twitch] Stream from ${event.broadcasterName} (ID ${event.broadcasterId}) is now online, sending Discord messages...`)) + logConsole('twitch', 'stream_online', { streamer: event.broadcasterName, id: event.broadcasterId }) const results = await Promise.allSettled(discordClient.guilds.cache.map(async guild => { + const processingKey = `${guild.id}-${event.broadcasterId}` 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) - 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 - 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) - 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] }) - 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 @@ -74,18 +77,20 @@ export const onlineSub = async (event: EventSubStreamOnlineEvent) => { startStreamWatching(guild.id, event.broadcasterId, event.broadcasterName, message.id) } catch (error) { - console.log(chalk.magenta(`[Twitch] Error processing guild ${guild.name}`)) - console.error(error) + processingStreamers.delete(processingKey) + logConsoleError('twitch', 'error_processing_guild', { name: guild.name }, error as Error) + } finally { + processingStreamers.delete(processingKey) } })) 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) => { - 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 stopStreamWatching(guild.id, event.broadcasterId, event.broadcasterName) @@ -95,8 +100,11 @@ export const offlineSub = async (event: EventSubStreamOfflineEvent) => { // Stream upadting intervals const streamIntervals = new Map() +// Tracking des streamers en cours de traitement pour éviter les doublons +const processingStreamers = new Set() + 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}` if (streamIntervals.has(key)) { @@ -109,21 +117,19 @@ export function startStreamWatching(guildId: string, streamerId: string, streame try { const guild = await discordClient.guilds.fetch(guildId) const notification = await generateNotification(guild, streamerId, streamerName) - if (notification.status !== "ok") { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Notification generation failed with status: ${notification.status}`)); return } + if (notification.status !== "ok") { logConsole('twitch', 'notification_failed', { guild: guild.name, status: notification.status }); return } 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 { const message = await channel.messages.fetch(messageId) await message.edit({ content, embeds: [embed] }) } catch (error) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Error editing message for ${streamerName} (ID ${streamerId})`)) - console.error(error) + logConsoleError('twitch', 'error_editing_message', { guild: guild.name, streamer: streamerName, id: streamerId }, error as Error) } } catch (error) { - console.log(chalk.magenta(`[Twitch] StreamWatching - Erreur lors du visionnage de ${streamerName} (ID ${streamerId}) sur ${guildId}`)) - console.error(error) + logConsoleError('twitch', 'error_watching', { streamer: streamerName, id: streamerId, guildId }, error as Error) await stopStreamWatching(guildId, streamerId, streamerName) } }, 60000) @@ -132,7 +138,7 @@ export function startStreamWatching(guildId: string, streamerId: string, streame } 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}` if (streamIntervals.has(key)) { @@ -140,116 +146,121 @@ export async function stopStreamWatching(guildId: string, streamerId: string, st streamIntervals.delete(key) } - const guild = await discordClient.guilds.fetch(guildId) - const guildProfile = await dbGuild.findOne({ guildId: guild.id }) - if (!guildProfile) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)); return } - - const dbData = guildProfile.get("guildTwitch") as GuildTwitch - if (!dbData.enabled || !dbData.channelId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)); return } - - const channel = await guild.channels.fetch(dbData.channelId) - if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Channel with ID ${dbData.channelId} not found for Twitch notifications`)) - return - } - - const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) - if (!streamer) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer not found in guild for ${streamerName} (ID ${streamerId})`)); return } - - const messageId = streamer.messageId - if (!messageId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message ID not found for ${streamerName} (ID ${streamerId})`)); return } - - const user = await twitchClient.users.getUserById(streamerId) - if (!user) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} User data not found for ${streamerName} (ID ${streamerId})`)) - - let duration_string = "" - const stream = await user?.getStream() - if (!stream) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Stream data not found for ${streamerName} (ID ${streamerId})`)) - duration_string = t(guild.preferredLocale, "twitch.notification.offline.duration_unknown") - } else { - const duration = new Date().getTime() - stream.startDate.getTime() - const seconds = Math.floor(duration / 1000) - const minutes = Math.floor(seconds / 60) - const hours = Math.floor(minutes / 60) - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - duration_string = `${hours ? hours + "H " : ""}${minutes % 60 ? (minutes % 60) + "M " : ""}${seconds % 60 ? (seconds % 60) + "S" : ""}` - } - - let content = "" - if (!streamer.discordUserId) content = t(guild.preferredLocale, "twitch.notification.offline.everyone", { streamer: streamerName }) - else content = t(guild.preferredLocale, "twitch.notification.offline.everyone_with_mention", { discordId: streamer.discordUserId }) - - const embed = new EmbedBuilder() - .setColor("#6441a5") - .setAuthor({ - name: t(guild.preferredLocale, "twitch.notification.offline.author", { duration: duration_string }), - iconURL: user?.profilePictureUrl ?? "https://static-cdn.jtvnw.net/emoticons/v2/58765/static/light/3.0" - }) - .setTimestamp() - try { - const message = await channel.messages.fetch(messageId) - await message.edit({ content, embeds: [embed] }) + const guild = await discordClient.guilds.fetch(guildId) + const guildProfile = await dbGuild.findOne({ guildId: guild.id }) + if (!guildProfile) { logConsole('twitch', 'database_not_exist', { guild: guild.name }); return } + + const dbData = guildProfile.get("guildTwitch") as GuildTwitch + if (!dbData.enabled || !dbData.channelId) { logConsole('twitch', 'database_not_exist', { guild: guild.name }); return } + + const channel = await guild.channels.fetch(dbData.channelId) + if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) { + logConsole('twitch', 'channel_not_found', { guild: guild.name, channelId: dbData.channelId }) + return + } + + const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) + if (!streamer) { logConsole('twitch', 'streamer_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }); return } + + const messageId = streamer.messageId + if (!messageId) { logConsole('twitch', 'message_id_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }); return } + + const user = await twitchClient.users.getUserById(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 = "" + const stream = await user?.getStream() + if (!stream) { + logConsole('twitch', 'stream_data_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) + duration_string = t(guildLocale, "twitch.notification.offline.duration_unknown") + } else { + const duration = new Date().getTime() - stream.startDate.getTime() + const seconds = Math.floor(duration / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + duration_string = `${hours ? hours + "H " : ""}${minutes % 60 ? (minutes % 60) + "M " : ""}${seconds % 60 ? (seconds % 60) + "S" : ""}` + } + + let content = "" + if (!streamer.discordUserId) content = t(guildLocale, "twitch.notification.offline.everyone", { streamer: streamerName }) + else content = t(guildLocale, "twitch.notification.offline.everyone_with_mention", { discordId: streamer.discordUserId }) + + const embed = new EmbedBuilder() + .setColor("#6441a5") + .setAuthor({ + 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" + }) + .setTimestamp() + + try { + const message = await channel.messages.fetch(messageId) + await message.edit({ content, embeds: [embed] }) + } catch (error) { + logConsoleError('twitch', 'error_editing_message', { guild: guild.name, streamer: streamerName, id: streamerId }, error as Error) + } + + const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === streamerId) + if (streamerIndex === -1) return + + dbData.streamers[streamerIndex].messageId = "" + + guildProfile.set("guildTwitch", dbData) + guildProfile.markModified("guildTwitch") + await guildProfile.save().catch(console.error) } catch (error) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Error editing message for ${streamerName} (ID ${streamerId})`)) - console.error(error) + logConsoleError('twitch', 'stop_watching_error', { streamer: streamerName, id: streamerId, guildId }, error as Error) } - - const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === streamerId) - if (streamerIndex === -1) return - - dbData.streamers[streamerIndex].messageId = "" - - guildProfile.set("guildTwitch", dbData) - guildProfile.markModified("guildTwitch") - await guildProfile.save().catch(console.error) } async function generateNotification(guild: Guild, streamerId: string, streamerName: string) { const guildProfile = await dbGuild.findOne({ guildId: guild.id }) if (!guildProfile) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Database data does not exist !`)) + logConsole('twitch', 'database_not_exist', { guild: guild.name }) return { status: "noProfile" } } const dbData = guildProfile.get("guildTwitch") as GuildTwitch if (!dbData.enabled || !dbData.channelId) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Twitch module is not enabled or channel ID is missing`)) + logConsole('twitch', 'module_disabled', { guild: guild.name }) return { status: "disabled" } } const channel = await guild.channels.fetch(dbData.channelId) if ((channel?.type !== ChannelType.GuildText && channel?.type !== ChannelType.GuildAnnouncement)) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Channel with ID ${dbData.channelId} not found for Twitch notifications`)) + logConsole('twitch', 'channel_not_found', { guild: guild.name, channelId: dbData.channelId }) return { status: "noChannel" } } const streamer = dbData.streamers.find(s => s.twitchUserId === streamerId) if (!streamer) { - console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Streamer not found in guild for ${streamerName} (ID ${streamerId})`)) + logConsole('twitch', 'streamer_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) return { status: "noStreamer" } } const user = await twitchClient.users.getUserById(streamerId) - if (!user) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} User data not found for ${streamerName} (ID ${streamerId})`)) + if (!user) logConsole('twitch', 'user_data_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) const stream = await user?.getStream() - if (!stream) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Stream data not found for ${streamerName} (ID ${streamerId})`)) + if (!stream) logConsole('twitch', 'stream_data_not_found', { guild: guild.name, streamer: streamerName, id: streamerId }) + const guildLocale = await getGuildLocale(guild.id) let content = "" - if (!streamer.discordUserId) content = t(guild.preferredLocale, "twitch.notification.online.everyone", { streamer: user?.displayName ?? streamerName }) - else content = t(guild.preferredLocale, "twitch.notification.online.everyone_with_mention", { discordId: streamer.discordUserId }) + if (!streamer.discordUserId) content = t(guildLocale, "twitch.notification.online.everyone", { streamer: user?.displayName ?? streamerName }) + else content = t(guildLocale, "twitch.notification.online.everyone_with_mention", { discordId: streamer.discordUserId }) const embed = new EmbedBuilder() .setColor("#6441a5") - .setTitle(stream?.title ?? t(guild.preferredLocale, "twitch.notification.online.title_unknown")) + .setTitle(stream?.title ?? t(guildLocale, "twitch.notification.online.title_unknown")) .setURL(`https://twitch.tv/${streamerName}`) .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" }) - .setDescription(t(guild.preferredLocale, "twitch.notification.online.description", { + .setDescription(t(guildLocale, "twitch.notification.online.description", { game: stream?.gameName ?? "?", viewers: stream?.viewers.toString() ?? "?" }))