zap-wa
v1.0.0
Published
Simulador de WhatsApp para desarrollo y testing. API propia con protección anti-ban integrada.
Maintainers
Readme
zap-wa
Simulador de WhatsApp para desarrollo y testing. API propia (no un clon de Baileys) con protección anti-ban avanzada, prompts interactivos, flujos de conversación, webhooks y mucho más. Zero dependencies.
Características
| Categoría | Feature |
|---|---|
| Anti-ban | Typing simulado, rate limit, pausas humanas, jitter, lectura previa |
| Prompts | ask() con opciones numeradas, validación estilo Zod, reintentos |
| Flujos | ZapFlow — conversaciones multi-paso con ramificación dinámica |
| Cola | ZapQueue — envío secuencial automático (evita ráfagas) |
| Webhooks | Despacho de eventos por POST a URL configurada |
| Broadcast | Difusión masiva con delay y jitter entre envíos |
| Programación | sendAt() para mensajes diferidos con cancelScheduled() |
| Auto-respuesta | Mensaje automático fuera de horario comercial configurable |
| Media | downloadMedia() — descarga de adjuntos como Uint8Array |
| Metadata | setMetadata/getMetadata/clearMetadata por chat |
| Sesiones | Serialización JSON para persistir en DB (PostgreSQL, Redis, etc.) |
| Multi-instancia | Cientos de cuentas por proceso (I/O Bound, sin browser) |
| Cancelación | cancelPrompt() para abortar prompts desde fuera |
| Inactividad | globalInactivityTimeout — cancela prompts si el usuario no responde |
Instalación
npm install zap-waUso Rápido
import { ZapClient, phone, text } from 'zap-wa'
const client = new ZapClient({
antiBan: { enabled: true },
showQR: true
})
client.on('status', async (status) => {
if (status === 'connected') {
await client.sendText(phone('5491112345678'), '¡Hola desde zap-wa!')
}
})Anti-ban
El motor anti-ban protege tu cuenta simulando comportamientos humanos reales.
const client = new ZapClient({
antiBan: {
enabled: true,
simulateTyping: true,
typingSpeed: 'human', // 'slow' | 'human' | 'fast' | number (cps)
preDelay: { min: 300, max: 900 },
postDelay: { min: 500, max: 2000 },
maxMessagesPerMinute: 15,
jitter: true,
readBeforeReply: true,
}
})Pipeline de envío:
checkRateLimit → maybeRandomPause → preDelay → typing → sleep(proporcional) → paused → ENVIAR → postDelayEventos de debugging:
client.on('antiban.typing', (chatId, ms) => {})
client.on('antiban.delay', (chatId, ms, reason) => {})
client.on('antiban.ratelimit', (chatId, ms) => {})Estimar tiempo / Saltar anti-ban:
client.estimateSendTime({ type: 'text', body: 'tu mensaje' })
await client.sendText(jid, 'Urgente', { skipAntiBan: true })Prompts Interactivos (ask)
Envía preguntas y espera respuestas válidas con reintentos automáticos.
const depto = await client.ask(jid, {
question: '¿Qué departamento buscas?',
options: ['Ventas', 'Soporte', 'Facturación'],
retryMessage: '❌ Opción inválida.',
maxRetries: 3, // undefined = infinitos
validate: (text) => {
const idx = parseInt(text) - 1
if (idx >= 0 && idx < 3) return ['ventas', 'soporte', 'facturación'][idx]
throw new Error('Inválido')
}
})Cancelar un prompt desde fuera:
const promise = client.ask(jid, { question: 'Esperando...' })
client.cancelPrompt(jid, 'INACTIVITY') // → promise rechaza con error INACTIVITYFlujos de Conversación (ZapFlow)
Encadena múltiples preguntas con lógica de ramificación dinámica.
import { ZapFlow } from 'zap-wa'
const flujo = new ZapFlow()
.addStep('nombre', {
question: '¿Cuál es tu nombre?'
})
.addStep('edad', {
question: (res) => `Hola ${res.nombre}, ¿cuántos años tenés?`,
validate: (val) => {
const n = parseInt(val)
if (isNaN(n) || n < 1) throw new Error('Edad inválida')
return n
}
})
.addStep('area', {
question: '¿Qué área te interesa?',
options: ['Desarrollo', 'Diseño', 'Marketing']
})
.onStep((stepId, value, results) => {
console.log(`Paso ${stepId} completado: ${value}`)
})
const resultados = await client.runFlow(jid, flujo)
// { nombre: 'Juan', edad: 25, area: 'Desarrollo' }Cola de Mensajes (ZapQueue)
Cuando useQueue: true (default), los mensajes se procesan uno por uno respetando el anti-ban.
const client = new ZapClient({ useQueue: true })
// Estos 3 se encolan y se envían secuencialmente
const p1 = client.sendText(jid, 'Mensaje 1')
const p2 = client.sendText(jid, 'Mensaje 2')
const p3 = client.sendText(jid, 'Mensaje 3')
await Promise.all([p1, p2, p3])Webhooks
Despacha eventos a una URL externa por POST.
const client = new ZapClient({
webhook: {
url: 'https://mi-servidor.com/webhook',
events: ['message', 'status'],
headers: { 'Authorization': 'Bearer token' }
}
})Broadcast (Difusión Masiva)
const targets = [phone('111'), phone('222'), phone('333')]
const res = await client.broadcast(targets, text('¡Promo!'), {
delayBetween: 1000, // ms entre envíos
jitter: 0.3 // ±30% variación
})
console.log(res) // { total: 3, success: 3, failed: 0, errors: [] }Mensajes Programados (sendAt)
const taskId = await client.sendAt(
Date.now() + 60_000, // dentro de 1 minuto
jid,
text('¡Recordatorio!')
)
client.cancelScheduled(taskId) // cancelar si es necesarioAuto-respuesta fuera de horario
const client = new ZapClient({
autoResponse: {
enabled: true,
onlyDMs: true,
message: (msg) => `Hola ${msg.pushName || ''}! Estamos fuera de horario.`,
schedule: [
{ day: 1, periods: [{ start: '09:00', end: '18:00' }] }, // Lunes
{ day: 2, periods: [{ start: '09:00', end: '18:00' }] },
{ day: 3, periods: [{ start: '09:00', end: '18:00' }] },
{ day: 4, periods: [{ start: '09:00', end: '18:00' }] },
{ day: 5, periods: [{ start: '09:00', end: '18:00' }] },
]
}
})Metadata por Chat
Almacena estado persistente por chat (FSM, preferencias, etc.).
client.setMetadata(jid, 'step', 'payment')
client.getMetadata(jid, 'step') // → 'payment'
client.clearMetadata(jid)Inactividad Global
Cancela automáticamente prompts y flujos pendientes si el usuario no responde.
const client = new ZapClient({
globalInactivityTimeout: 120_000 // 2 minutos
})Persistencia de Sesión
import { serializeSession, deserializeSession } from 'zap-wa'
// Guardar
client.on('session.saved', (session) => {
const raw = serializeSession(session)
await db.sessions.save(session.id, raw)
})
// Cargar (evita pedir QR de nuevo)
const raw = await db.sessions.get(id)
const client = new ZapClient({ session: deserializeSession(raw) })QR como Buffer / API
import * as QRCode from 'qrcode'
client.on('qr', async (qrString) => {
const buffer = await QRCode.toBuffer(qrString)
// Servir por Express, guardar como PNG, etc.
})Multi-Instancia
zap-wa es I/O Bound. Sin browsers reales, escala a cientos de sesiones por proceso.
const sessions = await db.sessions.findAll()
for (const s of sessions) {
const bot = new ZapClient({ session: deserializeSession(s.data) })
bot.on('message', handleMessage)
}API Completa
Crear cliente
const client = new ZapClient(config?: ZapConfig)Propiedades
client.me // { id, phone, name } | null
client.status // 'idle' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
client.connected // boolean
client.session // ZapSession | null
client.antiBanEnabled // boolean
client.store // ZapStore (mensajes, chats, contactos, grupos)Envío de mensajes
await client.sendText(jid, 'texto', opts?)
await client.sendImage(jid, srcUrlOrBuffer, caption?, opts?)
await client.sendVoice(jid, srcUrlOrBuffer, duration?, opts?)
await client.sendLocation(jid, lat, lng, name?, opts?)
await client.react(jid, messageId, emoji)
await client.deleteMessage(jid, messageId, forEveryone?)
await client.send(jid, content, opts?)SendOptions
{
quote?: ZapMessage
ephemeral?: number
id?: string
mentions?: ZapID[]
skipAntiBan?: boolean
forceDelay?: number
forwarded?: boolean
}Prompts y Flujos
await client.ask(jid, options: PromptOptions)
await client.runFlow(jid, flow: ZapFlow)
client.cancelPrompt(jid, reason?)Programación y Broadcast
await client.sendAt(timestamp, jid, content)
client.cancelScheduled(taskId)
await client.broadcast(targets, content, opts?: BroadcastOptions)Media
await client.downloadMedia(msg) // → Uint8ArrayMetadata
client.setMetadata(jid, key, value)
client.getMetadata(jid, key)
client.clearMetadata(jid)Operaciones de chat
await client.markRead(chatId)
await client.archiveChat(chatId, true)
await client.pinChat(chatId, true)
await client.muteChat(chatId, 8 * 60 * 60 * 1000)
await client.deleteChat(chatId)
client.getChat(chatId)
client.listChats()Grupos
await client.createGroup('Nombre', [jid1, jid2])
await client.addMembers(grupoId, [jid3])
await client.removeMembers(grupoId, [jid2])
await client.promoteMembers(grupoId, [jid1])
await client.demoteMembers(grupoId, [jid1])
await client.leaveGroup(grupoId)
await client.renameGroup(grupoId, 'Nuevo nombre')
await client.describeGroup(grupoId, 'Descripción')
await client.setGroupAnnouncement(grupoId, true)
await client.getGroupInviteLink(grupoId)Contactos y Presencia
client.getContact(jid)
client.displayName(jid)
await client.blockContact(jid)
await client.unblockContact(jid)
await client.updateStatus('Disponible')
await client.setPresence(chatId, 'typing')Eventos
client.on('status', (status, info) => {})
client.on('qr', (qr, attempt) => {})
client.on('message', (msg) => {})
client.on('message.sent', (msg) => {})
client.on('message.update', (id, chatId, update) => {})
client.on('message.delete', (id, chatId, by) => {})
client.on('message.reaction', (reaction, msgId, from) => {})
client.on('chat.new', (chat) => {})
client.on('chat.update', (chatId, changes) => {})
client.on('chat.delete', (chatId) => {})
client.on('contact.update', (contact) => {})
client.on('group.new', (group) => {})
client.on('group.update', (groupId, changes) => {})
client.on('group.join', (groupId, members) => {})
client.on('group.leave', (groupId, members) => {})
client.on('group.promote', (groupId, members) => {})
client.on('group.demote', (groupId, members) => {})
client.on('presence', (chatId, userId, presence) => {})
client.on('session.saved', (session) => {})
client.on('antiban.typing', (chatId, ms) => {})
client.on('antiban.delay', (chatId, ms, reason) => {})
client.on('antiban.ratelimit', (chatId, ms) => {})Helpers de simulación (testing)
await client.simulate(jid, content)
await client.simulateQRScan('5491199887766')
await client.simulateIncomingTyping(jid, 2000)
await client.simulateDrop(true)
client.simulateContact(jid, 'Juan Pérez')
client.simulateGroup('Mi Grupo', [jid1, jid2])
client.trigger('message.reaction', reaction, 'msg123', jid)ID Helpers
import { phone, group, isGroup, isDM, rawNumber, sameUser } from 'zap-wa'
phone('5491199887766') // → '[email protected]'
group('123456789-9876543') // → '[email protected]'
isGroup('[email protected]') // → true
isDM('[email protected]') // → true
rawNumber('[email protected]') // → '54911'
sameUser(jid1, jid2) // → booleanContent Builders
import { text, image, video, audio, voice, doc, location, react, poll } from 'zap-wa'
text('Hola!', mentions?)
image('https://...', caption?)
video('https://...', caption?)
audio('https://...')
voice('https://...', durationSecs?)
doc('https://...', 'application/pdf', 'archivo.pdf')
location(-34.6, -58.3, 'Buenos Aires')
react('❤️', 'messageId')
poll('¿Cuándo?', ['Viernes', 'Sábado'], allowMultiple?)Estructura del Proyecto
src/
├── index.ts ← Exports públicos
├── types/ ← Tipos TypeScript
├── core/
│ ├── client.ts ← ZapClient (lógica principal)
│ ├── session.ts ← Gestión de sesión
│ ├── flow.ts ← ZapFlow (conversaciones multi-paso)
│ └── queue.ts ← ZapQueue (cola secuencial)
├── plugins/
│ └── antiban.ts ← Motor anti-ban
├── store/ ← Stores en memoria
└── utils/ ← Helpers, builders, generadoresLicencia MIT.
