Skip to content

Commit 471eb62

Browse files
Merge pull request #65 from ignaciochemes/develop
Update 2025
2 parents 82313e8 + 166a93a commit 471eb62

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+8049
-3101
lines changed

.env-example

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
TOKEN=YOUR-DISCORD-BOT-TOKEN-HERE
1+
TOKEN=
22
STEAM_API=YOUR-STEAM-API-KEY-HERE
33
DB_URI=YOUR-MONGO-DB-URI-HERE
44
DBL_TOKEN=YOUR_DBL_TOKEN_HERE
5-
CLIENT_ID=YOUR_BOT_CLIENT_ID
6-
API_PORT=API_PORT_HERE
5+
CLIENT_ID=1437401678386036847
6+
API_PORT=8000
7+
API_ACTIVE=true

db/migrations/001_init.sql

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
-- Esquema base para la migración a PostgreSQL
2+
-- Crea tablas, constraints e índices necesarios para el bot
3+
4+
-- Usuarios (Discord)
5+
CREATE TABLE IF NOT EXISTS users (
6+
id BIGSERIAL PRIMARY KEY,
7+
discord_user_id TEXT UNIQUE NOT NULL,
8+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
9+
);
10+
11+
-- Catálogo de juegos/servicios
12+
CREATE TABLE IF NOT EXISTS games (
13+
key TEXT PRIMARY KEY,
14+
display_name TEXT NOT NULL
15+
);
16+
17+
-- Servidores configurados por usuario y juego
18+
CREATE TABLE IF NOT EXISTS user_servers (
19+
id BIGSERIAL PRIMARY KEY,
20+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
21+
game_key TEXT NOT NULL REFERENCES games(key),
22+
host TEXT NOT NULL,
23+
query_port INTEGER NOT NULL DEFAULT 2302,
24+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
25+
CONSTRAINT uq_user_game UNIQUE (user_id, game_key)
26+
);
27+
28+
-- Log de comandos ejecutados
29+
CREATE TABLE IF NOT EXISTS commands_log (
30+
id BIGSERIAL PRIMARY KEY,
31+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
32+
command_name TEXT NOT NULL,
33+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
34+
);
35+
36+
-- Conversiones GUID (SteamID64 -> GUID)
37+
CREATE TABLE IF NOT EXISTS guid_conversions (
38+
id BIGSERIAL PRIMARY KEY,
39+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
40+
steam_id64 TEXT NOT NULL,
41+
guid CHAR(32) NOT NULL UNIQUE,
42+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
43+
CONSTRAINT chk_guid_hex CHECK (guid ~ '^[0-9a-f]{32}$'),
44+
CONSTRAINT chk_steam_id64_digits CHECK (steam_id64 ~ '^[0-9]{17}$')
45+
);
46+
47+
-- Conversiones UID (SteamID64 -> CFTools/Bohemia UID)
48+
CREATE TABLE IF NOT EXISTS uid_conversions (
49+
id BIGSERIAL PRIMARY KEY,
50+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
51+
steam_id64 TEXT NOT NULL,
52+
cftools_uid VARCHAR(64) NOT NULL,
53+
bohemia_uid VARCHAR(64) NOT NULL,
54+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
55+
CONSTRAINT chk_uid_steam_id64_digits CHECK (steam_id64 ~ '^[0-9]{17}$'),
56+
CONSTRAINT uq_uid_steam_pair UNIQUE (steam_id64, cftools_uid)
57+
);
58+
59+
-- Pings a servidores (cache/observabilidad opcional)
60+
CREATE TABLE IF NOT EXISTS server_pings (
61+
id BIGSERIAL PRIMARY KEY,
62+
server_id BIGINT NOT NULL REFERENCES user_servers(id) ON DELETE CASCADE,
63+
status TEXT NOT NULL,
64+
ping_ms INTEGER,
65+
raw JSONB,
66+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
67+
);
68+
69+
-- Estadísticas diarias (agregados opcionales)
70+
CREATE TABLE IF NOT EXISTS stats_daily (
71+
day DATE PRIMARY KEY,
72+
commands_total INTEGER NOT NULL DEFAULT 0,
73+
guid_conversions_total INTEGER NOT NULL DEFAULT 0,
74+
uid_conversions_total INTEGER NOT NULL DEFAULT 0
75+
);
76+
77+
-- Índices recomendados
78+
CREATE INDEX IF NOT EXISTS idx_commands_log_user_id ON commands_log (user_id);
79+
CREATE INDEX IF NOT EXISTS idx_commands_log_cmd_created_at ON commands_log (command_name, created_at DESC);
80+
81+
CREATE INDEX IF NOT EXISTS idx_guid_conversions_user_created ON guid_conversions (user_id, created_at DESC);
82+
CREATE INDEX IF NOT EXISTS idx_uid_conversions_user_created ON uid_conversions (user_id, created_at DESC);
83+
84+
CREATE INDEX IF NOT EXISTS idx_server_pings_server_created ON server_pings (server_id, created_at DESC);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Permitir GUIDs duplicados en guid_conversions
2+
-- Se elimina la restricción UNIQUE y se agrega un índice no único
3+
4+
ALTER TABLE IF EXISTS guid_conversions
5+
DROP CONSTRAINT IF EXISTS guid_conversions_guid_key;
6+
7+
CREATE INDEX IF NOT EXISTS idx_guid_conversions_guid
8+
ON guid_conversions (guid);

