@arcaelas/whatsapp 1.0.21 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +776 -0
- package/DOC.md +532 -0
- package/build/Chat.d.ts +335 -0
- package/build/Chat.js +396 -0
- package/build/Chat.js.map +1 -0
- package/build/Contact.d.ts +828 -0
- package/build/Contact.js +188 -0
- package/build/Contact.js.map +1 -0
- package/build/Message.d.ts +525 -0
- package/build/Message.js +445 -0
- package/build/Message.js.map +1 -0
- package/build/WhatsApp.d.ts +68 -0
- package/build/WhatsApp.js +399 -0
- package/build/WhatsApp.js.map +1 -0
- package/build/index.d.ts +11 -107
- package/build/index.js +1 -1
- package/build/index.js.map +3 -3
- package/build/store/driver/FileEngine.d.ts +23 -0
- package/build/store/driver/FileEngine.js +90 -0
- package/build/store/driver/FileEngine.js.map +1 -0
- package/build/store/driver/RedisEngine.d.ts +38 -0
- package/build/store/driver/RedisEngine.js +69 -0
- package/build/store/driver/RedisEngine.js.map +1 -0
- package/build/store/engine.d.ts +54 -0
- package/build/store/engine.js +7 -0
- package/build/store/engine.js.map +1 -0
- package/build/store/index.d.ts +8 -0
- package/build/store/index.js +12 -0
- package/build/store/index.js.map +1 -0
- package/context7.json +4 -0
- package/package.json +59 -52
- package/tsconfig.json +25 -25
- package/build/model/base.d.ts +0 -28
- package/build/model/base.js +0 -65
- package/build/model/base.js.map +0 -1
- package/build/model/chat.d.ts +0 -112
- package/build/model/chat.js +0 -105
- package/build/model/chat.js.map +0 -1
- package/build/model/contact.d.ts +0 -16
- package/build/model/contact.js +0 -22
- package/build/model/contact.js.map +0 -1
- package/build/model/message.d.ts +0 -159
- package/build/model/message.js +0 -206
- package/build/model/message.js.map +0 -1
- package/build/static/Store.d.ts +0 -137
- package/build/static/Store.js +0 -238
- package/build/static/Store.js.map +0 -1
- package/build/static/useCache.d.ts +0 -12
- package/build/static/useCache.js +0 -43
- package/build/static/useCache.js.map +0 -1
package/API.md
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
# API Interna
|
|
2
|
+
|
|
3
|
+
## Ciclo de Vida
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
const wa = new WhatsApp({ phone: "56962816490" })
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**Inicializacion**: El constructor prepara configuracion, store, factories y listeners internos. No hay conexion.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
await wa.pair(code => console.log("Codigo: %s", code))
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Conexion**: Establece conexion con WhatsApp. Callback solo se ejecuta si necesita autenticacion. Resuelve cuando `connection: 'open'`.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Ya operativo: wa.on("message"), wa.Chat.text(), etc.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Listo**: Puedes enviar/recibir mensajes inmediatamente.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
await wa.sync(percent => console.log("%s%", percent))
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Sincronizacion (opcional)**: Espera hasta `progress === 100`. Util para garantizar historial completo.
|
|
28
|
+
|
|
29
|
+
| Fase | Metodo | Efecto |
|
|
30
|
+
|------|--------|--------|
|
|
31
|
+
| 1 | `new WhatsApp()` | Configura instancia, sin conexion |
|
|
32
|
+
| 2 | `pair()` | Conecta, resuelve al abrir |
|
|
33
|
+
| 3 | (listo) | Mensajes en tiempo real |
|
|
34
|
+
| 4 | `sync()` | Bloquea hasta historial completo |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Class WhatsApp
|
|
39
|
+
|
|
40
|
+
##### Configuracion
|
|
41
|
+
|
|
42
|
+
| Propiedad | Tipo | Descripcion |
|
|
43
|
+
|-----------|------|-------------|
|
|
44
|
+
| `phone` | `string` | Numero para auth por codigo (opcional) |
|
|
45
|
+
| `engine` | `Engine` | Engine de persistencia (default: FileStore) |
|
|
46
|
+
|
|
47
|
+
##### Retorno
|
|
48
|
+
|
|
49
|
+
| Propiedad | Tipo | Descripcion |
|
|
50
|
+
|-----------|------|-------------|
|
|
51
|
+
| `Contact` | `class` | Clase vinculada a esta conexion |
|
|
52
|
+
| `Chat` | `class` | Clase vinculada a esta conexion |
|
|
53
|
+
| `Message` | `class` | Clase vinculada a esta conexion |
|
|
54
|
+
| `pair` | `function` | Funcion de autenticacion |
|
|
55
|
+
| `sync` | `function` | Funcion de sincronizacion bloqueante |
|
|
56
|
+
| `on` / `off` | `function` | Suscripcion a eventos |
|
|
57
|
+
|
|
58
|
+
##### Arquitectura de Aislamiento
|
|
59
|
+
|
|
60
|
+
La clase `WhatsApp` extiende `EventEmitter<EventMap>`. Los eventos son propios, no de Baileys. Puedes suscribirte antes o despues de `pair()`.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// whatsapp.ts
|
|
64
|
+
class WhatsApp extends EventEmitter<EventMap> {
|
|
65
|
+
store: Store
|
|
66
|
+
socket: WASocket | null = null
|
|
67
|
+
|
|
68
|
+
constructor(options: Options) {
|
|
69
|
+
super()
|
|
70
|
+
this.store = new Store(options.engine ?? new FileStore()) // engine, no store
|
|
71
|
+
|
|
72
|
+
// Factories reciben solo client (this)
|
|
73
|
+
// No pasan store ni event separados, client tiene ambos
|
|
74
|
+
this.Contact = contact(this)
|
|
75
|
+
this.Chat = chat(this)
|
|
76
|
+
this.Message = message(this)
|
|
77
|
+
|
|
78
|
+
// Constructor se suscribe a sus propios eventos para persistir
|
|
79
|
+
this.on('contact:upsert', (c) => this.store.contact.set(c))
|
|
80
|
+
this.on('chat:upsert', (ch) => this.store.chat.set(ch))
|
|
81
|
+
this.on('chat:deleted', (cid) => this.store.chat.delete(cid))
|
|
82
|
+
this.on('message:created', async (m) => {
|
|
83
|
+
this.store.message.set(m)
|
|
84
|
+
const buffer = await m.content()
|
|
85
|
+
if (buffer.length) this.store.content.set(m.cid, m.id, buffer)
|
|
86
|
+
})
|
|
87
|
+
this.on('message:status', (m) => this.store.message.set(m))
|
|
88
|
+
this.on('message:updated', (m) => this.store.message.set(m))
|
|
89
|
+
this.on('message:deleted', ({ cid, mid }) => this.store.message.delete(cid, mid))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
##### Patron Factory
|
|
95
|
+
|
|
96
|
+
Cada entidad exporta una funcion que recibe `client` (la instancia WhatsApp) y retorna la clase:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// contact.ts
|
|
100
|
+
export function contact(client: WhatsApp) {
|
|
101
|
+
return class Contact {
|
|
102
|
+
readonly id: string
|
|
103
|
+
readonly name: string
|
|
104
|
+
// ...
|
|
105
|
+
|
|
106
|
+
constructor(data: ContactData) {
|
|
107
|
+
Object.assign(this, data)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Estaticos usan closure sobre client
|
|
111
|
+
static async get(uid: string) {
|
|
112
|
+
const data = await client.store.contact.get(uid)
|
|
113
|
+
return data ? new Contact(data) : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Instancia tambien usa closure
|
|
117
|
+
async chat() {
|
|
118
|
+
return client.Chat.get(this.id)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
##### Responsabilidades
|
|
125
|
+
|
|
126
|
+
| Componente | Tipo | Responsabilidad | Toca Baileys |
|
|
127
|
+
|------------|------|-----------------|--------------|
|
|
128
|
+
| `WhatsApp` | class extends EventEmitter | Store, factories, listeners propios | No |
|
|
129
|
+
| `pair()` | metodo | Conecta, transforma eventos Baileys → propios | **Si** |
|
|
130
|
+
| `sync()` | metodo | Escucha evento `progress`, resuelve al 100% | No |
|
|
131
|
+
| `Store` | class (interno) | Envuelve Engine, expone metodos tipados | No |
|
|
132
|
+
| `Engine` | interface | Contrato para el usuario: solo `get` y `set` | No |
|
|
133
|
+
| `contact/chat/message` | function | Factory que recibe `client` y retorna clase | No |
|
|
134
|
+
|
|
135
|
+
##### Beneficios
|
|
136
|
+
|
|
137
|
+
| Aspecto | Comportamiento |
|
|
138
|
+
|---------|----------------|
|
|
139
|
+
| Multiples clientes | Cada `new WhatsApp()` genera clases con su propio store/event |
|
|
140
|
+
| Codigo limpio | Solo 2 lineas extra por archivo (funcion + cierre) |
|
|
141
|
+
| Clase completa | Estaticos + instancia funcionan con el closure |
|
|
142
|
+
| Sin globals | Todo vive en el contexto de la instancia |
|
|
143
|
+
|
|
144
|
+
##### Eventos Internos (EventEmitter propio)
|
|
145
|
+
|
|
146
|
+
| Evento | Payload | Descripcion |
|
|
147
|
+
|--------|---------|-------------|
|
|
148
|
+
| `open` | `void` | Conexion establecida |
|
|
149
|
+
| `close` | `void` | Conexion cerrada |
|
|
150
|
+
| `progress` | `number` | Progreso de sincronizacion (0-100) |
|
|
151
|
+
| `error` | `Error` | Error de conexion o autenticacion |
|
|
152
|
+
| | | |
|
|
153
|
+
| `contact:upsert` | `Contact` | Contacto nuevo o actualizado |
|
|
154
|
+
| | | |
|
|
155
|
+
| `chat:upsert` | `Chat` | Chat nuevo o actualizado |
|
|
156
|
+
| `chat:deleted` | `string` | ID del chat eliminado |
|
|
157
|
+
| | | |
|
|
158
|
+
| `message:created` | `Message` | Mensaje nuevo |
|
|
159
|
+
| `message:status` | `Message` | Cambio de estado (pending→sent→delivered→read) |
|
|
160
|
+
| `message:updated` | `Message` | Mensaje editado |
|
|
161
|
+
| `message:deleted` | `{ cid, mid }` | Mensaje eliminado |
|
|
162
|
+
| `message:reaction` | `Message` | Reaccion emoji al mensaje |
|
|
163
|
+
|
|
164
|
+
##### Funcion pair(callback)
|
|
165
|
+
|
|
166
|
+
**Principio fundamental:** `pair()` es el **unico** punto de contacto con Baileys. Recibe TODOS los eventos de Baileys, los transforma a instancias propias y los emite con `this.emit()`. No persiste nada directamente, el constructor se encarga via sus propios listeners.
|
|
167
|
+
|
|
168
|
+
##### Eventos Baileys (solo en pair)
|
|
169
|
+
|
|
170
|
+
| Evento Baileys | Schema | Evento Emitido |
|
|
171
|
+
|----------------|--------|----------------|
|
|
172
|
+
| `connection.update` | `{ connection, qr }` | `open` / `close` / `error` |
|
|
173
|
+
| `messaging-history.set` | `{ chats, contacts, messages, progress, isLatest }` | `progress`, `contact:upsert`, `chat:upsert`, `message:created` |
|
|
174
|
+
| `contacts.upsert` | `[Contact]` | `contact:upsert` |
|
|
175
|
+
| `contacts.update` | `[Partial<Contact>]` | `contact:upsert` |
|
|
176
|
+
| `chats.upsert` | `[Chat]` | `chat:upsert` |
|
|
177
|
+
| `chats.update` | `[Partial<Chat>]` | `chat:upsert` (merge con existente) |
|
|
178
|
+
| `chats.delete` | `[string]` | `chat:deleted` |
|
|
179
|
+
| `messages.upsert` | `{ messages: [WAMessage] }` | `message:created` |
|
|
180
|
+
| `messages.update` | `[{ key, update }]` | `message:status` o `message:updated` (segun contenido) |
|
|
181
|
+
| `messages.delete` | `{ keys: [MessageKey] }` | `message:deleted` |
|
|
182
|
+
| `messages.reaction` | `[{ key, reaction }]` | `message:reaction` |
|
|
183
|
+
|
|
184
|
+
##### Flujo Interno
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
async function pair(callback?: (qr: Buffer) | (code: string) => void): Promise<void> {
|
|
188
|
+
const promise = promify<void>()
|
|
189
|
+
|
|
190
|
+
// Crear socket de Baileys
|
|
191
|
+
const socket = makeWASocket({
|
|
192
|
+
auth: await this.store.document.get('session/creds'),
|
|
193
|
+
syncFullHistory: true,
|
|
194
|
+
// ... otras opciones
|
|
195
|
+
})
|
|
196
|
+
this.socket = socket
|
|
197
|
+
|
|
198
|
+
// Escuchar eventos de Baileys y emitir eventos propios
|
|
199
|
+
socket.ev.on('connection.update', async ({ connection, qr }) => {
|
|
200
|
+
if (connection === 'open') {
|
|
201
|
+
this.emit('open')
|
|
202
|
+
promise.resolve()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (connection === 'close') {
|
|
207
|
+
this.socket = null
|
|
208
|
+
this.emit('close')
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Manejar autenticacion
|
|
213
|
+
if (qr && callback) {
|
|
214
|
+
if (this._phone) {
|
|
215
|
+
const code = await socket.requestPairingCode(this._phone)
|
|
216
|
+
callback(code)
|
|
217
|
+
} else {
|
|
218
|
+
const buffer = await QRCode.toBuffer(qr)
|
|
219
|
+
callback(buffer)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Transformar eventos Baileys → eventos propios
|
|
225
|
+
socket.ev.on('messaging-history.set', ({ contacts, chats, messages, progress, isLatest }) => {
|
|
226
|
+
contacts.forEach(c => this.emit('contact:upsert', new this.Contact(c)))
|
|
227
|
+
chats.forEach(ch => this.emit('chat:upsert', new this.Chat(ch)))
|
|
228
|
+
messages.forEach(m => this._emit_message(m, 'message:created'))
|
|
229
|
+
this.emit('progress', isLatest ? 100 : (progress ?? 0))
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
socket.ev.on('contacts.upsert', (contacts) => {
|
|
233
|
+
contacts.forEach(c => this.emit('contact:upsert', new this.Contact(c)))
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
socket.ev.on('contacts.update', (contacts) => {
|
|
237
|
+
contacts.forEach(c => this.emit('contact:upsert', new this.Contact(c)))
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
socket.ev.on('chats.upsert', (chats) => {
|
|
241
|
+
chats.forEach(ch => this.emit('chat:upsert', new this.Chat(ch)))
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
socket.ev.on('chats.update', (chats) => {
|
|
245
|
+
chats.forEach(ch => this.emit('chat:upsert', new this.Chat(ch)))
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
socket.ev.on('chats.delete', (ids) => {
|
|
249
|
+
ids.forEach(cid => this.emit('chat:deleted', cid))
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
socket.ev.on('messages.upsert', ({ messages }) => {
|
|
253
|
+
messages.forEach(m => this._emit_message(m, 'message:created'))
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
socket.ev.on('messages.update', (updates) => {
|
|
257
|
+
updates.forEach(({ key, update }) => {
|
|
258
|
+
const event = update.edit ? 'message:updated' : 'message:status'
|
|
259
|
+
this._emit_message_update(key, update, event)
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
socket.ev.on('messages.delete', ({ keys }) => {
|
|
264
|
+
keys.forEach(k => this.emit('message:deleted', { cid: k.remoteJid, mid: k.id }))
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
socket.ev.on('messages.reaction', (reactions) => {
|
|
268
|
+
reactions.forEach(r => this._emit_message_reaction(r))
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// ... otros eventos Baileys
|
|
272
|
+
|
|
273
|
+
return promise
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Metodo interno: transforma WAMessage y virtualiza content()
|
|
277
|
+
private _emit_message(raw: WAMessage) {
|
|
278
|
+
const instance = new this.Message(raw)
|
|
279
|
+
const buffer = await downloadMediaMessage(raw) // o extraer texto/location/poll
|
|
280
|
+
|
|
281
|
+
// Reemplazar content() con funcion que retorna buffer en memoria
|
|
282
|
+
instance.content = async () => buffer
|
|
283
|
+
|
|
284
|
+
this.emit('message', instance)
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
##### Puntos Clave
|
|
289
|
+
|
|
290
|
+
| Aspecto | Comportamiento |
|
|
291
|
+
|---------|----------------|
|
|
292
|
+
| Sin llamar `pair()` | No hay conexion, socket no existe |
|
|
293
|
+
| Sesion existente | `connection.update` emite `open` inmediatamente, callback no se ejecuta |
|
|
294
|
+
| Modo QR | Callback recibe `Buffer` cada ~20s hasta escanear |
|
|
295
|
+
| Modo Code | Callback recibe `string` una sola vez |
|
|
296
|
+
| `await callback()` | Permite al usuario guardar QR, mostrar en UI, etc. antes de continuar |
|
|
297
|
+
| Promesa resuelta | `pair()` termina, conexion establecida |
|
|
298
|
+
|
|
299
|
+
**Dependencia:** Requiere libreria `qrcode` para convertir string CSV a Buffer PNG.
|
|
300
|
+
|
|
301
|
+
##### Funcion sync(callback)
|
|
302
|
+
|
|
303
|
+
Bloquea hasta que la sincronizacion de WhatsApp termine. Escucha el evento `progress` y resuelve cuando llega a 100.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
async function sync(callback?: (progress: number) => void): Promise<void> {
|
|
307
|
+
const promise = promify<void>()
|
|
308
|
+
|
|
309
|
+
const handler = (percent: number) => {
|
|
310
|
+
callback?.(percent)
|
|
311
|
+
|
|
312
|
+
// Resolver cuando llega a 100
|
|
313
|
+
if (percent === 100) {
|
|
314
|
+
this.off('progress', handler)
|
|
315
|
+
promise.resolve()
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.on('progress', handler)
|
|
320
|
+
return promise
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
##### Puntos Clave
|
|
325
|
+
|
|
326
|
+
| Aspecto | Comportamiento |
|
|
327
|
+
|---------|----------------|
|
|
328
|
+
| Sin llamar `sync()` | El evento `progress` se emite pero no hay bloqueo |
|
|
329
|
+
| Con callback | Recibe 0-100 en cada emision de `progress` |
|
|
330
|
+
| Sin callback | Solo bloquea hasta `progress === 100` |
|
|
331
|
+
| Persistencia | El constructor se suscribe a eventos y persiste automaticamente |
|
|
332
|
+
|
|
333
|
+
##### Virtualizacion de content()
|
|
334
|
+
|
|
335
|
+
El metodo `_emit_message()` reemplaza el `content()` de cada mensaje con una funcion que retorna el buffer directamente desde memoria:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// En lugar de:
|
|
339
|
+
instance.content = async () => this.store.content.get(cid, mid) // Acceso a store
|
|
340
|
+
|
|
341
|
+
// Se hace:
|
|
342
|
+
const buffer = await downloadMediaMessage(raw) // Descargar una vez
|
|
343
|
+
instance.content = async () => buffer // Retornar desde memoria
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Beneficio:** Cuando el usuario llama `msg.content()`, obtiene el buffer inmediatamente sin ir al store. El store se actualiza en paralelo via el listener del constructor.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Interface Engine
|
|
351
|
+
|
|
352
|
+
##### Metodos
|
|
353
|
+
|
|
354
|
+
| Metodo | Argumentos | Retorno | Descripcion |
|
|
355
|
+
|--------|------------|---------|-------------|
|
|
356
|
+
| `get` | `(key, offset?, limit?)` | `Promise<string[]>` | Obtiene documentos por clave o namespace |
|
|
357
|
+
| `set` | `(key, value)` | `Promise<boolean>` | Escribe string, `null` elimina |
|
|
358
|
+
|
|
359
|
+
##### Logica de get()
|
|
360
|
+
|
|
361
|
+
El Engine detecta por la estructura de la clave:
|
|
362
|
+
- `contact/123` → clave especifica → array con 1 item
|
|
363
|
+
- `contact` → namespace → array paginado con offset/limit
|
|
364
|
+
- `chat/abc/message` → sub-namespace → mensajes del chat paginados
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Class Store
|
|
369
|
+
|
|
370
|
+
##### Arquitectura de 3 Niveles
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
Contact.get(id)
|
|
374
|
+
↓
|
|
375
|
+
store.contact.get(id)
|
|
376
|
+
↓ new Contact(data)
|
|
377
|
+
store.document.get(`contact/${id}`)
|
|
378
|
+
↓ JSON.parse(..., BufferJSON.reviver)
|
|
379
|
+
engine.get(`contact/${id}`)
|
|
380
|
+
↓ retorna string[]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
##### Flujo Interno
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// Nivel 1: Engine - retorna array de strings
|
|
387
|
+
engine.get('contact/123') → ['{"id":"123","name":"Juan",...}']
|
|
388
|
+
engine.get('contact', 0, 50) → ['{"id":"1",...}', '{"id":"2",...}', ...]
|
|
389
|
+
|
|
390
|
+
// Nivel 2: document - parsea cada item con BufferJSON
|
|
391
|
+
store.document.get('contact/123') → [{ id: '123', name: 'Juan', ... }]
|
|
392
|
+
store.document.get('contact', 0, 50) → [{ id: '1', ... }, { id: '2', ... }, ...]
|
|
393
|
+
|
|
394
|
+
// Nivel 3: entidad - metodos tipados
|
|
395
|
+
store.contact.get('123') → ContactData | null
|
|
396
|
+
store.contact.find(0, 50) → ContactData[]
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
##### Implementacion Interna
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
class Store {
|
|
403
|
+
constructor(private engine: Engine) {}
|
|
404
|
+
|
|
405
|
+
document = {
|
|
406
|
+
get: async <T>(key: string, offset?: number, limit?: number): Promise<T[]> => {
|
|
407
|
+
const items = await this.engine.get(key, offset, limit)
|
|
408
|
+
return items.map(raw => JSON.parse(raw, BufferJSON.reviver))
|
|
409
|
+
},
|
|
410
|
+
set: async (key: string, value: any) => {
|
|
411
|
+
const json = value === null ? null : JSON.stringify(value, BufferJSON.replacer)
|
|
412
|
+
return this.engine.set(key, json)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
contact = {
|
|
417
|
+
get: async (id: string) => {
|
|
418
|
+
const [item] = await this.document.get(`contact/${id}`)
|
|
419
|
+
return item ?? null
|
|
420
|
+
},
|
|
421
|
+
find: async (offset: number, limit: number) => {
|
|
422
|
+
return await this.document.get('contact', offset, limit)
|
|
423
|
+
},
|
|
424
|
+
set: async (data: ContactData) => {
|
|
425
|
+
return await this.document.set(`contact/${data.id}`, data)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
message = {
|
|
430
|
+
get: async (cid: string, mid: string) => {
|
|
431
|
+
const [item] = await this.document.get(`chat/${cid}/message/${mid}`)
|
|
432
|
+
return item ?? null
|
|
433
|
+
},
|
|
434
|
+
find: async (cid: string, offset: number, limit: number) => {
|
|
435
|
+
return await this.document.get(`chat/${cid}/message`, offset, limit)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
##### BufferJSON
|
|
442
|
+
|
|
443
|
+
Baileys provee `BufferJSON.replacer` y `BufferJSON.reviver` para serializar/deserializar Buffers en JSON.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// Guardar (con replacer)
|
|
447
|
+
const json = JSON.stringify({ data: buffer }, BufferJSON.replacer)
|
|
448
|
+
|
|
449
|
+
// Leer (con reviver)
|
|
450
|
+
const obj = JSON.parse(json, BufferJSON.reviver) // obj.data es Buffer
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Class Contact
|
|
456
|
+
|
|
457
|
+
##### Propiedades
|
|
458
|
+
|
|
459
|
+
| Propiedad | Tipo | Descripcion |
|
|
460
|
+
|-----------|------|-------------|
|
|
461
|
+
| `id` | `string` | JID del contacto (`@s.whatsapp.net`) |
|
|
462
|
+
| `name` | `string` | Nombre publico del perfil |
|
|
463
|
+
| `phone` | `string` | Numero extraido del JID |
|
|
464
|
+
| `photo` | `string \| null` | URL de foto de perfil |
|
|
465
|
+
| `custom_name` | `string` | Nombre en agenda local |
|
|
466
|
+
|
|
467
|
+
##### Metodos
|
|
468
|
+
|
|
469
|
+
| Metodo | Tipo | Descripcion |
|
|
470
|
+
|--------|------|-------------|
|
|
471
|
+
| `Contact.me()` | estatico | Retorna `socket.user` parseado |
|
|
472
|
+
| `Contact.get(uid)` | estatico | Busca en store, retorna `null` si no existe |
|
|
473
|
+
| `Contact.find(offset, limit)` | estatico | Pagina contactos desde store |
|
|
474
|
+
| `contact.chat()` | instancia | `Chat.get(this.id)` o crea nuevo |
|
|
475
|
+
| `contact.rename(name)` | instancia | Actualiza `custom_name` en store |
|
|
476
|
+
|
|
477
|
+
##### Eventos Baileys → Evento `contact`
|
|
478
|
+
|
|
479
|
+
| Evento Baileys | Schema | Accion |
|
|
480
|
+
|----------------|--------|--------|
|
|
481
|
+
| `contacts.upsert` | `[{ id, name, notify, imgUrl }]` | `_emit_contact()` → emite `contact` |
|
|
482
|
+
| `contacts.update` | `[{ id, ...parcial }]` | `_emit_contact()` → emite `contact` |
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// Transformacion en pair()
|
|
486
|
+
// notify → name, imgUrl → photo ('changed' → null)
|
|
487
|
+
this.emit('contact', new this.Contact({ id, name, phone, photo, custom_name }))
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
##### Metodos Baileys Utilizados
|
|
491
|
+
|
|
492
|
+
| Metodo Baileys | Uso en Contact |
|
|
493
|
+
|----------------|----------------|
|
|
494
|
+
| `socket.user` | `Contact.me()` obtiene contacto propio |
|
|
495
|
+
| `socket.profilePictureUrl(jid, 'image')` | Obtener `photo` cuando `imgUrl === 'changed'` |
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
// Contact.me()
|
|
499
|
+
// → parse_contact(socket.user)
|
|
500
|
+
|
|
501
|
+
// Contact.get(uid)
|
|
502
|
+
// → store.get(`contacts/${uid}`) → new Contact(data)
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Class Chat
|
|
508
|
+
|
|
509
|
+
##### Propiedades
|
|
510
|
+
|
|
511
|
+
| Propiedad | Tipo | Descripcion |
|
|
512
|
+
|-----------|------|-------------|
|
|
513
|
+
| `id` | `string` | ID del chat (remoteJid) |
|
|
514
|
+
| `name` | `string` | Nombre del grupo o contacto |
|
|
515
|
+
| `photo` | `string \| null` | URL de foto de perfil |
|
|
516
|
+
| `phone` | `string \| null` | Numero (null en grupos) |
|
|
517
|
+
| `type` | `'group' \| 'contact'` | Detectado por sufijo del id |
|
|
518
|
+
|
|
519
|
+
##### Metodos
|
|
520
|
+
|
|
521
|
+
| Metodo | Tipo | Descripcion |
|
|
522
|
+
|--------|------|-------------|
|
|
523
|
+
| `Chat.get(cid)` | estatico | Busca en store |
|
|
524
|
+
| `Chat.find(offset, limit)` | estatico | Pagina chats desde store |
|
|
525
|
+
| `Chat.seen(cid, mid?)` | estatico | `socket.readMessages([messageKey])`. Sin mid usa ultimo |
|
|
526
|
+
| `Chat.members(cid, offset, limit)` | estatico | Grupo: `groupMetadata()`, 1-1: `[contact, me]` |
|
|
527
|
+
| `Chat.messages(cid, offset, limit)` | estatico | Pagina mensajes desde store |
|
|
528
|
+
| `Chat.text(cid, content, mid?)` | estatico | `sendMessage(cid, { text })` |
|
|
529
|
+
| `Chat.image(cid, buffer, mid?)` | estatico | `sendMessage(cid, { image: buffer })` |
|
|
530
|
+
| `Chat.video(cid, buffer, mid?)` | estatico | `sendMessage(cid, { video: buffer })` |
|
|
531
|
+
| `Chat.audio(cid, buffer, mid?)` | estatico | `sendMessage(cid, { audio: buffer })` |
|
|
532
|
+
| `Chat.location(cid, {lat, lng}, mid?)` | estatico | `sendMessage(cid, { location })` |
|
|
533
|
+
| `Chat.poll(cid, text, options[])` | estatico | `sendMessage(cid, { poll })` |
|
|
534
|
+
| `Chat.typing(cid, bool)` | estatico | `sendPresenceUpdate('composing'/'available')` |
|
|
535
|
+
| `Chat.recording(cid, bool)` | estatico | `sendPresenceUpdate('recording'/'available')` |
|
|
536
|
+
|
|
537
|
+
##### Eventos Baileys → Evento `chat`
|
|
538
|
+
|
|
539
|
+
| Evento Baileys | Schema | Accion |
|
|
540
|
+
|----------------|--------|--------|
|
|
541
|
+
| `chats.upsert` | `Chat[]` | `_emit_chat()` → emite `chat` |
|
|
542
|
+
| `chats.update` | `Partial<Chat>[]` | Merge con existente → emite `chat` |
|
|
543
|
+
| `chats.delete` | `string[]` | Elimina del store (no emite evento) |
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// Transformacion en pair()
|
|
547
|
+
// Detecta type por sufijo: @g.us → 'group', @s.whatsapp.net → 'contact'
|
|
548
|
+
this.emit('chat', new this.Chat({ id, name, photo, phone, type }))
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
##### Metodos Baileys Utilizados
|
|
552
|
+
|
|
553
|
+
| Metodo Baileys | Uso en Chat |
|
|
554
|
+
|----------------|-------------|
|
|
555
|
+
| `socket.sendMessage(cid, content, options)` | Enviar texto, media, location, poll |
|
|
556
|
+
| `socket.readMessages([keys])` | `Chat.seen()` marca como leido |
|
|
557
|
+
| `socket.sendPresenceUpdate(type, cid)` | `Chat.typing()` y `Chat.recording()` |
|
|
558
|
+
| `socket.groupMetadata(cid)` | `Chat.members()` en grupos |
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// Chat.text(cid, content, mid?)
|
|
562
|
+
// → sendMessage(cid, { text: content }, mid ? { quoted: message } : {})
|
|
563
|
+
|
|
564
|
+
// Chat.seen(cid)
|
|
565
|
+
// → readMessages([{ remoteJid: cid, id: lastMsgId }])
|
|
566
|
+
|
|
567
|
+
// Chat.typing(cid, true)
|
|
568
|
+
// → sendPresenceUpdate('composing', cid)
|
|
569
|
+
|
|
570
|
+
// Chat.members(cid) en grupo
|
|
571
|
+
// → groupMetadata(cid).participants → Contact[]
|
|
572
|
+
|
|
573
|
+
// Chat.members(cid) en 1-1
|
|
574
|
+
// → [Contact.get(cid), Contact.me()]
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Nota:** El tipo de chat se detecta por el sufijo del id: `@g.us` = grupo, `@s.whatsapp.net` = contacto.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Namespaces
|
|
582
|
+
|
|
583
|
+
Los namespaces son claves estructuradas que el Store usa para organizar datos. El Engine solo ve strings, pero el Store les da semantica.
|
|
584
|
+
|
|
585
|
+
##### Estructura de Claves
|
|
586
|
+
|
|
587
|
+
| Namespace | Clave | Contenido JSON |
|
|
588
|
+
|-----------|-------|----------------|
|
|
589
|
+
| session | `session/creds`, `session/signal/{type}/{id}` | Credenciales y claves de sesion Baileys |
|
|
590
|
+
| contact | `contact/{id}` | `{ id, name, phone, photo, custom_name }` |
|
|
591
|
+
| chat | `chat/{id}` | `{ id, name, photo, phone, type }` |
|
|
592
|
+
| message | `chat/{cid}/message/{mid}` | `{ id, cid, uid, mid, type, mime, caption, me, status, created_at, edited }` |
|
|
593
|
+
| content | `chat/{cid}/message/{mid}/content` | Buffer serializado (media, location, poll) |
|
|
594
|
+
|
|
595
|
+
##### Formato de IDs
|
|
596
|
+
|
|
597
|
+
| Tipo | Formato | Ejemplo |
|
|
598
|
+
|------|---------|---------|
|
|
599
|
+
| Contact | `{phone}@s.whatsapp.net` | `5491155555555@s.whatsapp.net` |
|
|
600
|
+
| Grupo | `{id}@g.us` | `120363123456789@g.us` |
|
|
601
|
+
| Message | `{random}` | `3EB0ABC123DEF456` |
|
|
602
|
+
|
|
603
|
+
##### Ejemplos de Claves
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
session
|
|
607
|
+
contact/5491155555555@s.whatsapp.net
|
|
608
|
+
chat/5491155555555@s.whatsapp.net
|
|
609
|
+
chat/120363123456789@g.us
|
|
610
|
+
chat/5491155555555@s.whatsapp.net/message/3EB0ABC123DEF456
|
|
611
|
+
chat/5491155555555@s.whatsapp.net/message/3EB0ABC123DEF456/content
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Implementacion por Engine
|
|
617
|
+
|
|
618
|
+
Cada Engine puede optimizar el almacenamiento segun sus capacidades.
|
|
619
|
+
|
|
620
|
+
##### Memory (Map)
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
class MemoryEngine implements Engine {
|
|
624
|
+
private data = new Map<string, string>()
|
|
625
|
+
|
|
626
|
+
async get(key: string, offset = 0, limit = 50): Promise<string[]> {
|
|
627
|
+
if (this.data.has(key)) return [this.data.get(key)!]
|
|
628
|
+
const items: string[] = []
|
|
629
|
+
for (const [k, v] of this.data) {
|
|
630
|
+
if (k.startsWith(`${key}/`)) items.push(v)
|
|
631
|
+
}
|
|
632
|
+
return items.slice(offset, offset + limit)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async set(key: string, value: string | null): Promise<boolean> {
|
|
636
|
+
if (value === null) return this.data.delete(key)
|
|
637
|
+
this.data.set(key, value)
|
|
638
|
+
return true
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**Uso:** Desarrollo, testing, sesiones efimeras.
|
|
644
|
+
|
|
645
|
+
##### Redis
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
class RedisEngine implements Engine {
|
|
649
|
+
constructor(private redis: Redis) {}
|
|
650
|
+
|
|
651
|
+
async get(key: string, offset = 0, limit = 50): Promise<string[]> {
|
|
652
|
+
if (await this.redis.exists(key)) {
|
|
653
|
+
const value = await this.redis.get(key)
|
|
654
|
+
return value ? [value] : []
|
|
655
|
+
}
|
|
656
|
+
const keys = await this.redis.keys(`${key}/*`)
|
|
657
|
+
const slice = keys.slice(offset, offset + limit)
|
|
658
|
+
return slice.length ? await this.redis.mget(...slice) : []
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async set(key: string, value: string | null): Promise<boolean> {
|
|
662
|
+
if (value === null) return (await this.redis.del(key)) > 0
|
|
663
|
+
return (await this.redis.set(key, value)) === 'OK'
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Ventajas:** TTL nativo, clustering, pub/sub para eventos.
|
|
669
|
+
**Sugerencia:** Usar `SCAN` en lugar de `KEYS` en produccion.
|
|
670
|
+
|
|
671
|
+
##### PostgreSQL
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
class PostgresEngine implements Engine {
|
|
675
|
+
constructor(private pool: Pool) {}
|
|
676
|
+
|
|
677
|
+
async get(key: string, offset = 0, limit = 50): Promise<string[]> {
|
|
678
|
+
const { rows } = await this.pool.query(
|
|
679
|
+
`SELECT value FROM store WHERE key = $1 OR key LIKE $2 ORDER BY key LIMIT $3 OFFSET $4`,
|
|
680
|
+
[key, `${key}/%`, limit, offset]
|
|
681
|
+
)
|
|
682
|
+
return rows.map(r => r.value)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async set(key: string, value: string | null): Promise<boolean> {
|
|
686
|
+
if (value === null) {
|
|
687
|
+
const { rowCount } = await this.pool.query('DELETE FROM store WHERE key = $1', [key])
|
|
688
|
+
return (rowCount ?? 0) > 0
|
|
689
|
+
}
|
|
690
|
+
await this.pool.query(
|
|
691
|
+
'INSERT INTO store (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2',
|
|
692
|
+
[key, value]
|
|
693
|
+
)
|
|
694
|
+
return true
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**Schema sugerido:**
|
|
700
|
+
```sql
|
|
701
|
+
CREATE TABLE store (
|
|
702
|
+
key TEXT PRIMARY KEY,
|
|
703
|
+
value TEXT NOT NULL,
|
|
704
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
705
|
+
);
|
|
706
|
+
CREATE INDEX idx_store_prefix ON store (key text_pattern_ops);
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Ventajas:** Queries SQL, indices, transacciones.
|
|
710
|
+
|
|
711
|
+
##### SQLite
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
class SQLiteEngine implements Engine {
|
|
715
|
+
constructor(private db: Database) {
|
|
716
|
+
db.exec('CREATE TABLE IF NOT EXISTS store (key TEXT PRIMARY KEY, value TEXT)')
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
get(key: string, offset = 0, limit = 50): Promise<string[]> {
|
|
720
|
+
const rows = this.db.prepare(
|
|
721
|
+
'SELECT value FROM store WHERE key = ? OR key LIKE ? LIMIT ? OFFSET ?'
|
|
722
|
+
).all(key, `${key}/%`, limit, offset)
|
|
723
|
+
return Promise.resolve(rows.map((r: any) => r.value))
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
set(key: string, value: string | null): Promise<boolean> {
|
|
727
|
+
if (value === null) {
|
|
728
|
+
return Promise.resolve(this.db.prepare('DELETE FROM store WHERE key = ?').run(key).changes > 0)
|
|
729
|
+
}
|
|
730
|
+
this.db.prepare('INSERT OR REPLACE INTO store (key, value) VALUES (?, ?)').run(key, value)
|
|
731
|
+
return Promise.resolve(true)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Uso:** Aplicaciones desktop, single-file storage, bots simples.
|
|
737
|
+
|
|
738
|
+
##### MongoDB
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
class MongoEngine implements Engine {
|
|
742
|
+
constructor(private collection: Collection) {}
|
|
743
|
+
|
|
744
|
+
async get(key: string, offset = 0, limit = 50): Promise<string[]> {
|
|
745
|
+
const docs = await this.collection
|
|
746
|
+
.find({ $or: [{ _id: key }, { _id: { $regex: `^${key}/` } }] })
|
|
747
|
+
.skip(offset)
|
|
748
|
+
.limit(limit)
|
|
749
|
+
.toArray()
|
|
750
|
+
return docs.map(d => d.value)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async set(key: string, value: string | null): Promise<boolean> {
|
|
754
|
+
if (value === null) {
|
|
755
|
+
const { deletedCount } = await this.collection.deleteOne({ _id: key })
|
|
756
|
+
return (deletedCount ?? 0) > 0
|
|
757
|
+
}
|
|
758
|
+
await this.collection.updateOne({ _id: key }, { $set: { value } }, { upsert: true })
|
|
759
|
+
return true
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Sugerencia:** Parsear el JSON y guardar como documento BSON para queries avanzadas.
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## Consideraciones
|
|
769
|
+
|
|
770
|
+
| Aspecto | Recomendacion |
|
|
771
|
+
|---------|---------------|
|
|
772
|
+
| Listar contactos | El Store maneja paginacion internamente, Engine solo get/set |
|
|
773
|
+
| Buscar mensajes | Store conoce la estructura, puede iterar claves con prefijo |
|
|
774
|
+
| TTL/Expiracion | Responsabilidad del Engine (Redis TTL, cron jobs en SQL) |
|
|
775
|
+
| Backups | Engine puede implementar snapshots segun backend |
|
|
776
|
+
| Migraciones | Claves son strings, facil exportar/importar entre backends |
|