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" import { logConsole } from "@/utils/console" 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().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 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 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") logConsole('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 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 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 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 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 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 }) } } } } }