index.js

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
const fs = require('node:fs');
2-
const path = require('node:path');
3-
const { Client, GatewayIntentBits, REST, Collection, Routes, ActivityType } = require("discord.js");
4-
const { getEnvironment } = require("./src/Configs/EnvironmentSelector");
5-
const { DatabaseConnection } = require("./src/Database/DbConnection");
6-
const { WebServer } = require('./src/WebServer');
7-
const { default: AutoPoster } = require('topgg-autoposter');
8-
const config = require('./config.json');
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { Client, GatewayIntentBits, REST, Collection, Routes, ActivityType } from "discord.js";
4+
import { fileURLToPath, pathToFileURL } from 'node:url';
5+
import getEnvironment from "./src/Configs/EnvironmentSelector.js";
6+
// import { DatabaseConnection } from "./src/Database/DbConnection";
7+
import WebServerModule from './src/WebServer.js';
8+
// import { default as AutoPoster } from 'topgg-autoposter';
9+
// import raw from './config.json' assert { type: 'json' };
10+
// const config = raw;
11+
12+
// Resuelve __dirname en ESM
13+
const __filename = fileURLToPath(import.meta.url);
14+
const __dirname = path.dirname(__filename);
915

1016
getEnvironment();
11-
if (config.database.active === true) {
12-
DatabaseConnection.getInstance();
13-
}
14-
if (config.api.active === true) {
15-
new WebServer().listen();
17+
// if (config.database.active === true) {
18+
// DatabaseConnection.getInstance();
19+
// }
20+
// Arranque opcional del servidor HTTP según variable de entorno.
21+
if (process.env.API_ACTIVE === 'true') {
22+
new WebServerModule.WebServer().listen();
1623
}
1724

1825
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });
@@ -21,26 +28,34 @@ const commandsPath = path.join(__dirname, './src/Commands');
2128
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
2229
const commands = [];
2330

24-
for (const file of commandFiles) {
25-
const filePath = path.join(commandsPath, file);
26-
const command = require(filePath);
27-
client.commands.set(command.data.name, command);
28-
commands.push(command.data.toJSON());
31+
/**
32+
* Carga dinámicamente los comandos desde la carpeta ./src/Commands en ESM.
33+
*/
34+
async function loadCommands() {
35+
for (const file of commandFiles) {
36+
const filePath = path.join(commandsPath, file);
37+
const module = await import(pathToFileURL(filePath).href);
38+
const command = module.default;
39+
client.commands.set(command.data.name, command);
40+
commands.push(command.data.toJSON());
41+
}
2942
}
3043

31-
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);
44+
// Registro de comandos vía REST (ejecuta tras cargar comandos)
3245

33-
setInterval(async () => {
34-
if (process.env.STEAMID_ENV === 'production') {
35-
const ap = AutoPoster(process.env.DBL_TOKEN, client);
36-
ap.on('posted', () => {
37-
console.log('Server count posted!');
38-
})
39-
}
40-
}, 3600000);
46+
// setInterval(async () => {
47+
// if (process.env.STEAMID_ENV === 'production') {
48+
// const ap = AutoPoster(process.env.DBL_TOKEN, client);
49+
// ap.on('posted', () => {
50+
// console.log('Server count posted!');
51+
// })
52+
// }
53+
// }, 3600000);
4154

4255
(async () => {
4356
try {
57+
await loadCommands();
58+
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);
4459
console.log('Started refreshing application (/) commands.');
4560
await rest.put(
4661
Routes.applicationCommands(process.env.CLIENT_ID),
@@ -59,6 +74,14 @@ client.on('ready', () => {
5974
});
6075

6176
client.on('interactionCreate', async interaction => {
77+
/**
78+
* Maneja interacciones de comandos (slash) de Discord.
79+
* Si ocurre un error, responde de forma segura usando `reply` o `followUp`
80+
* según si la interacción ya fue reconocida (deferred/replied) para evitar
81+
* el error "Interaction has already been acknowledged".
82+
* Usa `flags: 64` para respuestas efímeras (deprecado `ephemeral`).
83+
* @param {import('discord.js').Interaction} interaction - Interacción entrante.
84+
*/
6285
if (!interaction.isChatInputCommand()) return;
6386
const command = client.commands.get(interaction.commandName);
6487
if (!command) return;
@@ -67,7 +90,17 @@ client.on('interactionCreate', async interaction => {
6790
await command.execute(interaction);
6891
} catch (error) {
6992
console.error(error);
70-
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
93+
const errorPayload = { content: 'There was an error while executing this command!', flags: 64 };
94+
try {
95+
if (interaction.deferred || interaction.replied) {
96+
await interaction.followUp(errorPayload);
97+
} else {
98+
await interaction.reply(errorPayload);
99+
}
100+
} catch (e) {
101+
// Evita que el proceso se caiga si no puede responder
102+
console.error('Failed to send error response for interaction:', e);
103+
}
71104
}
72105
});
73106

0 commit comments

Comments
 (0)