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,210 +1,241 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, EmbedBuilder, inlineCode, PermissionFlagsBits } from 'discord.js'
import dbGuild from '../../schemas/guild'
import * as AMP from '../../utils/amp'
interface InstanceFields {
name: string
value: string
inline: boolean
}
interface InstanceResult {
status: string
data: [
Host
]
}
interface Host {
AvailableInstances: Instance[]
FriendlyName: string
}
interface Instance {
InstanceID: string
FriendlyName: string
Running: boolean
Module: string
Port: number
}
interface FailMsgData {
Title: string
Message: string
}
interface ErrorMsgData {
error_code: string
}
function failMsg(data: FailMsgData) { return `La commande a échouée !\n${inlineCode(`${data.Title}: ${data.Message}`)}` }
function errorMsg(data: ErrorMsgData) { return `Y'a eu une erreur !\n${inlineCode(`${data.error_code}`)}` }
export default {
data: new SlashCommandBuilder()
.setName('amp')
.setDescription('Accède à mon panel de jeu AMP !')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand.setName('login').setDescription("Connectez-vous avant d'effectuer une autre commande !")
.addStringOption(option => option.setName('username').setDescription("Nom d'Utilisateur").setRequired(true))
.addStringOption(option => option.setName('password').setDescription('Mot de Passe').setRequired(true))
.addBooleanOption(option => option.setName('remember').setDescription('Mémoriser les identifiants').setRequired(true))
.addStringOption(option => option.setName('otp').setDescription('Code de double authentification')))
.addSubcommandGroup(subcommandgroup => subcommandgroup.setName('instances').setDescription('Intéragir avec les instances AMP.')
.addSubcommand(subcommand => subcommand.setName('list').setDescription('Liste toutes les instances disponibles.'))
.addSubcommand(subcommand => subcommand.setName('manage').setDescription('Gérer une instance.')
.addStringOption(option => option.setName('instance').setDescription("Nom de l'instance").setRequired(true).setAutocomplete(true)))
.addSubcommand(subcommand => subcommand.setName('restart').setDescription('Redémarre une instance.')
.addStringOption(option => option.setName('name').setDescription("Nom de l'instance").setRequired(true)))
),
async autocompleteRun(interaction: AutocompleteInteraction) {
let query = interaction.options.getString('instance', true)
let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id })
if (!guildProfile) return await interaction.respond([])
let dbData = guildProfile.get('guildAmp')
if (!dbData?.enabled) return await interaction.respond([])
let host = dbData.host as string
let username = dbData.username as string
let sessionID = dbData.sessionID as string
let rememberMeToken = dbData.rememberMeToken as string
// Check if the SessionID is still valid
let session = await AMP.CheckSession(host, sessionID)
if (session.status === 'fail') {
if (rememberMeToken) {
// Refresh the SessionID if the RememberMeToken is available
let details = { username, password: '', token: rememberMeToken, rememberMe: true }
let result = await AMP.Core.Login(host, details)
if (result.status === 'success') sessionID = result.data.sessionID
else if (result.status === 'fail') return interaction.respond([])
else if (result.status === 'error') return interaction.respond([])
} else return await interaction.respond([])
}
else if (session.status === 'error') return interaction.respond([])
let choices: ApplicationCommandOptionChoiceData[] = []
let result = await AMP.ADSModule.GetInstances(host, sessionID)
if (result.status === 'success') {
let hosts = result.data.result as Host[]
hosts.forEach(host => {
let instances = host.AvailableInstances as Instance[]
instances.forEach(instance => {
if (instance.FriendlyName.includes(query)) choices.push({ name: `${host.FriendlyName} - ${instance.FriendlyName}`, value: instance.InstanceID })
})
})
}
else if (result.status === 'fail') return interaction.respond([])
else if (result.status === 'error') return interaction.respond([])
return interaction.respond(choices)
},
async execute(interaction: ChatInputCommandInteraction) {
let guildProfile = await dbGuild.findOne({ guildId: interaction?.guild?.id })
if (!guildProfile) return interaction.reply({ content: `Database data for **${interaction.guild?.name}** does not exist, please initialize with \`/database init\` !` })
let dbData = guildProfile.get('guildAmp')
if (!dbData?.enabled) return interaction.reply({ content: `AMP module is disabled for **${interaction.guild?.name}**, please activate with \`/database edit guildAmp.enabled True\` !` })
let host = dbData.host as string
let username = dbData.username as string
let sessionID = dbData.sessionID as string
let rememberMeToken = dbData.rememberMeToken as string
// Let the user login
if (interaction.options.getSubcommand() == 'login') {
// Get a SessionID and a RememberMeToken if wanted
await interaction.deferReply({ ephemeral: true })
let details = {
username: interaction.options.getString('username') || '',
password: interaction.options.getString('password') || '',
rememberMe: interaction.options.getBoolean('remember') || '',
token: interaction.options.getString('otp') || ''
}
let result = await AMP.Core.Login(host, details)
if (result.status === 'success') {
username = dbData['username'] = result.data.userInfo.Username
sessionID = dbData['sessionID'] = result.data.sessionID
rememberMeToken = dbData['rememberMeToken'] = result.data.rememberMeToken
guildProfile.set('guildAmp', dbData)
guildProfile.markModified('guildAmp')
await guildProfile.save().catch(console.error)
return await interaction.followUp(`Tu es connecté au panel sous **${username}** !`)
}
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
await interaction.deferReply()
// Check if the SessionID is still valid
let session = await AMP.CheckSession(host, sessionID)
if (session.status === 'fail') {
if (rememberMeToken) {
// Refresh the SessionID if the RememberMeToken is available
let details = { username, password: '', token: rememberMeToken, rememberMe: true }
let result = await AMP.Core.Login(host, details)
if (result.status === 'success') sessionID = result.data.sessionID
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
else return await interaction.followUp(`Tu dois te connecter avant d'effectuer une autre commande !`)
}
else if (session.status === 'error') return await interaction.followUp(errorMsg(session.data))
if (interaction.options.getSubcommandGroup() == 'instances') {
if (interaction.options.getSubcommand() == 'list') {
let result = await AMP.ADSModule.GetInstances(host, sessionID) as InstanceResult
if (result.status === 'success') {
await interaction.followUp({ content: `${result.data.length} hôte(s) trouvé(s) !` })
result.data.forEach(async host => {
let fields = [] as InstanceFields[]
host.AvailableInstances.forEach((instance: Instance) => {
fields.push({
name: instance.FriendlyName,
value: `**Running:** ${instance.Running}\n**Port:** ${instance.Port}\n**Module:** ${instance.Module}`,
inline: true
})
})
let embed = new EmbedBuilder()
.setTitle(host.FriendlyName)
.setDescription(`Liste des ${host.AvailableInstances.length} instances :`)
.setColor(interaction.guild?.members.me?.displayColor || '#ffc370')
.setTimestamp()
.setFields(fields)
return await interaction.followUp({ embeds: [embed] })
})
}
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data as unknown as FailMsgData))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data as unknown as ErrorMsgData))
}
else if (interaction.options.getSubcommand() == 'manage') {
let instanceID = interaction.options.getString('instance', true)
let result = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID)
if (result.status === 'success') {
let server = await AMP.ADSModule.Servers(host, sessionID, instanceID)
if (server.status === 'success') return await interaction.followUp(`Ok !`)
else if (server.status === 'fail') return await interaction.followUp(failMsg(server.data))
else if (server.status === 'error') return await interaction.followUp(errorMsg(server.data))
}
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
else if (interaction.options.getSubcommand() == 'restart') {
let query = interaction.options.getString('name')
if (!query) return
let result = await AMP.ADSModule.RestartInstance(host, sessionID, query)
if (result.status === 'success') return await interaction.followUp(`Ok !`)
else if (result.status === 'fail') return await interaction.followUp(failMsg(result.data))
else if (result.status === 'error') return await interaction.followUp(errorMsg(result.data))
}
}
}
}
import { SlashCommandBuilder, EmbedBuilder, inlineCode, PermissionFlagsBits, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData, Locale } from "discord.js"
import * as AMP from "@/utils/amp"
import type { Host, Instance, InstanceFields, InstanceResult, LoginSuccessData } from "@/types/amp"
import type { ReturnMsgData } from "@/types"
import type { GuildAmp } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
function returnMsg(result: ReturnMsgData, locale: Locale) {
if (result.status === "fail") return `${t(locale, "common.failed")}\n${inlineCode(`${result.Title}: ${result.Message}`)}`
if (result.status === "error") return `${t(locale, "common.error_occurred")}\n${inlineCode(`${result.error_code}`)}`
}
export const data = new SlashCommandBuilder()
.setName("amp")
.setDescription("Access my AMP gaming panel")
.setDescriptionLocalizations({ fr: "Accède à mon panel de jeu AMP" })
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand
.setName("login")
.setDescription("Log in before performing another command")
.setNameLocalizations({ fr: "connexion" })
.setDescriptionLocalizations({ fr: "Connectez-vous avant d'effectuer une autre commande" })
.addStringOption(option => option
.setName("username")
.setDescription("Username")
.setNameLocalizations({ fr: "nom_utilisateur" })
.setDescriptionLocalizations({ fr: "Nom d'utilisateur" })
.setRequired(true)
)
.addStringOption(option => option
.setName("password")
.setDescription("Password")
.setNameLocalizations({ fr: "mot_de_passe" })
.setDescriptionLocalizations({ fr: "Mot de passe" })
.setRequired(true)
)
.addBooleanOption(option => option
.setName("remember")
.setDescription("Remember credentials")
.setNameLocalizations({ fr: "memoriser" })
.setDescriptionLocalizations({ fr: "Mémoriser les identifiants" })
.setRequired(true)
)
.addStringOption(option => option
.setName("otp")
.setDescription("Two-factor authentication code")
.setNameLocalizations({ fr: "otp" })
.setDescriptionLocalizations({ fr: "Code d'authentification à 2 facteurs" })
)
)
.addSubcommandGroup(subcommandgroup => subcommandgroup
.setName("instances")
.setDescription("Interact with AMP instances")
.setNameLocalizations({ fr: "instances" })
.setDescriptionLocalizations({ fr: "Intéragir avec les instances AMP" })
.addSubcommand(subcommand => subcommand
.setName("list")
.setDescription("List all available instances")
.setNameLocalizations({ fr: "liste" })
.setDescriptionLocalizations({ fr: "Lister toutes les instances disponibles" })
)
.addSubcommand(subcommand => subcommand
.setName("manage")
.setDescription("Manage an instance")
.setNameLocalizations({ fr: "gerer" })
.setDescriptionLocalizations({ fr: "Gérer une instance" })
.addStringOption(option => option
.setName("instance")
.setDescription("Instance name")
.setNameLocalizations({ fr: "instance" })
.setDescriptionLocalizations({ fr: "Nom de l'instance" })
.setRequired(true)
.setAutocomplete(true)
)
)
.addSubcommand(subcommand => subcommand
.setName("restart")
.setDescription("Restart an instance")
.setNameLocalizations({ fr: "redemarrer" })
.setDescriptionLocalizations({ fr: "Redémarrer une instance" })
.addStringOption(option => option
.setName("name")
.setDescription("Instance name")
.setNameLocalizations({ fr: "nom" })
.setDescriptionLocalizations({ fr: "Nom de l'instance" })
.setRequired(true)
)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildAmp") as GuildAmp
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "amp.module_disabled"), flags: MessageFlags.Ephemeral })
const host = dbData.host
if (!host) return interaction.reply({ content: t(interaction.locale, "amp.host_not_configured"), flags: MessageFlags.Ephemeral })
let username = dbData.username
let sessionID = dbData.sessionID
let rememberMeToken = dbData.rememberMeToken
const subcommandGroup = interaction.options.getSubcommandGroup(false)
const subcommand = interaction.options.getSubcommand(true)
if (subcommand == "login") {
// Get a SessionID and a RememberMeToken if wanted
await interaction.deferReply({ flags: MessageFlags.Ephemeral })
const details = {
username: interaction.options.getString("username", true),
password: interaction.options.getString("password", true),
token: interaction.options.getString("otp") ?? "",
rememberMe: interaction.options.getBoolean("remember", true)
}
const result = await AMP.Core.Login(host, details)
if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral })
const loginData = result.data as LoginSuccessData
username = dbData.username = loginData.userInfo.Username
sessionID = dbData.sessionID = loginData.sessionID
rememberMeToken = dbData.rememberMeToken = loginData.rememberMeToken
guildProfile.set("guildAmp", dbData)
guildProfile.markModified("guildAmp")
await guildProfile.save().catch(console.error)
return interaction.followUp({ content: t(interaction.locale, "amp.logged_in", { username }), flags: MessageFlags.Ephemeral })
}
await interaction.deferReply()
// Check if the SessionID is still valid
if (!sessionID) return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral })
const checkResult = await AMP.CheckSession(host, sessionID)
if (checkResult.status === "fail") {
if (rememberMeToken && username) {
// Refresh the SessionID if the RememberMeToken is available
const details = { username, password: "", token: rememberMeToken, rememberMe: true }
const loginResult = await AMP.Core.Login(host, details)
if (loginResult.status !== "success") return interaction.followUp({ content: returnMsg(loginResult, interaction.locale), flags: MessageFlags.Ephemeral })
const loginData = loginResult.data as LoginSuccessData
sessionID = loginData.sessionID
}
else return interaction.followUp({ content: t(interaction.locale, "amp.login_required"), flags: MessageFlags.Ephemeral })
}
else if (checkResult.status === "error") return interaction.followUp({ content: returnMsg(checkResult, interaction.locale), flags: MessageFlags.Ephemeral })
if (subcommandGroup == "instances") {
if (subcommand == "list") {
const result = (await AMP.ADSModule.GetInstances(host, sessionID)) as InstanceResult
if (result.status !== "success") return interaction.followUp({ content: returnMsg(result, interaction.locale), flags: MessageFlags.Ephemeral })
await interaction.followUp({ content: t(interaction.locale, "amp.hosts_found", { count: result.data.length }) })
await Promise.all(result.data.map(async host => {
const fields = [] as InstanceFields[]
host.AvailableInstances.forEach((instance: Instance) => {
fields.push({ name: instance.FriendlyName, value: `**${t(interaction.locale, "amp.running")}:** ${instance.Running}\n**${t(interaction.locale, "amp.port")}:** ${instance.Port}\n**${t(interaction.locale, "amp.module")}:** ${instance.Module}`, inline: true })
})
const embed = new EmbedBuilder()
.setTitle(host.FriendlyName)
.setDescription(t(interaction.locale, "amp.instance_list", { count: host.AvailableInstances.length }))
.setColor(interaction.guild?.members.me?.displayColor ?? "#ffc370")
.setTimestamp()
.setFields(fields)
return interaction.followUp({ embeds: [embed] })
}))
}
else if (subcommand == "manage") {
const instanceID = interaction.options.getString("instance", true)
const manageResult = await AMP.ADSModule.ManageInstance(host, sessionID, instanceID)
if (manageResult.status !== "success") return interaction.followUp({ content: returnMsg(manageResult, interaction.locale), flags: MessageFlags.Ephemeral })
const serversResult = await AMP.ADSModule.Servers(host, sessionID, instanceID)
if (serversResult.status !== "success") return interaction.followUp({ content: returnMsg(serversResult, interaction.locale), flags: MessageFlags.Ephemeral })
return interaction.followUp({ content: t(interaction.locale, "amp.manage_success") })
}
else if (subcommand == "restart") {
const query = interaction.options.getString("name", true)
const restartResult = await AMP.ADSModule.RestartInstance(host, sessionID, query)
if (restartResult.status !== "success") return interaction.followUp({ content: returnMsg(restartResult, interaction.locale), flags: MessageFlags.Ephemeral })
return interaction.followUp({ content: t(interaction.locale, "amp.restart_success") })
}
}
}
export async function autocompleteRun(interaction: AutocompleteInteraction) {
const query = interaction.options.getString("instance", true)
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.respond([])
const dbData = guildProfile.get("guildAmp") as GuildAmp
if (!dbData.enabled) return interaction.respond([])
const host = dbData.host
if (!host) return interaction.respond([])
let sessionID = dbData.sessionID
if (!sessionID) return interaction.respond([])
const username = dbData.username
const rememberMeToken = dbData.rememberMeToken
const checkResult = await AMP.CheckSession(host, sessionID)
if (checkResult.status === "fail") {
if (rememberMeToken && username) {
// Refresh the SessionID if the RememberMeToken is available
const details = { username, password: "", token: rememberMeToken, rememberMe: true }
const loginResult = await AMP.Core.Login(host, details)
if (loginResult.status !== "success") return interaction.respond([])
const loginData = loginResult.data as LoginSuccessData
sessionID = loginData.sessionID
}
else return interaction.respond([])
}
else if (checkResult.status === "error") return interaction.respond([])
const instancesResult = (await AMP.ADSModule.GetInstances(host, sessionID)) as InstanceResult
if (instancesResult.status !== "success") return interaction.respond([])
const choices: ApplicationCommandOptionChoiceData[] = []
const hosts = instancesResult.data as Host[]
hosts.forEach(host => {
const instances = host.AvailableInstances
instances.forEach(instance => {
if (instance.FriendlyName.includes(query)) choices.push({ name: `${host.FriendlyName} - ${instance.FriendlyName}`, value: instance.InstanceID })
})
})
return interaction.respond(choices)
}

