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

This commit is contained in:
2025-06-11 02:50:58 +02:00
parent 0cc81d6430
commit e714e94f85
31 changed files with 396 additions and 308 deletions

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 })
@@ -93,7 +94,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() }
}

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