Compare commits

...

10 Commits

Author SHA1 Message Date
f1b5592045 Merge pull request 'Update develop' (#13) from master into develop
Reviewed-on: #13
2025-08-19 21:54:27 +02:00
af4e6e2e69 Merge pull request 'Intégration dernières modifications' (#12) from build-and-deploy into master
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m53s
Reviewed-on: #12
2025-08-19 21:53:13 +02:00
6d0c0145ee Ajout console.log débug play
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 7m13s
Build and Push Docker Image / build-and-push (pull_request) Successful in 12m16s
2025-06-11 20:34:42 +02:00
e714e94f85 Fix duplicate streamWatching, locale guild et console log/error
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 3m43s
2025-06-11 02:50:58 +02:00
0cc81d6430 Fix copy chown
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 6m34s
2025-06-10 15:05:12 +02:00
1dcb8c6826 Fix run dockerfile & service
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 2m23s
2025-06-10 14:56:29 +02:00
2b6870b861 Suppression packages build
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 13m19s
2025-06-10 14:02:35 +02:00
ceb7a74b11 Modif apk en apt
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-06-10 13:00:26 +02:00
fd4e17a754 Try fix dns avec alpine HS
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m33s
2025-06-10 12:50:14 +02:00
4ed73f7c72 Ajout ingress et service pour Twurple 2025-06-10 11:09:12 +02:00
35 changed files with 481 additions and 319 deletions

View File

@@ -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

View 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 }}

View 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 }}

View File

@@ -15,4 +15,17 @@ deployment:
# Memory: "500Mi"
requests:
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

View File

@@ -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

View File

@@ -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 })
}
}

View File

@@ -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 })
}
}

View File

@@ -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}\``)
}
}))

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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[]

View 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
})
}

View File

@@ -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 })

View File

@@ -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})`}`,

View File

@@ -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())

View File

@@ -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)
}
}

View File

@@ -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())

View File

@@ -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)
}
}

View File

@@ -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<PlayerMetadata>, track: Track) {
// Emitted when the player adds a single song to its queue
if (!queue.metadata.channel) return
if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added", { title: track.title }) })
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 }) })
}
}

View File

@@ -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<PlayerMetadata>, track: Track[]) {
// Emitted when the player adds multiple songs to its queue
if (!queue.metadata.channel) return
if ("send" in queue.metadata.channel) return queue.metadata.channel.send({ content: t(queue.guild.preferredLocale, "player.track_added_playlist", { count: track.length.toString() }) })
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() }) })
}
}

View File

@@ -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<PlayerMetadata>) {
@@ -9,5 +9,9 @@ export async function execute(queue: GuildQueue<PlayerMetadata>) {
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") })
}
}

View File

@@ -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<PlayerMetadata>) {
@@ -10,5 +10,9 @@ export async function execute(queue: GuildQueue<PlayerMetadata>) {
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") })
}
}

View File

@@ -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<PlayerMetadata>) {
@@ -9,5 +9,9 @@ export async function execute(queue: GuildQueue<PlayerMetadata>) {
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") })
}
}

View File

@@ -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<PlayerMetadata>, 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 }) })
}
}

View File

@@ -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<PlayerMetadata>, 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 }) })
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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 },

View File

@@ -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

View File

@@ -5,6 +5,7 @@ export interface GuildSchema {
guildId: string
guildName: string
guildIcon: string
guildLocale: string
guildPlayer: GuildPlayer
guildAmp: GuildAmp
guildFbx: GuildFbx

View File

@@ -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 }
},

View File

@@ -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<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
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<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
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<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}`)
} 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 })
}
}
}

View File

@@ -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<string, unknown>
type ReplacementParams = Record<string, string | number>
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
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<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 { DEFAULT_LOCALE, CONSOLE_LOCALE }

View File

@@ -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<string, NodeJS.Timeout>()
@@ -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)

View File

@@ -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<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) {
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() ?? "?"
}))