@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.
Files changed (50) hide show
  1. package/API.md +776 -0
  2. package/DOC.md +532 -0
  3. package/build/Chat.d.ts +335 -0
  4. package/build/Chat.js +396 -0
  5. package/build/Chat.js.map +1 -0
  6. package/build/Contact.d.ts +828 -0
  7. package/build/Contact.js +188 -0
  8. package/build/Contact.js.map +1 -0
  9. package/build/Message.d.ts +525 -0
  10. package/build/Message.js +445 -0
  11. package/build/Message.js.map +1 -0
  12. package/build/WhatsApp.d.ts +68 -0
  13. package/build/WhatsApp.js +399 -0
  14. package/build/WhatsApp.js.map +1 -0
  15. package/build/index.d.ts +11 -107
  16. package/build/index.js +1 -1
  17. package/build/index.js.map +3 -3
  18. package/build/store/driver/FileEngine.d.ts +23 -0
  19. package/build/store/driver/FileEngine.js +90 -0
  20. package/build/store/driver/FileEngine.js.map +1 -0
  21. package/build/store/driver/RedisEngine.d.ts +38 -0
  22. package/build/store/driver/RedisEngine.js +69 -0
  23. package/build/store/driver/RedisEngine.js.map +1 -0
  24. package/build/store/engine.d.ts +54 -0
  25. package/build/store/engine.js +7 -0
  26. package/build/store/engine.js.map +1 -0
  27. package/build/store/index.d.ts +8 -0
  28. package/build/store/index.js +12 -0
  29. package/build/store/index.js.map +1 -0
  30. package/context7.json +4 -0
  31. package/package.json +59 -52
  32. package/tsconfig.json +25 -25
  33. package/build/model/base.d.ts +0 -28
  34. package/build/model/base.js +0 -65
  35. package/build/model/base.js.map +0 -1
  36. package/build/model/chat.d.ts +0 -112
  37. package/build/model/chat.js +0 -105
  38. package/build/model/chat.js.map +0 -1
  39. package/build/model/contact.d.ts +0 -16
  40. package/build/model/contact.js +0 -22
  41. package/build/model/contact.js.map +0 -1
  42. package/build/model/message.d.ts +0 -159
  43. package/build/model/message.js +0 -206
  44. package/build/model/message.js.map +0 -1
  45. package/build/static/Store.d.ts +0 -137
  46. package/build/static/Store.js +0 -238
  47. package/build/static/Store.js.map +0 -1
  48. package/build/static/useCache.d.ts +0 -12
  49. package/build/static/useCache.js +0 -43
  50. 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 |