npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arcaelas/whatsapp

v8.3.1

Published

A small box of tools, which are implemented in different factions of the library.

Readme

Arcaelas Insiders Arcaelas Insiders

@arcaelas/whatsapp

Cliente de WhatsApp para Node.js sobre baileys, con persistencia intercambiable.

TypeScript de punta a punta · Entidades con getters puros · Motores enchufables · DSL de decoradores


Contenido


Instalación

yarn add @arcaelas/whatsapp

Node 20 o superior. El paquete se distribuye en ESM y CJS.

Peers opcionales, solo si usás la función correspondiente:

| Paquete | Necesario para | | --- | --- | | @aws-sdk/client-s3 | S3Engine | | sharp o jimp | cuenta.picture(...) |

RedisEngine y SQLiteEngine no necesitan nada: reciben el cliente ya construido, así que servís vos el ioredis, better-sqlite3 o node:sqlite que prefieras.


Primeros pasos

import WhatsApp, { FileSystemEngine } from '@arcaelas/whatsapp';

const wa = new WhatsApp({
    engine: new FileSystemEngine('.sessions/5491112345678'),
    phone: 5491112345678,
});

wa.on('message:created', async (msg) => {
    if (!msg.me && msg.caption === 'ping') {
        await msg.text('pong 🏓');
    }
});

// Con `phone` llega un PIN (string); sin `phone`, el QR como Buffer PNG.
await wa.connect((auth) => {
    console.log(typeof auth === 'string' ? `PIN: ${auth}` : 'QR listo');
});

Cliente

new WhatsApp({ engine, phone?, method?, autoclean?, reconnect?, sync? })

| Opción | Descripción | | --- | --- | | engine | Motor de persistencia. Único obligatorio. | | phone | Teléfono de la cuenta. Habilita el emparejamiento por PIN; sin él la vinculación es siempre por QR. | | method | 'otp' (default) o 'qr'. Solo aplica cuando hay phone. | | autoclean | Al recibir loggedOut: true (default) vacía el motor; false borra solo las credenciales. | | reconnect | true (default) reintenta indefinidamente cada 60 s. Acepta false, un número de intentos o { max, interval }. | | sync | Descarga el historial completo al vincular (default true). |

Superficie

wa.engine                     // motor de persistencia
wa.Contact / wa.Chat / wa.Message   // entidades, publicadas al conectar
await wa.account()            // Account de la cuenta autenticada, o null sin usuario

await wa.connect(callback)    // callback recibe el PIN (string) o el QR (Buffer PNG)
await wa.disconnect({ silent?, destroy? })

wa.on(event, handler)         // devuelve la función para desuscribirse
wa.once(event, handler)
wa.off(event, handler)
wa.emit(event, ...args)

Las entidades y account() se publican dentro de connect, cuando el socket ya existe: antes de la primera conexión no están definidas. El estado interno (socket, credenciales, reintentos) vive en el closure de connect — no hay nada que hurgar en la instancia.

La cuenta: Account

await wa.account() devuelve un Account extends Contact: todos los getters de un contacto (name, phone, jid, lid, photo) más las operaciones que solo existen para uno mismo.

const cuenta = await wa.account();

await cuenta.rename('Ventas');            // nombre público
await cuenta.picture(buffer);             // foto de perfil (Buffer o URL)
await cuenta.picture(null);               // la elimina
await cuenta.content();                   // lee la bio
await cuenta.content('Atendemos 9-18h');  // la actualiza
await cuenta.online(true);                // presencia online/offline

const post = await cuenta.post({
    caption: '¡Estamos en vivo!',
    audience: [contacto, 5491112345678, '[email protected]', '999@lid'],
});

audience no es opcional: WhatsApp no entrega el estado a nadie fuera de esa lista, y acepta instancias de Contact, JIDs, LIDs o teléfonos. Con buffer se publica imagen o video —el tipo se deduce de la firma del binario— y caption queda como pie. La presencia arranca en offline (markOnlineOnConnect: false): online() es el único interruptor.


Entidades

Cada entidad expone getters puros sobre su documento y métodos que actúan contra WhatsApp. wa.Contact, wa.Chat y wa.Message ya vienen ligados a la sesión; las clases sueltas se importan del paquete cuando querés instanceof.

Contact

const person = await wa.Contact.get('5491112345678');   // teléfono, JID o LID
const page = await wa.Contact.list(0, 50);

if (person) {
    person.name      // agenda → nombre público → nombre verificado → teléfono
    person.phone     // solo del JID PN, nunca del LID; null si no es determinable
    person.jid       // '[email protected]' | null
    person.lid       // '123456789@lid' | null
    person.photo     // URL de la foto | null
    await person.chat();
}

