Skip to content

Template profesional de Discord Bot con TypeScript, decoradores, sistema de plugins, componentes interactivos (botones, selects, modals), validación de entorno, testing completo y arquitectura modular. Listo para producción.

License

Notifications You must be signed in to change notification settings

HormigaDev/patto-bot-template

Banner

Patto Bot Template

Discord.js TypeScript Jest License

Template moderno y escalable para bots de Discord con TypeScript

CaracterísticasInstalaciónUsoTestingDocumentaciónArquitectura



🌟 Características

🎯 Sistema de Comandos Avanzado

  • 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 @RequirePermissions con validación automática

🎨 Componentes Interactivos

  • 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.ts o .select.ts
  • Mejor performance - RichMessage usa 1 timeout para N componentes

🏗️ Arquitectura Limpia

  • 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.)

🛠️ Developer Experience

  • 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

⚙️ Configuración Flexible

  • Variables de entorno para configuración
  • Intents automáticos según características usadas
  • Presencias personalizables con templates
  • Manejo robusto de errores

📋 Requisitos Previos


🚀 Instalación

Tienes dos opciones para instalar Patto Bot Template:

🎯 Opción 1: Con Patto CLI (Recomendado)

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


📦 Opción 2: Instalación Manual

Si prefieres clonar el repositorio manualmente:

1. Clonar el Repositorio

git clone https://github.com/HormigaDev/patto-bot-template.git
cd patto-bot-template

2. Instalar Dependencias

npm install

3. Configurar Variables de Entorno (Ambas opciones)

Copia el template de configuración:

cp .env.template .env

Edita .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.

4. Configurar Discord Developer Portal

Habilitar Intents Privilegiados

Si configuraste USE_MESSAGE_CONTENT=true:

  1. Ve a Discord Developer Portal
  2. Selecciona tu aplicación
  3. Ve a BotPrivileged Gateway Intents
  4. Activa: ✅ MESSAGE CONTENT INTENT
  5. Guarda los cambios

Invitar el Bot

Genera una URL de invitación:

  1. Ve a OAuth2URL Generator
  2. Selecciona scopes:
    • bot
    • applications.commands
  3. Selecciona permisos del bot según tus necesidades
  4. Copia la URL generada y úsala para invitar el bot

🎮 Uso

Desarrollo

Inicia el bot en modo desarrollo con hot reload:

npm run dev

Producción

Compila y ejecuta:

npm run build
npm start

Testing

El 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-end

Linting y Formateo

Manté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.

🧪 Infraestructura de Testing

  • 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

📂 Estructura de 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.


📖 Crear tu Primer Comando

1. Crear la Definición

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
}

2. Crear la Implementación

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] });
    }
}

3. ¡Listo!

El comando se carga automáticamente. Reinicia el bot y prueba:

  • Slash: /ping
  • Texto: !ping, !latencia, !pong

🎯 Subcomandos y Grupos de Subcomandos

Este template soporta subcomandos y grupos de subcomandos para organizar comandos complejos.

💡 Nota importante: NO necesitas crear un archivo base (como config.command.ts o server.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.

Subcomandos (2 niveles)

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...');
    }
}

Grupos de Subcomandos (3 niveles)

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...');
    }
}

📁 Organización Recomendada

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

🤖 Sistema de Comandos Fantasma

Cuando defines subcomandos o grupos sin un comando base, el sistema automáticamente:

  1. ✅ Detecta que el comando padre no existe
  2. ✅ Crea un "comando fantasma" en Discord como contenedor
  3. ✅ Registra todos los subcomandos/grupos correctamente
  4. ✅ 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ías Detalladas


🔒 Ejemplo: Comando con Permisos

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


�📚 Documentación

Por Carpeta

Cada carpeta importante tiene su propio README con documentación detallada:

Guías


🏗️ Arquitectura

Estructura del Proyecto

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

Flujo de Ejecución

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.   │
└────────────────────┘

🎨 Ejemplos de Comandos

Comando con Argumentos

// 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}! 👋`);
    }
}

Comando con Usuario de Discord

// 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] });
    }
}

Comando con Validación

// 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;
}

Comando con Componentes Interactivos

// 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


🔧 Configuración Avanzada

Cambiar el Prefijo

Edita src/events/messageCreate.event.ts:

const PREFIX = '?'; // Cambia '!' por tu prefijo

Cambiar la Presencia

Edita src/events/ready.event.ts y descomenta/modifica los ejemplos.

Intents Personalizados

Si necesitas intents adicionales, edita src/bot.ts:

intents = [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildVoiceStates, // Ejemplo: estados de voz
    // ... más intents
];

🐛 Solución de Problemas

Error: "Missing Access"

Causa: Falta el scope applications.commands
Solución: Re-invita el bot con el scope correcto

Error: "Unknown interaction"

Causa: Los comandos no están registrados
Solución: Espera a que aparezca "✅ Comandos Slash registrados" en consola

Los comandos de texto no funcionan

Causa: USE_MESSAGE_CONTENT no está configurado o el intent no está habilitado
Solución: Ver docs/MESSAGE_CONTENT_CONFIG.md

Error: "Cannot find module '@/...'"

Causa: Path aliases no configurados
Solución: Asegúrate de ejecutar con ts-node -r tsconfig-paths/register


🛠️ Ecosistema Patto

Patto CLI ✅ (Disponible)

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-discord

Enlaces:


Patto Bot Features (Próximamente)

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!


🤝 Contribuir

Las contribuciones son bienvenidas! Por favor:

  1. Fork el proyecto
  2. Crea una rama para tu feature (git checkout -b feature/AmazingFeature)
  3. Commit tus cambios (git commit -m 'Add some AmazingFeature')
  4. Push a la rama (git push origin feature/AmazingFeature)
  5. Abre un Pull Request usando el template

📝 Licencia

Este proyecto está bajo la Licencia MIT. Ver el archivo LICENSE para más detalles.


👨‍💻 Autor

HormigaDev


🙏 Agradecimientos

📚 Librerías Principales

  • Discord.js - Librería de Discord para Node.js
  • TypeScript - Superset de JavaScript con tipos estáticos

🧪 Testing y Calidad

  • 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

🛠️ Desarrollo

  • 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

⚙️ Utilidades

  • dotenv - Carga variables de entorno desde .env
  • nanoid - Generador de IDs únicos pequeños y seguros

🚀 CI/CD


⭐ Si te gusta este proyecto, ¡Ayuda a Patto con una estrella en GitHub! ⭐

Reportar BugSolicitar Feature

About

Template profesional de Discord Bot con TypeScript, decoradores, sistema de plugins, componentes interactivos (botones, selects, modals), validación de entorno, testing completo y arquitectura modular. Listo para producción.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks