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.
- Visión General
- Requisitos y Dependencias
- Instalación y Configuración
- Arquitectura y Lógica
- Referencia de API
- Sistema de Plugins
- Ejemplos de Uso
- Edge Cases y Consideraciones
- Apéndices
- Hot-reload: Los plugins se recargan al guardar, sin reiniciar el bot
- Objeto
munificado: 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
| Tipo | Ejemplo |
|---|---|
| Moderación | Anti-spam, anti-links, bienvenidas automáticas |
| Utilidades | Stickers, descargas, conversiones |
| Juegos/Economía | Sistemas de puntos, tiendas virtuales, rankings |
| Integración | APIs externas, webhooks, notificaciones |
| Requisito | Versión Mínima |
|---|---|
| Node.js | 18.x LTS |
| npm | 9.x |
{
"@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"
}Crear archivo .env en la raíz:
GOOGLE_API_KEY=tu_api_key_aqui # Opcional: para integraciones con Google AIgit clone https://github.com/Zeppth/Jun-Base
cd Jun-Base
npm installEditar 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}}npm startEl sistema presentará un menú interactivo:
~> ¿Cómo desea conectarse?
1. Código QR.
2. Código de 8 dígitos.
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)
┌─────────────────────────────────────────────────────────────────────────┐
│ 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() │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
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/
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:
| Index | Propiedades Disponibles |
|---|---|
| 1 | m.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) |
| 2 | Todo 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 |
| 3 | Todo 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>}}}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 comomacOS('Desktop')pin-code: Solicita código de 8 dígitos, browser se establece comoubuntu('Chrome')
Sistema de plugins con hot-reload.
constplugins=newPlugins(folderPath,defaultContext)Métodos:
| Método | Firma | Retorno | Descripción |
|---|---|---|---|
load() | () -> Promise<void> | — | Inicia watcher y carga plugins existentes |
query(filter) | (Object) -> Array<Plugin> | Plugins que coinciden | Busca plugins por propiedades |
import(key) | (string | {file}) -> any | Valor exportado | Obtiene exports compartidos |
export(key, value) | (string, any) -> any | Valor almacenado | Registra valor compartido entre plugins |
remove(key) | (string) -> boolean | Éxito | Elimina plugin del registro |
Lógica de coincidencia en query():
| Query | Plugin | ¿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 |
Base de datos JSON con persistencia diferida.
importdbfrom'./library/db.js'awaitdb.start('./storage/store')// InicializarMétodos:
| Método | Firma | Retorno | Descripción |
|---|---|---|---|
start(path) | (string) -> Promise<db> | Instancia | Inicializa la base de datos |
open(name) | (string) -> Promise<{data, update}> | Objeto DB | Abre/crea una base de datos |
has(name) | (string) -> Promise<boolean> | Existe | Verifica existencia |
delete(name) | (string) -> Promise<boolean> | Éxito | Elimina 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:
| Nombre | Propósito |
|---|---|
@users | Datos globales de usuarios |
@chat:{jid} | Datos específicos de un grupo |
@reply:Handler | Reply handlers activos |
@history/{jid} | Historial de mensajes por chat |
@history/{jid}/{sender} | Historial por usuario en chat |
Gestiona procesos hijo con comunicación IPC.
constbot=newForkManager(modulePath,{execArgv: ['--max-old-space-size=512'],env: {dataConfig: {},connOptions: {}}})Métodos:
| Método | Firma | Retorno | Descripció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') -> Promise | — | Envía mensaje IPC |
event.set(name, fn) | (string, Function) -> boolean | — | Registra handler de evento |
Eventos disponibles:message, error, exit
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 valoresWrapper 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á corriendoColores 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)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:
| Tipo | Mensaje |
|---|---|
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" |
┌──────────────────────────────────────────────────────────────────────┐
│ 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 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() │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
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:
| Tipo | Propiedades de context |
|---|---|
| Comando | sock, plugin, store |
| Before | sock, plugin, store, control |
| StubType | sock, plugin, store, parameters, even |
// 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});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}`);}}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ámetro | Tipo | Descripción |
|---|---|---|
message | Object | Mensaje al que se responderá (debe tener key.id) |
options.security.userId | String | 'all' o JID específico |
options.security.chatId | String | 'all' o JID específico |
options.security.scope | String | 'all', 'private', 'group' |
options.lifecycle.consumeOnce | Boolean | Eliminar tras primera ejecución |
options.state | Object | Estado personalizado accesible en rutas |
options.routes | Array | Rutas ordenadas por priority |
expiresIn | Number | Milisegundos hasta expiración |
// 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.`)}}// 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')}}}// 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);}}// 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]});}}Este ejemplo demuestra un sistema completo con persistencia, roles, exportación entre plugins y flujos interactivos.
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
// 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(){}}// 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);}}// 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'));}}// 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'));}}// 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}}Los errores dentro de plugin.script() son capturados automáticamente. El bot:
- Reacciona con ❌ (
react('error')) - Envía un mensaje con el stack trace al chat
- Continúa procesando otros mensajes
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}}- 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
Si saveHistory: true:
- Se almacenan los últimos 50 mensajes por usuario por grupo
- Se puede recuperar un mensaje con
sock.loadMessage(jid, id)
El bot se reconecta automáticamente excepto en caso de loggedOut, donde es necesario re-autenticar eliminando /storage/creds/.
Lista de eventos de WebMessageInfo.StubType que pueden capturarse con plugins stubtype: true:
| Evento | Descripción |
|---|---|
GROUP_PARTICIPANT_ADD | Usuario añadido al grupo |
GROUP_PARTICIPANT_REMOVE | Usuario eliminado del grupo |
GROUP_PARTICIPANT_LEAVE | Usuario abandonó el grupo |
GROUP_PARTICIPANT_PROMOTE | Usuario promovido a admin |
GROUP_PARTICIPANT_DEMOTE | Admin degradado a miembro |
GROUP_CHANGE_SUBJECT | Nombre del grupo cambiado |
GROUP_CHANGE_DESCRIPTION | Descripción del grupo cambiada |
GROUP_CHANGE_ICON | Foto del grupo cambiada |
GROUP_CHANGE_INVITE_LINK | Link de invitación regenerado |
GROUP_CHANGE_RESTRICT | Configuración de restricción cambiada |
GROUP_CHANGE_ANNOUNCE | Modo solo admins activado/desactivado |
GROUP_PARTICIPANT_INVITE | Usuario invitado al grupo |
GROUP_CREATE | Grupo creado |
BROADCAST_CREATE | Lista de difusión creada |
BROADCAST_ADD | Añadido a lista de difusión |
BROADCAST_REMOVE | Eliminado de lista de difusión |
CALL_MISSED_VOICE | Llamada de voz perdida |
CALL_MISSED_VIDEO | Videollamada 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.`);}}| Variable | Tipo | Descripción |
|---|---|---|
global.config | Object | Configuración principal del bot |
global.config.name | String | Nombre del bot |
global.config.prefixes | String | Caracteres válidos como prefijo |
global.config.saveHistory | Boolean | Guardar historial de mensajes |
global.config.autoRead | Boolean | Marcar mensajes como leídos |
global.config.userRoles | Object | Roles predefinidos por número |
global.db | Object | Instancia del sistema de persistencia |
global.sock | Object | Socket de Baileys (disponible tras conexión) |
global.REACT_EMOJIS | Object | Mapeo de alias a emojis (wait, done, error) |
global.MSG | Object | Mensajes de sistema predefinidos |
global.PLUGINS_MSG | Object | Mensajes de gestión de plugins |
global.$proto | Object | Protobuf de WhatsApp |
global.$package | Object | Contenido de package.json |
global.$dir_main | Object | Rutas de directorios principales |
global.$dir_bot | Object | Rutas adicionales del bot |
global.readMore | String | Carácter invisible para "leer más" (850 repeticiones) |
global.googleApiKey | String | API 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'));}}