get lee del motor y, si el contacto no está persistido, lo descubre por red y lo materializa.

Chat

const chat = await wa.Chat.get('5491112345678');
const chats = await wa.Chat.list(0, 50);

if (chat) {
    chat.id        // teléfono en contactos; id crudo en grupos y LIDs
    chat.name
    chat.type      // 'contact' | 'group'
    chat.archived  // boolean
    chat.pinned    // boolean
    chat.muted     // fecha ISO UTC hasta la que está silenciado, o null
    chat.count     // mensajes sin leer

    await chat.content();    // descripción del grupo, o bio del contacto en un 1:1
    await chat.messages(0, 50);
    await chat.members(0, 50);
    await chat.typing(true);
    await chat.recording(true);
    await chat.archive(true);
    await chat.pin(true);          // false si ya hay 3 fijados: WhatsApp descarta el cuarto
    await chat.mute('2026-08-01T10:00:00Z');   // o false para desactivar
    await chat.seen();             // marca el chat completo como leído
    await chat.clear();            // vacía los mensajes, conserva el chat
    await chat.delete();           // elimina el chat (sale del grupo si aplica)
}

Message

import { Poll, Image } from '@arcaelas/whatsapp';

const page = await wa.Message.list(cid, 0, 50);   // del más reciente al más antiguo
const msg = await wa.Message.get(cid, mid);

Propiedades

| | | | --- | --- | | id cid mid | identificadores del mensaje, su chat y el mensaje citado | | from me | JID del autor y si soy yo | | type | text image video audio sticker document location poll vcard event | | mime | text/plain, text/json en poll/location/vcard/event, el real en media | | caption | texto del mensaje o pie del media | | status | 'error' 'pending' 'sent' 'delivered' 'read' 'played' | | read starred forwarded edited once | banderas del mensaje | | reason | motivo del rechazo cuando status es 'error' (restricted, invalid-session, o el código del servidor), si no null | | business | nombre del negocio verificado que firma el mensaje, o null | | created_at expires_at | fechas en ISO UTC (expires_at solo en mensajes temporales) |

Métodos

await msg.author();      // Contact
await msg.chat();        // Chat
await msg.message();     // el mensaje citado, si hay `mid`
await msg.content();     // Buffer
await msg.stream();      // Readable
await msg.reactions();   // [{ emoji, count }]

await msg.react('❤️');   // emoji vacío la retira
await msg.star(true);
await msg.seen();
await msg.edit('texto corregido');       // texto, imagen o video propios
await msg.forward('584121234567');       // CID, Chat o Contact destino
await msg.delete();                      // solo en mi dispositivo
await msg.delete(true);                  // para todos

await msg.text('respuesta');             // responder citando este mensaje
await msg.image(buffer, { caption: '…' });

Envío (los mismos nueve por tipo, en instancia para responder y en el cliente para iniciar):

await wa.Message.text(cid, 'hola', { once: true });
await wa.Message.image(cid, buffer, { caption: 'mirá' });
await wa.Message.video(cid, buffer);
await wa.Message.audio(cid, buffer, { ptt: true });
await wa.Message.location(cid, { lat: 8.3, lng: -62.7 });
await wa.Message.poll(cid, { content: '¿Qué pedimos?', options: [{ content: 'Pizza' }, { content: 'Sushi' }] });
await wa.Message.document(cid, buffer, { file_name: 'contrato.pdf' });
await wa.Message.vcard(cid, [{ name: 'Ana', phone: '+584121234567' }]);
await wa.Message.event(cid, { name: 'Demo', start: new Date() });

Subclases — el tipo decide la instancia, así que instanceof alcanza:

if (msg instanceof Image) console.log(msg.width, msg.height, msg.size, await msg.thumb());
if (msg instanceof Poll)  console.log(msg.options, msg.multiple, await msg.votes());

| Clase | Agrega | | --- | --- | | Text | preview(){ link, name, content, thumb } del enlace citado | | Image | width height size thumb() | | Video | width height size duration thumb() | | Audio | ptt duration size waveform (0-100, lista para pintar) | | Sticker | width height size animated | | Document | name pages size | | Location | lat lng live link (Google Maps) | | Poll | options multiple votes() select(i) | | VCard | contacts | | Event | name start end canceled place link |

Feed

Publicaciones de estado (status@broadcast). Extiende Message, así que hereda author(), content(), stream() y caption.

