· 3 months ago · Jun 25, 2025, 02:55 PM
1import pkg from 'discord.js';
2import fs from 'fs';
3import dotenv from 'dotenv';
4import winston from 'winston';
5import Database from 'better-sqlite3';
6
7dotenv.config();
8
9const {
10 Client,
11 GatewayIntentBits,
12 REST,
13 Routes,
14 SlashCommandBuilder,
15 EmbedBuilder,
16 PermissionFlagsBits,
17 ButtonBuilder,
18 ActionRowBuilder,
19 ButtonStyle,
20 ChannelType,
21 PermissionsBitField
22} = pkg;
23
24// -----------------------
25// Mapa para transformar gameid a nombre bonito
26const gamesMap = {
27 repo: "R.E.P.O.",
28 fifa17: "EA Sports FIFA 17",
29 lethalcompany: "Lethal Company",
30 iamfish: "I Am Fish",
31 planetzoo: "Planet Zoo",
32 arbs: "Animal Revolt Battle Simulator",
33 poppy2: "Poppy Playtime - Chapter 2 [DLC]"
34};
35
36// -----------------------
37// Logging con Winston
38const logger = winston.createLogger({
39 level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
40 format: winston.format.combine(
41 winston.format.timestamp(),
42 winston.format.json()
43 ),
44 transports: [
45 new winston.transports.Console(),
46 new winston.transports.File({ filename: 'bot-error.log', level: 'error' }),
47 new winston.transports.File({ filename: 'bot-combined.log' })
48 ],
49});
50
51process.on('uncaughtException', (err) => {
52 logger.error('uncaughtException', { error: err.stack || err });
53});
54process.on('unhandledRejection', (reason) => {
55 logger.error('unhandledRejection', { reason });
56});
57
58// -----------------------
59// Base de datos SQLite
60const dbPath = process.env.DB_FILE || './botdata.db';
61const db = new Database(dbPath);
62db.pragma('journal_mode = WAL');
63
64// Tabla códigos de compra
65db.exec(`
66 CREATE TABLE IF NOT EXISTS codes (
67 code TEXT PRIMARY KEY,
68 userId TEXT NOT NULL,
69 channelId TEXT NOT NULL,
70 messageId TEXT NOT NULL,
71 gameid TEXT NOT NULL,
72 used INTEGER NOT NULL DEFAULT 0,
73 createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
74 );
75`);
76
77// Tabla puntos usuarios
78db.exec(`
79 CREATE TABLE IF NOT EXISTS user_points (
80 userId TEXT PRIMARY KEY,
81 points INTEGER NOT NULL DEFAULT 0
82 );
83`);
84
85// Tabla configuración del sistema de puntos y setup
86db.exec(`
87 CREATE TABLE IF NOT EXISTS setup_config (
88 guildId TEXT PRIMARY KEY,
89 messages_needed INTEGER NOT NULL DEFAULT 10,
90 points INTEGER NOT NULL DEFAULT 1,
91 no_repetir INTEGER NOT NULL DEFAULT 0,
92 banco_channel_id TEXT DEFAULT NULL,
93 leaderboard_message_id TEXT DEFAULT NULL
94 );
95`);
96
97// -----------------------
98// Cargar puntos en memoria
99const pointsMap = new Map();
100try {
101 const rows = db.prepare('SELECT userId, points FROM user_points').all();
102 for (const row of rows) {
103 pointsMap.set(row.userId, row.points);
104 }
105 logger.info('Puntos cargados desde DB', { count: pointsMap.size });
106} catch (e) {
107 logger.error('Error cargando puntos desde DB', { error: e });
108}
109
110// -----------------------
111// Variables para control de mensajes para dar puntos
112// Por guildId: { messages_needed, points, no_repetir, banco_channel_id, leaderboard_message_id }
113const guildConfigs = new Map();
114
115// Para guardar el último mensaje por usuario para comprobar repetidos si no_repetir=true
116// Estructura: Map<guildId, Map<userId, { lastMessage: string, countMessages: int }>>
117const userMessageTrack = new Map();
118
119// -----------------------
120// Cliente Discord
121const client = new Client({
122 intents: [
123 GatewayIntentBits.Guilds,
124 GatewayIntentBits.GuildMessages,
125 GatewayIntentBits.MessageContent,
126 GatewayIntentBits.GuildMembers
127 ]
128});
129
130// -----------------------
131// Función generar código tipo XXXX-XXXX-XXXX-XXXX
132function generateCode() {
133 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
134 function part() {
135 let s = '';
136 for (let i = 0; i < 4; i++) {
137 s += chars.charAt(Math.floor(Math.random() * chars.length));
138 }
139 return s;
140 }
141 return `${part()}-${part()}-${part()}-${part()}`;
142}
143
144// -----------------------
145// Gestión puntos usuario
146function setUserPoints(userId, points) {
147 pointsMap.set(userId, points);
148 try {
149 db.prepare('INSERT OR REPLACE INTO user_points (userId, points) VALUES (?, ?)').run(userId, points);
150 logger.debug('Puntos guardados en DB', { userId, points });
151 } catch (e) {
152 logger.error('Error guardando puntos en DB', { error: e, userId, points });
153 }
154}
155
156function addUserPoints(userId, delta) {
157 const prev = pointsMap.get(userId) || 0;
158 const updated = prev + delta;
159 setUserPoints(userId, updated);
160 return updated;
161}
162
163// -----------------------
164// Guardar código en DB (ahora con gameid)
165function storeCodeInDb(code, userId, channelId, messageId, gameid) {
166 try {
167 const stmt = db.prepare('INSERT INTO codes (code, userId, channelId, messageId, gameid) VALUES (?, ?, ?, ?, ?)');
168 stmt.run(code, userId, channelId, messageId, gameid);
169 logger.debug('Código almacenado en DB', { code, userId, channelId, messageId, gameid });
170 } catch (err) {
171 logger.error('Error almacenando código en DB', { error: err, code });
172 }
173}
174
175// -----------------------
176// Funciones para setup config
177function getSetupConfig(guildId) {
178 let cfg = guildConfigs.get(guildId);
179 if (cfg) return cfg;
180 try {
181 const row = db.prepare('SELECT * FROM setup_config WHERE guildId = ?').get(guildId);
182 if (row) {
183 row.no_repetir = Boolean(row.no_repetir);
184 guildConfigs.set(guildId, row);
185 return row;
186 }
187 } catch (e) {
188 logger.error('Error leyendo setup config', { error: e, guildId });
189 }
190 // Defaults:
191 const defaultCfg = { guildId, messages_needed: 10, points: 1, no_repetir: false, banco_channel_id: null, leaderboard_message_id: null };
192 guildConfigs.set(guildId, defaultCfg);
193 return defaultCfg;
194}
195
196function saveSetupConfig(cfg) {
197 try {
198 db.prepare(`
199 INSERT OR REPLACE INTO setup_config (guildId, messages_needed, points, no_repetir, banco_channel_id, leaderboard_message_id)
200 VALUES (?, ?, ?, ?, ?, ?)
201 `).run(cfg.guildId, cfg.messages_needed, cfg.points, cfg.no_repetir ? 1 : 0, cfg.banco_channel_id, cfg.leaderboard_message_id);
202 guildConfigs.set(cfg.guildId, cfg);
203 } catch (e) {
204 logger.error('Error guardando setup config', { error: e, guildId: cfg.guildId });
205 }
206}
207
208// -----------------------
209// Función para crear o actualizar embed leaderboard en canal #banco
210async function updateLeaderboardEmbed(guild) {
211 try {
212 const cfg = getSetupConfig(guild.id);
213 if (!cfg.banco_channel_id) {
214 logger.warn('No banco_channel_id configurado para leaderboard', { guildId: guild.id });
215 return;
216 }
217 const channel = guild.channels.cache.get(cfg.banco_channel_id);
218 if (!channel) {
219 logger.warn('Canal banco_channel_id no encontrado', { guildId: guild.id, channelId: cfg.banco_channel_id });
220 return;
221 }
222
223 // Obtener usuarios con puntos, ordenar por puntos descendente
224 const usersWithPoints = Array.from(pointsMap.entries())
225 .filter(([userId]) => guild.members.cache.has(userId))
226 .map(([userId, pts]) => ({ userId, pts }))
227 .sort((a, b) => b.pts - a.pts);
228
229 if (usersWithPoints.length === 0) {
230 // Si no hay usuarios, manda embed con mensaje vacío
231 const embed = new EmbedBuilder()
232 .setTitle('Leaderboard')
233 .setDescription('No hay usuarios con puntos aún.')
234 .setColor('Blue')
235 .setTimestamp();
236
237 if (cfg.leaderboard_message_id) {
238 try {
239 const msg = await channel.messages.fetch(cfg.leaderboard_message_id);
240 await msg.edit({ embeds: [embed] });
241 return;
242 } catch {
243 // si no se puede editar, eliminar id para forzar crear de nuevo
244 cfg.leaderboard_message_id = null;
245 saveSetupConfig(cfg);
246 return;
247 }
248 } else {
249 const msg = await channel.send({ embeds: [embed] });
250 cfg.leaderboard_message_id = msg.id;
251 saveSetupConfig(cfg);
252 return;
253 }
254 }
255
256 // Construir texto de leaderboard (top 25 máximo)
257 const fields = [];
258 let position = 1;
259 for (const { userId, pts } of usersWithPoints.slice(0, 25)) {
260 const member = guild.members.cache.get(userId);
261 if (!member) continue;
262 fields.push({
263 name: `#${position} - ${member.user.tag}`,
264 value: `${pts} puntos`,
265 inline: false
266 });
267 position++;
268 }
269
270 const embed = new EmbedBuilder()
271 .setTitle('🏆 Leaderboard de Puntos')
272 .setColor('Gold')
273 .addFields(fields)
274 .setTimestamp();
275
276 if (cfg.leaderboard_message_id) {
277 // Editar mensaje existente
278 try {
279 const msg = await channel.messages.fetch(cfg.leaderboard_message_id);
280 await msg.edit({ embeds: [embed] });
281 } catch {
282 // Si no existe o no se puede editar, crear mensaje nuevo
283 const msg = await channel.send({ embeds: [embed] });
284 cfg.leaderboard_message_id = msg.id;
285 saveSetupConfig(cfg);
286 }
287 } else {
288 // Crear mensaje nuevo y guardar id
289 const msg = await channel.send({ embeds: [embed] });
290 cfg.leaderboard_message_id = msg.id;
291 saveSetupConfig(cfg);
292 }
293
294 } catch (e) {
295 logger.error('Error actualizando leaderboard', { error: e, guildId: guild.id });
296 }
297}
298
299// -----------------------
300// Función para crear canal #banco si no existe y darle permisos de lectura/escritura al bot
301async function ensureBancoChannel(guild) {
302 const cfg = getSetupConfig(guild.id);
303 if (cfg.banco_channel_id) {
304 const ch = guild.channels.cache.get(cfg.banco_channel_id);
305 if (ch) return ch;
306 }
307 // Crear canal texto llamado 'banco'
308 const ch = await guild.channels.create({
309 name: 'banco',
310 type: ChannelType.GuildText,
311 permissionOverwrites: [
312 {
313 id: guild.roles.everyone,
314 deny: [PermissionsBitField.Flags.SendMessages]
315 },
316 {
317 id: client.user.id,
318 allow: [PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel, PermissionsBitField.Flags.EmbedLinks]
319 }
320 ]
321 });
322 const cfgNew = getSetupConfig(guild.id);
323 cfgNew.banco_channel_id = ch.id;
324 saveSetupConfig(cfgNew);
325 return ch;
326}
327
328// -----------------------
329// Función para chequear si el usuario es owner
330function isOwner(userId) {
331 const owners = (process.env.OWNER_IDS || '').split(',');
332 return owners.includes(userId);
333}
334
335// -----------------------
336// Función para manejar mensaje nuevo y contar para puntos
337async function handleNewMessage(message) {
338 if (message.author.bot) return;
339 if (!message.guild) return;
340 const guildId = message.guild.id;
341 const userId = message.author.id;
342
343 // Cargar configuración
344 const cfg = getSetupConfig(guildId);
345
346 // Inicializar estructura para trackeo de mensajes por usuario en ese guild
347 if (!userMessageTrack.has(guildId)) userMessageTrack.set(guildId, new Map());
348 const userTrack = userMessageTrack.get(guildId);
349
350 let userData = userTrack.get(userId);
351 if (!userData) {
352 userData = { lastMessage: null, countMessages: 0 };
353 userTrack.set(userId, userData);
354 }
355
356 // Condiciones no_repetir
357 if (cfg.no_repetir) {
358 const content = message.content.trim().toLowerCase();
359 if (content.length < 4) return; // Mensajes muy cortos no cuentan
360 if (content === userData.lastMessage) return; // Repetición exacta, no cuenta
361 userData.lastMessage = content;
362 }
363
364 userData.countMessages++;
365
366 if (userData.countMessages >= cfg.messages_needed) {
367 // Dar puntos
368 const newPoints = addUserPoints(userId, cfg.points);
369 userData.countMessages = 0;
370
371 // Actualizar leaderboard
372 try {
373 const guild = message.guild;
374 await updateLeaderboardEmbed(guild);
375 } catch (e) {
376 logger.error('Error actualizando leaderboard tras sumar puntos', { error: e, guildId });
377 }
378 }
379}
380
381// -----------------------
382// Comandos slash que registrarás con /setup
383let commands = [
384 new SlashCommandBuilder()
385 .setName('botinfo')
386 .setDescription('Muestra información del bot con su icono'),
387
388 new SlashCommandBuilder()
389 .setName('createembed')
390 .setDescription('Crea un embed comprable (solo owners)')
391 .addStringOption(opt =>
392 opt.setName('title')
393 .setDescription('Título del embed')
394 .setRequired(true))
395 .addIntegerOption(opt =>
396 opt.setName('pointsneeded')
397 .setDescription('Puntos necesarios para comprar')
398 .setRequired(true))
399 .addStringOption(opt =>
400 opt.setName('gameid')
401 .setDescription('ID del juego (ejemplo: repo)')
402 .setRequired(true))
403 .addStringOption(opt =>
404 opt.setName('description')
405 .setDescription('Descripción del embed')
406 .setRequired(false))
407 .addStringOption(opt =>
408 opt.setName('color')
409 .setDescription('Color en hexadecimal o nombre')
410 .setRequired(false))
411 .addStringOption(opt =>
412 opt.setName('footer')
413 .setDescription('Texto de footer')
414 .setRequired(false))
415 .addStringOption(opt =>
416 opt.setName('thumbnail_url')
417 .setDescription('URL miniatura')
418 .setRequired(false))
419 .addStringOption(opt =>
420 opt.setName('image_url')
421 .setDescription('URL imagen grande')
422 .setRequired(false)),
423
424 new SlashCommandBuilder()
425 .setName('givepoints')
426 .setDescription('Dar puntos a un usuario (solo owners)')
427 .addUserOption(opt =>
428 opt.setName('usuario')
429 .setDescription('Usuario a quien dar puntos')
430 .setRequired(true))
431 .addIntegerOption(opt =>
432 opt.setName('puntos')
433 .setDescription('Cantidad de puntos a dar (puede ser negativo)')
434 .setRequired(true)),
435
436 new SlashCommandBuilder()
437 .setName('setup')
438 .setDescription('Configura y registra comandos del bot (solo owners)')
439 .addIntegerOption(opt =>
440 opt.setName('messages_needed')
441 .setDescription('Número de mensajes para ganar puntos')
442 .setRequired(true))
443 .addIntegerOption(opt =>
444 opt.setName('points')
445 .setDescription('Cantidad de puntos que se dan')
446 .setRequired(true))
447 .addBooleanOption(opt =>
448 opt.setName('no_repetir')
449 .setDescription('Evitar repetir mensajes cortos para puntuar')
450 .setRequired(true))
451];
452
453// Convertir a JSON para registro
454commands = commands.map(cmd => cmd.toJSON());
455
456// -----------------------
457// Función para registrar comandos (llamada por /setup)
458async function registerCommands(guildId) {
459 const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
460 try {
461 logger.info('Registrando comandos slash en guild', { guild: guildId });
462 await rest.put(Routes.applicationGuildCommands(process.env.CLIENT_ID, guildId), { body: [] });
463 logger.info('Comandos anteriores eliminados');
464 await rest.put(
465 Routes.applicationGuildCommands(process.env.CLIENT_ID, guildId),
466 { body: commands }
467 );
468 logger.info('Comandos registrados correctamente.');
469 } catch (err) {
470 logger.error('Error registrando comandos:', { error: err });
471 throw err;
472 }
473}
474
475client.on('ready', async () => {
476 logger.info('Bot listo', { tag: client.user.tag });
477 // Al iniciar, cargar configuración de todos los guilds
478 try {
479 const rows = db.prepare('SELECT * FROM setup_config').all();
480 for (const row of rows) {
481 row.no_repetir = Boolean(row.no_repetir);
482 guildConfigs.set(row.guildId, row);
483 }
484 } catch (e) {
485 logger.error('Error cargando setup configs al iniciar', { error: e });
486 }
487});
488
489// -----------------------
490// Manejo de interacciones
491client.on('interactionCreate', async interaction => {
492 try {
493 if (interaction.isChatInputCommand()) {
494 const cmd = interaction.commandName;
495
496 if (cmd === 'botinfo') {
497 const embed = new EmbedBuilder()
498 .setTitle(`¡Hola! Soy ${client.user.username}`)
499 .setThumbnail(client.user.displayAvatarURL({ format: 'png', size: 512 }));
500 await interaction.reply({ embeds: [embed], ephemeral: true });
501 logger.info('Ejecutado /botinfo', { user: interaction.user.id });
502 }
503
504 else if (cmd === 'createembed') {
505 if (!isOwner(interaction.user.id)) {
506 return interaction.reply({ content: 'No tienes permisos para usar este comando.', ephemeral: true });
507 }
508
509 const title = interaction.options.getString('title');
510 const needed = interaction.options.getInteger('pointsneeded');
511 const gameid = interaction.options.getString('gameid').toLowerCase();
512 const description = interaction.options.getString('description') || null;
513 const color = interaction.options.getString('color') || null;
514 const footer = interaction.options.getString('footer') || null;
515 const thumbnail = interaction.options.getString('thumbnail_url') || null;
516 const image = interaction.options.getString('image_url') || null;
517
518 const embed = new EmbedBuilder().setTitle(title);
519 if (description) embed.setDescription(description);
520 if (color) {
521 try { embed.setColor(color); } catch {}
522 }
523 if (footer) embed.setFooter({ text: footer });
524 if (thumbnail) embed.setThumbnail(thumbnail);
525 if (image) embed.setImage(image);
526
527 // Botón con puntos y gameid
528 const customId = `buy_embed_${needed}_${gameid}`;
529 const button = new ButtonBuilder()
530 .setCustomId(customId)
531 .setLabel(`💰 ${needed}`)
532 .setStyle(ButtonStyle.Success);
533 const row = new ActionRowBuilder().addComponents(button);
534
535 const sent = await interaction.reply({ embeds: [embed], components: [row], fetchReply: true, ephemeral: false });
536
537 logger.info('Embed comprable creado', { user: interaction.user.id, messageId: sent.id, needed, gameid });
538 }
539
540 else if (cmd === 'givepoints') {
541 if (!isOwner(interaction.user.id)) {
542 return interaction.reply({ content: 'No tienes permisos para usar este comando.', ephemeral: true });
543 }
544
545 const targetUser = interaction.options.getUser('usuario');
546 const pointsToAdd = interaction.options.getInteger('puntos');
547
548 if (!targetUser) {
549 return interaction.reply({ content: 'Usuario inválido.', ephemeral: true });
550 }
551 if (isNaN(pointsToAdd)) {
552 return interaction.reply({ content: 'Cantidad de puntos inválida.', ephemeral: true });
553 }
554
555 const newPoints = addUserPoints(targetUser.id, pointsToAdd);
556 await interaction.reply({ content: `✅ Ahora ${targetUser.tag} tiene ${newPoints} puntos.`, ephemeral: false });
557 logger.info('Puntos asignados', { from: interaction.user.id, to: targetUser.id, points: pointsToAdd });
558
559 // Actualizar leaderboard
560 try {
561 const guild = interaction.guild;
562 await updateLeaderboardEmbed(guild);
563 } catch (e) {
564 logger.error('Error actualizando leaderboard tras givepoints', { error: e, guildId: guild.id });
565 }
566 }
567
568 else if (cmd === 'setup') {
569 if (!isOwner(interaction.user.id)) {
570 return interaction.reply({ content: 'No tienes permisos para usar este comando.', ephemeral: true });
571 }
572
573 // Leer opciones
574 const messages_needed = interaction.options.getInteger('messages_needed');
575 const points = interaction.options.getInteger('points');
576 const no_repetir = interaction.options.getBoolean('no_repetir');
577
578 // Guardar configuración en DB y memoria
579 const cfg = getSetupConfig(interaction.guildId);
580 cfg.messages_needed = messages_needed;
581 cfg.points = points;
582 cfg.no_repetir = no_repetir;
583
584 // Crear o asegurar canal banco
585 const bancoChannel = await ensureBancoChannel(interaction.guild);
586 cfg.banco_channel_id = bancoChannel.id;
587
588 saveSetupConfig(cfg);
589
590 // Actualizar leaderboard embed (crearlo o actualizarlo)
591 await updateLeaderboardEmbed(interaction.guild);
592
593 await interaction.reply({
594 content: `✅ Setup completado.\n- messages_needed: ${messages_needed}\n- points: ${points}\n- no_repetir: ${no_repetir}\nLeaderboard creado/actualizado en ${bancoChannel.toString()}`,
595 ephemeral: true
596 });
597 logger.info('Setup completado', { guildId: interaction.guildId, user: interaction.user.id });
598 }
599 }
600 else if (interaction.isButton()) {
601 const customId = interaction.customId;
602
603 if (customId.startsWith('buy_embed_')) {
604 // customId = buy_embed_<points>_<gameid>
605 const parts = customId.split('_');
606 const needed = parseInt(parts[2]);
607 const gameid = parts.slice(3).join('_'); // para gameid con guiones
608
609 const userId = interaction.user.id;
610 const currentPoints = pointsMap.get(userId) || 0;
611
612 if (currentPoints < needed) {
613 return interaction.reply({ content: `❌ No tienes suficientes puntos. Necesitas ${needed}, tienes ${currentPoints}.`, ephemeral: true });
614 }
615
616 // Restar puntos
617 const after = addUserPoints(userId, -needed);
618
619 // Generar código único
620 let code;
621 do {
622 code = generateCode();
623 const exists = db.prepare('SELECT 1 FROM codes WHERE code = ?').get(code);
624 if (!exists) break;
625 } while (true);
626
627 // Guardar código con gameid
628 const msg = interaction.message;
629 storeCodeInDb(code, userId, msg.channelId, msg.id, gameid);
630
631 // Obtener nombre bonito o fallback
632 const gameName = gamesMap[gameid] || gameid.toUpperCase();
633
634 // Enviar código por DM
635 try {
636 const dm = await interaction.user.createDM();
637 await dm.send(`🎉 Gracias por tu compra. Aquí está tu código de uso único para el juego **${gameName}**:\n\`${code}\``);
638 logger.info('Código de compra enviado por DM', { to: userId, code, gameid });
639 } catch (e) {
640 logger.warn('No se pudo enviar DM al usuario', { userId, error: e });
641 return interaction.reply({ content: '❌ No pude enviarte un DM. Por favor, habilita tus mensajes directos.', ephemeral: true });
642 }
643
644 await interaction.reply({ content: `✅ Compra exitosa. Te quedan ${after} puntos. Código enviado por DM.`, ephemeral: true });
645 logger.info('Compra realizada', { user: userId, pointsSpent: needed, remaining: after, gameid });
646 }
647 }
648 } catch (err) {
649 logger.error('Error en interactionCreate', { error: err.stack || err });
650 if (!interaction.replied && !interaction.deferred) {
651 await interaction.reply({ content: 'Ocurrió un error al procesar la interacción.', ephemeral: true });
652 }
653 }
654});
655
656// -----------------------
657// Contar mensajes para puntos (listener)
658client.on('messageCreate', async message => {
659 try {
660 await handleNewMessage(message);
661 } catch (e) {
662 logger.error('Error en messageCreate', { error: e });
663 }
664});
665// Lógica para la WEB de GitHub
666import express from 'express';
667
668const app = express();
669app.use(express.json());
670
671// Ruta POST para validar código
672app.post('/check-code', (req, res) => {
673 const { code } = req.body;
674 if (!code) {
675 return res.status(400).json({ success: false, message: 'Código requerido' });
676 }
677 try {
678 const row = db.prepare('SELECT used, gameid FROM codes WHERE code = ?').get(code);
679 if (!row) {
680 return res.json({ success: false, message: 'Código no válido' });
681 }
682 if (row.used) {
683 return res.json({ success: false, message: 'Código ya usado' });
684 }
685 // Si quieres devolver también el gameid o nombre bonito:
686 const gameid = row.gameid;
687 const gameName = gamesMap[gameid] || gameid.toUpperCase();
688 return res.json({
689 success: true,
690 message: 'Código válido y sin usar',
691 gameid,
692 gameName
693 });
694 } catch (err) {
695 logger.error('Error en /check-code', { error: err });
696 return res.status(500).json({ success: false, message: 'Error interno del servidor' });
697 }
698});
699
700const PORT = process.env.PORT || 3000;
701app.listen(PORT, () => {
702 logger.info(`API HTTP escuchando en puerto ${PORT}`);
703});
704
705// -----------------------
706client.login(process.env.DISCORD_TOKEN)
707 .then(() => {
708 logger.info('login exitoso');
709 })
710 .catch(err => {
711 logger.error('Error al iniciar sesión en Discord', { error: err });
712 });
713
714process.on('SIGINT', async () => {
715 console.log('Recibido SIGINT. Cerrando cliente Discord...');
716 try {
717 await client.destroy();
718 console.log('Cliente Discord desconectado.');
719 process.exit(0);
720 } catch (e) {
721 console.error('Error al desconectar cliente Discord:', e);
722 process.exit(1);
723 }
724});
725