Decorators¶
API de decoradores Stage 3 construida sobre el cliente de WhatsApp. Opt-in a través de la subentrada @arcaelas/whatsapp/decorators; el paquete principal (@arcaelas/whatsapp) permanece sin cambios.
La capa de decoradores cablea los métodos declarados en una clase contra el event emitter real en el momento de connect(). No reemplaza al cliente: vincula métodos decorados a eventos, timers, callbacks de emparejamiento y workflows secuenciales.
Importación¶
import {
Bot,
on,
guard,
once,
connect,
disconnect,
every,
pair,
from,
pipe,
command,
WhatsAppBot,
} from "@arcaelas/whatsapp/decorators";
Requisitos¶
Entorno
- Node.js ≥ 20. El paquete incluye un polyfill interno de
Symbol.metadata, por lo que el runtime no necesita soporte nativo. - TypeScript ≥ 5. Usa decoradores Stage 3 nativos. No habilites
experimentalDecoratorsniemitDecoratorMetadataentsconfig.json: apuntan a la propuesta legacy y son incompatibles. - No se requiere dependencia de Reflect-metadata.
tsconfig.json mínimo:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true
}
}
Visión general¶
| Decorador | Signatura | Resumen |
|---|---|---|
@Bot | (options: IWhatsApp) => ClassDecorator | Convierte una clase en una subclase de WhatsAppBot con opciones por defecto. |
@on | (event: string) => MethodDecorator | Suscribe el método a un evento del cliente. Apilable. |
@guard | (pred: (...args) => boolean \| Promise<boolean>) => MethodDecorator | Pre-verificación ejecutada antes del handler. Apilable (AND). |
@once | (event?: string) => MethodDecorator | Ejecuta el handler una vez, luego se desuscribe. Acepta un atajo opcional de evento. |
@connect | () => MethodDecorator | Alias de @on('connected'). |
@disconnect | () => MethodDecorator | Alias de @on('disconnected'). |
@every | (ms: number) => MethodDecorator | Timer periódico vinculado al ciclo de vida de la conexión. |
@pair | () => MethodDecorator | Callback de emparejamiento (PIN/QR). Múltiples métodos se ejecutan en paralelo. |
@from | (src: string \| string[] \| (jid) => boolean) => MethodDecorator | Filtra por autor del mensaje (JID, LID o teléfono). |
@command | (pattern: string \| RegExp) => MethodDecorator | Comando textual sobre message:created con parsing de argumentos. |
@pipe | (workflow: string, index: number) => MethodDecorator | Paso de pipeline secuencial compartiendo argumentos mutables. |
@Bot(options)¶
Decorador de clase que convierte el target en una subclase de WhatsAppBot. El consumidor no necesita extender WhatsAppBot manualmente. En tiempo de construcción, el override parcial pasado a new Bot(override?) se fusiona sobre los default_options entregados al decorador.
Signatura
Comportamiento
- La subclase producida hereda de
WhatsAppBot, por lo queconnect()es el punto de entrada del cableado. - Los métodos y la metadata de la clase original se copian a la subclase generada.
- El constructor acepta un
Partial<IWhatsApp>que sobreescribe los defaults del decorador.
import Redis from "ioredis";
import { Bot, connect, RedisEngine } from "@arcaelas/whatsapp/decorators";
@Bot({
engine: new RedisEngine(new Redis()),
phone: "5491112345678",
})
class MyBot {
@connect()
on_open() {
console.log("connected");
}
}
const bot = new MyBot();
await bot.connect();
Pasando un override en tiempo de construcción:
@on(event)¶
Suscribe el método a un evento del cliente. El decorador es apilable: múltiples entradas @on sobre el mismo método registran múltiples suscripciones sin duplicación dentro del mismo método.
@on("message:created")
@on("message:updated")
log_message(msg: Message, chat: Chat, wa: WhatsApp) {
console.log(msg.id);
}
Los nombres de eventos válidos están documentados en References / Events. Valores comunes incluyen connected, disconnected, message:created, message:updated, message:reacted, contact:created, contact:updated, chat:created, chat:updated.
Payload del listener
Los argumentos del handler reflejan el payload del emisor. Para eventos de mensaje la signatura es (msg, chat, wa); para eventos de contacto (contact, chat, wa).
@guard(pred)¶
Registra un predicado evaluado antes del handler. Múltiples guards se acumulan y se evalúan secuencialmente en orden de declaración con semántica AND: cualquier guard que devuelva falsy hace corto-circuito y el handler no se ejecuta.
@on("message:created")
@guard((msg: Message) => !msg.me)
@guard((msg: Message) => msg.type === "text")
on_inbound_text(msg: Message) {
/* ... */
}
Auto-registro: si el método no tiene un @on explícito pero tiene al menos un @guard (o @from, que añade un guard internamente), se registra implícitamente a message:created.
// Equivalente a @on('message:created') + @guard(...)
@guard((msg: Message) => msg.type === "image")
on_image(msg: Message) {
/* ... */
}
@once() / @once(event)¶
Marca el handler para dispararse una vez y luego autodesuscribirse. Dos formas:
@once()— modificador puro, combínalo con@on(o con un auto-registro implícito).@once(event)— atajo equivalente a@on(event) + @once().
@once("connected")
greet_once() {
console.log("first connection");
}
@on("message:created")
@once()
first_message(msg: Message) {
console.log("first inbound message");
}
@connect() / @disconnect()¶
Alias semánticos de @on('connected') y @on('disconnected'). El método se ejecuta cuando la conexión de WhatsApp se abre o se cierra respectivamente.
@connect()
on_open() {
console.log("connected");
}
@disconnect()
on_close() {
console.log("disconnected");
}
@every(ms)¶
Instala un timer periódico. El intervalo comienza cuando se emite connected y se limpia en disconnected, por lo que el callback no se ejecuta mientras el cliente está offline.
Advertencia
Los callbacks de timer no reciben argumentos. Si necesitas acceso al cliente, captúralo vía this (el método está vinculado a la instancia del bot).
@pair()¶
Marca el método como callback de emparejamiento. Cuando baileys entrega un PIN o QR, todos los métodos @pair son invocados en paralelo vía Promise.all. Un argumento connect(callback?) — si se pasa — se ejecuta junto a ellos.
@pair()
async on_pin(code: string | Buffer) {
if (Buffer.isBuffer(code)) {
await writeFile("qr.png", code);
} else {
console.log("pair code:", code);
}
}
Dado que connect() ya no necesita un callback explícito cuando @pair está presente:
@from(source)¶
Filtra message:created por el autor del mensaje. La fuente es una de:
string— JID (5491112345678@s.whatsapp.net), LID (<digits>@lid) o número de teléfono plano (5491112345678).string[]— cualquiera de las entradas coincide (OR).(jid: string) => boolean— predicado personalizado sobremsg.from.
Los strings se normalizan la primera vez que el guard se ejecuta, usando el resolver interno wa._resolve_jid(uid). Los resultados se cachean en un Set del handler, por lo que las invocaciones subsecuentes son O(1).
@command("/ban")
@from(["5491111111111", "5492222222222"])
ban_user(msg: Message, chat: Chat, args: string[]) {
/* admin-only */
}
@from((jid) => jid.endsWith("@s.whatsapp.net"))
personal_only(msg: Message) {
/* ... */
}
Auto-registro: como con @guard, un método decorado solo con @from (sin @on) se autoregistra a message:created.
@command(pattern)¶
Atajo para un comando textual sobre message:created. Internamente aplica:
@on('message:created').- Un guard que coincide
patterncontramsg.caption: - patrón
string→startsWith. - patrón
RegExp→test. - Una transformación que reescribe los argumentos a
(msg, chat, args): - Para un patrón string,
argses el texto restante dividido por whitespace. - Para un
RegExp,argsesmatch.slice(1)(capture groups).
@command("/help")
show_help(msg: Message, chat: Chat, args: string[]) {
/* args = [] para "/help", ["topic"] para "/help topic" */
}
@command(/^\/echo\s+(.+)$/)
echo(msg: Message, chat: Chat, args: string[]) {
const [text] = args;
/* ... */
}
Forma del argumento
@command reescribe la signatura del handler de (msg, chat, wa) a (msg, chat, args). La instancia wa sigue accesible vía this.
@pipe(workflow, index)¶
Registra el método como un paso dentro de un workflow con nombre. Todos los pasos con el mismo nombre workflow se ejecutan secuencialmente en cada message:created, ordenados por index ascendente. Los pasos comparten los mismos argumentos (msg, chat, wa), por lo que las mutaciones sobre esos objetos se propagan a los pasos posteriores.
@pipe("inbound", 0)
async step_1(msg: Message) {
(msg as any).tags = ["fresh"];
}
@pipe("inbound", 1)
async step_2(msg: Message) {
(msg as any).tags.push("audited");
}
Contrato:
- Secuencial: cada paso es esperado antes del siguiente.
- Estado compartido: las mutaciones sobre
msg/chatson visibles aguas abajo. - No aplican guards / eventos —
@pipees autónomo. - Múltiples workflows coexisten; cada uno se ejecuta independientemente sobre
message:created.
No mezcles con @on en el mismo método
Un método decorado con @pipe está registrado únicamente como un paso. Añadir @on o @guard sobre el mismo método no tiene efecto sobre el workflow.
Reglas de apilado¶
Leyenda: ✅ componer · ⚠️ componible, lee la nota · ❌ no soportado.
| Con → / Base ↓ | @on | @guard | @once | @from | @command | @pipe | @every | @pair |
|---|---|---|---|---|---|---|---|---|
@on (apilable) | ✅ | ✅ | ✅ | ✅ | ⚠️ redundante | ❌ | ⚠️ | ❌ |
@guard (apilable) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ⚠️ | ❌ |
@once | ✅ | ✅ | — | ✅ | ✅ | ❌ | ❌ | ❌ |
@from (único) | ✅ | ✅ | ✅ | ❌ dos @from | ✅ | ❌ | ❌ | ❌ |
@command (único) | ⚠️ | ✅ | ✅ | ✅ | ❌ dos @command | ❌ | ❌ | ❌ |
@pipe | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ colisiona mismo index de paso | ❌ | ❌ |
@every | ⚠️ emite evento __every:* — no combines con eventos reales | ⚠️ guards se ejecutan sin mensaje | ❌ | ❌ | ❌ | ❌ | ❌ ms duplicados crean dos timers | ❌ |
@pair | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ múltiples métodos se ejecutan en paralelo |
Notas clave:
@command+@commandsobre el mismo método es inválido — el segundo patrón reemplaza la primera transformación; declara dos métodos en su lugar.@from+@fromsobre el mismo método: solo el último conjunto registrado se respeta en términos de resolución de fuente; para OR matching pasa un array a un único@from.@pipees terminal: un método marcado como paso de pipe no debe llevar ningún otro decorador.@every+@onregistra el método tanto al timer como a un evento — la invocación del timer no recibe args, lo que puede romper handlers que esperan(msg, chat, wa).@paires su propio canal (__pair); nunca combines con@on.
Semántica de ejecución¶
Despacho de listeners¶
Los listeners del mismo evento se ejecutan concurrentemente bajo el EventEmitter — el emitter subyacente llama a los listeners sincrónicamente sin esperarlos, por lo que dos handlers para message:created comienzan en paralelo.
Dentro de un handler único el flujo es secuencial:
- Todos los
guardsse esperan en orden de declaración (AND con corto-circuito). - Todos los
transformsse esperan en orden de declaración, produciendo la lista final de argumentos. - El cuerpo del handler se ejecuta con los argumentos transformados.
Timers¶
Los handlers de @every(ms) arrancan con el evento connected vía setInterval(run, ms) y se cancelan en disconnected. Un ciclo de reconexión, por lo tanto, los rearma desde cero.
Pairing¶
Los callbacks de @pair se recolectan en connect() e invocan en paralelo con Promise.all. Si el consumidor pasa un callback a connect(callback), este se ejecuta en paralelo junto a los callbacks basados en decoradores.
Workflows¶
Un grupo @pipe(workflow, _) se registra como un único listener sobre message:created. Cuando el evento se dispara, los pasos se ordenan por index y se esperan secuencialmente:
Como los argumentos son compartidos, las mutaciones sobre msg o chat son observables por pasos subsecuentes.
Avanzado: decoradores personalizados¶
La infraestructura expone una factory decorator<P>() para construir tus propios decoradores paramétricos sin tocar la capa de metadata directamente. El callback muta la entrada resuelta HandlerMeta: empuja eventos, guards, transforms o cambia once.
Signatura
function decorator<P extends unknown[]>(
callback: (
metadata: Record<string | symbol, unknown>,
handler: HandlerMeta,
params: P,
) => void,
): (...params: P) => MethodDecorator;
Ejemplo — @onlyType('image')
import { decorator } from "@arcaelas/whatsapp/decorators";
import type { Message } from "@arcaelas/whatsapp";
export const only_type = decorator<[type: Message["type"]]>(
(_meta, handler, [type]) => {
handler.guards.push((...args) => {
const msg = args[0] as Message;
return msg.type === type;
});
},
);
Uso:
La factory autoregistra a message:created en virtud del guard añadido sin un @on: comportamiento idéntico al de los @guard / @from incluidos.
Primitivas expuestas
Para casos más complejos (timers, workflows, nuevos canales de eventos) los siguientes son públicos: HANDLERS (symbol), HandlerMeta, BotSchema, WorkflowStep, register_workflow_step(). Ver src/lib/bot/decorator.ts para el contrato de schema completo.
Ver también¶
- Examples / Decorator bot — ejemplo ejecutable completo.
- References / Events — nombres y payloads de eventos.
- References / WhatsApp — cliente subyacente.