Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ Thumbs.db
# Temporary files
tmp/
temp/

# TypeScript
dist/
*.tsbuildinfo
60 changes: 44 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Bot de Discord para gamificación educativa con sistema de monedas virtuales.

**Nota**: Este proyecto está completamente escrito en **TypeScript** para mejor seguridad de tipos y mantenibilidad.

## 📋 Características

- **Sistema de Monedas**: Los estudiantes pueden ganar y gastar monedas virtuales
Expand All @@ -16,6 +18,8 @@ Bot de Discord para gamificación educativa con sistema de monedas virtuales.
### Requisitos Previos

- Node.js 16.x o superior
- npm o yarn
- TypeScript (se instala automáticamente con las dependencias)
- Cuenta de Discord con permisos de desarrollador
- Cuenta de Supabase
- API Key de OpenRouter
Expand Down Expand Up @@ -206,12 +210,28 @@ Ejecuta el script:
node register-commands.js
```

### Paso 6: Iniciar el Bot
### Paso 6: Construir el Proyecto

El bot está escrito en TypeScript y debe ser compilado antes de ejecutarse:

```bash
npm run build
```

Esto compilará todos los archivos TypeScript a JavaScript en el directorio `dist/`.

### Paso 7: Iniciar el Bot

```bash
npm start
```

Para desarrollo con recarga automática:

```bash
npm run dev
```

## 📚 Comandos Disponibles

### Comandos para Estudiantes
Expand All @@ -233,21 +253,22 @@ npm start

```
reactify-bot/
├── src/
│ ├── index.js # Punto de entrada principal
│ ├── commands/ # Comandos slash
│ │ ├── coins/ # Comandos de monedas
│ │ └── activity/ # Comandos de actividades
│ ├── events/ # Manejadores de eventos
│ ├── interactions/ # Manejadores de interacciones
│ │ ├── buttons/ # Botones
│ │ └── modals/ # Modales
│ ├── services/ # Servicios externos
│ │ ├── supabase.js # Base de datos
│ │ ├── openrouter.js # IA
│ │ └── n8n.js # Webhooks
│ ├── utils/ # Utilidades
│ └── config/ # Configuración
├── commands/ # Comandos slash (TypeScript)
│ ├── coins/ # Comandos de monedas
│ └── activity/ # Comandos de actividades
├── events/ # Manejadores de eventos (TypeScript)
├── interactions/ # Manejadores de interacciones (TypeScript)
│ ├── buttons/ # Botones
│ └── modals/ # Modales
├── services/ # Servicios externos (TypeScript)
│ ├── supabase.ts # Base de datos
│ ├── openrouter.ts # IA
│ └── n8n.ts # Webhooks
├── utils/ # Utilidades (TypeScript)
├── config/ # Configuración (TypeScript)
├── dist/ # JavaScript compilado (generado)
├── index.ts # Punto de entrada principal
├── tsconfig.json # Configuración de TypeScript
├── package.json
├── .env.example
└── README.md
Expand All @@ -263,12 +284,19 @@ reactify-bot/

## 🛠️ Tecnologías Utilizadas

- **TypeScript** - Lenguaje de programación con tipos estáticos
- **Discord.js v14** - Librería para interactuar con Discord
- **Supabase** - Base de datos PostgreSQL
- **OpenRouter** - API de IA para resúmenes automáticos
- **Axios** - Cliente HTTP para llamadas API
- **dotenv** - Gestión de variables de entorno

## 🔧 Scripts Disponibles

- `npm run build` - Compila TypeScript a JavaScript
- `npm start` - Construye y ejecuta el bot
- `npm run dev` - Ejecuta el bot en modo desarrollo con ts-node

## 📝 Notas

- Todas las respuestas del bot están en español
Expand Down
84 changes: 84 additions & 0 deletions commands/activity/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { SlashCommandBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ChatInputCommandInteraction } from 'discord.js';
import { checkTeacherRole } from '../../utils/permissions';
import * as pendingAttachments from '../../utils/pendingAttachments';
import { MODALS, ERRORS } from '../../config/strings';