View File

@@ -1,37 +1,39 @@
import { SlashCommandBuilder, EmbedBuilder, ChatInputCommandInteraction, TextChannel, PermissionFlagsBits } from 'discord.js'
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
import { logConsole } from "@/utils/console"
module.exports = {
data: new SlashCommandBuilder()
.setName('boost')
.setDescription('Tester le boost du serveur !')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction: ChatInputCommandInteraction) {
if (interaction.guild?.id !== '796327643783626782') return interaction.reply({ content: 'Non !' })// Jujul Community
let member = interaction.member
if (!member) return console.log(`\u001b[1;31m Aucun membre trouvé !`)
export const data = new SlashCommandBuilder()
.setName("boost")
.setDescription("Test the server boost")
.setDescriptionLocalizations({ fr: "Tester le boost du serveur" })
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
let guild = interaction.guild
if (!guild) return console.log(`\u001b[1;31m Aucun serveur trouvé !`)
export async function execute(interaction: ChatInputCommandInteraction) {
if (interaction.guild?.id !== "796327643783626782") return interaction.reply({ content: t(interaction.locale, "boost.not_authorized") }) // Jujul Community
let channel = guild.channels.cache.get('924353449930412153') as TextChannel
if (!channel) return console.log(`\u001b[1;31m Aucun channel trouvé avec l'id "924353449930412153" !`)
const member = interaction.member
if (!member) { logConsole('discordjs', 'boost.no_member'); return }
let boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) return console.log(`\u001b[1;31m Aucun rôle de boost trouvé !`)
const guild = interaction.guild
if (!guild.members.me) { logConsole('discordjs', 'boost.not_in_guild'); return }
if (!guild.members.me) return console.log(`\u001b[1;31m Je ne suis pas sur le serveur !`)
let embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(`Nouveau boost de ${member.user.username} !`)
.setDescription(`
Merci à toi pour ce boost.\n
Grâce à toi, on a atteint ${guild.premiumSubscriptionCount} boosts !
`)
.setThumbnail(member.user.avatar)
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
await interaction.reply({ content: 'Va voir dans <#924353449930412153> !' })
const channel = await guild.channels.fetch("924353449930412153")
if (!channel || (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)) {
logConsole('discordjs', 'boost.no_channel', { channelId: "924353449930412153" })
return
}
}
const boostRole = guild.roles.premiumSubscriberRole
if (!boostRole) { logConsole('discordjs', 'boost.no_boost_role'); return }
const embed = new EmbedBuilder()
.setColor(guild.members.me.displayHexColor)
.setTitle(t(interaction.locale, "boost.new_boost_title", { username: member.user.username }))
.setDescription(t(interaction.locale, "boost.new_boost_description", { count: (guild.premiumSubscriptionCount ?? 0).toString() }))
.setThumbnail(member.user.avatar)
.setTimestamp(new Date())
await channel.send({ embeds: [embed] })
return interaction.reply({ content: t(interaction.locale, "boost.check_channel", { channelId: "924353449930412153" }) })
}

