Réécriture complète 4.0
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 6m16s

This commit is contained in:
2025-06-09 16:29:12 +02:00
parent f2c6388da6
commit ddd617317c
133 changed files with 8092 additions and 4332 deletions

View File

@@ -1,89 +1,57 @@
import axios from 'axios'
export interface LoginDetails {
username: string
password: string
remember?: boolean
otp?: string
}
import axios from "axios"
import type { AxiosResponse } from "axios"
import type { APIResponse } from "@/types"
import type { LoginDetails } from "@/types/amp"
export const ADSModule = {
async GetInstances(host: string, SESSIONID: string) {
return await axios.post(host + '/API/ADSModule/GetInstances', {
SESSIONID
}).then(response => {
if (!Array.isArray(response.data)) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
async GetInstances(host: string, SESSIONID: string) {
return axios.post(host + "/API/ADSModule/GetInstances", { SESSIONID })
.then((response: AxiosResponse<APIResponse>) => {
if (!Array.isArray(response.data)) return { status: "fail", data: response.data }
return { status: "success", data: response.data }
})
.catch((error: unknown) => { console.error(error); return { status: "error", data: error } })
},
async ManageInstance(host: string, SESSIONID: string, InstanceId: string) {
return await axios.post(host + '/API/ADSModule/ManageInstance', {
SESSIONID,
InstanceId
}).then(response => {
if (!response.data.result) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
return axios.post(host + "/API/ADSModule/ManageInstance", { SESSIONID, InstanceId })
.then((response: AxiosResponse<APIResponse>) => {
if (!response.data.result) return { status: "fail", data: response.data }
return { status: "success", data: response.data }
})
.catch((error: unknown) => { console.error(error); return { status: "error", data: error } })
},
async RestartInstance(host: string, SESSIONID: string, InstanceName: string) {
return await axios.post(host + '/API/ADSModule/RestartInstance', {
SESSIONID,
InstanceName
}).then(response => {
//if (!response.data.success) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
return axios.post(host + "/API/ADSModule/RestartInstance", { SESSIONID, InstanceName })
.then((response: AxiosResponse<APIResponse>) => {
//if (!response.data.success) return { status: "fail", data: response.data }
return { status: "success", data: response.data }
})
.catch((error: unknown) => { console.error(error); return { status: "error", data: error } })
},
async Servers(host: string, SESSIONID: string, InstanceId: string) {
return await axios.get(host + '/API/ADSModule/Servers', {
data: {
SESSIONID,
InstanceId
}
}).then(response => {
if (!response.data.result) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
return axios.get(host + "/API/ADSModule/Servers", { data: { SESSIONID, InstanceId } })
.then((response: AxiosResponse<APIResponse>) => {
if (!response.data.result) return { status: "fail", data: response.data }
return { status: "success", data: response.data }
})
.catch((error: unknown) => { console.error(error); return { status: "error", data: error } })
}
}
export const Core = {
async Login(host: string, details: LoginDetails) {
return await axios.post(host + '/API/Core/Login',
details
).then(response => {
if (!response.data.success) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
return axios.post(host + "/API/Core/Login", details)
.then((response: AxiosResponse<APIResponse>) => {
if (!response.data.success) return { status: "fail", data: response.data }
return { status: "success", data: response.data }
})
.catch((error: unknown) => { console.error(error); return { status: "error", data: error } })
}
}
export async function CheckSession(host: string, SESSIONID: string) {
return await axios.post(host + '/API/ADSModule/GetInstances', {
SESSIONID
}).then(response => {
if (!Array.isArray(response.data)) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
}
return axios.post(host + "/API/ADSModule/GetInstances", { SESSIONID })
.then((response: AxiosResponse<APIResponse>) => {
if (!Array.isArray(response.data)) return { status: "fail", data: response.data }
return { status: "success", data: response.data }
})
.catch((error: unknown) => { console.error(error); return { status: "error", data: error } })
}

54
src/utils/console.ts Normal file
View File

@@ -0,0 +1,54 @@
import chalk from "chalk"
import { t, CONSOLE_LOCALE } from "./i18n"
/**
* Fonction utilitaire pour les logs console localisés avec couleurs
* @param service - Le service (discordjs, discord_player, mongoose, twitch)
* @param key - La clé de traduction sans le préfixe "console.{service}."
* @param params - Les paramètres à remplacer dans le message
*/
export function logConsole(service: 'discordjs' | 'discord_player' | 'mongoose' | 'twitch' | 'freebox', key: string, params?: Record<string, string>) {
const locale = CONSOLE_LOCALE // Utilise la locale configurée pour les logs console
const fullKey = `console.${service}.${key}`
const message = t(locale, fullKey, params)
// Couleurs selon le service
switch (service) {
case 'discordjs':
console.log(chalk.blue(message))
break
case 'discord_player':
console.log(chalk.cyan(message))
break
case 'mongoose':
console.log(chalk.green(message))
break
case 'twitch':
console.log(chalk.magenta(message))
break
case 'freebox':
console.log(chalk.yellow(message))
break
default:
console.log(message)
}
}
/**
* Fonction pour les logs d'erreur console
* @param service - Le service
* @param key - La clé de traduction
* @param params - Les paramètres
* @param error - L'erreur à logger après le message
*/
export function logConsoleError(service: 'discordjs' | 'discord_player' | 'mongoose' | 'twitch' | 'freebox', key: string, params?: Record<string, string>, error?: Error) {
logConsole(service, key, params)
if (error) console.error(error)
}
/**
* Log conditionnel en mode développement
*/
export function logConsoleDev(service: 'discordjs' | 'discord_player' | 'mongoose' | 'twitch' | 'freebox', key: string, params?: Record<string, string>) {
if (process.env.NODE_ENV === "development") logConsole(service, key, params)
}

View File

@@ -1,98 +1,102 @@
import parseTorrent, { toMagnetURI } from 'parse-torrent'
import iconv from 'iconv-lite'
import axios from 'axios'
import path from 'path'
import fs from 'fs'
export interface Game {
name: string
link: string
}
import parseTorrent, { toMagnetURI } from "parse-torrent"
import axios from "axios"
import iconv from "iconv-lite"
import type { AxiosResponse } from "axios"
import type { Readable } from "stream"
import { createWriteStream, readFileSync } from "fs"
import { join } from "path"
import type { CrackGame } from "@/types"
const headers = {
h1: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"content-type": "application/x-www-form-urlencoded charset=UTF-8",
"x-requested-with": "XMLHttpRequest"
},
h2: {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
"accept": "text/html,application/xhtml+xml,application/xmlq=0.9,image/avif,image/webp,image/apng,*/*q=0.8,application/signed-exchangev=b3q=0.7",
"accept-language": "fr-FR,frq=0.9,en-USq=0.8,enq=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"sec-ch-ua": "\"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"110\", \"Opera GX\";v=\"96\"",
"sec-ch-ua": '"Not=A?Brand"v="8", "Chromium"v="110", "Opera GX"v="96"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"cookie": "online_fix_auth=gAAAAABkKM0s9WNLe_V6euTnJD7UQjppVty9B7OOyHBYOyVcbcejj8F6KveBcLxlf3mlx_vE7JEFPHlrpj-Aq6BFJyKPGzxds_wpcPV2MdXPyDGQLsz4mAvt3qgTgGg25MapWo_fSIOMiAAsF4Gv_uh4kUOiR_jgbHCZWJGPgpNQwU2HFFyvahYR6MzR7nYE9-fCmrev3obkRbro43vIVTTX4UyJMRHadrsY5Q-722TzinCZVmAuJfc=; dle_password=89465c26673e0199e5272e4730772c35; _ym_uid=1670534560361937997; _ym_d=1680394955; _ym_isad=2; dle_user_id=2619796; PHPSESSID=3v8sd281sr0n1n9f1p66q25sa2",
"cookie": "online_fix_auth=gAAAAABkKM0s9WNLe_V6euTnJD7UQjppVty9B7OOyHBYOyVcbcejj8F6KveBcLxlf3mlx_vE7JEFPHlrpj-Aq6BFJyKPGzxds_wpcPV2MdXPyDGQLsz4mAvt3qgTgGg25MapWo_fSIOMiAAsF4Gv_uh4kUOiR_jgbHCZWJGPgpNQwU2HFFyvahYR6MzR7nYE9-fCmrev3obkRbro43vIVTTX4UyJMRHadrsY5Q-722TzinCZVmAuJfc= dle_password=89465c26673e0199e5272e4730772c35 _ym_uid=1670534560361937997 _ym_d=1680394955 _ym_isad=2 dle_user_id=2619796 PHPSESSID=3v8sd281sr0n1n9f1p66q25sa2",
"Referer": "https://online-fix.me/",
"Referrer-Policy": "strict-origin-when-cross-origin"
}
}
export async function search(query: string) {
let body = await fetch("https://online-fix.me/engine/ajax/search.php", { headers: headers.h1, body: `query=${query}`, method: "POST" })
const body = await fetch("https://online-fix.me/engine/ajax/search.php", { headers: headers.h1, body: `query=${query}`, method: "POST" })
.then(response => response.arrayBuffer())
.then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), 'win1251') })
.then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), "win1251") })
.catch(console.error)
try {
if (!body) return
let matches = body.split('</div>')[1].split('<span class="seperator fastfullsearch">')[0].split('</a>')
let games = [] as Game[]
const matches = body.split("</div>")[1].split('<span class="seperator fastfullsearch">')[0].split("</a>")
const games = [] as CrackGame[]
matches.pop()
matches.forEach(async match => {
let name = match.split('"><span class="searchheading">')[1].split('</span>')[0].slice(0, -8)
let link = match.split('<a href="')[1].split('"><span class="searchheading">')[0]
for (const match of matches) {
const name = match.split('"><span class="searchheading">')[1].split("</span>")[0].slice(0, -8)
const link = match.split('<a href="')[1].split('"><span class="searchheading">')[0]
games.push({ name, link })
})
}
return games
} catch (error) { return error }
} catch (error) { console.error(error) }
}
export async function repo(game: Game) {
let body = await fetch(game.link, { headers: headers.h2, body: null, method: "GET" })
export async function repo(game: CrackGame) {
const body = await fetch(game.link, { headers: headers.h2, body: null, method: "GET" })
.then(response => response.arrayBuffer())
.then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), 'win1251') })
.then(arrayBuffer => { return iconv.decode(Buffer.from(arrayBuffer), "win1251") })
.catch(console.error)
try {
if (!body) return
let name = body.split('https://uploads.online-fix.me:2053/torrents/')[1].split('"')[0]
let url = `https://uploads.online-fix.me:2053/torrents/${name}`
const name = body.split("https://uploads.online-fix.me:2053/torrents/")[1].split('"')[0]
const url = `https://uploads.online-fix.me:2053/torrents/${name}`
return url
} catch (error) { console.error(error) }
}
export async function torrent(url: string) {
let response = await fetch(url, { headers: headers.h2, body: null, method: "GET" }).catch(console.error)
const response = await fetch(url, { headers: headers.h2, body: null, method: "GET" })
.catch(console.error)
try {
if (!response) return
let body = await response.text()
let file = body.split('<a href="')[2].split('">')[0]
const body = await response.text()
const file = body.split('<a href="')[2].split('">')[0]
return file
} catch (error) { console.error(error) }
}
export async function download(url: string, file: string) {
let filePath = path.join(__dirname, '../../public/cracks/', file)
let writer = fs.createWriteStream(filePath)
const filePath = join(__dirname, "@/public/cracks/", file)
const writer = createWriteStream(filePath)
try {
await axios({ url: url + file, method: 'GET', responseType: 'stream', headers: headers.h2 }).then(response => {
return new Promise((resolve, reject) => {
response.data.pipe(writer)
let error = null as unknown as Error
writer.on('error', err => { error = err; writer.close(); reject(err) })
writer.on('close', () => { if (!error) resolve(true) })
await axios({ url: url + file, method: "GET", responseType: "stream", headers: headers.h2 })
.then((response: AxiosResponse<Readable>) => {
return new Promise((resolve, reject) => {
response.data.pipe(writer)
writer.on("error", err => { writer.close(); reject(err) })
writer.on("close", () => { resolve(true) })
})
})
}).catch(console.error)
.catch(console.error)
return filePath
} catch (error) { console.error(error) }
}
export async function magnet(filePath: string) {
let torrentData = parseTorrent(fs.readFileSync(filePath))
let uri = toMagnetURI(torrentData)
export function magnet(filePath: string) {
const torrentData = parseTorrent(readFileSync(filePath))
const uri = toMagnetURI(torrentData)
return uri
}
}

View File

@@ -1,22 +1,26 @@
import { Guild } from 'discord.js'
import { Types } from 'mongoose'
import dbGuild from '../schemas/guild'
import { Guild } from "discord.js"
import { Types } from "mongoose"
import dbGuild from "@/schemas/guild"
import { logConsole } from "@/utils/console"
export default async (guild: Guild) => {
let guildProfile = new dbGuild({
logConsole('mongoose', 'guild_init', { name: guild.name, id: guild.id })
const guildProfile = new dbGuild({
_id: new Types.ObjectId(),
guildId: guild.id,
guildName: guild.name,
guildIcon: guild.iconURL() ?? 'None',
guildIcon: guild.iconURL() ?? "None",
guildPlayer: {
replay: { enabled: false },
disco: { enabled: false }
},
guildRss: { enabled: false, feeds: [] },
guildAmp: { enabled: false },
guildFbx: { enabled: false },
guildFbx: { enabled: false,
lcd: { enabled: false }
},
guildTwitch: { enabled: false }
})
await guildProfile.save().catch(console.error)
return guildProfile
}
}

View File

@@ -1,79 +1,192 @@
import axios from 'axios'
import https from 'https'
import axios from "axios"
import type { AxiosError } from "axios"
import crypto from "crypto"
import { MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, ButtonInteraction, Client } from "discord.js"
import type {
APIResponseData, APIResponseDataError, APIResponseDataVersion,
ConnectionStatus, GetChallenge, LcdConfig, OpenSession, RequestAuthorization, TokenRequest, TrackAuthorizationProgress
} from "@/types/freebox"
import type { GuildFbx } from "@/types/schemas"
import { t } from "@/utils/i18n"
export interface App {
app_id: string
app_name: string
app_version: string
device_name: string
const app: TokenRequest = {
app_id: "fr.angels-dev.tamiseur",
app_name: "Bot Tamiseur",
app_version: process.env.npm_package_version ?? "1.0.0",
device_name: "Bot Discord NodeJS",
}
export const Core = {
async Version(host: string, httpsAgent: https.Agent) {
let request = axios.get(host + '/api_version', { httpsAgent })
return await request.then(response => {
if (response.status !== 200) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
async Version(host: string) {
try {
const res = await axios.get<APIResponseDataVersion>(host + `/api_version`)
return res.data
} catch (error) { return (error as AxiosError<APIResponseDataError>).response?.data }
},
async Init(host: string, version: number, httpsAgent: https.Agent, app: App, trackId: string) {
async Init(host: string, trackId?: string) {
let request
if (trackId) request = axios.get(host + `/api/v${version}/login/authorize/` + trackId, { httpsAgent })
else request = axios.post(host + `/api/v${version}/login/authorize/`, app, { httpsAgent })
return await request.then(response => {
if (!response.data.success) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data.result }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
if (trackId) request = axios.get<APIResponseData<TrackAuthorizationProgress>>(host + `/api/v8/login/authorize/` + trackId)
else request = axios.post<APIResponseData<RequestAuthorization>>(host + `/api/v8/login/authorize/`, app)
try {
const res = await request
return res.data
} catch (error) { return (error as AxiosError<APIResponseDataError>).response?.data }
}
}
export const Login = {
async Challenge(host: string, version: number, httpsAgent: https.Agent) {
let request = axios.get(host + `/api/v${version}/login/`, { httpsAgent })
return await request.then(response => {
console.log(response.data)
if (response.status !== 200) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data.result }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
async Challenge(host: string) {
try {
const res = await axios.get<APIResponseData<GetChallenge>>(host + `/api/v8/login/`)
return res.data
} catch (error) { return (error as AxiosError<APIResponseDataError>).response?.data }
},
async Session(host: string, version: number, httpsAgent: https.Agent, app_id: string, password: string) {
let request = axios.post(host + `/api/v${version}/login/session/`, { app_id, password }, { httpsAgent })
return await request.then(response => {
console.log(response.data)
if (response.status !== 200) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data.result }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
async Session(host: string, password: string) {
try {
const res = await axios.post<APIResponseData<OpenSession>>(host + `/api/v8/login/session/`, { app_id: app.app_id, password })
return res.data
} catch (error) { return (error as AxiosError<APIResponseDataError>).response?.data }
}
}
export const Get = {
async Connection(host: string, version: number, httpsAgent: https.Agent, sessionToken: string) {
let request = axios.get(host + `/api/v${version}/connection/`, { httpsAgent, headers: { 'X-Fbx-App-Auth': sessionToken } })
return await request.then(response => {
console.log(response.data)
if (!response.data.success) return { status: 'fail', data: response.data }
return { status: 'success', data: response.data.result }
}).catch(error => {
console.error(error)
return { status: 'error', data: error }
})
async Connection(host: string, sessionToken: string) {
try {
const res = await axios.get<APIResponseData<ConnectionStatus>>(host + `/api/v11/connection/`, { headers: { "X-Fbx-App-Auth": sessionToken } })
return res.data
} catch (error) { return (error as AxiosError<APIResponseDataError>).response?.data }
},
async LcdConfig(host: string, sessionToken: string) {
try {
const res = await axios.get<APIResponseData<LcdConfig>>(host + `/api/v8/lcd/config/`, { headers: { "X-Fbx-App-Auth": sessionToken } })
return res.data
} catch (error) { return (error as AxiosError<APIResponseDataError>).response?.data }
}
}
}
export const Set = {
async LcdConfig(host: string, sessionToken: string, config: LcdConfig) {
try {
const res = await axios.put<APIResponseData<LcdConfig>>(host + `/api/v8/lcd/config/`, config, { headers: { "X-Fbx-App-Auth": sessionToken } })
return res.data
} catch (error) { return (error as AxiosError<APIResponseDataError>).response?.data }
}
}
export async function handleError(error: APIResponseDataError, interaction: ChatInputCommandInteraction | ButtonInteraction, reply = true) {
if (reply) return await interaction.reply({ content: t(interaction.locale, `freebox.error.${error.error_code}`), flags: MessageFlags.Ephemeral })
else return await interaction.followUp({ content: t(interaction.locale, `freebox.error.${error.error_code}`), flags: MessageFlags.Ephemeral })
}
// Stockage des timers actifs pour chaque guild
const activeTimers = new Map<string, { morning: NodeJS.Timeout | null, night: NodeJS.Timeout | null }>()
// Gestion du timer LCD
export const Timer = {
// Fonction pour programmer le timer LCD Freebox
schedule(client: Client, guildId: string, dbData: GuildFbx) {
const lcd = dbData.lcd
if (!lcd?.morningTime || !lcd.nightTime) return
// Nettoyer les anciens timers pour cette guild
Timer.clear(guildId)
const now = new Date()
const [morningHour, morningMinute] = lcd.morningTime.split(':').map(Number)
const [nightHour, nightMinute] = lcd.nightTime.split(':').map(Number)
// Calculer le prochain allumage (matin)
const nextMorning = new Date(now.getFullYear(), now.getMonth(), now.getDate(), morningHour, morningMinute, 0, 0)
if (nextMorning <= now) nextMorning.setDate(nextMorning.getDate() + 1)
// Calculer la prochaine extinction (nuit)
const nextNight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), nightHour, nightMinute, 0, 0)
if (nextNight <= now) nextNight.setDate(nextNight.getDate() + 1)
// Programmer l'allumage le matin
const morningDelay = nextMorning.getTime() - now.getTime()
const morningTimer = setTimeout(() => {
void Timer.controlLeds(guildId, dbData, true)
// Reprogrammer pour le cycle suivant
Timer.schedule(client, guildId, dbData)
}, morningDelay)
// Programmer l'extinction le soir
const nightDelay = nextNight.getTime() - now.getTime()
const nightTimer = setTimeout(() => {
void Timer.controlLeds(guildId, dbData, false)
// Note: pas besoin de reprogrammer ici car le timer du matin s'en charge
}, nightDelay)
// 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()}`)
},
// Fonction utilitaire pour calculer la prochaine occurrence d'une heure donnée
getNextOccurrence(now: Date, hour: number, minute: number): Date {
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)
}
return target
},
// Fonction pour nettoyer les timers d'une guild
clear(guildId: string) {
const timers = activeTimers.get(guildId)
if (timers) {
if (timers.morning) clearTimeout(timers.morning)
if (timers.night) clearTimeout(timers.night)
activeTimers.delete(guildId)
console.log(`[Freebox LCD] Timers nettoyés pour ${guildId}`)
}
},
// Fonction pour nettoyer tous les timers (utile lors de l'arrêt du bot)
clearAll() {
for (const [guildId] of activeTimers) {
Timer.clear(guildId)
}
console.log(`[Freebox LCD] Tous les timers ont été nettoyés`)
},
// 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
}
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 }
const challenge = challengeData.result.challenge
if (!challenge) { console.error(`[Freebox LCD] Challenge introuvable pour ${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 }
const sessionToken = sessionData.result.session_token
if (!sessionToken) { console.error(`[Freebox LCD] Token de session introuvable pour ${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 }
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)
}
}
}

View File

@@ -1,10 +0,0 @@
import { Client } from 'discord.js'
export default function (uptime: Client["uptime"]) {
if (!uptime) return '0J, 0H, 0M et 0S'
let days = Math.floor(uptime / 86400000)
let hours = Math.floor(uptime / 3600000) % 24
let minutes = Math.floor(uptime / 60000) % 60
let seconds = Math.floor(uptime / 1000) % 60
return `${days}J, ${hours}H, ${minutes}M et ${seconds}S`
}

111
src/utils/i18n.ts Normal file
View File

@@ -0,0 +1,111 @@
import type { Locale } from "discord.js"
import frLocale from "@/locales/fr.json"
import enLocale from "@/locales/en.json"
// Variables d'environnement pour les locales avec valeurs par défaut
const DEFAULT_LOCALE = process.env.DEFAULT_LOCALE ?? 'fr'
const CONSOLE_LOCALE = process.env.CONSOLE_LOCALE ?? 'en'
// Types pour l'internationalisation
type LocaleData = Record<string, unknown>
type ReplacementParams = Record<string, string | number>
type TranslationKey = string
// Conversion des imports en type LocaleData
const frLocaleData = frLocale as unknown as LocaleData
const enLocaleData = enLocale as unknown as LocaleData
// Locales supportées
const locales = {
'fr': frLocaleData,
'en-US': enLocaleData,
'en-GB': enLocaleData
} as const
type SupportedLocale = keyof typeof locales
/**
* Récupère une traduction basée sur la locale et la clé
*/
function getNestedValue(obj: LocaleData, path: string): string | undefined {
try {
const keys = path.split('.')
let current: unknown = obj
for (const key of keys) {
if (current !== null && typeof current === 'object' && key in current) current = (current as Record<string, unknown>)[key]
else return undefined
}
return typeof current === 'string' ? current : undefined
} catch { return undefined }
}
/**
* Remplace les paramètres dans une chaîne de caractères
* Exemple: "Hello {name}" avec {name: "World"} devient "Hello World"
*/
function replaceParams(text: string, params: ReplacementParams = {}): string {
return text.replace(/\{(\w+)\}/g, (match, key: string) => {
if (Object.prototype.hasOwnProperty.call(params, key)) return params[key].toString()
return match
})
}
/**
* Fonction principale de localisation
* @param locale - La locale Discord de l'utilisateur
* @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 t(locale: Locale | string, key: TranslationKey, params: ReplacementParams = {}): string {
// Détermine la locale à utiliser (par défaut de la config)
const supportedLocale: SupportedLocale = (Object.keys(locales).includes(locale)) ? locale as SupportedLocale : DEFAULT_LOCALE as SupportedLocale
// Récupère les données de locale
const localeData = locales[supportedLocale]
// Récupère la traduction
const translation = getNestedValue(localeData, key)
if (!translation) {
console.warn(`[Localization] Translation not found for key: ${key} in locale: ${supportedLocale}`)
return key // Retourne la clé si la traduction n'est pas trouvée
}
// Remplace les paramètres et retourne la traduction
return replaceParams(translation, params)
}
/**
* Fonction helper pour obtenir la locale française par défaut
*/
export function fr(key: TranslationKey, params: ReplacementParams = {}): string {
return t('fr', key, params)
}
/**
* Fonction helper pour obtenir la locale anglaise
*/
export function en(key: TranslationKey, params: ReplacementParams = {}): string {
return t('en-US', key, params)
}
/**
* Obtient les locales supportées pour une commande
* Utilisé pour les localisations des commandes slash
*/
export function getCommandLocalizations(baseKey: string) {
return {
fr: getNestedValue(frLocaleData, baseKey) ?? baseKey,
'en-US': getNestedValue(enLocaleData, baseKey) ?? baseKey,
'en-GB': getNestedValue(enLocaleData, baseKey) ?? baseKey
}
}
// Export des constantes de locale
export { DEFAULT_LOCALE, CONSOLE_LOCALE }
// Export des types pour utilisation externe
export type { TranslationKey, ReplacementParams, SupportedLocale }

View File

@@ -1,178 +1,118 @@
import { Client, TextChannel, CommandInteraction, Guild, GuildChannel, TextBasedChannel, VoiceChannel, EmbedBuilder, ButtonBuilder, ActionRowBuilder, ButtonInteraction } from 'discord.js'
import { useMainPlayer, useQueue } from 'discord-player'
import { Document } from 'mongoose'
import getUptime from './getUptime'
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, MessageFlags, ComponentType } from "discord.js"
import type { ButtonInteraction, Client, Guild, GuildChannel, Locale } from "discord.js"
import { useMainPlayer, useQueue } from "discord-player"
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"
type ChannelInferrable = {
channel: TextBasedChannel | VoiceChannel
guild?: Guild
}
const progressIntervals = new Map<string, NodeJS.Timeout>()
export class PlayerMetadata {
public constructor(public data: ChannelInferrable) {
if (data.channel.isDMBased()) { throw new Error('PlayerMetadata cannot be created from a DM') }
if (!data.channel) { throw new Error('PlayerMetadata can only be created from a channel') }
export function startProgressSaving(guildId: string, botId: string) {
if (!guildId || !botId) { logConsole('discord_player', 'progress_saving.missing_ids'); return }
logConsole('discord_player', 'progress_saving.start', { guildId, botId })
const key = `${guildId}-${botId}`
if (progressIntervals.has(key)) {
clearInterval(progressIntervals.get(key))
progressIntervals.delete(key)
}
public get channel() { return this.data.channel! }
public get guild() { return this.data.guild || (this.data.channel as GuildChannel).guild }
public static create(data: ChannelInferrable | CommandInteraction) {
if (data instanceof CommandInteraction) {
if (!data.inGuild()) { throw new Error('PlayerMetadata cannot be created from a DM') }
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const interval = setInterval(async () => {
try {
const queue = useQueue(guildId)
if (!queue || !queue.isPlaying() || !queue.currentTrack) { startProgressSaving(guildId, botId); return }
return new PlayerMetadata({ channel: data.channel!, guild: data.guild! })
}
return new PlayerMetadata(data);
}
}
const guildProfile = await dbGuild.findOne({ guildId })
if (!guildProfile) { await stopProgressSaving(guildId, botId); return }
export const bots = ['1065047326860783636', '1119343522059927684', '1119344050412204032', '1210714000321548329', '660961595006124052']
export const playerButtons = ['loop', 'pause', 'previous', 'resume', 'shuffle', 'skip', 'stop', 'volume_down', 'volume_up']
const dbData = guildProfile.get("guildPlayer") as GuildPlayer
dbData.instances ??= []
export async function playerDisco (client: Client, guildProfile: Document) {
try {
let guild = client.guilds.cache.get(guildProfile.get('guildId'))
if (!guild) {
clearInterval(client.disco.interval)
return 'clear'
}
const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId)
if (instanceIndex === -1) {
dbData.instances.push({ botId, replay: {
textChannelId: (queue.metadata as PlayerMetadata).channel?.id ?? "",
voiceChannelId: queue.connection?.joinConfig.channelId ?? "",
trackUrl: queue.currentTrack.url,
progress: queue.node.playbackTime
} })
} else {
dbData.instances[instanceIndex].replay.trackUrl = queue.currentTrack.url
dbData.instances[instanceIndex].replay.progress = queue.node.playbackTime
}
let dbData = guildProfile.get('guildPlayer.disco')
let queue = useQueue(guild.id)
if (queue) if (queue.isPlaying()) {
dbData['progress'] = queue.node.playbackTime.toString()
guildProfile.set('guildPlayer.disco', dbData)
guildProfile.markModified('guildPlayer.disco')
guildProfile.set("guildPlayer", dbData)
guildProfile.markModified("guildPlayer")
await guildProfile.save().catch(console.error)
} catch (error) {
logConsole('discord_player', 'progress_saving.error', { guildId, botId })
console.error(error)
await stopProgressSaving(guildId, botId)
}
}, 3000)
let channel = client.channels.cache.get(dbData.channelId) as TextChannel
if (!channel) {
console.log(`Aucun channel trouvé avec l'id \`${dbData.channelId}\`, veuillez utiliser la commande \`/database edit 'value': guildPlayer.disco.channelId\` !`)
clearInterval(client.disco.interval)
return 'clear'
progressIntervals.set(key, interval)
}
export async function stopProgressSaving(guildId: string, botId: string) {
if (!guildId || !botId) { logConsole('discord_player', 'progress_saving.missing_ids'); return }
logConsole('discord_player', 'progress_saving.stop', { guildId, botId })
const key = `${guildId}-${botId}`
if (progressIntervals.has(key)) {
clearInterval(progressIntervals.get(key))
progressIntervals.delete(key)
}
const guildProfile = await dbGuild.findOne({ guildId: guildId })
if (!guildProfile) { logConsole('discord_player', 'progress_saving.database_not_exist'); return }
const dbData = guildProfile.get("guildPlayer") as GuildPlayer
if (dbData.instances) {
const instanceIndex = dbData.instances.findIndex(instance => instance.botId === botId)
if (instanceIndex === -1) return
dbData.instances[instanceIndex].replay = {
textChannelId: "",
voiceChannelId: "",
trackUrl: "",
progress: 0
}
let { embed, components } = await playerGenerate(guild)
if (components && embed.data.footer) embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)} \n ${embed.data.footer.text}` })
else embed.setFooter({ text: `Uptime: ${getUptime(client.uptime)}` })
let messages = await channel.messages.fetch()
messages.forEach(msg => { if (!bots.includes(msg.author.id)) msg.delete() })
let botMessage = messages.find(msg => client.user && msg.author.id === client.user.id)
if (botMessage) {
if (!components && botMessage.components.length > 0) {
await botMessage.delete()
return channel.send({ embeds: [embed] })
} else if (components) return botMessage.edit({ embeds: [embed], components })
else return botMessage.edit({ embeds: [embed] })
} else return channel.send({ embeds: [embed] })
} catch (error) {
console.error(error);
return 'clear'
guildProfile.set("guildPlayer", dbData)
guildProfile.markModified("guildPlayer")
await guildProfile.save().catch(console.error)
}
}
export async function playerEdit (interaction: ButtonInteraction) {
let guild = interaction.guild
if (!guild) return await interaction.reply({ content: 'Cette commande n\'est pas disponible en message privé.', ephemeral: true })
let { components } = await playerGenerate(guild)
if (!components) return
components.forEach((actionRow) => actionRow.components.forEach((button) => button.setDisabled(true)))
await interaction.update({ components })
}
export async function playerGenerate (guild: Guild) {
let embed = new EmbedBuilder().setColor('#ffc370')
let queue = useQueue(guild.id)
if (!queue) {
embed.setTitle('Aucune session d\'écoute en cours !')
return ({ embed, components: null })
export async function playerReplay(client: Client, dbData: GuildPlayer) {
const botId = client.user?.id ?? ""
const instance = dbData.instances?.find(instance => instance.botId === botId)
if (!instance) { logConsole('discordjs', 'replay.no_data', { botId }); return }
if (!instance.replay.textChannelId) { logConsole('discordjs', 'replay.no_text_channel_id', { botId }); return }
if (!instance.replay.voiceChannelId) { logConsole('discordjs', 'replay.no_voice_channel_id', { botId }); return }
const textChannel = await client.channels.fetch(instance.replay.textChannelId)
if (!textChannel || (textChannel.type !== ChannelType.GuildText && textChannel.type !== ChannelType.GuildAnnouncement)) {
logConsole('discordjs', 'replay.text_channel_not_found', { channelId: instance.replay.textChannelId, botId })
return
}
let track = queue.currentTrack
if (!track) {
embed.setTitle('Aucune musique en cours de lecture !')
return ({ embed, components: null })
const voiceChannel = await client.channels.fetch(instance.replay.voiceChannelId)
if (!voiceChannel || (voiceChannel.type !== ChannelType.GuildVoice && voiceChannel.type !== ChannelType.GuildStageVoice)) {
logConsole('discordjs', 'replay.voice_channel_not_found', { channelId: instance.replay.textChannelId, botId })
return
}
embed.setTitle(track.title)
.setAuthor({ name: track.author })
.setURL(track.url)
.setImage(track.thumbnail)
.addFields(
{ name: 'Durée', value: track.duration, inline: true },
{ name: 'Source', value: track.source === 'youtube' ? 'Youtube' : track.source === 'spotify' ? 'Spotify' : 'Inconnu', inline: true },
{ name: 'Volume', value: `${queue.node.volume}%`, inline: true },
{ name: queue.node.isPaused() ? 'Progression (en pause)' : 'Progression', value: queue.node.createProgressBar() || 'Aucune' },
{ name: 'Loop', value: queue.repeatMode === 3 ? 'Autoplay' : queue.repeatMode === 2 ? 'File d\'Attente' : queue.repeatMode === 1 ? 'Titre' : 'Off', inline: true }
)
.setDescription(`**Musique suivante :** ${queue.tracks.data[0] ? queue.tracks.data[0].title : 'Aucune'}`)
.setFooter({ text: `Demandé par ${track.requestedBy ? track.requestedBy.tag : 'Inconnu'}` })
let components = [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setLabel(queue.node.isPaused() ? '▶️' : '⏸️')
.setStyle(2)
.setCustomId(queue.node.isPaused() ? 'resume' : 'pause'),
new ButtonBuilder()
.setLabel('⏹️')
.setStyle(2)
.setCustomId('stop'),
new ButtonBuilder()
.setLabel('⏭️')
.setStyle(2)
.setCustomId('skip')
.setDisabled(queue.tracks.data.length !== 0),
new ButtonBuilder()
.setLabel('🔉')
.setStyle(2)
.setCustomId('volume_down')
.setDisabled(queue.node.volume === 0),
new ButtonBuilder()
.setLabel('🔊')
.setStyle(2)
.setCustomId('volume_up')
.setDisabled(queue.node.volume === 100)
),
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setLabel('🔀')
.setStyle(2)
.setCustomId('shuffle'),
new ButtonBuilder()
.setLabel('🔁')
.setStyle(2)
.setCustomId('loop'),
new ButtonBuilder()
.setLabel('⏮️')
.setStyle(2)
.setCustomId('previous')
.setDisabled(queue.history.previousTrack ? false : true)
)
]
return ({ embed, components })
}
export async function playerReplay (client: Client, guildProfile: Document) {
let dbData = guildProfile.get('guildPlayer.replay')
let textChannel = client.channels.cache.get(dbData.textChannelId) as TextChannel
if (!textChannel) return console.log(`Aucun channel trouvé avec l'id \`${dbData.textChannelId}\`, veuillez utiliser la commande \`/setchannel\` !`)
let voiceChannel = client.channels.cache.get(dbData.voiceChannelId) as VoiceChannel
if (!voiceChannel) return console.log(`Aucun channel trouvé avec l'id \`${dbData.voiceChannelId}\`, veuillez utiliser la commande \`/setchannel\` !`)
let player = useMainPlayer()
let queue = player.nodes.create(textChannel.guild, {
const player = useMainPlayer()
const queue = player.nodes.create((textChannel as GuildChannel).guild, {
metadata: {
channel: textChannel,
client: textChannel.guild.members.me,
@@ -188,19 +128,193 @@ export async function playerReplay (client: Client, guildProfile: Document) {
try { if (!queue.connection) await queue.connect(voiceChannel) }
catch (error) { console.error(error) }
if (!instance.replay.trackUrl) return
let result = await player.search(dbData.trackUrl as string, { requestedBy: client.user || undefined })
if (!result.hasTracks()) await textChannel.send(`Aucune musique trouvée pour **${dbData.trackUrl}** !`)
let track = result.tracks[0]
let entry = queue.tasksQueue.acquire()
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 }))
const track = result.tracks[0]
const entry = queue.tasksQueue.acquire()
await entry.getTask()
queue.addTrack(track)
try {
await queue.node.play()
await queue.node.seek(Number(dbData.progress) / 1000)
await textChannel.send(`Relancement de la musique suite à mon redémarrage...`)
} catch (error) { console.error(error) }
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"))
}
catch (error) { console.error(error) }
finally { queue.tasksQueue.release() }
}
}
export async function playerDisco(client: Client, guild: Guild, dbData: Disco) {
try {
if (!dbData.channelId) {
logConsole('discord_player', 'disco.channel_not_configured', { guild: guild.name })
clearInterval(client.disco.interval)
return "clear"
}
const channel = await client.channels.fetch(dbData.channelId)
if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) {
logConsole('discord_player', 'disco.channel_not_found', { guild: guild.name, channelId: dbData.channelId })
clearInterval(client.disco.interval)
return "clear"
}
const { embed, components } = generatePlayerEmbed(guild, guild.preferredLocale)
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)}` })
const messages = await channel.messages.fetch()
const bots = ["1065047326860783636", "1119343522059927684", "1119344050412204032", "1210714000321548329", "660961595006124052"]
await Promise.all(messages.map(async msg => { if (!bots.includes(msg.author.id)) await msg.delete() }))
const botMessage = messages.find(msg => client.user && msg.author.id === client.user.id)
if (botMessage) {
if (!components && botMessage.components.length > 0) {
await botMessage.delete()
return await channel.send({ embeds: [embed] })
}
else if (components) return await botMessage.edit({ embeds: [embed], components })
else return await botMessage.edit({ embeds: [embed] })
}
else return await channel.send({ embeds: [embed] })
} catch (error) {
console.error(error)
return "clear"
}
}
export async function playerEdit(interaction: ButtonInteraction) {
const guild = interaction.guild
if (!guild) return interaction.reply({ content: t(interaction.locale, "common.no_dm"), flags: MessageFlags.Ephemeral })
const { components } = generatePlayerEmbed(guild, interaction.locale)
if (!components) return
components.forEach(actionRow => { actionRow.components.forEach((button) => button.setDisabled(true)) })
await interaction.update({ components })
}
export function generatePlayerEmbed(guild: Guild, locale: Locale) {
const embed = new EmbedBuilder().setColor("#ffc370")
const queue = useQueue(guild.id)
if (!queue) {
embed.setTitle(t(locale, "player.no_session"))
return { embed, components: null }
}
const track = queue.currentTrack
if (!track) {
embed.setTitle(t(locale, "player.no_track"))
return { embed, components: null }
}
const sourceValue = track.source === "spotify" ? t(locale, "player.sources.spotify") :
track.source === "youtube" ? t(locale, "player.sources.youtube") :
t(locale, "player.sources.unknown")
const loopValue = queue.repeatMode === 3 ? t(locale, "player.loop_modes.autoplay") :
queue.repeatMode === 2 ? t(locale, "player.loop_modes.queue") :
queue.repeatMode === 1 ? t(locale, "player.loop_modes.track") :
t(locale, "player.loop_modes.off")
const progressionName = queue.node.isPaused() ?
t(locale, "player.progression_paused") :
t(locale, "player.progression")
embed
.setTitle(track.title)
.setAuthor({ name: track.author })
.setURL(track.url)
.setImage(track.thumbnail)
.addFields(
{ name: t(locale, "player.duration"), value: track.duration, inline: true },
{ name: t(locale, "player.source"), value: sourceValue, inline: true },
{ name: t(locale, "player.volume"), value: `${queue.node.volume}%`, inline: true },
{ name: progressionName, value: queue.node.createProgressBar() ?? t(locale, "common.none") },
{ name: t(locale, "player.loop"), value: loopValue, inline: true }
)
.setDescription(`**${t(locale, "player.next_track")} :** ${queue.tracks.data[0] ? queue.tracks.data[0].title : t(locale, "player.no_next_track")}`)
.setFooter({ text: t(locale, "player.requested_by", { user: track.requestedBy ? track.requestedBy.tag : t(locale, "common.unknown") }) })
const components = [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setLabel(queue.node.isPaused() ? "▶️" : "⏸️").setStyle(2).setCustomId(queue.node.isPaused() ? "player_resume" : "player_pause"),
new ButtonBuilder().setLabel("⏹️").setStyle(2).setCustomId("player_stop"),
new ButtonBuilder().setLabel("⏭️").setStyle(2).setCustomId("player_skip").setDisabled(queue.tracks.data.length !== 0),
new ButtonBuilder().setLabel("🔉").setStyle(2).setCustomId("player_volume_down").setDisabled(queue.node.volume === 0),
new ButtonBuilder().setLabel("🔊").setStyle(2).setCustomId("player_volume_up").setDisabled(queue.node.volume === 100)
),
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setLabel("🔀").setStyle(2).setCustomId("player_shuffle"),
new ButtonBuilder().setLabel("🔁").setStyle(2).setCustomId("player_loop"),
new ButtonBuilder().setLabel("⏮️").setStyle(2).setCustomId("player_previous").setDisabled(queue.history.previousTrack ? false : true)
)
]
return { embed, components }
}
/**
* Génère l'embed et les composants pour la configuration du module Disco
* @param dbData - Données du module Disco depuis la base de données
* @param client - Client Discord
* @param guildId - ID de la guilde
* @param locale - Locale pour la traduction
* @returns Objet contenant l'embed et les composants
*/
export function generateDiscoEmbed(dbData: Disco, client: Client, guildId: string, locale: Locale) {
// Récupérer les informations du canal
let channelInfo = t(locale, "player.disco.channel_not_configured")
if (dbData.channelId) {
const guild = client.guilds.cache.get(guildId)
const channel = guild?.channels.cache.get(dbData.channelId)
channelInfo = channel ? `<#${channel.id}>` : t(locale, "player.common.channel_not_found")
}
// Créer l'embed principal
const embed = new EmbedBuilder()
.setTitle(t(locale, "player.disco.title"))
.setColor(dbData.enabled ? 0xFFC370 : 0x808080)
.setDescription(dbData.enabled ?
t(locale, "player.disco.description_enabled") :
t(locale, "player.disco.description_disabled")
)
.addFields(
{ name: t(locale, "common.status"), value: dbData.enabled ? t(locale, "player.common.enabled") : t(locale, "player.common.disabled"), inline: true },
{ name: t(locale, "common.channel"), value: channelInfo, inline: true }
)
.setTimestamp()
// Bouton toggle - désactivé si pas de canal configuré pour l'activation
const toggleButton = new ButtonBuilder()
.setCustomId(dbData.enabled ? "player_disco_disable" : "player_disco_enable")
.setLabel(dbData.enabled ? t(locale, "common.disable") : t(locale, "common.enable"))
.setStyle(dbData.enabled ? ButtonStyle.Danger : ButtonStyle.Success)
.setEmoji(dbData.enabled ? "❌" : "✅")
// Désactiver le bouton d'activation si aucun canal n'est configuré
if (!dbData.enabled && !dbData.channelId) {
toggleButton.setDisabled(true)
}
// Bouton de configuration du canal
const channelButton = new ButtonBuilder()
.setCustomId("player_disco_channel")
.setLabel(t(locale, "player.common.configure_channel"))
.setStyle(ButtonStyle.Secondary)
.setEmoji("📺")
const components = [
{
type: ComponentType.ActionRow,
components: [toggleButton, channelButton]
}
]
return { embed, components }
}

View File

@@ -1,123 +0,0 @@
import { Client, TextChannel } from 'discord.js'
import { Document } from 'mongoose'
import Parser from 'rss-parser'
import 'dotenv/config'
export interface Feed {
name: string
url: string
token?: string
}
export default async (client: Client, guildProfile: Document) => {
try {
let guild = client.guilds.cache.get(guildProfile.get('guildId'))
if (!guild) {
clearInterval(client.disco.interval)
console.log(`Aucun serveur trouvé avec l'id \`${guildProfile.get('guildId')}\`, veuillez utiliser la commande \`/setchannel\` !`)
return 'clear'
}
let dbData = guildProfile.get('guildRss')
let channel = client.channels.cache.get(dbData.channelId) as TextChannel
if (!channel) {
clearInterval(client.disco.interval)
console.log(`Aucun channel trouvé avec l'id \`${dbData.channelId}\`, veuillez utiliser la commande \`/setchannel\` !`)
return 'clear'
}
let feeds = [
{
name: 'Nautiljon - Actualités',
url: 'https://www.nautiljon.com/actualite/rss.php'
},
{
name: 'Nautiljon - Les Brèves',
url: 'https://www.nautiljon.com/breves/rss.php'
},
{
name: 'YGG - Application',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2144&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - Audio',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2139&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - eBook',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2140&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - Emulation',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2141&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - Flim/Vidéo',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2145&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - GPS',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2143&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - Imprimante 3D',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2200&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - Jeu Vidéo',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2142&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - Nulled',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2300&passkey=',
token: process.env.YGG_PASSKEY
},
{
name: 'YGG - XXX',
url: 'https://www3.yggtorrent.qa/rss?action=generate&type=cat&id=2188&passkey=',
token: process.env.YGG_PASSKEY
}
]
if (!dbData.feeds) dbData.feeds = feeds
dbData.feeds.forEach((feed: Feed, i: number) => { setTimeout(async () => {
let parser = new Parser()
if (feed.token) feed.url += feed.token
let feedData = await parser.parseURL(feed.url)
let thread = channel.threads.cache.find(thread => thread.name === feed.name)
if (!thread) {
thread = await channel.threads.create({
name: feed.name,
autoArchiveDuration: 60,
reason: 'Création du fil RSS'
})
await thread.send({ content: `Fil RSS créé !\nVisionnage de **${feedData.title}** sur ${feedData.link}...` })
feedData.items.reverse().forEach(async (item, i) => {
setTimeout(async () => await thread?.send({ content: `**${item.title}**\n${item.link}` }), i * 1000)
})
}
let lastItem = feedData.items[0]
if (!lastItem) return console.log('No last item found for ' + feed.name)
let messages = await thread.messages.fetch({ limit: 1 })
if (!messages) return console.log('No messages found for ' + feed.name)
let lastMessage = messages.first()
if (!lastMessage) return console.log('No last message found for ' + feed.name)
if (lastMessage.content !== `**${lastItem.title}**\n${lastItem.link}`) await thread.send({ content: `**${lastItem.title}**\n${lastItem.link}` })
//else console.log('No new item found for ' + feed.name)
}, Number(i) * 1000) })
} catch (error) { console.error(error); return 'clear' }
}

View File

@@ -1,193 +1,327 @@
import { Guild, TextChannel, EmbedBuilder, hyperlink } from 'discord.js'
import axios, { AxiosHeaderValue } from 'axios'
import dbGuild from '../schemas/guild'
import chalk from 'chalk'
interface NotificationData {
metadata: {
message_type: string
}
payload: {
subscription: {
type: string
}
event: {
broadcaster_user_name: string
broadcaster_user_login: string
}
session: {
id: string
}
}
}
interface Condition {
broadcaster_user_id: string
}
export async function checkChannel (client_id: string, client_secret: string, channel_access_token: string, guild: Guild) {
let guildProfile = await dbGuild.findOne({ guildId: guild?.id })
if (!guildProfile) return console.log(chalk.magenta(`[Twitch] Database data for ${guild?.name} does not exist, please initialize with \`/database init\` !`))
let dbData = guildProfile.get('guildTwitch')
if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] module is disabled for "${guild?.name}", please activate with \`/database edit guildTwitch.enabled True\` !`))
if (!await validateToken(channel_access_token)) {
let channel_refresh_token = dbData.channelRefreshToken
if (!channel_refresh_token) return console.log(chalk.magenta('[Twitch] No refresh token found in database !'))
let GetAccessFromRefresh = await refreshToken(client_id, client_secret, channel_refresh_token)
if (!GetAccessFromRefresh) return false;
[channel_access_token, channel_refresh_token] = [dbData.channelAccessToken, dbData.channelRefreshToken] = GetAccessFromRefresh
guildProfile.set('guildTwitch', dbData)
guildProfile.markModified('guildTwitch')
await guildProfile.save().catch(console.error)
}
return channel_access_token
}
export async function validateToken (access_token: string) {
return await axios.get('https://id.twitch.tv/oauth2/validate', {
headers: {
'Authorization': `OAuth ${access_token}`,
}
}).then(() => {
return true
}).catch(error => { console.error(error.response.data) })
}
export async function refreshToken (client_id: string, client_secret: string, refresh_token: string) {
return await axios.post('https://id.twitch.tv/oauth2/token', {
client_id,
client_secret,
refresh_token,
grant_type: 'refresh_token'
}).then(response => {
if (response.data.token_type === 'bearer') return [response.data.access_token, response.data.refresh_token]
}).catch(error => { console.error(error.response.data) })
}
export async function getStreams (client_id: string, access_token: string, user_login: string) {
return await axios.get(`https://api.twitch.tv/helix/streams?user_login=${user_login}`, {
headers: {
'Authorization': `Bearer ${access_token}`,
'Client-Id': client_id as AxiosHeaderValue
}
}).then(response => {
return response.data.data[0]
}).catch(error => { console.error(error.response) })
}
export async function getUserInfo (client_id: string, access_token: string, type: string) {
return await axios.get('https://api.twitch.tv/helix/users', {
headers: {
'Authorization': `Bearer ${access_token}`,
'Client-Id': client_id as AxiosHeaderValue
}
}).then(response => {
if (type === 'login') return response.data.data[0].login
if (type === 'id') return response.data.data[0].id
if (type === 'profile_image_url') return response.data.data[0].profile_image_url
}).catch(error => { console.error(error.response.data) })
}
export async function notification (client_id: string, client_secret: string, channel_access_token: string, data: NotificationData, guild: Guild) {
if (!guild) return console.log(chalk.magenta('[Twitch] Can\'t find guild !'))
let { subscription, event } = data.payload
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
if (!guildProfile) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Database data does not exist, please initialize with \`/database init\` !`))
let dbData = guildProfile.get('guildTwitch')
if (!dbData?.enabled) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Module is disabled, please activate with \`/database edit guildTwitch.enabled True\` !`))
let liveChannelId = dbData.liveChannelId
if (!liveChannelId) return console.log(chalk.magenta(`[Twitch] {${guild.name}} No live channel id found in database !`))
let liveChannel = guild.channels.cache.get(liveChannelId) as TextChannel
if (!liveChannel) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find channel with id ${liveChannelId}`))
let liveMessageInterval
// Check if the channel access token is still valid before connecting
channel_access_token = await checkChannel(client_id, client_secret, channel_access_token, guild) as string
if (!channel_access_token) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't refresh channel access token !`))
if (subscription.type === 'stream.online') {
console.log(chalk.magenta(`[Twitch] {${guild.name}} Stream from ${event.broadcaster_user_name} is now online, sending Discord message...`))
let stream_data = await getStreams(client_id, channel_access_token, event.broadcaster_user_login)
let user_profile_image_url = await getUserInfo(client_id, channel_access_token, 'profile_image_url')
let embed = new EmbedBuilder()
.setColor('#6441a5')
.setTitle(stream_data.title)
.setURL(`https://twitch.tv/${event.broadcaster_user_login}`)
.setAuthor({ name: `🔴 ${event.broadcaster_user_name.toUpperCase()} EST ACTUELLEMENT EN LIVE ! 🎥`, iconURL: user_profile_image_url })
.setDescription(`Joue à ${stream_data.game_name} avec ${stream_data.viewer_count} viewers`)
.setImage(stream_data.thumbnail_url.replace('{width}', '1920').replace('{height}', '1080'))
.setTimestamp()
let hidden = hyperlink('démarre un live', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&pp=ygUJcmljayByb2xs')
let message = await liveChannel.send({ content: `Hey @everyone ! <@${dbData.liveBroadcasterId}> ${hidden} sur **Twitch**, venez !`, embeds: [embed] })
dbData.liveMessageId = message.id
guildProfile.set('guildTwitch', dbData)
guildProfile.markModified('guildTwitch')
await guildProfile.save().catch(console.error)
liveMessageInterval = setInterval(async () => {
let stream_data = await getStreams(client_id, channel_access_token, event.broadcaster_user_login)
if (!stream_data) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find stream data for ${event.broadcaster_user_name}`))
embed.setTitle(stream_data.title)
.setDescription(`Joue à ${stream_data.game_name} avec ${stream_data.viewer_count} viewers`)
.setImage(stream_data.thumbnail_url.replace('{width}', '1920').replace('{height}', '1080'))
.setTimestamp()
message.edit({ content: `Hey @everyone !\n<@${dbData.liveBroadcasterId}> est en live sur **Twitch**, venez !`, embeds: [embed] }).catch(console.error)
}, 60000)
}
else if (subscription.type === 'stream.offline') {
console.log(chalk.magenta(`[Twitch] {${guild.name}} Stream from ${event.broadcaster_user_name} is now offline, editing Discord message...`))
let message = await liveChannel.messages.fetch(dbData.liveMessageId as string)
if (!message) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find message with id ${dbData.liveMessageId}`))
if (!message.embeds[0]) return console.log(chalk.magenta(`[Twitch] {${guild.name}} Can't find embed in message with id ${dbData.liveMessageId}`))
let duration = new Date().getTime() - new Date(message.embeds[0].data.timestamp ?? 0).getTime()
let seconds = Math.floor(duration / 1000)
let minutes = Math.floor(seconds / 60)
let hours = Math.floor(minutes / 60)
let duration_string = `${hours ? hours + 'H ' : ''}${minutes % 60 ? minutes % 60 + 'M ' : ''}${seconds % 60 ? seconds % 60 + 'S' : ''}`
let user_profile_image_url = await getUserInfo(client_id, channel_access_token, 'profile_image_url')
let embed = new EmbedBuilder()
.setColor('#6441a5')
.setAuthor({ name: `⚫ C'EST FINI, LE LIVE A DURÉ ${duration_string} ! 📼`, iconURL: user_profile_image_url })
.setTimestamp()
message.edit({ content: `Re @everyone !\n<@${dbData.liveBroadcasterId}> a terminé son live sur **Twitch** !`, embeds: [embed] }).catch(console.error)
clearInterval(liveMessageInterval)
}
}
export async function subscribeToEvents (client_id: string, access_token: string, session_id: string, type: string, version: string, condition: Condition) {
return await axios.post('https://api.twitch.tv/helix/eventsub/subscriptions', {
type,
version,
condition,
transport: {
method: 'websocket',
session_id
}
}, {
headers: {
'Authorization': `Bearer ${access_token}`,
'Client-Id': client_id,
'Content-Type': 'application/json'
}
}).then(response => {
return response.data.data[0].status
}).catch(error => { return error.response.data })
}
// ENVIRONMENT VARIABLES
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!"))
process.exit(1)
}
// PACKAGES
import { AppTokenAuthProvider } from "@twurple/auth"
import { ApiClient } from "@twurple/api"
import { ReverseProxyAdapter, EventSubHttpListener } from "@twurple/eventsub-http"
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"
// Twurple API client setup
const authProvider = new AppTokenAuthProvider(clientId, clientSecret)
export const twitchClient = new ApiClient({ authProvider })
let adapter
if (process.env.NODE_ENV === "development") {
const authtoken = process.env.NGROK_AUTHTOKEN
logConsole('twitch', 'starting_listener_ngrok')
adapter = new NgrokAdapter({ ngrokConfig: { authtoken } })
} else {
const hostName = process.env.TWURPLE_HOSTNAME ?? "localhost"
const port = process.env.TWURPLE_PORT ?? "3000"
console.log(chalk.magenta(`[Twitch] Starting listener with port ${port}...`))
adapter = new ReverseProxyAdapter({ hostName, port: parseInt(port) })
}
const secret = process.env.TWURPLE_SECRET ?? "VeryUnsecureSecretPleaseChangeMe"
export const listener = new EventSubHttpListener({ apiClient: twitchClient, adapter, secret })
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...`))
const results = await Promise.allSettled(discordClient.guilds.cache.map(async guild => {
try {
console.log(chalk.magenta(`[Twitch] Processing guild: ${guild.name} (ID: ${guild.id}) for 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 }
const { guildProfile, dbData, channel, content, embed } = notification
if (!dbData) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} No dbData found`)); 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 (dbData.streamers[streamerIndex].messageId) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message already exists for ${event.broadcasterName}, skipping`)); return }
console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Sending notification for ${event.broadcasterName}`))
const message = await channel.send({ content, embeds: [embed] })
console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Message sent with ID: ${message.id}`))
dbData.streamers[streamerIndex].messageId = message.id
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
startStreamWatching(guild.id, event.broadcasterId, event.broadcasterName, message.id)
} catch (error) {
console.log(chalk.magenta(`[Twitch] Error processing guild ${guild.name}`))
console.error(error)
}
}))
results.forEach((result, index) => {
if (result.status === "rejected") console.log(chalk.magenta(`[Twitch] Guild ${index} failed:`), 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...`))
await Promise.all(discordClient.guilds.cache.map(async guild => {
await stopStreamWatching(guild.id, event.broadcasterId, event.broadcasterName)
}))
}
// Stream upadting intervals
const streamIntervals = new Map<string, NodeJS.Timeout>()
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}`))
const key = `${guildId}-${streamerId}`
if (streamIntervals.has(key)) {
clearInterval(streamIntervals.get(key))
streamIntervals.delete(key)
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const interval = setInterval(async () => {
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 }
const { channel, content, embed } = notification
if (!embed) { console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Embed is missing`)); 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)
}
} catch (error) {
console.log(chalk.magenta(`[Twitch] StreamWatching - Erreur lors du visionnage de ${streamerName} (ID ${streamerId}) sur ${guildId}`))
console.error(error)
await stopStreamWatching(guildId, streamerId, streamerName)
}
}, 60000)
streamIntervals.set(key, interval)
}
export async function stopStreamWatching(guildId: string, streamerId: string, streamerName: string) {
console.log(chalk.magenta(`[Twitch] StreamWatching - Arrêt du visionnage de ${streamerName} (ID ${streamerId})`))
const key = `${guildId}-${streamerId}`
if (streamIntervals.has(key)) {
clearInterval(streamIntervals.get(key))
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] })
} catch (error) {
console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Error editing message for ${streamerName} (ID ${streamerId})`))
console.error(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 !`))
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`))
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`))
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})`))
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})`))
const stream = await user?.getStream()
if (!stream) console.log(chalk.magenta(`[Twitch] StreamWatching - {${guild.name}} Stream data not found for ${streamerName} (ID ${streamerId})`))
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 })
const embed = new EmbedBuilder()
.setColor("#6441a5")
.setTitle(stream?.title ?? t(guild.preferredLocale, "twitch.notification.online.title_unknown"))
.setURL(`https://twitch.tv/${streamerName}`)
.setAuthor({
name: t(guild.preferredLocale, "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", {
game: stream?.gameName ?? "?",
viewers: stream?.viewers.toString() ?? "?"
}))
.setImage(stream?.thumbnailUrl.replace("{width}", "1920").replace("{height}", "1080") ?? "https://assets.help.twitch.tv/article/img/000002222-01a.png")
.setTimestamp()
return { status: "ok", guildProfile, dbData, channel, content, embed }
}
export function generateTwitchEmbed(dbData: GuildTwitch, client: Client, guildId: string, locale: Locale) {
// Récupérer les informations du canal
let channelInfo = t(locale, "twitch.channel_not_configured")
if (dbData.channelId) {
const guild = client.guilds.cache.get(guildId)
const channel = guild?.channels.cache.get(dbData.channelId)
channelInfo = channel ? `<#${channel.id}>` : t(locale, "twitch.common.channel_not_found")
}
// Créer l'embed principal
const embed = new EmbedBuilder()
.setTitle(t(locale, "twitch.title"))
.setColor(dbData.enabled ? 0x9146FF : 0x808080)
.addFields(
{ name: t(locale, "common.status"), value: dbData.enabled ? t(locale, "twitch.common.enabled") : t(locale, "twitch.common.disabled"), inline: true },
{ name: t(locale, "common.channel"), value: channelInfo, inline: true },
{ name: "👥 Streamers", value: t(locale, "twitch.streamers_count", { count: dbData.streamers.length.toString() }), inline: true }
)
.setFooter({ text: t(locale, "twitch.managed_by", { bot: client.user?.displayName ?? "Bot" }) })
.setTimestamp()
// Boutons première ligne - Toggle et configuration
const toggleButton = new ButtonBuilder()
.setCustomId(dbData.enabled ? "twitch_disable" : "twitch_enable")
.setLabel(dbData.enabled ? t(locale, "common.disable") : t(locale, "common.enable"))
.setStyle(dbData.enabled ? ButtonStyle.Danger : ButtonStyle.Success)
.setEmoji(dbData.enabled ? "❌" : "✅")
const channelButton = new ButtonBuilder()
.setCustomId("twitch_channel")
.setLabel(t(locale, "twitch.common.configure_channel"))
.setStyle(ButtonStyle.Secondary)
.setEmoji("📺")
// Boutons seconde ligne - Gestion des streamers
const listButton = new ButtonBuilder()
.setCustomId("twitch_streamer_list")
.setLabel(t(locale, "common.list"))
.setStyle(ButtonStyle.Secondary)
.setEmoji("📋")
const addButton = new ButtonBuilder()
.setCustomId("twitch_streamer_add")
.setLabel(t(locale, "common.add"))
.setStyle(ButtonStyle.Primary)
.setEmoji("")
const removeButton = new ButtonBuilder()
.setCustomId("twitch_streamer_remove")
.setLabel(t(locale, "common.remove"))
.setStyle(ButtonStyle.Danger)
.setEmoji("🗑️")
const components = [
{
type: ComponentType.ActionRow,
components: [toggleButton, channelButton]
},
{
type: ComponentType.ActionRow,
components: [listButton, addButton, removeButton]
}
]
return { embed, components }
}

10
src/utils/uptime.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { Client } from "discord.js"
export default function (uptime: Client["uptime"]) {
if (!uptime) return "0J, 0H, 0M et 0S"
const days = Math.floor(uptime / 86400000)
const hours = Math.floor(uptime / 3600000) % 24
const minutes = Math.floor(uptime / 60000) % 60
const seconds = Math.floor(uptime / 1000) % 60
return `${days}J, ${hours}H, ${minutes}M et ${seconds}S`
}