wa.on('feed:created', async (post) => {
    console.log((await post.author()).name, post.caption, post.expires_at);
    await post.view();     // envía el read receipt
});

Lo que un estado no admite (react, star, edit, forward, delete, responder) lanza ERR_FEED_UNSUPPORTED.


Eventos

const off = wa.on('message:created', (msg, chat) => { … });
off();   // desuscribe

| Evento | Argumentos | | --- | --- | | connected disconnected | (wa) | | contact:created contact:updated | (contact, chat, wa) | | chat:created chat:deleted | (chat, wa) | | chat:pinned chat:unpinned | (chat, wa) | | chat:archived chat:unarchived | (chat, wa) | | chat:muted chat:unmuted | (chat, wa) | | message:created message:updated message:deleted | (message, chat, wa) | | message:starred message:unstarred message:forwarded message:seen | (message, chat, wa) | | message:reacted | (message, chat, emoji, wa) | | feed:created feed:updated feed:deleted | (feed, wa) |


Motores de persistencia

Un motor es un almacén key/value de strings bajo rutas jerárquicas. El contrato completo:

interface Engine {
    get(path: string): Promise<string | null>;
    set(path: string, value: string, score?: number): Promise<void>;
    unset(path: string): Promise<boolean>;                          // borra el sub-árbol
    list(path: string, offset?: number, limit?: number): Promise<string[]>;   // hijos directos, score DESC
    count(path: string): Promise<number>;
    clear(): Promise<void>;

    get_buffer?(path: string): Promise<Buffer | null>;              // opcional: binarios sin base64
    set_buffer?(path: string, data: Buffer, score?: number): Promise<void>;
}

score fija el orden de list (los mensajes pasan su created_at), de modo que reescribir historia antigua no altera la cronología. Los binarios son opcionales: un motor que no los implemente sigue siendo válido y la librería cae al documento serializado.

import Database from 'better-sqlite3';
import IORedis from 'ioredis';
import { S3Client } from '@aws-sdk/client-s3';
import { FileSystemEngine, SQLiteEngine, RedisEngine, S3Engine } from '@arcaelas/whatsapp';

new FileSystemEngine('.sessions/5491112345678');
new SQLiteEngine(new Database('.sessions/5491112345678.db'));
new RedisEngine(new IORedis(), 'wa:5491112345678');
new S3Engine({ s3: new S3Client({}), bucket: 'sesiones', basedir: 'wa/5491112345678' });

SQLiteEngine es el más eficiente de los integrados. Sobre un chat real de 55.146 mensajes, frente al filesystem: 220 MB → 64 MB en disco, primer list 115 ms → 0,6 ms, y dos archivos en total en lugar de ~110.000 inodes.

Cada cliente necesita su propio motor: nunca compartas una instancia entre dos cuentas.


Bots con decoradores

import { FileSystemEngine, type Chat, type Message } from '@arcaelas/whatsapp';
import { WhatsAppBot, connect, command, from, every } from '@arcaelas/whatsapp/decorators';

class Bot extends WhatsAppBot {
    @connect()
    async on_ready() {
        console.log('conectado');
    }

    @command('/precio')
    async price(msg: Message, chat: Chat, args: string[]) {
        await msg.text(`Consultando ${args[0] ?? 'el catálogo'}…`);
    }

    @from('5491112345678')
    async only_admin(msg: Message) {
        await msg.react('👑');
    }

    @every(3_600_000)
    async hourly() {
        console.log('cada hora, mientras esté conectado');
    }
}

const bot = new Bot({ engine: new FileSystemEngine('.sessions/bot'), phone: 5491112345678 });
await bot.connect((auth) => console.log(auth));

Decoradores disponibles: @on, @once, @connect, @disconnect, @command, @guard, @from, @pipe, @every, @delay, @pair, y @Bot como decorador de clase.


Recetas

Descargar el media de cada imagen recibida

import { writeFile } from 'node:fs/promises';
import { Image } from '@arcaelas/whatsapp';

wa.on('message:created', async (msg) => {
    if (msg instanceof Image) {
        await writeFile(`${msg.id}.jpg`, await msg.content());
    }
});

Responder solo en grupos

wa.on('message:created', async (msg, chat) => {
    if (chat.type === 'group' && !msg.me) {
        await msg.react('👀');
    }
});

Reconectar con límite y cerrar en silencio

const wa = new WhatsApp({ engine, phone: 5491112345678, reconnect: { max: 5, interval: 30 } });
await wa.disconnect({ silent: true });   // no emite `disconnected`

Licencia

ISC — © 2026 Miguel Alejandro / Arcaelas Insiders.