View File

@@ -1,76 +1,108 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, APIEmbedField, PermissionFlagsBits } from 'discord.js'
import dbGuildInit from '../../utils/dbGuildInit'
import dbGuild from '../../schemas/guild'
const parseObject = (obj: object, prefix = ''): { name: string, value: object | string | boolean }[] => {
let fields: { name: string, value: object | string | boolean }[] = []
for (let [key, value] of Object.entries(obj)) {
if (typeof value === 'object') fields.push(...parseObject(value, `${prefix}${key}.`))
else {
if (typeof value === 'boolean') value = value ? 'True' : 'False'
else if (!value) value = 'None'
else value = value.toString()
fields.push({ name: `${prefix}${key}`, value })
}
}
return fields
}
export default {
data: new SlashCommandBuilder()
.setName('database')
.setDescription('Communicate with the database')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand.setName('info').setDescription('Returns information about the current guild'))
.addSubcommand(subcommand => subcommand.setName('init').setDescription('Force initialize an entry for the current guild in the database'))
.addSubcommand(subcommand => subcommand.setName('edit').setDescription('Modify parameters for the current guild')
.addStringOption(option => option.setName('key').setDescription('Key to modify').setRequired(true))
.addStringOption(option => option.setName('value').setDescription('Value to set').setRequired(true))),
async execute(interaction: ChatInputCommandInteraction) {
let guild = interaction.guild
if (!guild) return await interaction.reply({ content: 'This command must be used in a server.', ephemeral: true })
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
if (interaction.options.getSubcommand() === 'info') {
if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist !` })
let fields = parseObject(guildProfile.toObject())
let embed = new EmbedBuilder()
.setTitle('Database Information')
.setDescription(`Guild **${guildProfile.guildName}** (ID: ${guildProfile.guildId})`)
.setThumbnail(guildProfile.guildIcon as string)
.setTimestamp()
//.addFields(fields as APIEmbedField[])
// Limit the number of fields to 25
.addFields(fields.slice(0, 25) as APIEmbedField[])
return await interaction.reply({ embeds: [embed] })
} else if (interaction.options.getSubcommand() === 'init') {
if (guildProfile) return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** already exists !` })
guildProfile = await dbGuildInit(guild)
if (!guildProfile) return await interaction.reply({ content: `An error occured while initializing database data for **${guild.name}** !` })
return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully initialized !` })
} else if (interaction.options.getSubcommand() === 'edit') {
if (!guildProfile) return await interaction.reply({ content: `Database data for **${guild.name}** does not exist, please init with \`/database init\` !` })
let key = interaction.options.getString('key', true)
let value = interaction.options.getString('value', true)
let oldValue = guildProfile.get(key)
if (!oldValue) oldValue = 'None'
guildProfile.set(key, value)
await guildProfile.save().catch(console.error)
return await interaction.reply({ content: `Database data for **${guildProfile.guildName}** successfully updated !\n**${key}**: ${oldValue} -> ${value}` })
}
}
}
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"
import type { ChatInputCommandInteraction, APIEmbedField } from "discord.js"
import dbGuildInit from "@/utils/dbGuildInit"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
const parseObject = (obj: object, prefix = ""): { name: string, value: object | string | boolean }[] => {
const fields: { name: string, value: object | string | boolean }[] = []
for (const [key, value] of Object.entries(obj)) {
if (value !== null && typeof value === "object") fields.push(...parseObject(value as object, `${prefix}${key}.`))
else {
let newValue: string
if (typeof value === "boolean") newValue = value ? "True" : "False"
else if (value === null || value === undefined) newValue = "None"
else newValue = String(value)
fields.push({ name: `${prefix}${key}`, value: newValue })
}
}
return fields
}
export const data = new SlashCommandBuilder()
.setName("database")
.setDescription("Communicate with the database")
.setDescriptionLocalizations({ fr: "Communiquer avec la base de données" })
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addSubcommand(subcommand => subcommand
.setName("info")
.setDescription("Returns information about the current guild")
.setNameLocalizations({ fr: "info" })
.setDescriptionLocalizations({ fr: "Retourne les informations sur le serveur actuel" })
)
.addSubcommand(subcommand => subcommand
.setName("init")
.setDescription("Force initialize an entry for the current guild in the database")
.setNameLocalizations({ fr: "init" })
.setDescriptionLocalizations({ fr: "Initialiser de force une entrée pour le serveur actuel dans la base de données" })
)
.addSubcommand(subcommand => subcommand
.setName("edit")
.setDescription("Modify parameters for the current guild")
.setNameLocalizations({ fr: "modifier" })
.setDescriptionLocalizations({ fr: "Modifier les paramètres pour le serveur actuel" })
.addStringOption(option => option
.setName("key")
.setDescription("Key to modify")
.setNameLocalizations({ fr: "cle" })
.setDescriptionLocalizations({ fr: "Clé à modifier" })
.setRequired(true)
)
.addStringOption(option => option
.setName("value")
.setDescription("Value to set")
.setNameLocalizations({ fr: "valeur" })
.setDescriptionLocalizations({ fr: "Valeur à définir" })
.setRequired(true)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
if (interaction.user !== interaction.client.application.owner) return interaction.reply({ content: t(interaction.locale, "database.owner_only"), flags: MessageFlags.Ephemeral })
const guild = interaction.guild
if (!guild) return interaction.reply({ content: t(interaction.locale, "database.server_only"), flags: MessageFlags.Ephemeral })
let guildProfile = await dbGuild.findOne({ guildId: guild.id })
const subcommand = interaction.options.getSubcommand(true)
if (subcommand === "info") {
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const fields = parseObject(guildProfile.toObject())
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "database.info_title"))
.setDescription(t(interaction.locale, "database.guild_info", { name: guildProfile.guildName, id: guildProfile.guildId }))
.setThumbnail(guildProfile.guildIcon)
.setTimestamp()
//.addFields(fields as APIEmbedField[])
// Limit the number of fields to 25
.addFields(fields.slice(0, 25) as APIEmbedField[])
return interaction.reply({ embeds: [embed] })
}
else if (subcommand === "init") {
if (guildProfile) return interaction.reply({ content: t(interaction.locale, "database.already_exists", { name: guildProfile.guildName }), flags: MessageFlags.Ephemeral })
guildProfile = await dbGuildInit(guild)
return interaction.reply({ content: t(interaction.locale, "database.initialized", { name: guildProfile.guildName }), flags: MessageFlags.Ephemeral })
}
else if (subcommand === "edit") {
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const key = interaction.options.getString("key", true)
const value = interaction.options.getString("value", true)
let oldValue: string = guildProfile.get(key) as string
if (!oldValue) oldValue = t(interaction.locale, "common.none")
guildProfile.set(key, value)
guildProfile.markModified(key)
await guildProfile.save().catch(console.error)
return interaction.reply({ content: t(interaction.locale, "database.updated", { name: guildProfile.guildName, key, oldValue, value }), flags: MessageFlags.Ephemeral })
}
}

