Template moderno y escalable para bots de Discord con TypeScript
Características • Instalación • Uso • Testing • Documentación • Arquitectura
- ✅ Decoradores TypeScript para definición declarativa de comandos
- ✅ Slash Commands (/comando) - Siempre disponibles
- ✅ Text Commands (!comando) - Opcionales y configurables
- ✅ Subcomandos (
@Subcommand) - Organiza comandos en 2 niveles:/config get - ✅ Grupos de Subcomandos (
@SubcommandGroup) - Jerarquía de 3 niveles:/server config get - ✅ Resolución automática de argumentos con validación
- ✅ Raw Text Capture - Captura texto completo sin comillas (ej:
!say Hola mundo) - ✅ Options/Choices - Argumentos con valores predefinidos y dropdown en slash commands
- ✅ Aliases para comandos de texto
- ✅ Tipos Discord (User, Role, Channel, Member) resueltos automáticamente
- ✅ Custom Type Parsers para tipos personalizados (ej: MinecraftPlayer, CustomDate)
- ✅ Sistema de Plugins extensible con decoradores y scopes
- ✅ Plugin Scopes - Aplica plugins por carpeta, comando, o globalmente
- ✅ Sistema de Permisos - Decorador
@RequirePermissionscon validación automática
- ✅ Button Wrapper - Crea botones con callbacks inline (Primary, Success, Danger, Secondary)
- ✅ Select Wrapper - Crea select menus con onChange inline
- ✅ Modal Wrapper - Crea formularios (modales) con onSubmit inline
- ✅ RichMessage - Gestión centralizada de componentes con timeout global único
- ✅ Registry Global - Almacena componentes automáticamente (sin archivos separados)
- ✅ Timeout Automático - Componentes se limpian automáticamente (20 segundos por defecto)
- ✅ Type-Safe - Callbacks con tipos completos de Discord.js
- ✅ Sin boilerplate - No necesitas crear archivos
.button.tso.select.ts - ✅ Mejor performance - RichMessage usa 1 timeout para N componentes
- ✅ Principios SOLID aplicados
- ✅ Separación de responsabilidades (Loaders, Handlers, Resolvers, Plugins)
- ✅ Código modular y fácil de testear
- ✅ Decoradores reutilizables (@Command, @Arg, @UsePlugins)
- ✅ Context unificado para Messages e Interactions
- ✅ Plugins reutilizables (Cooldowns, Permisos, Logging, etc.)
- ✅ TypeScript con strict mode
- ✅ Path aliases (@/core, @/commands, etc.)
- ✅ Hot reload en desarrollo (ts-node)
- ✅ Testing completo (Unit, Integration, E2E con Jest)
- ✅ Mocks incluidos para Discord.js
- ✅ Documentación completa por carpeta
- ✅ Ejemplos listos para usar
- ✅ Variables de entorno para configuración
- ✅ Intents automáticos según características usadas
- ✅ Presencias personalizables con templates
- ✅ Manejo robusto de errores
- Node.js v18 o superior
- npm o yarn
- Bot de Discord creado en Discord Developer Portal
Tienes dos opciones para instalar Patto Bot Template:
La forma más rápida y sencilla usando la herramienta oficial:
# Instalar Patto CLI globalmente
npm install -g patto-cli
# Crear un nuevo proyecto
patto init mi-bot-discord
# Entrar al proyecto
cd mi-bot-discord✨ Ventajas:
- ✅ Setup automático en 2-3 minutos
- ✅ Instalación de dependencias automática
- ✅ Generación de código integrada
- ✅ Validaciones y mejores prácticas incluidas
📚 Guía completa: Ver docs/Patto_CLI_Installation.README.md
Si prefieres clonar el repositorio manualmente:
git clone https://github.com/HormigaDev/patto-bot-template.git
cd patto-bot-templatenpm installCopia el template de configuración:
cp .env.template .envEdita .env con tus credenciales:
# Variables OBLIGATORIAS
BOT_TOKEN=tu_token_aqui # Token del bot
CLIENT_ID=tu_client_id_aqui # ID de la aplicación
# Variables OPCIONALES
USE_MESSAGE_CONTENT=true # true = habilitar comandos de texto | false/vacío = solo slash commands
COMMAND_PREFIX=! # Prefijo para comandos de texto (default: !)
INTENTS= # Intents personalizados (dejar vacío para automático)Validación automática: El bot valida todas las variables al iniciar y muestra errores claros si falta algo obligatorio.
Si configuraste USE_MESSAGE_CONTENT=true:
- Ve a Discord Developer Portal
- Selecciona tu aplicación
- Ve a Bot → Privileged Gateway Intents
- Activa: ✅ MESSAGE CONTENT INTENT
- Guarda los cambios
Genera una URL de invitación:
- Ve a OAuth2 → URL Generator
- Selecciona scopes:
- ✅
bot - ✅
applications.commands
- ✅
- Selecciona permisos del bot según tus necesidades
- Copia la URL generada y úsala para invitar el bot
Inicia el bot en modo desarrollo con hot reload:
npm run devCompila y ejecuta:
npm run build
npm startEl proyecto incluye una infraestructura completa de testing con Jest y TypeScript:
# Todos los tests
npm test
# Tests con cobertura detallada
npm run test:coverage
# Tests en modo watch (desarrollo)
npm run test:watch
# Tests por categoría
npm run test:unit # Solo tests unitarios
npm run test:integration # Solo tests de integración
npm run test:e2e # Solo tests end-to-endMantén el código limpio y consistente:
# Ejecutar linter (ESLint)
npm run lint
# Auto-fix de problemas de linting
npm run lint -- --fix
# Formatear código con Prettier
npm run format💡 Tip: Ejecuta npm run lint y npm run format antes de hacer commits para asegurar calidad de código.
- Jest 29 con soporte completo para TypeScript
- Mocks de Discord.js pre-configurados (User, Guild, Message, Interaction, etc.)
- Path aliases (
@/,@tests/*) funcionando en tests - Coverage reports con umbrales configurables
- CI/CD con GitHub Actions (tests automáticos en cada push/PR)
- Debug en VSCode configurado para tests
tests/
├── unit/ # Tests unitarios (utils, errors, etc.)
├── integration/ # Tests de integración (commands, handlers)
├── e2e/ # Tests end-to-end (flujos completos)
├── mocks/ # Mocks reutilizables de Discord.js
├── fixtures/ # Datos de prueba
└── helpers/ # Utilidades para tests
Documentación completa: Ver /tests/README.md para ejemplos, guías de escritura de tests y mejores prácticas.
Crea src/definition/ping.definition.ts:
import { Command } from '@/core/decorators/command.decorator';
import { BaseCommand } from '@/core/structures/BaseCommand';
import { CommandCategoryTag } from '@/utils/CommandCategories';
@Command({
name: 'ping',
description: 'Verifica la latencia del bot',
category: CommandCategoryTag.Info, // Opcional (default: Other)
aliases: ['latencia', 'pong'],
})
export abstract class PingDefinition extends BaseCommand {
// Sin argumentos para este comando
}Crea src/commands/ping.command.ts:
import { EmbedBuilder } from 'discord.js';
import { PingDefinition } from '@/definition/ping.definition';
export class PingCommand extends PingDefinition {
public async run(): Promise<void> {
const embed = new EmbedBuilder()
.setTitle('🏓 Pong!')
.setDescription(`Latencia: ${this.ctx.client.ws.ping}ms`)
.setColor('#5180d6')
.setFooter({
text: this.user.username,
iconURL: this.user.displayAvatarURL(),
});
await this.reply({ embeds: [embed] });
}
}El comando se carga automáticamente. Reinicia el bot y prueba:
- Slash:
/ping - Texto:
!ping,!latencia,!pong
Este template soporta subcomandos y grupos de subcomandos para organizar comandos complejos.
💡 Nota importante: NO necesitas crear un archivo base (como
config.command.tsoserver.command.ts). El sistema crea automáticamente "comandos fantasma" en Discord cuando detecta subcomandos sin comando base. Esto reduce overhead, mejora la DX y evita código verboso innecesario.
Para comandos relacionados simples: /config get, /config set
// src/commands/config/get.command.ts
import { Subcommand } from '@/core/decorators/subcommand.decorator';
import { BaseCommand } from '@/core/structures/BaseCommand';
@Subcommand({
parent: 'config',
name: 'get',
description: 'Ver la configuración actual',
category: 'Utility',
})
export class ConfigGetCommand extends BaseCommand {
async run(): Promise<void> {
await this.reply('Configuración actual...');
}
}Para sistemas complejos: /server config get, /server user info
// src/commands/server/config/get.command.ts
import { SubcommandGroup } from '@/core/decorators/subcommand-group.decorator';
import { BaseCommand } from '@/core/structures/BaseCommand';
@SubcommandGroup({
parent: 'server',
name: 'config',
subcommand: 'get',
description: 'Ver la configuración del servidor',
})
export class ServerConfigGetCommand extends BaseCommand {
async run(): Promise<void> {
await this.reply('Configuración del servidor...');
}
}src/commands/
├── info/ # Comandos base simples
│ ├── help.command.ts # /help
│ └── ping.command.ts # /ping
├── config/ # Subcomandos (2 niveles)
│ ├── get.command.ts # /config get
│ ├── set.command.ts # /config set
│ └── reset.command.ts # /config reset
└── server/ # Grupos de subcomandos (3 niveles)
├── config/ # Grupo: config
│ ├── get.command.ts # /server config get
│ └── set.command.ts # /server config set
└── user/ # Grupo: user
├── info.command.ts # /server user info
└── list.command.ts # /server user list
Cuando defines subcomandos o grupos sin un comando base, el sistema automáticamente:
- ✅ Detecta que el comando padre no existe
- ✅ Crea un "comando fantasma" en Discord como contenedor
- ✅ Registra todos los subcomandos/grupos correctamente
- ✅ Muestra en logs:
👻 Comando fantasma creado: "config" (solo contenedor de subcomandos)
Beneficios:
- 🚀 Sin overhead de archivos vacíos
- 🎯 DX mejorada - solo código funcional
- 📦 Menos verboso y más limpio
- ⚡ Automático - sin configuración adicional
- 📄 Guía de Subcomandos - Comandos de 2 niveles
- 📄 Guía de Grupos de Subcomandos - Comandos de 3 niveles
El template incluye un sistema de permisos integrado. Usa el decorador @RequirePermissions:
import { Command } from '@/core/decorators/command.decorator';
import { RequirePermissions } from '@/core/decorators/permission.decorator';
import { Permissions } from '@/utils/Permissions';
import { Arg } from '@/core/decorators/argument.decorator';
import { BaseCommand } from '@/core/structures/BaseCommand';
import { User } from 'discord.js';
@Command({
name: 'ban',
description: 'Banea un usuario del servidor',
})
@RequirePermissions(Permissions.BanMembers)
export class BanCommand extends BaseCommand {
@Arg({
name: 'usuario',
description: 'Usuario a banear',
index: 0,
required: true,
})
public usuario!: User;
@Arg({
name: 'razon',
description: 'Razón del baneo',
index: 1,
required: false,
})
public razon?: string;
public async run(): Promise<void> {
// Usuario ya validado con permisos
await this.usuario.ban({ reason: this.razon || 'No especificada' });
const embed = this.getEmbed('success')
.setTitle('✅ Usuario Baneado')
.setDescription(`${this.usuario.tag} ha sido baneado`)
.addFields({ name: 'Razón', value: this.razon || 'No especificada' });
await this.reply({ embeds: [embed] });
}
}Características:
- ✅ El comando solo aparece para usuarios con el permiso
BanMembers - ✅ Validación doble: en Discord (registro) y en ejecución (runtime)
- ✅ Sin boilerplate: No necesitas validar manualmente
- ✅ Funciona con el PermissionsPlugin incluido (inmutable, no modifica JSON original)
- ✅ 20 tests completos (unit + integration) garantizan su correcto funcionamiento
Más información: Ver /src/plugins/permissions.plugin.README.md
Cada carpeta importante tiene su propio README con documentación detallada:
- 📁
/src/commands/- Implementaciones de comandos - 📁
/src/definition/- Definiciones de comandos (opcional) - 📁
/src/plugins/- Plugins extensibles (Cooldowns, Permisos, etc.) - 📁
/src/utils/- Utilidades y helpers reutilizables - 📁
/src/error/- Manejo de errores (ValidationError, ReplyError) - 📁
/tests/- Infraestructura de testing completa - 📁
/src/core/- Núcleo del framework- 📁
/decorators/- Decoradores @Command y @Arg - 📁
/handlers/- CommandHandler - 📁
/loaders/- Cargadores de comandos - 📁
/resolvers/- Resolvedores de tipos - 📁
/structures/- BaseCommand, CommandContext, BasePlugin - 📁
/components/- Button, Select, Modal, RichMessage
- 📁
- 📁
/src/error/- Errores personalizados - 📁
/src/events/- Eventos de Discord - 📁
/tests/- Sistema de testing completo (Unit, Integration, E2E)
- 📄
ARCHITECTURE.md- Arquitectura completa del sistema - 📄
docs/MESSAGE_CONTENT_CONFIG.md- Configuración de comandos de texto
patto-bot-template/
├── src/
│ ├── bot.ts # Clase principal del bot
│ ├── index.ts # Punto de entrada
│ ├── commands/ # Implementaciones de comandos
│ │ └── *.command.ts
│ ├── core/ # Núcleo del framework
│ │ ├── decorators/ # @Command, @Arg
│ │ ├── handlers/ # CommandHandler
│ │ ├── loaders/ # CommandLoader, SlashCommandLoader
│ │ ├── resolvers/ # TypeResolver, ArgumentResolver
│ │ └── structures/ # BaseCommand, CommandContext, BasePlugin
│ ├── definition/ # Definiciones de comandos (opcional)
│ │ └── *.definition.ts
│ ├── plugins/ # Plugins extensibles
│ │ └── *.plugin.ts
│ ├── error/ # Errores personalizados
│ │ ├── ValidationError.ts
│ │ └── ReplyError.ts
│ └── events/ # Eventos de Discord
│ ├── ready.event.ts
│ ├── interactionCreate.event.ts
│ └── messageCreate.event.ts
├── .env.template # Template de configuración
├── package.json
├── tsconfig.json
└── README.md
Usuario ejecuta comando
↓
┌────────────────────┐
│ Event Handler │ (interactionCreate o messageCreate)
│ • Detecta comando │
│ • Busca en loader │
└────────────────────┘
↓
┌────────────────────┐
│ Plugins Before │
│ • onBeforeExecute │
│ • Validaciones │
└────────────────────┘
↓
┌────────────────────┐
│ CommandHandler │
│ • Instancia │
│ • Inyecta ctx │
└────────────────────┘
↓
┌────────────────────┐
│ ArgumentResolver │
│ • Obtiene args │
│ • Valida │
│ • Resuelve tipos │
└────────────────────┘
↓
┌────────────────────┐
│ Command.run() │
│ • Lógica del │
│ comando │
└────────────────────┘
↓
┌────────────────────┐
│ Plugins After │
│ • onAfterExecute │
│ • Logging, etc. │
└────────────────────┘
// definition/greet.definition.ts
@Command({
name: 'greet',
description: 'Saluda a alguien',
})
export abstract class GreetDefinition extends BaseCommand {
@Arg({
name: 'nombre',
description: 'Nombre de la persona',
index: 0,
required: true,
})
public nombre!: string;
}
// commands/greet.command.ts
export class GreetCommand extends GreetDefinition {
public async run(): Promise<void> {
await this.reply(`¡Hola ${this.nombre}! 👋`);
}
}// definition/hug.definition.ts
@Command({
name: 'hug',
description: 'Abraza a un usuario',
})
export abstract class HugDefinition extends BaseCommand {
@Arg({
name: 'usuario',
description: 'Usuario a abrazar',
index: 0,
required: true,
})
public usuario!: User;
}
// commands/hug.command.ts
export class HugCommand extends HugDefinition {
public async run(): Promise<void> {
const embed = new EmbedBuilder()
.setDescription(`${this.user} abraza a ${this.usuario}! 🤗`)
.setColor('#5180d6');
await this.reply({ embeds: [embed] });
}
}// definition/transfer.definition.ts
@Command({
name: 'transfer',
description: 'Transfiere monedas',
})
export abstract class TransferDefinition extends BaseCommand {
@Arg({
name: 'cantidad',
description: 'Cantidad a transferir',
index: 0,
required: true,
validate: (value: number) => {
if (value <= 0) return 'Debe ser mayor a 0';
if (value > 1000000) return 'Máximo 1,000,000';
return true;
},
})
public cantidad!: number;
@Arg({
name: 'destinatario',
description: 'Usuario destinatario',
index: 1,
required: true,
})
public destinatario!: User;
}// commands/panel.command.ts
import { RichMessage, Button, Select } from '@/core/components';
import { Times } from '@/utils/Times';
export class PanelCommand extends PanelDefinition {
public async run(): Promise<void> {
// Crear botones con callbacks inline
const infoBtn = Button.primary('Ver Info', 'ℹ️').onClick(async (interaction) => {
await interaction.reply({
content: '📊 Información del servidor...',
ephemeral: true,
});
});
const configBtn = Button.secondary('Configurar', '⚙️').onClick(async (interaction) => {
await interaction.reply({
content: '⚙️ Panel de configuración...',
ephemeral: true,
});
});
const helpBtn = Button.success('Ayuda', '❓').onClick(async (interaction) => {
await interaction.reply({
content: '❓ ¿Necesitas ayuda? Visita nuestra guía...',
ephemeral: true,
});
});
// Crear select menu
const categorySelect = new Select({
placeholder: 'Selecciona una categoría',
options: [
{ label: 'Moderación', value: 'mod', emoji: '🛡️' },
{ label: 'Utilidades', value: 'util', emoji: '🔧' },
{ label: 'Diversión', value: 'fun', emoji: '🎮' },
],
}).onChange(async (interaction, values) => {
await interaction.reply({
content: `Categoría seleccionada: **${values[0]}**`,
ephemeral: true,
});
});
// Crear RichMessage con timeout global de 5 minutos
const panel = new RichMessage({
embeds: [
this.getEmbed('info')
.setTitle('🎛️ Panel de Control')
.setDescription('Usa los botones y el menú para interactuar'),
],
components: [infoBtn, configBtn, helpBtn, categorySelect],
timeout: Times.minutes(5), // Timeout único para todos los componentes
});
await panel.send(this.ctx);
}
}Ventajas:
- ✅ Callbacks inline (sin archivos separados)
- ✅ RichMessage gestiona un timeout global único
- ✅ Limpieza automática del registry
- ✅ Método
edit()para actualizar mensajes dinámicamente - ✅ Type-safe con Discord.js
Ver más en src/core/components/README.md
Edita src/events/messageCreate.event.ts:
const PREFIX = '?'; // Cambia '!' por tu prefijoEdita src/events/ready.event.ts y descomenta/modifica los ejemplos.
Si necesitas intents adicionales, edita src/bot.ts:
intents = [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates, // Ejemplo: estados de voz
// ... más intents
];Causa: Falta el scope applications.commands
Solución: Re-invita el bot con el scope correcto
Causa: Los comandos no están registrados
Solución: Espera a que aparezca "✅ Comandos Slash registrados" en consola
Causa: USE_MESSAGE_CONTENT no está configurado o el intent no está habilitado
Solución: Ver docs/MESSAGE_CONTENT_CONFIG.md
Causa: Path aliases no configurados
Solución: Asegúrate de ejecutar con ts-node -r tsconfig-paths/register
Patto CLI es la herramienta oficial de línea de comandos para trabajar con Patto Bot Template. Agiliza el desarrollo de bots con generación automática de código y setup instantáneo.
Características:
- 🚀 Inicialización rápida de proyectos
- 🎨 Generación de comandos, subcomandos y plugins
- ✅ Validaciones integradas y mejores prácticas
- 📦 30+ tests garantizando su funcionamiento
Instalación:
npm install -g patto-cli
patto init mi-bot-discordEnlaces:
Patto Bot Features será un conjunto de paquetes modulares y editables para expandir tu bot de Discord. Podrás agregar funcionalidades como persistencia con MongoDB, sistemas de economía o herramientas de moderación con un simple comando. Cada feature será flexible, integrable con el template y personalizable según tu estilo. ¡En desarrollo para potenciar tu bot!
Las contribuciones son bienvenidas! Por favor:
- Fork el proyecto
- Crea una rama para tu feature (
git checkout -b feature/AmazingFeature) - Commit tus cambios (
git commit -m 'Add some AmazingFeature') - Push a la rama (
git push origin feature/AmazingFeature) - Abre un Pull Request usando el template
Este proyecto está bajo la Licencia MIT. Ver el archivo LICENSE para más detalles.
HormigaDev
- GitHub: @HormigaDev
- Servidor de Discord: Próximamente
- Discord.js - Librería de Discord para Node.js
- TypeScript - Superset de JavaScript con tipos estáticos
- Jest - Framework de testing delightful
- ESLint - Linter para identificar y reportar patrones en código
- Prettier - Formateador de código automático
- typescript-eslint - Parser y plugin de ESLint para TypeScript
- ts-node-dev - Compilador TypeScript con hot reload para desarrollo
- tsconfig-paths - Soporte para path aliases en runtime
- tsc-alias - Resuelve path aliases de TypeScript después de compilar
- reflect-metadata - Metadata Reflection API para decoradores
- GitHub Actions - CI/CD para tests automáticos
⭐ Si te gusta este proyecto, ¡Ayuda a Patto con una estrella en GitHub! ⭐
