diff --git a/src/commands/sessiones.js b/src/commands/sessiones.js new file mode 100644 index 000000000..a5f2d9856 --- /dev/null +++ b/src/commands/sessiones.js @@ -0,0 +1,129 @@ +import { ApplicationCommandOptionType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; + +if (!global.mapaVotos) { + global.mapaVotos = new Map(); +} + +export default { + data: { + name: 'sesiones_00y4n', + description: 'Gestión de sesiones de Roleplay y Car Meets', + options: [ + { + name: 'startup_rp', + description: 'Lanza un inicio de sesión de Roleplay convencional.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { name: 'reacciones', description: 'Cantidad de reacciones necesarias.', type: ApplicationCommandOptionType.Integer, required: true }, + { name: 'limite', description: 'Ejemplo: 80 MPH', type: ApplicationCommandOptionType.String, required: false }, + { name: 'peacetime', description: '¿Peacetime activo? (On / Off)', type: ApplicationCommandOptionType.String, required: false }, + { name: 'imagen', description: 'Link de la foto/banner para el Roleplay (opcional).', type: ApplicationCommandOptionType.String, required: false } + ] + }, + { + name: 'startup_meet', + description: 'Lanza un inicio de sesión para un Car Meet.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { name: 'reacciones', description: 'Cantidad de reacciones necesarias.', type: ApplicationCommandOptionType.Integer, required: true }, + { name: 'tematica', description: 'Ejemplo: JDM, Exóticos', type: ApplicationCommandOptionType.String, required: true }, + { name: 'spots', description: 'Ejemplo: 2-3 SPOTS + BOTM', type: ApplicationCommandOptionType.String, required: true }, + { name: 'ubicacion', description: 'Lugar de inicio (Ej: Spawn)', type: ApplicationCommandOptionType.String, required: true }, + { name: 'imagen', description: 'Link de la foto/banner para el Car Meet (opcional).', type: ApplicationCommandOptionType.String, required: false } + ] + }, + { + name: 'release', + description: 'Lanza el botón del link vinculándolo al inicio.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { name: 'id_inicio', description: 'Copia el ID del mensaje de Startup.', type: ApplicationCommandOptionType.String, required: true }, + { name: 'tipo', description: '¿RP o Meet?', type: ApplicationCommandOptionType.String, required: true, choices: [{ name: 'Roleplay', value: 'rp' }, { name: 'Car Meet', value: 'meet' }] }, + { name: 'imagen', description: 'Link de la foto/banner para la apertura (opcional).', type: ApplicationCommandOptionType.String, required: false } + ] + } + ] + }, + + async execute(interaction) { + const sub = interaction.options.getSubcommand(); + const urlImagen = interaction.options.getString('imagen'); + + if (sub === 'startup_rp') { + const reacciones = interaction.options.getInteger('reacciones'); + const limite = interaction.options.getString('limite') || '80 MPH'; + const peacetime = interaction.options.getString('peacetime') || 'Off'; + + const embedRP = new EmbedBuilder() + .setTitle('__SWFL RP Startup__') + .setDescription(`> **Anfitrión:** <@${interaction.user.id}>\n\n*Asegúrate de haber leído las normativas en el canal correspondiente y tener tu vehículo registrado antes de ingresar a la sesión.*\n\n**¡Necesitamos ${reacciones} reacciones para iniciar!**`) + .addFields( + { name: '› Límite de Velocidad (FRP)', value: `${limite}`, inline: true }, + { name: '› Estado de Peacetime', value: `${peacetime}`, inline: true } + ) + .setColor('#1E90FF'); + + if (urlImagen) embedRP.setImage(urlImagen); + + await interaction.reply({ content: 'Lanzando Startup...', ephemeral: true }); + const msg = await interaction.channel.send({ content: '@everyone', embeds: [embedRP] }); + await msg.react('✅'); + + global.mapaVotos.set(msg.id, new Set()); + } + + if (sub === 'startup_meet') { + const reacciones = interaction.options.getInteger('reacciones'); + const tematica = interaction.options.getString('tematica'); + const spots = interaction.options.getString('spots'); + const ubicacion = interaction.options.getString('ubicacion'); + + const embedMeet = new EmbedBuilder() + .setTitle('__SWFL Meet Startup__') + .setDescription(`> **Anfitrión:** <@${interaction.user.id}>\n\n*¡Atención amantes de los fierros! Se viene una juntada oficial.*\n\n**¡Necesitamos ${reacciones} reacciones para iniciar!**`) + .addFields( + { name: '❗ Temática del Meet', value: `${tematica}`, inline: false }, + { name: '❗ Duración / Spots', value: `${spots}`, inline: true }, + { name: '❗ Lugar de Inicio', value: `${ubicacion}`, inline: true } + ) + .setColor('#00FF7F'); + + if (urlImagen) embedMeet.setImage(urlImagen); + + await interaction.reply({ content: 'Lanzando Car Meet...', ephemeral: true }); + const msg = await interaction.channel.send({ content: '@everyone', embeds: [embedMeet] }); + await msg.react('✅'); + + global.mapaVotos.set(msg.id, new Set()); + } + + if (sub === 'release') { + const idInicio = interaction.options.getString('id_inicio'); + const tipo = interaction.options.getString('tipo'); + + if (!global.mapaVotos || !global.mapaVotos.has(idInicio)) { + return interaction.reply({ content: '❌ El ID de mensaje no es válido o el bot se reinició borrando la memoria.', ephemeral: true }); + } + + const titulo = tipo === 'rp' ? '__SWFL Roleplay Release__' : '__SWFL Meet Release__'; + const color = tipo === 'rp' ? '#1E90FF' : '#00FF7F'; + + const embedRelease = new EmbedBuilder() + .setTitle(titulo) + .setDescription(`> **Anfitrión:** <@${interaction.user.id}>\n\nLa sesión fue oficialmente lanzada. Si aportaste tu reacción en el mensaje de inicio, toca el botón de abajo para obtener el acceso.\n\n⚠️ *Filtrar el enlace directo es motivo de ban.*`) + .setColor(color); + + if (urlImagen) embedRelease.setImage(urlImagen); + + const fila = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`link_session_${idInicio}`) + .setLabel('Link de la Sesión') + .setStyle(ButtonStyle.Primary) + ); + + await interaction.reply({ content: 'Lanzando release...', ephemeral: true }); + await interaction.channel.send({ content: '@everyone', embeds: [embedRelease], components: [fila] }); + } + } +}; diff --git a/src/config/bot.js b/src/config/bot.js index 36e588cd4..870d6ae00 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -25,7 +25,7 @@ export const botConfig = { activities: [ { // Text users will see (example: "Playing /help | Titan Bot"). - name: "Made with ❤️", + name: "00Y4n Comunidad SWFL", // Activity type number (0 = Playing). type: 0, }, @@ -88,14 +88,14 @@ export const botConfig = { embeds: { colors: { // Main brand colors. - primary: "#336699", - secondary: "#2F3136", + primary: "#ff6600", + secondary: "#ff6600", // Standard status colors for success/error/warning/info messages. - success: "#57F287", + success: "#ff6600", error: "#ED4245", - warning: "#FEE75C", - info: "#3498DB", + warning: "#ff6600", + info: "#ff6600", // Neutral utility colors. light: "#FFFFFF", @@ -112,18 +112,18 @@ export const botConfig = { // Feature-specific colors. giveaway: { - active: "#57F287", - ended: "#ED4245", + active: "#ff6600", + ended: "#ff6600", }, ticket: { - open: "#57F287", - claimed: "#FAA61A", - closed: "#ED4245", - pending: "#99AAB5", + open: "#ff6600", + claimed: "#ff6600", + closed: "#ff6600", + pending: "#ff6600", }, - economy: "#F1C40F", - birthday: "#E91E63", - moderation: "#9B59B6", + economy: "#ff6600", + birthday: "#ff6600", + moderation: "#ff6600", // Ticket priority color mapping. priority: { diff --git a/src/events/reactionHandler.js b/src/events/reactionHandler.js new file mode 100644 index 000000000..a2d838f92 --- /dev/null +++ b/src/events/reactionHandler.js @@ -0,0 +1,14 @@ +export default { + name: 'messageReactionAdd', + async execute(reaction, user) { + if (user.bot) return; + if (reaction.partial) { + try { await reaction.fetch(); } catch (error) { return; } + } + + if (reaction.emoji.name === '✅' && global.mapaVotos && global.mapaVotos.has(reaction.message.id)) { + global.mapaVotos.get(reaction.message.id).add(user.id); + console.log(`[00Y4n Votos] Voto registrado para ${user.username}`); + } + } +}; diff --git a/src/events/reactionRemoveHandler.js b/src/events/reactionRemoveHandler.js new file mode 100644 index 000000000..f3613ceb0 --- /dev/null +++ b/src/events/reactionRemoveHandler.js @@ -0,0 +1,14 @@ +export default { + name: 'messageReactionRemove', + async execute(reaction, user) { + if (user.bot) return; + if (reaction.partial) { + try { await reaction.fetch(); } catch (error) { return; } + } + + if (reaction.emoji.name === '✅' && global.mapaVotos && global.mapaVotos.has(reaction.message.id)) { + global.mapaVotos.get(reaction.message.id).delete(user.id); + console.log(`[00Y4n Votos] Voto removido para ${user.username}`); + } + } +}; diff --git a/src/handlers/commandLoader.js b/src/handlers/commandLoader.js index 5273f2934..e372e1d7d 100644 --- a/src/handlers/commandLoader.js +++ b/src/handlers/commandLoader.js @@ -10,8 +10,6 @@ const __dirname = path.dirname(__filename); - - function getSubcommandInfo(commandData) { const subcommands = []; @@ -37,6 +35,24 @@ if (subOption.type === 1) { +function getCommandDataJSON(command) { + if (!command.data) { + return null; + } + + // If it's a SlashCommandBuilder, call toJSON() + if (typeof command.data.toJSON === 'function') { + return command.data.toJSON(); + } + + // If it's already a plain object, return it + if (typeof command.data === 'object') { + return command.data; + } + + return null; +} + @@ -62,8 +78,6 @@ async function getAllFiles(directory, fileList = []) { - - export async function loadCommands(client) { client.commands = new Collection(); const commandsPath = path.join(__dirname, '../commands'); @@ -100,7 +114,13 @@ export async function loadCommands(client) { client.commands.set(primaryCommandName, command); } - const subcommands = getSubcommandInfo(command.data.toJSON()); + const commandDataJSON = getCommandDataJSON(command); + if (!commandDataJSON) { + logger.warn(`Command at ${filePath} has invalid data structure.`); + continue; + } + + const subcommands = getSubcommandInfo(commandDataJSON); logger.info(`Loaded command: ${primaryCommandName} from ${normalizedPath} (category: ${category})`); @@ -114,12 +134,16 @@ export async function loadCommands(client) { } const commandsWithSubcommands = Array.from(client.commands.values()).filter(cmd => { - const subcommands = getSubcommandInfo(cmd.data.toJSON()); + const commandDataJSON = getCommandDataJSON(cmd); + if (!commandDataJSON) return false; + const subcommands = getSubcommandInfo(commandDataJSON); return subcommands.length > 0; }); const totalSubcommands = commandsWithSubcommands.reduce((total, cmd) => { - return total + getSubcommandInfo(cmd.data.toJSON()).length; + const commandDataJSON = getCommandDataJSON(cmd); + if (!commandDataJSON) return total; + return total + getSubcommandInfo(commandDataJSON).length; }, 0); const uniqueCommands = new Set(); @@ -136,9 +160,6 @@ export async function loadCommands(client) { - - - export async function registerCommands(client, guildId) { try { const commands = []; @@ -146,17 +167,22 @@ export async function registerCommands(client, guildId) { const registeredNames = new Set(); for (const command of client.commands.values()) { - if (command.data && typeof command.data.toJSON === 'function') { + if (command.data) { + const commandDataJSON = getCommandDataJSON(command); + if (!commandDataJSON) { + logger.warn(`Command has invalid data structure: ${command}`); + continue; + } + const commandName = command.data.name; logger.debug(`Processing command for registration: ${commandName}`); if (!registeredNames.has(commandName)) { registeredNames.add(commandName); - const commandJson = command.data.toJSON(); - commands.push(commandJson); + commands.push(commandDataJSON); - const subcommands = getSubcommandInfo(commandJson); + const subcommands = getSubcommandInfo(commandDataJSON); totalSubcommands += subcommands.length; if (process.env.NODE_ENV !== 'production') { @@ -166,7 +192,7 @@ const registeredNames = new Set(); logger.debug(`Skipping duplicate command: ${commandName}`); } } else { - logger.warn(`Command missing data or toJSON method: ${command}`); + logger.warn(`Command missing data: ${command}`); } } @@ -299,9 +325,6 @@ const registeredNames = new Set(); - - - export async function reloadCommand(client, commandName) { const command = client.commands.get(commandName); diff --git a/src/interactions/sessionButtons.js b/src/interactions/sessionButtons.js new file mode 100644 index 000000000..e4ce61cc7 --- /dev/null +++ b/src/interactions/sessionButtons.js @@ -0,0 +1,25 @@ +const LINK_ROBLOX = "https://www.roblox.com/games/XXXXXX/Southwest-Florida"; // Pega acá tu link real + +export async function execute(interaction) { + if (!interaction.isButton()) return; + + if (interaction.customId.startsWith('link_session_')) { + const idStartupAsociado = interaction.customId.replace('link_session_', ''); + const userId = interaction.user.id; + + const conjuntoVotos = global.mapaVotos ? global.mapaVotos.get(idStartupAsociado) : null; + + // VERIFICACIÓN ESTRICTA DE VOTO + if (conjuntoVotos && conjuntoVotos.has(userId)) { + await interaction.reply({ + content: `🎉 **¡Voto verificado!** Acá tenés el acceso a la sesión de **00Y4n**:\n🔗 ${LINK_ROBLOX}\n\n*Respetá las reglas y evitá compartir el link.*`, + ephemeral: true + }); + } else { + await interaction.reply({ + content: `❌ **No podés obtener el link.**\nNo reaccionaste con el \`✅\` en el mensaje de inicio de esta sesión.`, + ephemeral: true + }); + } + } +}