Réécriture complète 4.0
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 6m16s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 6m16s
This commit is contained in:
116
src/utils/amp.ts
116
src/utils/amp.ts
@@ -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
54
src/utils/console.ts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
111
src/utils/i18n.ts
Normal 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 }
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
123
src/utils/rss.ts
123
src/utils/rss.ts
@@ -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' }
|
||||
}
|
||||
@@ -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
10
src/utils/uptime.ts
Normal 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`
|
||||
}
|
||||
Reference in New Issue
Block a user