Skip to content

Latest commit

History

72 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Jun-Base

Sinopsis

Framework modular para bots de WhatsApp construido sobre @whiskeysockets/baileys. Implementa una arquitectura Event-Driven con aislamiento de procesos mediante child_process.fork(). El sistema de plugins utiliza hot-reload vía chokidar, persistencia JSON con escritura diferida (debounced writes), y un pipeline de procesamiento de mensajes basado en handlers encadenados. Requiere Node.js ≥18.x con soporte ESM nativo.


Tabla de Contenidos


Visión General

Características

  • Hot-reload: Los plugins se recargan al guardar, sin reiniciar el bot
  • Objeto m unificado: Acceso normalizado a mensaje, remitente, chat y contenido
  • Sistema de roles: root, owner, mod, vip, admin (configurable)
  • Persistencia JSON: Base de datos con escritura diferida y auto-descarga de memoria
  • Flujos conversacionales: ReplyHandler para interacciones multi-paso
  • Eventos de grupo: Captura de joins, leaves, promociones, cambios de configuración
  • Aislamiento de procesos: El bot corre en proceso hijo con reconexión automática

Casos de Uso

TipoEjemplo
ModeraciónAnti-spam, anti-links, bienvenidas automáticas
UtilidadesStickers, descargas, conversiones
Juegos/EconomíaSistemas de puntos, tiendas virtuales, rankings
IntegraciónAPIs externas, webhooks, notificaciones

Requisitos y Dependencias

Runtime

RequisitoVersión Mínima
Node.js18.x LTS
npm9.x

Dependencias Principales

{
"@whiskeysockets/baileys": "^7.0.0-rc.8",
"chokidar": "^4.0.1",
"@hapi/boom": "^10.0.1",
"pino": "9.1.0",
"chalk": "^5.3.0",
"dotenv": "^17.0.0",
"lodash": "^4.17.21",
"moment-timezone": "^0.5.43"
}

Variables de Entorno

Crear archivo .env en la raíz:

GOOGLE_API_KEY=tu_api_key_aqui # Opcional: para integraciones con Google AI

Instalación y Configuración

1. Clonar e instalar dependencias

git clone https://github.com/Zeppth/Jun-Base
cd Jun-Base
npm install

2. Configurar el bot

Editar config.js:

