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

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