View File

@@ -0,0 +1,353 @@
import { SlashCommandBuilder, EmbedBuilder, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder, inlineCode } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import crypto from "crypto"
import * as Freebox from "@/utils/freebox"
import type {
APIResponseData, APIResponseDataError, APIResponseDataVersion,
ConnectionStatus, GetChallenge, LcdConfig, OpenSession, RequestAuthorization, TrackAuthorizationProgress
} from "@/types/freebox"
import type { GuildFbx } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("freebox")
.setDescription("Access FreeboxOS API")
.setDescriptionLocalizations({ fr: "Accéder à l'API FreeboxOS" })
.addSubcommand(subcommand => subcommand
.setName("status")
.setDescription("Display Freebox configuration and status")
.setDescriptionLocalizations({ fr: "Afficher la configuration et l'état de la Freebox" })
)
.addSubcommand(subcommand => subcommand
.setName("setup")
.setDescription("Configure Freebox settings easily")
.setDescriptionLocalizations({ fr: "Configurer facilement les paramètres Freebox" })
.addStringOption(option => option
.setName("host")
.setDescription("Freebox host (IP address)")
.setNameLocalizations({ fr: "hote" })
.setDescriptionLocalizations({ fr: "Hôte Freebox (adresse IP)" })
.setRequired(false)
)
.addIntegerOption(option => option
.setName("version")
.setDescription("Freebox API version")
.setNameLocalizations({ fr: "version" })
.setDescriptionLocalizations({ fr: "Version de l'API Freebox" })
.setRequired(false)
)
.addBooleanOption(option => option
.setName("enabled")
.setDescription("Enable or disable the Freebox module")
.setNameLocalizations({ fr: "active" })
.setDescriptionLocalizations({ fr: "Activer ou désactiver le module Freebox" })
.setRequired(false)
)
)
.addSubcommand(subcommand => subcommand
.setName("init")
.setDescription("Create an app on the Freebox to authenticate")
.setDescriptionLocalizations({ fr: "Créer une app sur la Freebox pour s'authentifier" })
.addStringOption(option => option
.setName("host")
.setDescription("Freebox host (IP or domain)")
.setNameLocalizations({ fr: "hote" })
.setDescriptionLocalizations({ fr: "Hôte Freebox (IP ou domaine)" })
.setRequired(true)
)
)
.addSubcommandGroup(subcommandGroup => subcommandGroup
.setName("get")
.setDescription("Retrieve data")
.setNameLocalizations({ fr: "recuperer" })
.setDescriptionLocalizations({ fr: "Récupérer des données" })
.addSubcommand(subcommand => subcommand
.setName("version")
.setDescription("Display API version")
.setDescriptionLocalizations({ fr: "Afficher la version de l'API" })
)
.addSubcommand(subcommand => subcommand
.setName("connection")
.setDescription("Retrieve connection information")
.setNameLocalizations({ fr: "connexion" })
.setDescriptionLocalizations({ fr: "Récupérer les informations de connexion" })
)
.addSubcommand(subcommand => subcommand
.setName("lcd")
.setDescription("Retrieve LCD configuration")
.setDescriptionLocalizations({ fr: "Récupérer la configuration de l'écran LCD" })
)
)
.addSubcommandGroup(subcommandGroup => subcommandGroup
.setName("lcd")
.setDescription("Control LCD features")
.setDescriptionLocalizations({ fr: "Contrôler les fonctionnalités LCD" })
.addSubcommand(subcommand => subcommand
.setName("leds")
.setDescription("Toggle LED strip on/off")
.setNameLocalizations({ fr: "leds" })
.setDescriptionLocalizations({ fr: "Allumer/éteindre le bandeau LED" })
.addBooleanOption(option => option
.setName("enabled")
.setDescription("Enable or disable LED strip")
.setNameLocalizations({ fr: "active" })
.setDescriptionLocalizations({ fr: "Activer ou désactiver le bandeau LED" })
.setRequired(true)
)
)
.addSubcommand(subcommand => subcommand
.setName("timer")
.setDescription("Setup automatic LED timer")
.setNameLocalizations({ fr: "minuteur" })
.setDescriptionLocalizations({ fr: "Configurer le minuteur automatique des LEDs" })
.addStringOption(option => option
.setName("action")
.setDescription("Timer action")
.setNameLocalizations({ fr: "action" })
.setDescriptionLocalizations({ fr: "Action du minuteur" })
.setRequired(true)
.addChoices(
{ name: "Enable timer", value: "enable", name_localizations: { fr: "Activer minuteur" } },
{ name: "Disable timer", value: "disable", name_localizations: { fr: "Désactiver minuteur" } },
{ name: "Status", value: "status", name_localizations: { fr: "Statut" } }
)
)
.addStringOption(option => option
.setName("morning_time")
.setDescription("Morning time (HH:MM format, 24h)")
.setNameLocalizations({ fr: "heure_matin" })
.setDescriptionLocalizations({ fr: "Heure du matin (format HH:MM, 24h)" })
.setRequired(false)
)
.addStringOption(option => option
.setName("night_time")
.setDescription("Night time (HH:MM format, 24h)")
.setNameLocalizations({ fr: "heure_nuit" })
.setDescriptionLocalizations({ fr: "Heure du soir (format HH:MM, 24h)" })
.setRequired(false)
)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildFbx") as GuildFbx
let host = dbData.host
let appToken = dbData.appToken
const subcommandGroup = interaction.options.getSubcommandGroup(false)
const subcommand = interaction.options.getSubcommand(true)
if (subcommand === "status") {
// Construire l'embed de statut
const embed = new EmbedBuilder()
.setTitle(t(interaction.locale, "freebox.status.title"))
.setColor(dbData.enabled ? 0x00ff00 : 0xff0000)
.addFields({
name: t(interaction.locale, "freebox.status.config_section"),
value: [
t(interaction.locale, "freebox.status.module_field", { status: dbData.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }),
t(interaction.locale, "freebox.status.host_field", { value: host ? `\`${host}\`` : t(interaction.locale, "freebox.status.host_not_configured") }),
t(interaction.locale, "freebox.status.token_field", { status: appToken ? t(interaction.locale, "freebox.status.token_configured") : t(interaction.locale, "freebox.status.token_not_configured") })
].join('\n'),
inline: false
})
// Informations LCD si disponibles
if (dbData.lcd) {
const lcdStatus = dbData.lcd.enabled ? t(interaction.locale, "freebox.status.timer_enabled") : t(interaction.locale, "freebox.status.timer_disabled")
const botManaged = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "freebox.status.timer_no_manager")
const morningTime = dbData.lcd.morningTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
const nightTime = dbData.lcd.nightTime ?? t(interaction.locale, "freebox.status.timer_not_configured")
embed.addFields({
name: t(interaction.locale, "freebox.status.timer_section"),
value: [
t(interaction.locale, "freebox.timer.status_field", { status: lcdStatus }),
t(interaction.locale, "freebox.timer.managed_by", { manager: botManaged }),
t(interaction.locale, "freebox.timer.morning", { time: morningTime }),
t(interaction.locale, "freebox.timer.night", { time: nightTime })
].join('\n'),
inline: false
})
}
// Boutons d'action
const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("freebox_test_connection")
.setLabel(t(interaction.locale, "freebox.buttons.test_connection"))
.setEmoji("🔌")
.setStyle(ButtonStyle.Primary)
.setDisabled(!appToken),
new ButtonBuilder()
.setCustomId("freebox_lcd_status")
.setLabel(t(interaction.locale, "freebox.buttons.lcd_status"))
.setEmoji("💡")
.setStyle(ButtonStyle.Secondary)
.setDisabled(!appToken),
new ButtonBuilder()
.setCustomId("freebox_refresh_status")
.setLabel(t(interaction.locale, "freebox.buttons.refresh_status"))
.setEmoji("🔄")
.setStyle(ButtonStyle.Success)
)
return interaction.reply({ embeds: [embed], components: [buttons], flags: MessageFlags.Ephemeral })
}
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "common.module_disabled"), flags: MessageFlags.Ephemeral })
if (subcommand === "init") {
if (appToken) return interaction.reply({ content: t(interaction.locale, "freebox.auth.app_token_already_set"), flags: MessageFlags.Ephemeral })
host = interaction.options.getString("host", true)
if (host === "mafreebox.freebox.fr") return interaction.reply({ content: t(interaction.locale, "freebox.auth.host_not_allowed"), flags: MessageFlags.Ephemeral })
const initData = await Freebox.Core.Init(host) as APIResponseData<RequestAuthorization>
if (!initData.success) return Freebox.handleError(initData as APIResponseDataError, interaction)
appToken = initData.result.app_token
const trackId = initData.result.track_id
if (!trackId) return interaction.reply({ content: t(interaction.locale, "freebox.auth.track_id_failed"), flags: MessageFlags.Ephemeral })
// Si l'utilisateur n'a pas encore autorisé l'application, on lui demande de le faire
await interaction.reply({ content: t(interaction.locale, "freebox.auth.init_in_progress", { trackId }), flags: MessageFlags.Ephemeral })
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const initCheck = setInterval(async () => {
if (!host || !trackId) { clearInterval(initCheck); return }
const trackData = await Freebox.Core.Init(host, trackId) as APIResponseData<TrackAuthorizationProgress>
if (!trackData.success) return Freebox.handleError(trackData as APIResponseDataError, interaction, false)
const status = trackData.result.status
if (status === "granted") {
clearInterval(initCheck)
dbData.appToken = appToken
guildProfile.set("guildFbx", dbData)
guildProfile.markModified("guildFbx")
await guildProfile.save().catch(console.error)
return interaction.followUp({ content: t(interaction.locale, "common.success"), flags: MessageFlags.Ephemeral })
} else if (status === "denied") {
clearInterval(initCheck)
return interaction.followUp({ content: t(interaction.locale, "freebox.auth.user_denied_access"), flags: MessageFlags.Ephemeral })
} else if (status === "pending") { console.log("Freebox authorization pending...") }
}, 2000)
}
else {
if (!host) return interaction.reply({ content: t(interaction.locale, "freebox.general.host_not_set"), flags: MessageFlags.Ephemeral })
if (subcommand === "version") {
const versionData = await Freebox.Core.Version(host) as APIResponseDataVersion
const embed = new EmbedBuilder()
.setTitle("FreeboxOS API Version")
.setDescription(`Version: ${versionData.api_version || "Unknown"}`)
return interaction.reply({ embeds: [embed] })
}
if (!appToken) return interaction.reply({ content: t(interaction.locale, "freebox.general.app_token_not_set"), flags: MessageFlags.Ephemeral })
const challengeData = await Freebox.Login.Challenge(host) as APIResponseData<GetChallenge>
if (!challengeData.success) return Freebox.handleError(challengeData as APIResponseDataError, interaction)
const password = crypto.createHmac("sha1", appToken).update(challengeData.result.challenge).digest("hex")
const sessionData = await Freebox.Login.Session(host, password) as APIResponseData<OpenSession>
if (!sessionData.success) return Freebox.handleError(sessionData as APIResponseDataError, interaction)
const sessionToken = sessionData.result.session_token
if (!sessionToken) return interaction.reply({ content: t(interaction.locale, "freebox.auth.session_token_failed"), flags: MessageFlags.Ephemeral })
if (subcommandGroup === "get") {
if (subcommand === "connection") {
const connectionData = await Freebox.Get.Connection(host, sessionToken) as APIResponseData<ConnectionStatus>
if (!connectionData.success) return Freebox.handleError(connectionData as APIResponseDataError, interaction)
return interaction.reply({ content: t(interaction.locale, "freebox.api.connection_details", { details: inlineCode(JSON.stringify(connectionData.result)) }), flags: MessageFlags.Ephemeral })
}
else if (subcommand === "lcd") {
const lcdData = await Freebox.Get.LcdConfig(host, sessionToken) as APIResponseData<LcdConfig>
if (!lcdData.success) return Freebox.handleError(lcdData as APIResponseDataError, interaction)
return interaction.reply({ content: t(interaction.locale, "freebox.api.lcd_details", { details: inlineCode(JSON.stringify(lcdData.result)) }), flags: MessageFlags.Ephemeral })
}
}
else if (subcommandGroup === "lcd") {
// Initialiser l'objet LCD s'il n'existe pas
dbData.lcd ??= { enabled: false }
// Vérifier si le bot est autorisé pour ce serveur
if (dbData.lcd.enabled && dbData.lcd.botId && dbData.lcd.botId !== interaction.client.user.id) {
return interaction.reply({ content: t(interaction.locale, "freebox.lcd.managed_by_other_bot"), flags: MessageFlags.Ephemeral })
}
if (subcommand === "leds") {
const enabled = interaction.options.getBoolean("enabled", true)
const lcdData = await Freebox.Set.LcdConfig(host, sessionToken, { led_strip_enabled: enabled }) as APIResponseData<LcdConfig>
if (!lcdData.success) return Freebox.handleError(lcdData as APIResponseDataError, interaction)
return interaction.reply({ content: t(interaction.locale, "freebox.lcd.leds_success", { status: enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled") }), flags: MessageFlags.Ephemeral })
}
else if (subcommand === "timer") {
const action = interaction.options.getString("action")
if (!action) return interaction.reply({ content: t(interaction.locale, "freebox.general.invalid_action"), flags: MessageFlags.Ephemeral })
if (action === "status") {
const status = dbData.lcd.enabled ? t(interaction.locale, "freebox.common.enabled") : t(interaction.locale, "freebox.common.disabled")
const managedBy = dbData.lcd.botId ? `<@${dbData.lcd.botId}>` : t(interaction.locale, "common.none")
return interaction.reply({ content: t(interaction.locale, "freebox.timer.status_display", { status, managedBy }), flags: MessageFlags.Ephemeral })
}
else if (action === "enable") {
const morningTime = interaction.options.getString("morning_time")
const nightTime = interaction.options.getString("night_time")
if (!morningTime || !nightTime) return interaction.reply({ content: t(interaction.locale, "freebox.timer.times_required"), flags: MessageFlags.Ephemeral })
// Valider le format HH:MM
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/
if (!timeRegex.test(morningTime) || !timeRegex.test(nightTime)) return interaction.reply({ content: t(interaction.locale, "freebox.general.invalid_time_format"), flags: MessageFlags.Ephemeral })
// Activer le timer et enregistrer ce bot comme responsable
dbData.lcd.enabled = true
dbData.lcd.botId = interaction.client.user.id
dbData.lcd.morningTime = morningTime
dbData.lcd.nightTime = nightTime
guildProfile.set("guildFbx", dbData)
guildProfile.markModified("guildFbx")
await guildProfile.save().catch(console.error)
// Démarrer les timers automatiques
if (interaction.guildId) Freebox.Timer.schedule(interaction.client, interaction.guildId, dbData)
return interaction.reply({ content: t(interaction.locale, "freebox.timer.enabled", { morningTime, nightTime }), flags: MessageFlags.Ephemeral })
}
else if (action === "disable") {
// Arrêter les timers actifs avant de désactiver
if (interaction.guildId) Freebox.Timer.clear(interaction.guildId)
// Désactiver le timer
dbData.lcd.enabled = false
dbData.lcd.botId = ""
dbData.lcd.morningTime = ""
dbData.lcd.nightTime = ""
guildProfile.set("guildFbx", dbData)
guildProfile.markModified("guildFbx")
await guildProfile.save().catch(console.error)
return interaction.reply({ content: t(interaction.locale, "freebox.timer.disabled"), flags: MessageFlags.Ephemeral })
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
import * as amp from "./amp"
import * as boost from "./boost"
import * as database from "./database"
import * as freebox from "./freebox"
import * as ping from "./ping"
import * as twitch from "./twitch"
import type { Command } from "@/types"
export default [
amp,
boost,
database,
freebox,
ping,
twitch
] as Command[]

View File

@@ -1,11 +1,17 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
export default {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check the latency of the bot'),
async execute(interaction: ChatInputCommandInteraction) {
let sent = await interaction.reply({ content: 'Pinging...', fetchReply: true })
interaction.editReply(`Websocket heartbeat: ${interaction.client.ws.ping}ms.\nRoundtrip latency: ${sent.createdTimestamp - interaction.createdTimestamp}ms`)
}
}
import { SlashCommandBuilder } from "discord.js"
import type { ChatInputCommandInteraction } from "discord.js"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("ping")
.setDescription("Check the latency of the bot")
.setDescriptionLocalizations({ fr: "Vérifier la latence du bot" })
export async function execute(interaction: ChatInputCommandInteraction) {
await interaction.reply({ content: t(interaction.locale, "ping.pinging") })
const sent = await interaction.fetchReply()
return interaction.editReply(t(interaction.locale, "ping.response", {
heartbeat: interaction.client.ws.ping.toString(),
latency: (sent.createdTimestamp - interaction.createdTimestamp).toString()
}))
}

View File

@@ -0,0 +1,198 @@
import { SlashCommandBuilder, ChannelType, MessageFlags, PermissionFlagsBits } from "discord.js"
import type { ChatInputCommandInteraction, AutocompleteInteraction, ApplicationCommandOptionChoiceData } from "discord.js"
import chalk from "chalk"
import { twitchClient, listener, onlineSub, offlineSub, generateTwitchEmbed } from "@/utils/twitch"
import type { GuildTwitch } from "@/types/schemas"
import dbGuild from "@/schemas/guild"
import { t } from "@/utils/i18n"
export const data = new SlashCommandBuilder()
.setName("twitch")
.setDescription("Manage streamers notifications")
.setDescriptionLocalizations({ fr: "Gérer les notifications des streameurs" })
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(subcommand => subcommand
.setName("status")
.setDescription("Display Twitch module status")
.setNameLocalizations({ fr: "statut" })
.setDescriptionLocalizations({ fr: "Afficher le statut du module Twitch" })
)
.addSubcommand(subcommand => subcommand
.setName("channel")
.setDescription("Set the channel to send notifications")
.setNameLocalizations({ fr: "canal" })
.setDescriptionLocalizations({ fr: "Définir le canal pour envoyer les notifications" })
.addChannelOption(option => option
.setName("channel")
.setDescription("The channel to send notifications")
.setNameLocalizations({ fr: "canal" })
.setDescriptionLocalizations({ fr: "Le canal pour envoyer les notifications" })
.setRequired(true)
)
)
.addSubcommandGroup(subcommandgroup => subcommandgroup
.setName("streamer")
.setDescription("Manage streamers")
.setNameLocalizations({ fr: "streameur" })
.setDescriptionLocalizations({ fr: "Gérer les streameurs" })
.addSubcommand(subcommand => subcommand
.setName("list")
.setDescription("List all streamers")
.setNameLocalizations({ fr: "liste" })
.setDescriptionLocalizations({ fr: "Lister tous les streameurs" })
)
.addSubcommand(subcommand => subcommand
.setName("add")
.setDescription("Add a streamer")
.setNameLocalizations({ fr: "ajouter" })
.setDescriptionLocalizations({ fr: "Ajouter un streameur" })
.addStringOption(option => option
.setName("username")
.setDescription("The username of the streamer to add")
.setNameLocalizations({ fr: "nom_utilisateur" })
.setDescriptionLocalizations({ fr: "Le nom d'utilisateur du streameur à ajouter" })
.setRequired(true)
.setAutocomplete(true)
)
.addUserOption(option => option
.setName("member")
.setDescription("The member on the guild to mention")
.setNameLocalizations({ fr: "membre" })
.setDescriptionLocalizations({ fr: "Le membre sur le serveur à mentionner" })
.setRequired(false)
)
)
.addSubcommand(subcommand => subcommand
.setName("remove")
.setDescription("Remove a streamer")
.setNameLocalizations({ fr: "supprimer" })
.setDescriptionLocalizations({ fr: "Supprimer un streameur" })
.addStringOption(option => option
.setName("username")
.setDescription("The username of the streamer to remove")
.setNameLocalizations({ fr: "nom_utilisateur" })
.setDescriptionLocalizations({ fr: "Le nom d'utilisateur du streameur à supprimer" })
.setRequired(true)
.setAutocomplete(true)
)
)
)
export async function execute(interaction: ChatInputCommandInteraction) {
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.reply({ content: t(interaction.locale, "common.database_not_found"), flags: MessageFlags.Ephemeral })
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
const subcommandGroup = interaction.options.getSubcommandGroup(false)
const subcommand = interaction.options.getSubcommand(true)
if (subcommand == "status") {
// Utiliser la fonction utilitaire pour générer l'embed et les composants
const { embed, components } = generateTwitchEmbed(dbData, interaction.client, interaction.guild?.id ?? "", interaction.locale)
return interaction.reply({ embeds: [embed], components: components, flags: MessageFlags.Ephemeral })
}
if (!dbData.enabled) return interaction.reply({ content: t(interaction.locale, "twitch.module_disabled_activate"), flags: MessageFlags.Ephemeral })
if (subcommand == "channel") {
const channel = interaction.options.getChannel("channel", true)
if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement)
return interaction.reply({ content: t(interaction.locale, "common.invalid_text_channel"), flags: MessageFlags.Ephemeral })
dbData.channelId = channel.id
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
return interaction.reply({ content: t(interaction.locale, "twitch.notifications_channel_set", { channel: channel.name ?? channel.id }), flags: MessageFlags.Ephemeral })
}
else if (subcommandGroup == "streamer") {
if (!dbData.channelId) return interaction.reply({ content: t(interaction.locale, "twitch.configure_channel_first"), flags: MessageFlags.Ephemeral })
if (subcommand == "list") {
if (!dbData.streamers.length) return interaction.reply({ content: t(interaction.locale, "twitch.no_streamers_list"), flags: MessageFlags.Ephemeral })
const streamers = [] as string[]
await Promise.all(dbData.streamers.map(async streamer => {
try {
const user = await twitchClient.users.getUserById(streamer.twitchUserId)
if (user) streamers.push(`- ${user.displayName} (${streamer.twitchUserId})`)
else streamers.push(`- ${t(interaction.locale, "twitch.user_not_found_id", { id: streamer.twitchUserId })}`)
} catch (error) {
console.log(chalk.magenta(`[Twitch] Error fetching user for ID ${streamer.twitchUserId}`))
console.error(error)
}
}))
const streamerList = streamers.length > 0 ? streamers.join("\n") : t(interaction.locale, "twitch.no_streamers")
return interaction.reply({ content: `${t(interaction.locale, "twitch.list.title")}:\n${streamerList}`, flags: MessageFlags.Ephemeral })
}
else if (subcommand == "add") {
const username = interaction.options.getString("username", true)
const member = interaction.options.getUser("member", false)
const user = await twitchClient.users.getUserByName(username)
if (!user) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_found", { username }), flags: MessageFlags.Ephemeral })
if (dbData.streamers.some(s => s.twitchUserId === user.id)) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_already_added", { username }), flags: MessageFlags.Ephemeral })
dbData.streamers.push({ twitchUserId: user.id, discordUserId: member?.id ?? "", messageId: "" })
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(user.id)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.online")) listener.onStreamOnline(user.id, onlineSub)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
if (!userSubs.data.find(sub => sub.transportMethod === "webhook" && sub.type === "stream.offline")) listener.onStreamOffline(user.id, offlineSub)
return interaction.reply({ content: t(interaction.locale, "twitch.streamer_added", { username, id: user.id }), flags: MessageFlags.Ephemeral })
}
else if (subcommand == "remove") {
const username = interaction.options.getString("username", true)
const user = await twitchClient.users.getUserByName(username)
if (!user) return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_found", { username }), flags: MessageFlags.Ephemeral })
const streamerIndex = dbData.streamers.findIndex(s => s.twitchUserId === user.id)
if (streamerIndex === -1)return interaction.reply({ content: t(interaction.locale, "twitch.streamer_not_in_list", { username }), flags: MessageFlags.Ephemeral })
dbData.streamers.splice(streamerIndex, 1)
guildProfile.set("guildTwitch", dbData)
guildProfile.markModified("guildTwitch")
await guildProfile.save().catch(console.error)
if (!await dbGuild.exists({ "guildTwitch.streamers.twitchUserId": user.id })) {
const userSubs = await twitchClient.eventSub.getSubscriptionsForUser(user.id)
await Promise.all(userSubs.data.map(async sub => { if (sub.transportMethod === "webhook" && (sub.type === "stream.online" || sub.type === "stream.offline")) await sub.unsubscribe() }))
console.log(chalk.magenta(`[Twitch] Listener removed for ${user.displayName} (ID ${user.id})`))
}
return interaction.reply({ content: t(interaction.locale, "twitch.streamer_removed", { username }), flags: MessageFlags.Ephemeral })
}
}
}
export async function autocompleteRun(interaction: AutocompleteInteraction) {
const query = interaction.options.getString("username", true)
if (!query || query.length < 3) return interaction.respond([])
const guildProfile = await dbGuild.findOne({ guildId: interaction.guild?.id })
if (!guildProfile) return interaction.respond([])
const dbData = guildProfile.get("guildTwitch") as GuildTwitch
if (!dbData.enabled) return interaction.respond([])
const choices: ApplicationCommandOptionChoiceData[] = []
const searchResult = await twitchClient.search.searchChannels(query)
if (searchResult.data.length === 0) return interaction.respond([])
searchResult.data.forEach(streamerResult => {
if (dbData.streamers.some(s => s.twitchUserId === streamerResult.id)) return
choices.push({ name: streamerResult.displayName, value: streamerResult.name })
})
return interaction.respond(choices)
}