Skip to content
129 changes: 129 additions & 0 deletions src/commands/sessiones.js
Original file line number Diff line number Diff line change
@@ -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] });
}
}
};
30 changes: 15 additions & 15 deletions src/config/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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",
Expand All @@ -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: {
Expand Down
14 changes: 14 additions & 0 deletions src/events/reactionHandler.js
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
};
14 changes: 14 additions & 0 deletions src/events/reactionRemoveHandler.js
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
};
59 changes: 41 additions & 18 deletions src/handlers/commandLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const __dirname = path.dirname(__filename);





function getSubcommandInfo(commandData) {
const subcommands = [];

Expand All @@ -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;
}




Expand All @@ -62,8 +78,6 @@ async function getAllFiles(directory, fileList = []) {





export async function loadCommands(client) {
client.commands = new Collection();
const commandsPath = path.join(__dirname, '../commands');
Expand Down Expand Up @@ -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})`);

Expand All @@ -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();
Expand All @@ -136,27 +160,29 @@ export async function loadCommands(client) {






export async function registerCommands(client, guildId) {
try {
const commands = [];
let totalSubcommands = 0;
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') {
Expand All @@ -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}`);
}
}

Expand Down Expand Up @@ -299,9 +325,6 @@ const registeredNames = new Set();






export async function reloadCommand(client, commandName) {
const command = client.commands.get(commandName);

Expand Down
25 changes: 25 additions & 0 deletions src/interactions/sessionButtons.js
Original file line number Diff line number Diff line change
@@ -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
});
}
}
}