export default {
data: new SlashCommandBuilder()
.setName('activity')
.setDescription('Comandos de actividades (solo docentes)')
.addAttachmentOption(option =>
option
.setName('attachment')
.setDescription('Adjunto de referencia (archivo) - Opcional')
.setRequired(false)
),

async execute(interaction: ChatInputCommandInteraction) {
try {
// Check teacher permission
if (!checkTeacherRole(interaction.member as any)) {
await interaction.reply({
content: ERRORS.NO_PERMISSION,
flags: 64 // Ephemeral
});
return;
}

// If the user provided an attachment option with the command, store it
// so the modal submit handler can access it later.
const attachmentOption = interaction.options.get('attachment');
if (attachmentOption && attachmentOption.attachment) {
pendingAttachments.set(interaction.user.id, attachmentOption.attachment as any);
}

// Show modal to create activity
const modal = new ModalBuilder()
.setCustomId('activitycreate')
.setTitle(MODALS.ACTIVITY_TITLE);

const titleInput = new TextInputBuilder()
.setCustomId('title')
.setLabel('Título')
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setMaxLength(100);

const descriptionInput = new TextInputBuilder()
.setCustomId('description')
.setLabel('Descripción')
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setMaxLength(1000);

const docsInput = new TextInputBuilder()
.setCustomId('documentation')
.setLabel('Documentación (URLs separadas por comas)')
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setMaxLength(500);

const rewardInput = new TextInputBuilder()
.setCustomId('reward')
.setLabel('Recompensa en monedas - Opcional')
.setStyle(TextInputStyle.Short)
.setRequired(false)
.setMaxLength(5);

const row1 = new ActionRowBuilder<TextInputBuilder>().addComponents(titleInput);
const row2 = new ActionRowBuilder<TextInputBuilder>().addComponents(descriptionInput);
const row3 = new ActionRowBuilder<TextInputBuilder>().addComponents(docsInput);
const row4 = new ActionRowBuilder<TextInputBuilder>().addComponents(rewardInput);

modal.addComponents(row1, row2, row3, row4);

await interaction.showModal(modal);
} catch (error) {
console.error('Error in activity create command:', error);
await interaction.reply({
content: ERRORS.DATABASE_ERROR,
flags: 64 // Ephemeral
});
}
},
};
94 changes: 94 additions & 0 deletions commands/coins/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { SlashCommandSubcommandBuilder, MessageFlags, ChatInputCommandInteraction } from 'discord.js';
import { ensureUserExists } from '../../utils/userManager';
import { checkTeacherRole } from '../../utils/permissions';
import supabaseService from '../../services/supabase';
import { replacePlaceholders } from '../../utils/formatting';
import { SUCCESS, ERRORS, DM, PLACEHOLDERS } from '../../config/strings';
import { getLogger } from '../../utils/logger';
import consoleLogger from '../../utils/consoleLogger';

export default {
data: new SlashCommandSubcommandBuilder()
.setName('add')
.setDescription('Añadir monedas a un usuario (solo docentes)')
.addUserOption(option =>
option.setName('user')
.setDescription('Usuario al que añadir monedas')
.setRequired(true))
.addIntegerOption(option =>
option.setName('amount')
.setDescription('Cantidad de monedas a añadir')
.setRequired(true)
.setMinValue(1))
.addStringOption(option =>
option.setName('reason')
.setDescription('Razón de la adición')
.setRequired(false)
.setMaxLength(200)),

async execute(interaction: ChatInputCommandInteraction) {
try {
// Check teacher permission
if (!checkTeacherRole(interaction.member as any)) {
await interaction.reply({
content: ERRORS.NO_PERMISSION,
flags: MessageFlags.Ephemeral
});
return;
}

await interaction.deferReply();

const targetUser = interaction.options.getUser('user', true);
const amount = interaction.options.get('amount', true).value as number;
const reason = interaction.options.get('reason')?.value as string || PLACEHOLDERS.NOT_SPECIFIED;

// Ensure target user exists
await ensureUserExists(targetUser.id, targetUser.username);

// Add coins
await supabaseService.addCoins(targetUser.id, amount);

// Get user data for curso (displayed as nivel)
const userData = await supabaseService.getUser(targetUser.id);

// Log transaction
const logger = getLogger();
await logger.logTransaction({
type: 'COINS_ADDED',
userId: targetUser.id,
username: targetUser.username,
amount: amount,
reason: reason,
performedBy: interaction.user.id,
nivel: userData?.curso || 'No configurado'
});

consoleLogger.transaction('AÑADIR', amount, targetUser.tag);

// Send response
const response = replacePlaceholders(SUCCESS.COINS_ADDED, {
amount: amount,
user: targetUser.toString()
});

await interaction.editReply(response);

// Try to DM the recipient
try {
const dmMessage = replacePlaceholders(DM.COINS_RECEIVED, {
amount: amount,
reason: reason
});
await targetUser.send(dmMessage);
} catch (error) {
consoleLogger.warn('DM', 'No se pudo enviar DM al usuario');
}
} catch (error) {
consoleLogger.error('COMANDO', 'Error en coins add', error as Error);
await interaction.editReply({
content: ERRORS.DATABASE_ERROR
});
}
},
};
Loading