global.config={name: "MiBot",// Nombre del botprefixes: ".¿?¡!#%&/,~@",// Caracteres que activan comandossaveHistory: true,// Guardar historial de mensajesautoRead: true// Marcar mensajes como leídos};// Roles de usuario (usar número sin símbolos)global.config.userRoles={"521234567890": {root: true,// Acceso totalowner: true,// Propietariomod: true,// Moderadorvip: true// Usuario premium}}

3. Iniciar el bot

npm start

El sistema presentará un menú interactivo:

~> ¿Cómo desea conectarse?
1. Código QR.
2. Código de 8 dígitos.

4. Estructura de almacenamiento generada

storage/
├── creds/ # Credenciales de sesión (creds.json)
├── store/ # Base de datos JSON
│ ├── index.json # Índice de bases de datos
│ └── *.json # Datos persistidos
└── temp/ # Archivos temporales (purgados cada 60s)

Arquitectura y Lógica

Diagrama de Flujo

┌─────────────────────────────────────────────────────────────────────────┐
│ PROCESO PRINCIPAL │
│ index.js │
│ └─> ForkManager ──fork()──> core/index.js (PROCESO HIJO) │
│ │ │ │
│ │ IPC ├─> Baileys WebSocket │
│ │ (process.send) ├─> Plugin Watcher │
│ ▼ └─> Handler Pipeline │
│ ┌───────────────┐ │ │
│ │ Event Handler │◄────────────────────────┘ │
│ │ - qr-code │ │
│ │ - pin-code │ ┌──────────────────────────────────┐ │
│ │ - connection │ │ HANDLER PIPELINE │ │
│ │ - console:log │ │ │ │
│ └───────────────┘ │ message ──► m.content.js │ │
│ │ ──► m.bot.js │ │
│ │ ──► m.chat.js │ │
│ │ ──► m.sender.js │ │
│ │ ──► m.parser.js │ │
│ │ ──► plugin.script() │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘

Estructura de Directorios

SimpleBase-1.2.5/
├── index.js # Entry point: inicia ForkManager
├── config.js # Configuración global del bot
├── package.json
│
├── core/
│ ├── index.js # Bootstrap del proceso hijo
│ ├── main.js # Conexión Baileys + event listeners
│ ├── config.js # Carga package.json, define rutas globales
│ ├── format.js # Schema TypeScript-like del objeto `m`
│ │
│ └── handlers/ # Pipeline de procesamiento
│ ├── core.handler.js # Orquestador principal
│ ├── m.content.js # Extrae texto/media del mensaje
│ ├── m.bot.js # Info del bot (id, nombre, métodos)
│ ├── m.chat.js # Info del chat (grupo/privado)
│ ├── m.chat.group.js # Metadata de grupos
│ ├── m.sender.js # Info del remitente + roles
│ ├── m.quoted.sender.js # Info del mensaje citado
│ ├── m.assign.js # Métodos utilitarios (reply, react)
│ ├── m.parser.js # Parsea comandos
│ ├── m.pre.parser.js # ReplyHandler (flujos conversacionales)
│ ├── m.cache.js # Cache memoizado (fotos, metadata)
│ └── [+] extrator.content.js # Extractores por tipo de mensaje
│
├── library/
│ ├── client.js # Factory: MakeBot() crea conexión Baileys
│ ├── plugins.js # Sistema de plugins con hot-reload
│ ├── db.js # Base de datos JSON con escritura diferida
│ ├── fork.js # ForkManager: gestión de subprocesos
│ ├── loader.js # Carga dinámica de handlers
│ ├── bind.js # Extiende sock con métodos adicionales
│ ├── process.js # Wrapper de process para IPC
│ ├── utils.js # SimpleTimer, TmpStore, color
│ ├── log.js # Logger centralizado
│ ├── setup.js # Wizard de configuración inicial
│ └── purge.js # Limpia /temp cada 60 segundos
│
├── plugins/ # Directorio de plugins (hot-reload)
│ └── *.plugin.js
│
└── storage/ # Generado en runtime
├── creds/
├── store/
└── temp/

Disponibilidad de Propiedades por Index

El objeto m se construye progresivamente. Acceder a propiedades antes de su inicialización produce undefined. La siguiente tabla muestra qué propiedades están disponibles en cada punto del pipeline:

IndexPropiedades Disponibles
1m.id, m.message, m.cache, m.bot.id, m.bot.name, m.bot.fromMe, m.chat.id, m.chat.isGroup, m.sender.id, m.sender.name, m.sender.number, m.content.text, m.content.args, m.content.media, m.quoted (si existe)
2Todo lo anterior + m.chat.admins, m.chat.participants, m.chat.name, m.chat.desc, m.chat.size, m.chat.owner, m.bot.roles.admin, m.sender.roles.admin
3Todo lo anterior + m.command, m.args, m.text, m.body, m.tag, m.isCmd, m.plugin

Ejemplo de acceso seguro en plugin before:

// INCORRECTO: m.chat.admins no existe en index=1exportdefault{before: true,index: 1,script: async(m)=>{console.log(m.chat.admins);// undefined}}// CORRECTO: Verificar existencia o usar index apropiadoexportdefault{before: true,index: 2,// Aquí ya existe m.chat.adminsscript: async(m)=>{if(m.chat.isGroup){console.log(m.chat.admins);// Array<String>}}}

Referencia de API

Core Modules

MakeBot(options, store)library/client.js

Crea una conexión autenticada con WhatsApp.

/** * @param {Object} options * @param {string} options.connectType - 'qr-code' | 'pin-code' * @param {string} options.phoneNumber - Número para pin-code (sin símbolos) * @param {Object} store - Instancia de store (opcional) *  * @returns {Promise<Object>} sock - Instancia de Baileys extendida */

Comportamiento:

  • qr-code: Muestra QR en terminal, browser se establece como macOS('Desktop')
  • pin-code: Solicita código de 8 dígitos, browser se establece como ubuntu('Chrome')

class Pluginslibrary/plugins.js

Sistema de plugins con hot-reload.

constplugins=newPlugins(folderPath,defaultContext)

Métodos:

MétodoFirmaRetornoDescripción
load()() -> Promise<void>Inicia watcher y carga plugins existentes
query(filter)(Object) -> Array<Plugin>Plugins que coincidenBusca plugins por propiedades
import(key)(string | {file}) -> anyValor exportadoObtiene exports compartidos
export(key, value)(string, any) -> anyValor almacenadoRegistra valor compartido entre plugins
remove(key)(string) -> booleanÉxitoElimina plugin del registro

Lógica de coincidencia en query():

QueryPlugin¿Coincide?
case: 'help'case: ['help', 'ayuda']✓ Sí
case: ['help', 'ayuda']case: 'help'✓ Sí
case: ['a', 'b']case: ['b', 'c']✓ Sí (intersección)
case: 'test'case: 'otro'✗ No

dblibrary/db.js

Base de datos JSON con persistencia diferida.

importdbfrom'./library/db.js'awaitdb.start('./storage/store')// Inicializar

Métodos:

MétodoFirmaRetornoDescripción
start(path)(string) -> Promise<db>InstanciaInicializa la base de datos
open(name)(string) -> Promise<{data, update}>Objeto DBAbre/crea una base de datos
has(name)(string) -> Promise<boolean>ExisteVerifica existencia
delete(name)(string) -> Promise<boolean>ÉxitoElimina base de datos

Comportamiento de update():

  • Las escrituras se agrupan (debounce de 5 segundos)
  • Después de 5 llamadas consecutivas, fuerza escritura inmediata
  • Bases inactivas por 60 segundos se descargan de memoria

Bases de datos predefinidas:

NombrePropósito
@usersDatos globales de usuarios
@chat:{jid}Datos específicos de un grupo
@reply:HandlerReply handlers activos
@history/{jid}Historial de mensajes por chat
@history/{jid}/{sender}Historial por usuario en chat

class ForkManagerlibrary/fork.js

Gestiona procesos hijo con comunicación IPC.

constbot=newForkManager(modulePath,{execArgv: ['--max-old-space-size=512'],env: {dataConfig: {},connOptions: {}}})

Métodos:

MétodoFirmaRetornoDescripción
start(callback?)(Function?) -> Promise<void>Inicia el proceso hijo
stop(callback?)(Function?) -> Promise<void>Detiene el proceso (SIGTERM)
send(content, type?)(Object, 'send'|'request') -> PromiseEnvía mensaje IPC
event.set(name, fn)(string, Function) -> booleanRegistra handler de evento

Eventos disponibles:message, error, exit


Library Modules

TmpStorelibrary/utils.js

Cache en memoria con TTL automático.

constcache=newTmpStore(60000)// 60 segundos TTLcache.set('key',value)// Almacena con TTLcache.get('key')// Obtiene valorcache.has('key')// Verifica existenciacache.delete('key')// Elimina manualmentecache.clear()// Limpia todocache.keys()// Array de clavescache.values()// Array de valores

SimpleTimerlibrary/utils.js

Wrapper para setTimeout/setInterval con control de estado.

consttimer=newSimpleTimer(()=>console.log('tick'),5000,'interval'// 'timeout' | 'interval')timer.start()// Iniciatimer.stop()// Detienetimer.status// true si está corriendo

colorlibrary/utils.js

Colores ANSI para terminal.

import{color}from'./library/utils.js';console.log(color.rgb(255,100,0)+'Texto naranja'+color.reset)console.log(color.bg.rgb(0,0,255)+'Fondo azul'+color.reset)

Objeto m (Message Context)

El objeto m se construye en cada mensaje y contiene toda la información normalizada.

interfaceMessageContext{id: string// ID único del mensajetype: string// Tipo: 'conversation', 'imageMessage', etc.message: Object// Mensaje raw de Baileysbody: string// Texto del mensajecommand: string// Comando extraído (sin prefijo)args: string[]// Argumentos del comandotext: string// Texto completo después del comandotag: string[]// Tags extraídos (tag=value)isCmd: boolean// ¿Es un comando válido?plugin: Object|null// Plugin que maneja el comandobot: {id: string// JID del botname: string// Nombre del botnumber: string// Número sin @lidfromMe: boolean// ¿Mensaje enviado por el bot?roles: {admin: boolean}// Roles del bot en el chat// MétodosgetDesc(): Promise<string>getPhoto(): Promise<string>setPhoto(image: Buffer): Promise<void>setDesc(desc: string): Promise<void>setName(name: string): Promise<void>join(inviteCode: string): Promise<void>mute(id: string,state: boolean,time?: number): Promise<void>block(id: string,state: boolean): Promise<void>role(...roles: string[]): boolean}chat: {id: string// JID del chatisGroup: boolean// ¿Es un grupo?name: string// Nombre del grupo/contactodesc: string// Descripciónsize: number// Número de participantescreated: number// Timestamp de creaciónowner: string// JID del creadorparticipants: Object[]// Lista de participantesadmins: string[]// JIDs de administradores// Métodos (solo grupos)db(): Promise<{data,update}>add(user: string): Promise<void>remove(user: string): Promise<void>promote(user: string): Promise<void>demote(user: string): Promise<void>getPhoto(type?: string): Promise<string>setPhoto(image: Buffer): Promise<void>setDesc(desc: string): Promise<void>setName(name: string): Promise<void>getCodeInvite(): Promise<string>getLinkInvite(): Promise<string>revoke(): Promise<void>settings: {lock(bool: boolean): Promise<void>announce(bool: boolean): Promise<void>memberAdd(bool: boolean): Promise<void>joinApproval(bool: boolean): Promise<void>}}sender: {id: string// JID del remitentename: string// pushNamenumber: string// Número sin @liduser: string// Formato @númeromentioned: string[]// JIDs mencionadosroles: {root: boolean// Dueño absolutoowner: boolean// Propietariomod: boolean// Moderadorvip: boolean// Usuario premiumadmin: boolean// Admin del grupobot: boolean// Es el bot}// Métodosdb(): Promise<{data,_data,update}>getDesc(): Promise<string>getPhoto(): Promise<string>role(...roles: string[]): boolean}content: {text: string// Texto del mensajeargs: string[]// Texto dividido por espaciosmedia: false|{mimeType: stringfileName: stringdownload(): Promise<Buffer>}}quoted?: {// Presente si cita un mensajeid: stringtype: stringsender: {/* igual que sender */}content: {/* igual que content */}}// Métodos utilitariosreply(text: string|Object): Promise<Message>react(emoji: string): Promise<void>// 'wait' | 'done' | 'error' | emojisms(type: string): Promise<void>// Envía mensaje predefinidodb(id: string): Promise<{data,update}>setBan(id: string,state: boolean): Promise<void>setRole(id: string,state: boolean, ...roles: string[]): Promise<boolean>}

Tipos de sms() disponibles:

TipoMensaje
root"Este comando solo puede ser utilizado por el dueño"
owner"Este comando solo puede ser utilizado por un propietario"
mod"Este comando solo puede ser utilizado por un moderador"
vip"Esta solicitud es solo para usuarios premium"
group"Este comando solo se puede usar en grupos"
private"Este comando solo se puede usar por chat privado"
admin"Este comando solo puede ser usado por los administradores del grupo"
botAdmin"El bot necesita ser administrador para usar este comando"
unreg"Regístrese para usar esta función..."
restrict"Esta función está desactivada"

Sistema de Plugins

Taxonomía de Plugins

┌──────────────────────────────────────────────────────────────────────┐
│ TAXONOMÍA DE PLUGINS │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 1. PLUGINS DE COMANDO │ │
│ │ command: true │ │
│ │ case: String | Array<String> │ │
│ │ usePrefix: Boolean (default: true) │ │
│ │ │ │
│ │ Se activan cuando m.command coincide con case │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 2. PLUGINS DE INTERCEPTACIÓN (BEFORE) │ │
│ │ before: true │ │
│ │ index: 1 | 2 | 3 │ │
│ │ priority: Number (menor = mayor prioridad) │ │
│ │ │ │
│ │ Se ejecutan en puntos específicos del pipeline │ │
│ │ Pueden interrumpir el flujo con control.end = true │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 3. PLUGINS DE EVENTO (STUBTYPE) │ │
│ │ stubtype: true │ │
│ │ case: String (nombre del evento WebMessageInfo.StubType) │ │
│ │ │ │
│ │ Se activan con eventos del protocolo WhatsApp │ │
│ │ Ejemplos: GROUP_PARTICIPANT_ADD, GROUP_PARTICIPANT_LEAVE │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘

Ciclo de Vida

┌────────────────────────────────────────────────────────────────────────┐
│ CICLO DE VIDA DE UN PLUGIN │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ CARGA │ Plugins.load() → fs.readdir() → import() │
│ │ INICIAL │ Se almacena en Map con fileName como key │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ REGISTRO │ Se parsean propiedades (case, command, etc.) │
│ │ EN MAP │ Se mezclan con defaultObjects del constructor │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ OBSERVACIÓN │ chokidar.watch() monitorea cambios │
│ │ (WATCHER) │ Eventos: add, change, unlink │
│ └──────┬──────┘ │
│ │ │
│ ├──────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ CAMBIO │ │ ELIMINACIÓN │ │
│ │ (change) │ │ (unlink) │ │
│ │ │ │ │ │
│ │ delete(key) │ │ delete(key) │ │
│ │ reimport() │ │ │ │
│ │ (delay 1s) │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ EJECUCIÓN │ │
│ │ │ │
│ │ messages.upsert → core.handler → plugins.query() → script() │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘

Estructura de un Plugin

Los archivos deben terminar en .plugin.js y ubicarse en /plugins/.

// plugins/ejemplo.plugin.jsexportdefault{// === IDENTIFICACIÓN ===case: ['ping','p'],// String o Array<String>// === CLASIFICACIÓN ===usePrefix: true,// Requiere prefijo (default: true)command: true,// Plugin de comando// === PARA PLUGINS BEFORE ===// before: true,// index: 1, // Punto de ejecución (1, 2 o 3)// priority: 10, // Menor = mayor prioridad// === PARA PLUGINS STUBTYPE ===// stubtype: true,// case: 'GROUP_PARTICIPANT_ADD',// === FUNCIÓN PRINCIPAL ===asyncscript(m,context){const{ sock, plugin, store }=context// Para plugins before: context.control// Para plugins stubtype: context.parameters, context.evenawaitm.reply('Pong!')}}

Objeto context según tipo de plugin:

TipoPropiedades de context
Comandosock, plugin, store
Beforesock, plugin, store, control
StubTypesock, plugin, store, parameters, even

Sistema de Consulta

// Buscar comandos con prefijoconstcmds=awaitplugins.query({case: 'ping',usePrefix: true,command: true});// Buscar plugins before con index 2constbeforePlugins=awaitplugins.query({before: true,index: 2});// Buscar eventos stubtypeconstevents=awaitplugins.query({case: 'GROUP_PARTICIPANT_ADD',stubtype: true});

Exportación entre Plugins

Los plugins pueden exportar funciones y valores para ser consumidos por otros:

Plugin que exporta:

// plugins/utils/helpers.plugin.jsconstformatNumber=(n)=>n.toLocaleString('es-ES');constrandomInt=(min,max)=>Math.floor(Math.random()*(max-min+1))+min;exportdefault{before: true,index: 1,export: {'@helpers': {
formatNumber,
randomInt
}},asyncscript(){// Plugin mínimo, solo exporta}}

Plugin que consume:

// plugins/comandos/dado.plugin.jsexportdefault{case: 'dado',command: true,asyncscript(m,{ plugin }){consthelpers=plugin.import('@helpers');constresultado=helpers.randomInt(1,6);awaitm.reply(`🎲 Obtuviste: ${resultado}`);}}

ReplyHandler (Flujos Conversacionales)

Permite crear interacciones multi-paso donde el bot espera respuestas específicas:

asyncscript(m,{ sock }){constmsg=awaitm.reply('¿Cuál es tu nombre?')awaitsock.setReplyHandler(msg,{security: {userId: m.sender.id,// Solo este usuario puede responderchatId: m.chat.id,// Solo en este chatscope: 'all'// 'all' | 'private' | 'group'},lifecycle: {consumeOnce: true// Eliminar después de una respuesta},state: {step: 'name',// Estado personalizadointentos: 0},routes: [{priority: 1,code: {// guard retorna true para SALTAR esta rutaguard: (m,ctx)=>m.body.length<2,// executor se ejecuta si guard retorna false/undefinedexecutor: async(m,ctx)=>{awaitm.reply(`¡Hola ${m.body}!`)}}}]},1000*60*5)// Expira en 5 minutos}

Parámetros de setReplyHandler:

ParámetroTipoDescripción
messageObjectMensaje al que se responderá (debe tener key.id)
options.security.userIdString'all' o JID específico
options.security.chatIdString'all' o JID específico
options.security.scopeString'all', 'private', 'group'
options.lifecycle.consumeOnceBooleanEliminar tras primera ejecución
options.stateObjectEstado personalizado accesible en rutas
options.routesArrayRutas ordenadas por priority
expiresInNumberMilisegundos hasta expiración

Ejemplos de Uso

Ejemplo 1: Comando con Verificación de Roles

// plugins/admin/ban.plugin.jsexportdefault{case: 'ban',usePrefix: true,command: true,asyncscript(m,{ sock }){// Verificar que el ejecutor sea moderador o superiorif(!m.sender.role('root','owner','mod')){returnm.sms('mod')}// Verificar que haya un usuario mencionado o citadoconsttarget=m.sender.mentioned[0]||m.quoted?.sender.idif(!target){returnm.reply('Menciona o cita al usuario a banear')}// Banear usuarioawaitm.setBan(target,true)awaitm.reply(`Usuario @${target.split('@')[0]} baneado.`)}}

Ejemplo 2: Descarga de Media

// plugins/media/sticker.plugin.jsexportdefault{case: ['sticker','s'],usePrefix: true,command: true,asyncscript(m,{ sock }){// Verificar si hay imagen en el mensaje o citadaconstmedia=m.content.media||m.quoted?.content.mediaif(!media||!media.mimeType.startsWith('image/')){returnm.reply('Envía o cita una imagen')}awaitm.react('wait')try{constbuffer=awaitmedia.download()awaitsock.sendMessage(m.chat.id,{sticker: buffer},{quoted: m.message})awaitm.react('done')}catch(e){awaitm.react('error')awaitm.reply('Error al crear el sticker')}}}

Ejemplo 3: Plugin Before (Middleware Anti-Spam)

// plugins/middleware/antispam.plugin.jsconstcooldowns=newMap();constCOOLDOWN_MS=3000;exportdefault{before: true,index: 1,priority: 5,asyncscript(m,{ control }){// Ignorar al botif(m.sender.roles.bot)return// Ignorar admins/ownersif(m.sender.role('root','owner','mod'))returnconstkey=m.sender.id;constnow=Date.now();if(cooldowns.has(key)){constlastTime=cooldowns.get(key);if(now-lastTime<COOLDOWN_MS){control.end=true;// Detiene el pipelinereturn;}}cooldowns.set(key,now);}}

Ejemplo 4: Plugin de Evento (Bienvenida)

// plugins/events/bienvenida.plugin.jsexportdefault{case: 'GROUP_PARTICIPANT_ADD',stubtype: true,asyncscript(m,{ sock, parameters }){constnewMember=parameters[0];constgroupName=m.chat.name||'el grupo';awaitsock.sendMessage(m.chat.id,{text: `¡Bienvenido/a a *${groupName}*, @${newMember.split('@')[0]}! 🎉`,mentions: [newMember]});}}

Ejemplo 5: Sistema de Economía Completo

Este ejemplo demuestra un sistema completo con persistencia, roles, exportación entre plugins y flujos interactivos.

Estructura de archivos

plugins/
└── economia/
├── _init.plugin.js # Inicialización y exports
├── balance.plugin.js # Consulta de saldo
├── daily.plugin.js # Recompensa diaria
├── transferir.plugin.js # Transferencias
└── tienda.plugin.js # Tienda con ReplyHandler

Plugin de Inicialización

// plugins/economia/_init.plugin.jsconstMONEDA='💎';constINICIAL=1000;constobtenerCuenta=async(userId)=>{constdb=awaitglobal.db.open('@economia');if(!db.data[userId]){db.data[userId]={balance: INICIAL,banco: 0,ultimoDaily: 0,streak: 0,inventario: [],creado: Date.now()};awaitdb.update();}return{cuenta: db.data[userId],guardar: async()=>awaitdb.update()};};constformatearBalance=(cantidad)=>{return`${cantidad.toLocaleString('es-ES')}${MONEDA}`;};exportdefault{before: true,index: 1,export: {'@economia': {
obtenerCuenta,
formatearBalance,MONEDA,INICIAL}},asyncscript(){}}

Plugin de Balance

// plugins/economia/balance.plugin.jsexportdefault{case: ['balance','bal','saldo'],command: true,usePrefix: true,asyncscript(m,{ plugin }){consteco=plugin.import('@economia');const{ cuenta }=awaiteco.obtenerCuenta(m.sender.id);consttexto=[`*💰 Balance de ${m.sender.name}*`,'',`├ Efectivo: ${eco.formatearBalance(cuenta.balance)}`,`├ Banco: ${eco.formatearBalance(cuenta.banco)}`,`└ Total: ${eco.formatearBalance(cuenta.balance+cuenta.banco)}`].join('\n');awaitm.reply(texto);}}

Plugin de Recompensa Diaria

// plugins/economia/daily.plugin.jsconstCOOLDOWN=24*60*60*1000;// 24 horasconstRECOMPENSA_BASE=500;constBONUS_POR_STREAK=50;constMAX_BONUS=500;exportdefault{case: ['daily','diario'],command: true,usePrefix: true,asyncscript(m,{ plugin }){consteco=plugin.import('@economia');const{ cuenta, guardar }=awaiteco.obtenerCuenta(m.sender.id);constahora=Date.now();constdiferencia=ahora-cuenta.ultimoDaily;// Verificar cooldownif(diferencia<COOLDOWN){constrestante=COOLDOWN-diferencia;consthoras=Math.floor(restante/(60*60*1000));constminutos=Math.floor((restante%(60*60*1000))/(60*1000));returnm.reply(`⏰ Debes esperar *${horas}h ${minutos}m* para tu próxima recompensa.`);}// Calcular streakconstdentroDeVentana=diferencia<COOLDOWN*2;constnuevoStreak=dentroDeVentana ? cuenta.streak+1 : 1;// Calcular recompensaconstbonus=Math.min(nuevoStreak*BONUS_POR_STREAK,MAX_BONUS);constrecompensa=RECOMPENSA_BASE+bonus;// Actualizar cuentacuenta.balance+=recompensa;cuenta.ultimoDaily=ahora;cuenta.streak=nuevoStreak;awaitguardar();awaitm.reply([`*🎁 Recompensa Diaria*`,'',`├ Base: ${eco.formatearBalance(RECOMPENSA_BASE)}`,`├ Bonus (x${nuevoStreak}): +${eco.formatearBalance(bonus)}`,`├ Total: ${eco.formatearBalance(recompensa)}`,`└ Nuevo balance: ${eco.formatearBalance(cuenta.balance)}`,'',`🔥 Racha: ${nuevoStreak} día${nuevoStreak>1 ? 's' : ''}`].join('\n'));}}

Plugin de Transferencias

// plugins/economia/transferir.plugin.jsconstCOMISION=0.05;// 5%exportdefault{case: ['transferir','pay','enviar'],command: true,usePrefix: true,asyncscript(m,{ plugin }){consteco=plugin.import('@economia');// Validar destinatarioif(m.sender.mentioned.length===0){returnm.reply(['*📤 Transferir*','','Uso: .transferir @usuario <cantidad>','Ejemplo: .transferir @Juan 1000','',`Comisión: ${COMISION*100}%`].join('\n'));}constdestinatarioId=m.sender.mentioned[0];// No transferir a sí mismoif(destinatarioId===m.sender.id){returnm.reply('❌ No puedes transferirte a ti mismo.');}// Validar cantidadconstcantidad=parseInt(m.args[1]);if(isNaN(cantidad)||cantidad<=0){returnm.reply('❌ Especifica una cantidad válida.');}// Obtener cuentasconst{cuenta: origen,guardar: guardarOrigen}=awaiteco.obtenerCuenta(m.sender.id);const{cuenta: destino,guardar: guardarDestino}=awaiteco.obtenerCuenta(destinatarioId);// Calcular comisiónconstcomision=Math.floor(cantidad*COMISION);consttotal=cantidad+comision;// Validar balanceif(origen.balance<total){returnm.reply(['❌ *Balance insuficiente*','',`├ Cantidad: ${eco.formatearBalance(cantidad)}`,`├ Comisión: ${eco.formatearBalance(comision)}`,`├ Total requerido: ${eco.formatearBalance(total)}`,`└ Tu balance: ${eco.formatearBalance(origen.balance)}`].join('\n'));}// Ejecutar transferenciaorigen.balance-=total;destino.balance+=cantidad;awaitguardarOrigen();awaitguardarDestino();awaitm.reply(['✅ *Transferencia Exitosa*','',`├ Enviado: ${eco.formatearBalance(cantidad)}`,`├ Comisión: ${eco.formatearBalance(comision)}`,`├ Destinatario: @${destinatarioId.split('@')[0]}`,`└ Tu nuevo balance: ${eco.formatearBalance(origen.balance)}`].join('\n'));}}

Plugin de Tienda con ReplyHandler

// plugins/economia/tienda.plugin.jsconstCATALOGO=[{id: 'vip_1d',nombre: '⭐ VIP 1 Día',precio: 5000,tipo: 'rol'},{id: 'vip_7d',nombre: '🌟 VIP 7 Días',precio: 25000,tipo: 'rol'},{id: 'lootbox',nombre: '📦 Caja Misteriosa',precio: 1000,tipo: 'item'},{id: 'titulo_custom',nombre: '🏷️ Título Personalizado',precio: 10000,tipo: 'item'}];exportdefault{case: ['tienda','shop'],command: true,usePrefix: true,asyncscript(m,{ sock, plugin }){consteco=plugin.import('@economia');const{ cuenta }=awaiteco.obtenerCuenta(m.sender.id);// Construir catálogolettexto=[`*🛒 Tienda*`,'',`Tu balance: ${eco.formatearBalance(cuenta.balance)}`,''].join('\n');CATALOGO.forEach((item,index)=>{texto+=`${index+1}. ${item.nombre}\n`;texto+=` └ ${eco.formatearBalance(item.precio)}\n`;});texto+='\n_Responde con el número del artículo que deseas comprar._';constmensaje=awaitm.reply(texto);// Registrar ReplyHandlerawaitsock.setReplyHandler(mensaje,{security: {userId: m.sender.id,chatId: m.chat.id,scope: 'all'},lifecycle: {consumeOnce: true},state: {catalogo: CATALOGO,compradorId: m.sender.id},routes: [{priority: 1,code: {// Validar entradaguard: (m,ctx)=>{constseleccion=parseInt(m.content.text);returnisNaN(seleccion)||seleccion<1||seleccion>ctx.state.catalogo.length;},// Procesar compraexecutor: async(m,ctx)=>{constseleccion=parseInt(m.content.text)-1;constitem=ctx.state.catalogo[seleccion];// Obtener cuenta actualizadaconstdb=awaitglobal.db.open('@economia');constcuenta=db.data[ctx.state.compradorId];// Verificar balanceif(cuenta.balance<item.precio){returnm.reply(['❌ *Balance insuficiente*','',`├ Precio: ${item.precio.toLocaleString()} 💎`,`└ Tu balance: ${cuenta.balance.toLocaleString()} 💎`].join('\n'));}// Procesar compracuenta.balance-=item.precio;cuenta.inventario.push({id: item.id,nombre: item.nombre,tipo: item.tipo,obtenido: Date.now()});awaitdb.update();awaitm.reply(['✅ *Compra Exitosa*','',`├ Artículo: ${item.nombre}`,`├ Precio: ${item.precio.toLocaleString()} 💎`,`└ Nuevo balance: ${cuenta.balance.toLocaleString()} 💎`].join('\n'));}}},{priority: 2,code: {// Ruta por defecto si guard anterior fue trueexecutor: async(m)=>{awaitm.reply('❌ Opción no válida. Escribe un número del 1 al '+CATALOGO.length);}}}]},1000*60*2);// 2 minutos}}

Edge Cases y Consideraciones

Manejo de Errores en Plugins

Los errores dentro de plugin.script() son capturados automáticamente. El bot:

  1. Reacciona con ❌ (react('error'))
  2. Envía un mensaje con el stack trace al chat
  3. Continúa procesando otros mensajes

Mutabilidad del Objeto m

El objeto m es mutable. Las modificaciones persisten a lo largo del pipeline:

// Plugin before:index=2exportdefault{before: true,index: 2,script: async(m)=>{m.customFlag=true;m.sender.roles.customRole=true;}}// Plugin de comando posteriorexportdefault{case: 'test',command: true,script: async(m)=>{console.log(m.customFlag);// trueconsole.log(m.sender.roles.customRole);// true}}

Límites de la Base de Datos

  • Las bases inactivas por 60s se descargan de memoria
  • Escrituras se agrupan cada 5 segundos o cada 5 llamadas a update()
  • No hay límite de tamaño, pero archivos JSON grandes impactan rendimiento

Historial de Mensajes

Si saveHistory: true:

  • Se almacenan los últimos 50 mensajes por usuario por grupo
  • Se puede recuperar un mensaje con sock.loadMessage(jid, id)

Reconexión Automática

El bot se reconecta automáticamente excepto en caso de loggedOut, donde es necesario re-autenticar eliminando /storage/creds/.


Apéndices

Apéndice A: Eventos StubType

Lista de eventos de WebMessageInfo.StubType que pueden capturarse con plugins stubtype: true:

EventoDescripción
GROUP_PARTICIPANT_ADDUsuario añadido al grupo
GROUP_PARTICIPANT_REMOVEUsuario eliminado del grupo
GROUP_PARTICIPANT_LEAVEUsuario abandonó el grupo
GROUP_PARTICIPANT_PROMOTEUsuario promovido a admin
GROUP_PARTICIPANT_DEMOTEAdmin degradado a miembro
GROUP_CHANGE_SUBJECTNombre del grupo cambiado
GROUP_CHANGE_DESCRIPTIONDescripción del grupo cambiada
GROUP_CHANGE_ICONFoto del grupo cambiada
GROUP_CHANGE_INVITE_LINKLink de invitación regenerado
GROUP_CHANGE_RESTRICTConfiguración de restricción cambiada
GROUP_CHANGE_ANNOUNCEModo solo admins activado/desactivado
GROUP_PARTICIPANT_INVITEUsuario invitado al grupo
GROUP_CREATEGrupo creado
BROADCAST_CREATELista de difusión creada
BROADCAST_ADDAñadido a lista de difusión
BROADCAST_REMOVEEliminado de lista de difusión
CALL_MISSED_VOICELlamada de voz perdida
CALL_MISSED_VIDEOVideollamada perdida

Ejemplo de uso:

exportdefault{case: 'GROUP_PARTICIPANT_LEAVE',stubtype: true,asyncscript(m,{ parameters }){constusuario=parameters[0];awaitm.reply(`👋 @${usuario.split('@')[0]} ha abandonado el grupo.`);}}

Apéndice B: Variables Globales

VariableTipoDescripción
global.configObjectConfiguración principal del bot
global.config.nameStringNombre del bot
global.config.prefixesStringCaracteres válidos como prefijo
global.config.saveHistoryBooleanGuardar historial de mensajes
global.config.autoReadBooleanMarcar mensajes como leídos
global.config.userRolesObjectRoles predefinidos por número
global.dbObjectInstancia del sistema de persistencia
global.sockObjectSocket de Baileys (disponible tras conexión)
global.REACT_EMOJISObjectMapeo de alias a emojis (wait, done, error)
global.MSGObjectMensajes de sistema predefinidos
global.PLUGINS_MSGObjectMensajes de gestión de plugins
global.$protoObjectProtobuf de WhatsApp
global.$packageObjectContenido de package.json
global.$dir_mainObjectRutas de directorios principales
global.$dir_botObjectRutas adicionales del bot
global.readMoreStringCarácter invisible para "leer más" (850 repeticiones)
global.googleApiKeyStringAPI Key de Google (desde .env)

Ejemplo de acceso:

exportdefault{case: 'info',command: true,asyncscript(m){awaitm.reply([`*${global.config.name}*`,`Versión: ${global.$package.version}`,`Prefijos: ${global.config.prefixes}`,`Historial: ${global.config.saveHistory ? 'Sí' : 'No'}`].join('\n'));}}

About

Framework Event-Driven modular para bots de WhatsApp (Node.js ≥18.x) con aislamiento de procesos, hot-reload de plugins y pipeline de handlers encadenados.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages