Toolkit para implementar conexiones no oficiales a WhatsApp usando la librería Baileys...
Habilita el desarrollo completo de integraciones con WhatsApp usando Baileys, una librería TypeScript/JavaScript basada en WebSockets que no requiere navegadores ni Selenium.
Activa esta skill cuando el usuario mencione:
Antes de usar esta skill, asegurar que el entorno tiene:
| Requisito | Versión Mínima | Verificación |
|---|---|---|
| Node.js | 16.x o superior | node --version |
| npm/yarn | npm 7+ o yarn 1.22+ | npm --version o yarn --version |
| TypeScript | 4.5+ (opcional) | tsc --version |
La forma más rápida de empezar sin errores comunes:
# 1. Crear proyecto
mkdir mi-bot-whatsapp && cd mi-bot-whatsapp
npm init -y
# 2. Instalar dependencias (versiones correctas)
npm install @whiskeysockets/baileys pino qrcode-terminal
# 3. Copiar ejemplo básico
curl -o index.js https://raw.githubusercontent.com/.../basic-bot-example.js
# O copiar desde: .github/skills/whatsapp-baileys/scripts/basic-bot-example.js
# 4. Crear carpetas para media (opcional)
mkdir -p public/video public/images
# 5. Ejecutar
node index.js
Este ejemplo incluye todas las correcciones para evitar errores 405, 440, 408 y problemas de QR.
Ver código completo: basic-bot-example.js
Para gestionar múltiples cuentas de WhatsApp simultáneamente con dashboard HTML:
# 1. Copiar scripts al proyecto (qrcode.min.js YA INCLUIDO)
cp .github/skills/whatsapp-baileys/scripts/multi-session.js ./
cp -r .github/skills/whatsapp-baileys/scripts/multi-session-dashboard ./
# 2. Instalar dependencias
npm install @whiskeysockets/baileys pino qrcode-terminal express
# 3. Ejecutar servidor multi-sesión
node multi-session.js
# 4. Abrir dashboard en navegador
open http://localhost:3000
Incluye:
Ver guía completa: multi-session-guide.md
⚠️ IMPORTANTE:
qrcode.min.js está en scripts/multi-session-dashboard/ y NO debe descargarse de internet⚠️ Errores comunes evitados:
new QRCode(div, {...}), no QRCode.toCanvas()<div>, no <canvas>Al crear el socket de WhatsApp, el campo browser define cómo aparece el dispositivo en la app móvil:
const sock = makeWASocket({
browser: ['Nombre App', 'Navegador', 'Versión']
})
Navegadores reconocidos que muestran logo en WhatsApp:
| Nombre (2do argumento) | Logo que muestra |
|---|---|
'Chrome' |
Logo de Chrome |
'Firefox' |
Logo de Firefox |
'Safari' |
Logo de Safari |
'Edge' |
Logo de Edge |
'Opera' |
Logo de Opera |
Ejemplos de uso:
// Mostrar logo de Chrome
browser: ['Mi Bot', 'Chrome', '1.0.0']
// Mostrar logo de Safari
browser: ['Mi Bot', 'Safari', '1.0.0']
// Mostrar logo de Firefox
browser: ['Mi Bot', 'Firefox', '1.0.0']
También puedes usar los helpers de Baileys:
import { Browsers } from '@whiskeysockets/baileys'
browser: Browsers.ubuntu('Mi App') // Logo de Ubuntu
browser: Browsers.macOS('Mi App') // Logo de macOS/Apple
browser: Browsers.windows('Mi App') // Logo de Windows
Importante: Si usas un nombre no reconocido, WhatsApp mostrará un icono genérico en lugar del logo del navegador.
Para descargar y guardar localmente todos los archivos multimedia recibidos (imágenes, videos, audios, documentos):
# 1. Copiar script al proyecto
cp .github/skills/whatsapp-baileys/scripts/multimedia-downloader.js ./
# 2. Instalar dependencias
npm install @whiskeysockets/baileys pino qrcode-terminal
# 3. Ejecutar bot de descargas
node multimedia-downloader.js
Características del script:
Tipos de multimedia soportados:
| Tipo | Carpeta Destino | Formatos |
|---|---|---|
| Imágenes | downloads/images/ |
JPG, PNG, GIF, WEBP |
| Videos | downloads/videos/ |
MP4, 3GP, MOV |
| Audios | downloads/audios/ |
MP3, OGG, AAC, AMR |
| Documentos | downloads/documents/ |
PDF, DOC, XLS, etc. |
| Stickers | downloads/stickers/ |
WEBP |
Ejemplo de uso en código:
import { downloadMediaMessage } from '@whiskeysockets/baileys';
import fs from 'fs';
// En el evento messages.upsert
sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
// Detectar si es multimedia
const hasImage = msg.message?.imageMessage;
const hasVideo = msg.message?.videoMessage;
const hasAudio = msg.message?.audioMessage;
const hasDocument = msg.message?.documentMessage;
if (hasImage || hasVideo || hasAudio || hasDocument) {
try {
// Descargar como Buffer
const buffer = await downloadMediaMessage(
msg,
'buffer',
{}
);
// Obtener metadata
const mediaMsg = msg.message.imageMessage ||
msg.message.videoMessage ||
msg.message.audioMessage ||
msg.message.documentMessage;
const mimetype = mediaMsg.mimetype;
const fileSize = mediaMsg.fileLength;
const fileName = mediaMsg.fileName || `${Date.now()}`;
// Guardar archivo
const filePath = `./downloads/${fileName}`;
fs.writeFileSync(filePath, buffer);
console.log(`✅ Archivo guardado: ${filePath}`);
console.log(` Tipo: ${mimetype}`);
console.log(` Tamaño: ${(fileSize / 1024).toFixed(2)} KB`);
// Responder confirmación
await sock.sendMessage(msg.key.remoteJid, {
text: `✅ Archivo descargado: ${fileName}\nTamaño: ${(fileSize / 1024).toFixed(2)} KB`
});
} catch (error) {
console.error('❌ Error al descargar:', error);
}
}
}
});
Configuración avanzada:
const CONFIG = {
// Tamaño máximo (50 MB)
MAX_DOWNLOAD_SIZE: 50 * 1024 * 1024,
// Carpetas personalizadas
DOWNLOAD_FOLDERS: {
image: './media/imagenes',
video: './media/videos',
audio: './media/audios',
document: './media/documentos'
},
// Respuesta automática
AUTO_REPLY: true,
// Log de metadata
LOG_METADATA: true
};
Metadata disponible:
const metadata = {
mimetype: 'image/jpeg',
fileLength: 245678, // bytes
fileName: 'foto.jpg',
caption: 'Mi foto',
width: 1920, // solo imágenes/videos
height: 1080,
duration: 30, // solo videos/audios (segundos)
pageCount: 5, // solo documentos PDF
timestamp: 1735689600000
};
Extensiones detectadas automáticamente:
El script incluye mapeo de 30+ tipos MIME a extensiones:
Ver implementación completa: multimedia-downloader.js
# Crear directorio del proyecto
mkdir mi-whatsapp-bot && cd mi-whatsapp-bot
# Inicializar proyecto Node.js
npm init -y
# Instalar Baileys
npm install @whiskeysockets/baileys
# Instalar dependencias recomendadas
npm install @hapi/boom pino qrcode-terminal
# Instalar TypeScript (opcional)
npm install -D typescript @types/node ts-node
Usar el template de conexión básica como punto de partida:
Usar el template de bot completo:
processCommand()Consultar esquemas de base de datos para:
useCustomAuthState() para persistenciaVer guía de deployment para:
⚠️ Configuración Crítica: Para evitar errores de conexión, siempre incluir estas configuraciones:
import { fetchLatestBaileysVersion } from '@whiskeysockets/baileys'
const { version } = await fetchLatestBaileysVersion() // CRÍTICO
const sock = makeWASocket({
version, // ← Incluir versión actualizada
auth: state,
// Evitar errores 405 y timeouts
syncFullHistory: false,
markOnlineOnConnect: false,
defaultQueryTimeoutMs: undefined,
connectTimeoutMs: 60000,
keepAliveIntervalMs: 30000
})
| Método | Cuándo Usar | Referencia |
|---|---|---|
| QR en Terminal | Desarrollo local rápido | Conexión QR |
| QR en Web | Interfaces web, dashboards | Conexión Web QR |
| Código de Emparejamiento | Onboarding sin QR visual | Pairing Code |
| Multi-dispositivo | Conectar múltiples clientes | Config avanzada |
Todos los tipos de mensajes están soportados:
Ver guía completa de mensajes.
Para implementar una API sólida, es necesario manejar tres escenarios de envío de multimedia:
| Escenario | Método | Endpoint | Payload | Descripción |
|---|---|---|---|---|
| URL Remota | POST |
/api/send/url |
{ to, url, caption } |
Envía contenido desde un link externo. El backend deduce si es imagen, video, etc. |
| URI Local | POST |
/api/send/local |
{ to, fileName, caption } |
Envía un archivo que ya reside en el servidor. Solo requiere el nombre del archivo. |
| Carga (Upload) | POST |
/api/send/upload |
multipart/form-data |
Sube un archivo (file, to, caption) y lo envía inmediatamente. |
Ejemplo de implementación de los escenarios:
// ESCENARIO 1: URL (El backend deduce el tipo)
app.post('/api/send/url', async (req, res) => {
const { to, url, caption } = req.body;
const type = detectType(url); // Helper para detectar image, video, etc.
await sock.sendMessage(to, { [type]: { url }, caption });
});
// ESCENARIO 2: URI Local (Solo nombre de archivo)
app.post('/api/send/local', async (req, res) => {
const { to, fileName, caption } = req.body;
const filePath = path.join(__dirname, 'public/media', fileName);
const type = detectType(fileName);
await sock.sendMessage(to, { [type]: { url: filePath }, caption });
});
// ESCENARIO 3: Upload (Subir y enviar)
app.post('/api/send/upload', upload.single('file'), async (req, res) => {
const { to, caption } = req.body;
const file = req.file;
const type = detectType(file.mimetype);
await sock.sendMessage(to, {
[type]: { url: file.path },
caption,
mimetype: file.mimetype
});
});
Ver bot completo con estos endpoints: multimedia-api.js
| Evento | Propósito | Cuándo Escuchar |
|---|---|---|
connection.update |
Estado de conexión | Siempre (requerido) |
creds.update |
Actualización de credenciales | Siempre (requerido) |
messages.upsert |
Nuevos mensajes recibidos | Para procesar mensajes |
messages.update |
Ediciones, reacciones, votos | Para updates de mensajes |
groups.update |
Cambios en grupos | Si trabajas con grupos |
presence.update |
Estado online/typing | Para features de presencia |
call |
Llamadas entrantes | Para rechazar/manejar llamadas |
Ver lista completa de eventos.
| Método | Propósito | Ejemplo |
|---|---|---|
profilePictureUrl(jid, type) |
Foto de perfil | await sock.profilePictureUrl(jid, 'image') |
fetchStatus(jid) |
Estado/About del usuario | await sock.fetchStatus(jid) |
onWhatsApp(number) |
Verificar si está en WA | await sock.onWhatsApp('5491234567890') |
getBusinessProfile(jid) |
Perfil de negocio | await sock.getBusinessProfile(jid) |
groupMetadata(groupJid) |
Información completa grupo | await sock.groupMetadata(groupJid) |
// Obtener foto de perfil en alta resolución
const profileUrl = await sock.profilePictureUrl(jid, 'image') // 'image' o 'preview'
console.log('Foto:', profileUrl)
// Verificar estado/about
const status = await sock.fetchStatus(jid)
console.log('Estado:', status)
// Verificar si un número está en WhatsApp
const [result] = await sock.onWhatsApp('5491234567890@s.whatsapp.net')
console.log('Existe:', result.exists)
console.log('JID:', result.jid)
Permite excluir números específicos del procesamiento del bot.
Implementación Técnica:
messages.upsert, en la primera línea del bucle for que itera los mensajes.msg.key.remoteJid.remoteJid contra la fuente de datos definida (Variable en memoria, JSON o Base de Datos).continue para ignorar silenciosamente el mensaje y pasar al siguiente.Ejemplo de flujo de validación:
sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
// 1. Obtener número
const remoteJid = msg.key.remoteJid;
// 2. Validar contra blacklist (Función agnóstica a la fuente de datos)
if (isBlacklisted(remoteJid)) {
continue; // Ignorar mensaje
}
// 3. Procesar mensaje normalmente...
}
});
Cuando un usuario responde a un mensaje anterior, WhatsApp incluye información del mensaje original en contextInfo.quotedMessage. Esta funcionalidad es esencial para bots que necesitan procesar respuestas contextuales.
// Estructura de un mensaje que ES UNA RESPUESTA a otro mensaje
{
key: {
remoteJid: '5491123456789@s.whatsapp.net',
fromMe: false,
id: 'ABCD1234567890'
},
message: {
extendedTextMessage: {
text: 'Esta es mi respuesta',
contextInfo: {
stanzaId: 'ID_DEL_MENSAJE_CITADO', // ID único del mensaje original
participant: '5491198765432@s.whatsapp.net', // Autor del mensaje citado (en grupos)
quotedMessage: {
// Aquí está el contenido del mensaje original citado
// La estructura varía según el tipo de mensaje
}
}
}
}
}
contextInfo| Campo | Tipo | Descripción |
|---|---|---|
stanzaId |
string |
ID único del mensaje citado (útil para referencias) |
participant |
string |
JID del autor del mensaje citado (presente en grupos) |
quotedMessage |
object |
Contenido completo del mensaje citado (estructura variable) |
remoteJid |
string |
JID del chat donde está el mensaje citado (opcional) |
mentionedJid |
array |
Lista de JIDs mencionados en el mensaje (si aplica) |
quotedMessage según TipocontextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
conversation: 'Este es el texto del mensaje original'
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
extendedTextMessage: {
text: 'Texto con @mención o enlace https://ejemplo.com',
matchedText: 'https://ejemplo.com',
canonicalUrl: 'https://ejemplo.com',
description: 'Descripción del enlace',
title: 'Título del enlace',
contextInfo: {
mentionedJid: ['5491198765432@s.whatsapp.net']
}
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
imageMessage: {
url: 'https://mmg.whatsapp.net/...', // URL para descarga (temporal)
mimetype: 'image/jpeg',
caption: 'Pie de foto opcional',
fileSha256: Buffer, // Hash SHA256 del archivo
fileLength: 245678, // Tamaño en bytes
height: 1080,
width: 1920,
mediaKey: Buffer, // Clave para descifrar
fileEncSha256: Buffer,
directPath: '/v/t62.1234-24/...',
mediaKeyTimestamp: 1706832000,
jpegThumbnail: Buffer // Miniatura en base64
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
videoMessage: {
url: 'https://mmg.whatsapp.net/...',
mimetype: 'video/mp4',
caption: 'Descripción del video',
fileSha256: Buffer,
fileLength: 15678900, // Tamaño en bytes
seconds: 45, // Duración en segundos
height: 720,
width: 1280,
mediaKey: Buffer,
fileEncSha256: Buffer,
directPath: '/v/t62.1234-24/...',
mediaKeyTimestamp: 1706832000,
jpegThumbnail: Buffer, // Miniatura del video
gifPlayback: false // true si es GIF
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
audioMessage: {
url: 'https://mmg.whatsapp.net/...',
mimetype: 'audio/ogg; codecs=opus', // Notas de voz
// mimetype: 'audio/mpeg', // Archivos de audio
fileSha256: Buffer,
fileLength: 34567,
seconds: 15, // Duración en segundos
ptt: true, // true = nota de voz, false = archivo
mediaKey: Buffer,
fileEncSha256: Buffer,
directPath: '/v/t62.1234-24/...',
mediaKeyTimestamp: 1706832000,
waveform: Buffer // Forma de onda (solo notas de voz)
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
documentMessage: {
url: 'https://mmg.whatsapp.net/...',
mimetype: 'application/pdf', // O cualquier tipo MIME
title: 'documento.pdf', // Nombre mostrado
fileSha256: Buffer,
fileLength: 1234567,
pageCount: 10, // Solo para PDFs
mediaKey: Buffer,
fileName: 'documento.pdf', // Nombre real del archivo
fileEncSha256: Buffer,
directPath: '/v/t62.1234-24/...',
mediaKeyTimestamp: 1706832000,
jpegThumbnail: Buffer // Miniatura (si aplica)
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
locationMessage: {
degreesLatitude: -34.603722,
degreesLongitude: -58.381592,
name: 'Obelisco de Buenos Aires', // Nombre del lugar (opcional)
address: 'Av. 9 de Julio, Buenos Aires', // Dirección (opcional)
url: 'https://foursquare.com/v/...', // URL del lugar (opcional)
jpegThumbnail: Buffer // Miniatura del mapa
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
liveLocationMessage: {
degreesLatitude: -34.603722,
degreesLongitude: -58.381592,
accuracyInMeters: 15,
speedInMps: 2.5, // Velocidad en m/s
degreesClockwiseFromMagneticNorth: 180, // Dirección
caption: 'Compartiendo ubicación',
sequenceNumber: 1,
timeOffset: 300, // Tiempo restante en segundos
jpegThumbnail: Buffer
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
contactMessage: {
displayName: 'Juan Pérez',
vcard: 'BEGIN:VCARD\nVERSION:3.0\nN:Pérez;Juan;;;\nFN:Juan Pérez\nTEL;type=CELL:+5491123456789\nEND:VCARD'
}
}
}
// Múltiples contactos
contextInfo: {
quotedMessage: {
contactsArrayMessage: {
displayName: '2 contactos',
contacts: [
{ displayName: 'Juan', vcard: '...' },
{ displayName: 'María', vcard: '...' }
]
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
stickerMessage: {
url: 'https://mmg.whatsapp.net/...',
mimetype: 'image/webp',
fileSha256: Buffer,
fileLength: 23456,
height: 512,
width: 512,
mediaKey: Buffer,
fileEncSha256: Buffer,
directPath: '/v/t62.1234-24/...',
mediaKeyTimestamp: 1706832000,
isAnimated: false, // true si es sticker animado
pngThumbnail: Buffer
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
pollCreationMessage: {
name: '¿Cuál prefieres?', // Pregunta de la encuesta
options: [
{ optionName: 'Opción A' },
{ optionName: 'Opción B' },
{ optionName: 'Opción C' }
],
selectableOptionsCount: 1 // Máximo de opciones seleccionables
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
productMessage: {
product: {
productId: 'PROD_123',
title: 'Producto Ejemplo',
description: 'Descripción del producto',
currencyCode: 'ARS',
priceAmount1000: 150000, // Precio * 1000 (150.00)
retailerId: 'SKU_001',
productImageCount: 3
},
businessOwnerJid: '5491123456789@s.whatsapp.net'
}
}
}
// Respuesta a botones
contextInfo: {
quotedMessage: {
buttonsResponseMessage: {
selectedButtonId: 'btn_1',
selectedDisplayText: 'Opción seleccionada',
contextInfo: { /* ... */ }
}
}
}
// Respuesta a lista
contextInfo: {
quotedMessage: {
listResponseMessage: {
title: 'Título seleccionado',
listType: 1,
singleSelectReply: {
selectedRowId: 'row_1'
}
}
}
}
contextInfo: {
stanzaId: 'MSG_ID_123',
participant: '5491123456789@s.whatsapp.net',
quotedMessage: {
viewOnceMessage: {
message: {
imageMessage: { /* estructura de imagen */ }
// O videoMessage, etc.
}
}
}
}
// View Once V2 (versión más reciente)
contextInfo: {
quotedMessage: {
viewOnceMessageV2: {
message: {
imageMessage: { /* ... */ }
}
}
}
}
import { downloadMediaMessage, getContentType } from '@whiskeysockets/baileys';
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return;
for (const msg of messages) {
if (!msg.message || msg.key.fromMe) continue;
const content = msg.message;
const messageType = getContentType(content);
// Obtener contextInfo (puede estar en diferentes lugares)
const contextInfo =
content.extendedTextMessage?.contextInfo ||
content.imageMessage?.contextInfo ||
content.videoMessage?.contextInfo ||
content.audioMessage?.contextInfo ||
content.documentMessage?.contextInfo ||
content.stickerMessage?.contextInfo ||
content.locationMessage?.contextInfo ||
content.contactMessage?.contextInfo;
// Verificar si es una respuesta (tiene mensaje citado)
if (contextInfo?.quotedMessage) {
const quotedMsg = contextInfo.quotedMessage;
const quotedId = contextInfo.stanzaId;
const quotedAuthor = contextInfo.participant || msg.key.remoteJid;
// Detectar tipo del mensaje citado
const quotedType = getContentType(quotedMsg);
console.log('═══════════════════════════════════════');
console.log('📩 Mensaje recibido ES UNA RESPUESTA');
console.log('═══════════════════════════════════════');
console.log('ID mensaje citado:', quotedId);
console.log('Autor original:', quotedAuthor);
console.log('Tipo citado:', quotedType);
// Extraer contenido según el tipo citado
let quotedContent = null;
switch (quotedType) {
case 'conversation':
quotedContent = quotedMsg.conversation;
console.log('Texto citado:', quotedContent);
break;
case 'extendedTextMessage':
quotedContent = quotedMsg.extendedTextMessage.text;
console.log('Texto citado:', quotedContent);
break;
case 'imageMessage':
quotedContent = {
caption: quotedMsg.imageMessage.caption || '',
mimetype: quotedMsg.imageMessage.mimetype,
size: quotedMsg.imageMessage.fileLength
};
console.log('Imagen citada:', quotedContent);
break;
case 'videoMessage':
quotedContent = {
caption: quotedMsg.videoMessage.caption || '',
mimetype: quotedMsg.videoMessage.mimetype,
duration: quotedMsg.videoMessage.seconds,
size: quotedMsg.videoMessage.fileLength
};
console.log('Video citado:', quotedContent);
break;
case 'audioMessage':
quotedContent = {
mimetype: quotedMsg.audioMessage.mimetype,
duration: quotedMsg.audioMessage.seconds,
isVoiceNote: quotedMsg.audioMessage.ptt || false
};
console.log('Audio citado:', quotedContent);
break;
case 'documentMessage':
quotedContent = {
fileName: quotedMsg.documentMessage.fileName,
mimetype: quotedMsg.documentMessage.mimetype,
size: quotedMsg.documentMessage.fileLength,
pageCount: quotedMsg.documentMessage.pageCount
};
console.log('Documento citado:', quotedContent);
break;
case 'locationMessage':
quotedContent = {
latitude: quotedMsg.locationMessage.degreesLatitude,
longitude: quotedMsg.locationMessage.degreesLongitude,
name: quotedMsg.locationMessage.name,
address: quotedMsg.locationMessage.address
};
console.log('Ubicación citada:', quotedContent);
break;
case 'contactMessage':
quotedContent = {
displayName: quotedMsg.contactMessage.displayName,
vcard: quotedMsg.contactMessage.vcard
};
console.log('Contacto citado:', quotedContent);
break;
case 'stickerMessage':
quotedContent = {
mimetype: quotedMsg.stickerMessage.mimetype,
isAnimated: quotedMsg.stickerMessage.isAnimated
};
console.log('Sticker citado:', quotedContent);
break;
case 'pollCreationMessage':
quotedContent = {
question: quotedMsg.pollCreationMessage.name,
options: quotedMsg.pollCreationMessage.options.map(o => o.optionName)
};
console.log('Encuesta citada:', quotedContent);
break;
default:
console.log('Tipo de mensaje citado no manejado:', quotedType);
quotedContent = quotedMsg;
}
// Ejemplo: Responder confirmando la cita
const textoRespuesta = content.extendedTextMessage?.text ||
content.conversation ||
'[mensaje multimedia]';
await sock.sendMessage(msg.key.remoteJid, {
text: `✅ Recibí tu respuesta: "${textoRespuesta}"\n\n` +
`📎 Citaste un mensaje de tipo: ${quotedType}\n` +
`🆔 ID original: ${quotedId}`
});
}
}
});
/**
* Extrae el texto legible de un mensaje citado, independiente del tipo
* @param {object} quotedMessage - El objeto quotedMessage del contextInfo
* @returns {string} - Texto extraído o descripción del contenido
*/
function getQuotedText(quotedMessage) {
if (!quotedMessage) return '';
// Texto simple
if (quotedMessage.conversation) {
return quotedMessage.conversation;
}
// Texto extendido
if (quotedMessage.extendedTextMessage?.text) {
return quotedMessage.extendedTextMessage.text;
}
// Imagen con caption
if (quotedMessage.imageMessage?.caption) {
return `[Imagen] ${quotedMessage.imageMessage.caption}`;
}
// Video con caption
if (quotedMessage.videoMessage?.caption) {
return `[Video] ${quotedMessage.videoMessage.caption}`;
}
// Documento
if (quotedMessage.documentMessage) {
return `[Documento] ${quotedMessage.documentMessage.fileName || 'archivo'}`;
}
// Audio / Nota de voz
if (quotedMessage.audioMessage) {
const tipo = quotedMessage.audioMessage.ptt ? 'Nota de voz' : 'Audio';
return `[${tipo}] ${quotedMessage.audioMessage.seconds || 0}s`;
}
// Ubicación
if (quotedMessage.locationMessage) {
return `[Ubicación] ${quotedMessage.locationMessage.name || 'Ubicación compartida'}`;
}
// Contacto
if (quotedMessage.contactMessage) {
return `[Contacto] ${quotedMessage.contactMessage.displayName}`;
}
// Sticker
if (quotedMessage.stickerMessage) {
return '[Sticker]';
}
// Encuesta
if (quotedMessage.pollCreationMessage) {
return `[Encuesta] ${quotedMessage.pollCreationMessage.name}`;
}
// View Once
if (quotedMessage.viewOnceMessage || quotedMessage.viewOnceMessageV2) {
return '[Mensaje de vista única]';
}
// Respuesta a botones
if (quotedMessage.buttonsResponseMessage) {
return quotedMessage.buttonsResponseMessage.selectedDisplayText;
}
// Respuesta a lista
if (quotedMessage.listResponseMessage) {
return quotedMessage.listResponseMessage.title;
}
return '[Mensaje no soportado]';
}
import { downloadMediaMessage } from '@whiskeysockets/baileys';
/**
* Descarga el archivo multimedia de un mensaje citado
* NOTA: Requiere reconstruir el objeto mensaje completo
*/
async function downloadQuotedMedia(sock, msg) {
const contextInfo = msg.message?.extendedTextMessage?.contextInfo;
if (!contextInfo?.quotedMessage) return null;
const quotedMsg = contextInfo.quotedMessage;
const quotedType = getContentType(quotedMsg);
// Solo procesar tipos multimedia
const mediaTypes = ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage'];
if (!mediaTypes.includes(quotedType)) return null;
// Reconstruir mensaje para downloadMediaMessage
const quotedMsgObj = {
key: {
remoteJid: msg.key.remoteJid,
id: contextInfo.stanzaId,
participant: contextInfo.participant
},
message: quotedMsg
};
try {
const buffer = await downloadMediaMessage(quotedMsgObj, 'buffer', {});
const mediaMsg = quotedMsg[quotedType];
return {
buffer,
mimetype: mediaMsg.mimetype,
fileName: mediaMsg.fileName || `quoted_${Date.now()}`,
size: mediaMsg.fileLength
};
} catch (error) {
console.error('Error descargando media citada:', error.message);
return null;
}
}
async function sendWithRetry(sock: WASocket, jid: string, content: any, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await sock.sendMessage(jid, content)
} catch (error) {
if (i === maxRetries - 1) throw error
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
}
const messageQueue: Array<() => Promise<void>> = []
let processing = false
async function processQueue() {
if (processing) return
processing = true
while (messageQueue.length > 0) {
const task = messageQueue.shift()
if (task) await task()
await new Promise(r => setTimeout(r, 100)) // Rate limiting
}
processing = false
}
import { getContentType } from '@whiskeysockets/baileys'
function getMessageContent(msg: WAMessage): string | null {
if (!msg.message) return null
const messageType = getContentType(msg.message)
switch (messageType) {
case 'conversation':
return msg.message.conversation
case 'extendedTextMessage':
return msg.message.extendedTextMessage?.text || null
case 'imageMessage':
return msg.message.imageMessage?.caption || null
case 'videoMessage':
return msg.message.videoMessage?.caption || null
default:
return null
}
}
| Problema | Causa Probable | Solución | Código |
|---|---|---|---|
| Error 405: Connection Failure | Versión de Baileys desactualizada o configuración incorrecta | Usar fetchLatestBaileysVersion() y configurar syncFullHistory: false, markOnlineOnConnect: false |
Ver ejemplo |
| Error 440: Stream Errored (conflict) | Múltiples instancias del bot o reconexiones simultáneas | Evitar reconexiones concurrentes con flag isReconnecting, cerrar instancias anteriores |
Ver ejemplo |
| Error 408: WebSocket Error | Timeout de conexión | Aumentar connectTimeoutMs: 60000 y keepAliveIntervalMs: 30000 |
Ver ejemplo |
| QR no se genera | Sesión anterior corrupta o printQRInTerminal deprecado |
Eliminar auth_info/ y usar qrcode-terminal manualmente (NO usar printQRInTerminal) |
Ver ejemplo |
| Mensajes no llegan | Evento mal configurado | Verificar sock.ev.on('messages.upsert') con type === 'notify' |
Ver ejemplo básico |
| Sesión se cierra | creds.update no guarda |
Implementar saveCreds correctamente con sock.ev.on('creds.update', saveCreds) |
Ver ejemplo básico |
| Sesión no persiste | auth_info no se mantiene entre reinicios | Verificar que useMultiFileAuthState() apunte a la carpeta correcta |
Ver ejemplo básico |
| Sesión no se limpia al desvincular | No detecta logout desde móvil | Escuchar DisconnectReason.loggedOut y eliminar carpeta auth |
Ver ejemplo |
| Error 428 al enviar | Número no registrado | Verificar formato de JID y registro en WhatsApp | await sock.onWhatsApp(number) |
| Archivos grandes fallan | Buffer en memoria | Usar { stream: ... } en lugar de Buffer |
Ver docs de media |
| Múltiples dispositivos | Sesión compartida | Una sesión = un dispositivo activo | Max 4 dispositivos vinculados |
| Rate limit | Muchos mensajes rápido | Implementar cola con delays | Ver patrón de cola |
| Media no descarga | Mensaje muy antiguo | Usar sock.updateMediaMessage() para re-upload |
- |
import { fetchLatestBaileysVersion } from '@whiskeysockets/baileys'
// IMPORTANTE: Obtener versión actualizada
const { version } = await fetchLatestBaileysVersion()
const sock = makeWASocket({
version, // ← Incluir versión
auth: state,
logger,
// Configuración crítica para evitar 405
syncFullHistory: false,
markOnlineOnConnect: false,
defaultQueryTimeoutMs: undefined
})
// Variables de control
let sock = null
let isReconnecting = false
async function connectToWhatsApp() {
// Evitar reconexiones simultáneas
if (isReconnecting) {
console.log('⏳ Reconexión en proceso...')
return
}
isReconnecting = true
// Cerrar socket anterior
if (sock) {
try {
sock.ev.removeAllListeners()
sock.ws.close()
} catch (e) {}
sock = null
}
// ... crear nuevo socket
sock.ev.on('connection.update', (update) => {
if (update.connection === 'open') {
isReconnecting = false // Reset flag
}
})
// Timeout de seguridad
setTimeout(() => {
isReconnecting = false
}, 2000)
}
const sock = makeWASocket({
auth: state,
// Timeouts más generosos
connectTimeoutMs: 60000,
keepAliveIntervalMs: 30000,
defaultQueryTimeoutMs: undefined
})
import qrcode from 'qrcode-terminal'
const sock = makeWASocket({
auth: state,
// NO usar printQRInTerminal (deprecado)
// printQRInTerminal: true // ❌ Deprecado
})
sock.ev.on('connection.update', (update) => {
if (update.qr) {
// Generar QR manualmente
qrcode.generate(update.qr, { small: true })
console.log('📱 Escanea el código QR')
}
})
import fs from 'fs'
import { DisconnectReason } from '@whiskeysockets/baileys'
sock.ev.on('connection.update', (update) => {
if (update.connection === 'close') {
const statusCode = update.lastDisconnect?.error?.output?.statusCode
// Usuario desvinculó desde el móvil
if (statusCode === DisconnectReason.loggedOut) {
console.log('🚪 Sesión cerrada desde el móvil')
// Eliminar credenciales
if (fs.existsSync('auth_info')) {
fs.rmSync('auth_info', { recursive: true, force: true })
console.log('✅ Credenciales eliminadas')
}
console.log('💡 Reinicia el bot para generar nuevo QR')
process.exit(0)
}
}
})
Ver basic-bot-example.js para un ejemplo completo y funcional que incluye todas las correcciones mencionadas.
# Ejecutar ejemplo
cd .github/skills/whatsapp-baileys/scripts
npm install @whiskeysockets/baileys pino qrcode-terminal
node basic-bot-example.js
auth_info_baileys/ a repositorios públicos| Técnica | Impacto | Cuándo Aplicar |
|---|---|---|
| Usar streams para media | Alto | Archivos >5MB |
| Cachear metadata de grupos | Medio | Apps con muchos grupos |
Implementar getMessage |
Alto | Para reenvíos y encuestas |
| Cola de mensajes | Alto | >10 msgs/seg |
| Lazy loading de contactos | Medio | >1000 contactos |
| Comprimir imágenes | Medio | Envío masivo de imágenes |
| Usar base de datos externa | Alto | Siempre en producción |
import { WA_DEFAULT_EPHEMERAL } from '@whiskeysockets/baileys'
// Activar en el chat (7 días)
await sock.sendMessage(jid, {
disappearingMessagesInChat: WA_DEFAULT_EPHEMERAL
})
// Enviar mensaje temporal
await sock.sendMessage(jid, { text: 'Hola' }, {
ephemeralExpiration: WA_DEFAULT_EPHEMERAL
})
// Crear grupo
const group = await sock.groupCreate('Mi Grupo', participants)
// Metadata completa
const metadata = await sock.groupMetadata(groupJid)
// Modificar participantes
await sock.groupParticipantsUpdate(groupJid, [jid], 'add' | 'remove' | 'promote' | 'demote')
// Configurar grupo
await sock.groupSettingUpdate(groupJid, 'announcement') // Solo admins pueden enviar
// Crear encuesta
await sock.sendMessage(jid, {
poll: {
name: '¿Qué prefieres?',
values: ['Opción A', 'Opción B', 'Opción C'],
selectableCount: 1
}
})
// Descifrar votos
sock.ev.on('messages.update', async (updates) => {
for(const { key, update } of updates) {
if(update.pollUpdates) {
const pollMsg = await getMessage(key)
const votes = getAggregateVotesInPollMessage({
message: pollMsg,
pollUpdates: update.pollUpdates
})
console.log(votes)
}
}
})
| Baileys Version | Node.js | Breaking Changes |
|---|---|---|
| 7.0.0+ | 16+ | Migration guide |
| 6.x | 14+ | Multi-device API changes |
| 5.x | 12+ | Legacy (deprecated) |
Nota: Esta skill está optimizada para Baileys v7.0.0+. Consultar la guía de migración si usas versiones anteriores.
Para mantener el proyecto actualizado:
# Verificar versión actual
npm list @whiskeysockets/baileys
# Actualizar a la última versión
npm update @whiskeysockets/baileys
# O instalar versión específica
npm install @whiskeysockets/baileys@latest
Seguir el changelog oficial para breaking changes.