@vanzxy/baileys 1.6.2 → 1.6.4

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 (51) hide show
  1. package/NOTICE.md +50 -0
  2. package/lib/Utils/A2UI.js +217 -0
  3. package/lib/Utils/MessageBuilder.js +332 -46
  4. package/lib/Utils/MessageBuilder_d.ts +45 -0
  5. package/lib/Utils/PersistentStore.js +592 -0
  6. package/lib/Utils/PersistentStore_d.ts +60 -0
  7. package/lib/Utils/anti-delete.d.ts +68 -0
  8. package/lib/Utils/anti-delete.js +185 -0
  9. package/lib/Utils/auto-reply.d.ts +47 -0
  10. package/lib/Utils/auto-reply.js +155 -0
  11. package/lib/Utils/button-helper-utils.js +314 -0
  12. package/lib/Utils/button-sender.js +817 -0
  13. package/lib/Utils/chat-history-helpers.d.ts +21 -0
  14. package/lib/Utils/chat-history-helpers.js +71 -0
  15. package/lib/Utils/index.d.ts +11 -0
  16. package/lib/Utils/index.js +16 -0
  17. package/lib/Utils/media-messages.d.ts +18 -0
  18. package/lib/Utils/media-messages.js +71 -0
  19. package/lib/Utils/media-set.d.ts +13 -0
  20. package/lib/Utils/media-set.js +165 -0
  21. package/lib/Utils/message-kind.js +139 -0
  22. package/lib/Utils/message-search.d.ts +44 -0
  23. package/lib/Utils/message-search.js +174 -0
  24. package/lib/Utils/scheduling.d.ts +42 -0
  25. package/lib/Utils/scheduling.js +140 -0
  26. package/lib/Utils/status.d.ts +50 -0
  27. package/lib/Utils/status.js +108 -0
  28. package/lib/Utils/stickerpack.d.ts +51 -0
  29. package/lib/Utils/stickerpack.js +276 -0
  30. package/lib/Utils/templates.d.ts +76 -0
  31. package/lib/Utils/templates.js +151 -0
  32. package/lib/Utils/use-sqlite-auth-state.js +28 -1
  33. package/lib/Utils/vcard.d.ts +58 -0
  34. package/lib/Utils/vcard.js +94 -0
  35. package/lib/VoIP/audio-feeder.d.ts +15 -0
  36. package/lib/VoIP/audio-feeder.js +132 -0
  37. package/lib/VoIP/index.js +277 -0
  38. package/lib/VoIP/relay-transport.d.ts +43 -0
  39. package/lib/VoIP/relay-transport.js +559 -0
  40. package/lib/VoIP/signaling.js +624 -0
  41. package/lib/VoIP/types.d.ts +69 -0
  42. package/lib/VoIP/types.js +17 -0
  43. package/lib/VoIP/wasm-engine.d.ts +103 -0
  44. package/lib/VoIP/wasm-engine.js +1214 -0
  45. package/lib/VoIP/worker-bootstrap.js +1042 -0
  46. package/lib/WABinary/generic-utils.js +8 -0
  47. package/lib/assets/wasm/loader.js +5 -0
  48. package/lib/assets/wasm/whatsapp.wasm +0 -0
  49. package/lib/assets/wasm/worker-modules.js +273 -0
  50. package/lib/index.js +4 -0
  51. package/package.json +22 -1
@@ -0,0 +1,592 @@
1
+ /**
2
+ * Vanz@Add 29-08-26 --- Persisted Store: chat/contact/message/group-metadata
3
+ * persistence for @vanzxy/baileys, across 5 pluggable backends.
4
+ *
5
+ * Single-file by design (same style as MessageBuilder.js) instead of split
6
+ * across lib/Store/adapters/*: everything — the 5 backend adapters plus the
7
+ * makePersistentStore() wrapper — lives here as one module with multiple
8
+ * named exports.
9
+ *
10
+ * Design note: rather than reimplementing chat ordering (KeyedDB), message
11
+ * pagination, and label-association queries separately per SQL dialect, this
12
+ * wraps the existing makeInMemoryStore() (which already owns and tests that
13
+ * logic) and adds a write-through + startup-hydration layer via a generic
14
+ * "adapter" contract:
15
+ *
16
+ * adapter.init() create tables/collections/indexes (idempotent)
17
+ * adapter.get(domain, id) -> value | undefined
18
+ * adapter.set(domain, id, value)
19
+ * adapter.delete(domain, id)
20
+ * adapter.list(domain) -> [[id, value], ...]
21
+ * adapter.listDomains(prefix?) -> [domain, ...]
22
+ * adapter.clear(domain)
23
+ * adapter.close()
24
+ *
25
+ * Every adapter stores rows as (domain, id, value) — the exact shape
26
+ * useSqliteAuthState() already uses for its `signal_keys` table. Domains:
27
+ * 'chats' id = chat.id
28
+ * 'contacts' id = contact.id
29
+ * 'groupMetadata' id = jid
30
+ * 'labels' id = label.id
31
+ * 'labelAssociations' id = `${chatId}:${messageId || ''}:${labelId}`
32
+ * `messages:${jid}` id = message.key.id (one domain per chat)
33
+ *
34
+ * Values are (de)serialized with BufferJSON so Buffers inside WAMessage /
35
+ * signal payloads round-trip byte-for-byte identically across every backend.
36
+ *
37
+ * Each loadX() below lazy-imports its driver and throws a clear, actionable
38
+ * error if it isn't installed — none of the 5 drivers are hard dependencies
39
+ * of this package; pick whichever backend(s) you actually use and
40
+ * `npm install` that one driver.
41
+ */
42
+ import { BufferJSON } from './generics.js';
43
+ import { WAProto } from '../Types/index.js';
44
+ import { makeInMemoryStore } from '../Store/make-in-memory-store.js';
45
+
46
+ // ============================================================================
47
+ // Shared helpers
48
+ // ============================================================================
49
+
50
+ const labelAssociationId = (la) => `${la.chatId}:${la.messageId || ''}:${la.labelId}`;
51
+
52
+ function requireDriver(loader, pkgName, installHint) {
53
+ return loader().catch((err) => {
54
+ const helpful = new Error(`\`${pkgName}\` is required for this store adapter. Install it: \`npm install ${installHint || pkgName}\`.`);
55
+ helpful.cause = err;
56
+ throw helpful;
57
+ });
58
+ }
59
+
60
+ // ============================================================================
61
+ // SQLite adapter (better-sqlite3)
62
+ // ============================================================================
63
+
64
+ const SQLITE_CREATE_SCHEMA_SQL = `
65
+ CREATE TABLE IF NOT EXISTS wa_store (
66
+ domain TEXT NOT NULL,
67
+ id TEXT NOT NULL,
68
+ value TEXT NOT NULL,
69
+ PRIMARY KEY (domain, id)
70
+ );
71
+ CREATE INDEX IF NOT EXISTS wa_store_domain_idx ON wa_store(domain);
72
+ `;
73
+
74
+ /**
75
+ * @param {{ dbPath?: string, database?: import('better-sqlite3').Database }} opts
76
+ * @returns {Promise<StoreAdapter>}
77
+ */
78
+ export async function createSqliteStoreAdapter(opts = {}) {
79
+ let db;
80
+ let ownsConnection = false;
81
+ if (opts.database) {
82
+ db = opts.database;
83
+ } else {
84
+ const mod = await requireDriver(() => import('better-sqlite3'), 'better-sqlite3');
85
+ const Database = mod.default ?? mod;
86
+ db = new Database(opts.dbPath || 'wa-store.db');
87
+ ownsConnection = true;
88
+ }
89
+
90
+ db.pragma('journal_mode = WAL');
91
+ db.pragma('synchronous = NORMAL');
92
+
93
+ let stmts;
94
+
95
+ return {
96
+ async init() {
97
+ db.exec('CREATE TABLE IF NOT EXISTS wa_migrations (id TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)');
98
+ const applied = new Set(db.prepare('SELECT id FROM wa_migrations').all().map((r) => r.id));
99
+ if (!applied.has('0001_init')) {
100
+ const tx = db.transaction(() => {
101
+ db.exec(SQLITE_CREATE_SCHEMA_SQL);
102
+ db.prepare('INSERT INTO wa_migrations (id, applied_at) VALUES (?, ?)').run('0001_init', Date.now());
103
+ });
104
+ tx();
105
+ }
106
+ stmts = {
107
+ get: db.prepare('SELECT value FROM wa_store WHERE domain = ? AND id = ?'),
108
+ set: db.prepare('INSERT INTO wa_store (domain, id, value) VALUES (?, ?, ?) ON CONFLICT(domain, id) DO UPDATE SET value = excluded.value'),
109
+ del: db.prepare('DELETE FROM wa_store WHERE domain = ? AND id = ?'),
110
+ list: db.prepare('SELECT id, value FROM wa_store WHERE domain = ?'),
111
+ listDomains: db.prepare('SELECT DISTINCT domain FROM wa_store'),
112
+ listDomainsPrefix: db.prepare("SELECT DISTINCT domain FROM wa_store WHERE domain LIKE ? ESCAPE '\\'"),
113
+ clear: db.prepare('DELETE FROM wa_store WHERE domain = ?')
114
+ };
115
+ },
116
+ async get(domain, id) {
117
+ const row = stmts.get.get(domain, id);
118
+ return row ? JSON.parse(row.value, BufferJSON.reviver) : undefined;
119
+ },
120
+ async set(domain, id, value) {
121
+ stmts.set.run(domain, id, JSON.stringify(value, BufferJSON.replacer));
122
+ },
123
+ async delete(domain, id) {
124
+ stmts.del.run(domain, id);
125
+ },
126
+ async list(domain) {
127
+ return stmts.list.all(domain).map((r) => [r.id, JSON.parse(r.value, BufferJSON.reviver)]);
128
+ },
129
+ async listDomains(prefix) {
130
+ if (!prefix) return stmts.listDomains.all().map((r) => r.domain);
131
+ const escaped = prefix.replace(/[\\%_]/g, (c) => `\\${c}`);
132
+ return stmts.listDomainsPrefix.all(`${escaped}%`).map((r) => r.domain);
133
+ },
134
+ async clear(domain) {
135
+ stmts.clear.run(domain);
136
+ },
137
+ async close() {
138
+ if (ownsConnection) {
139
+ try {
140
+ db.close();
141
+ } catch {
142
+ /* already closed */
143
+ }
144
+ }
145
+ }
146
+ };
147
+ }
148
+
149
+ // ============================================================================
150
+ // MongoDB adapter
151
+ // ============================================================================
152
+
153
+ // BufferJSON round-trips Buffers as { type: 'Buffer', data: [...] }, which is
154
+ // exactly what a JSON.stringify(value, BufferJSON.replacer) -> JSON.parse
155
+ // round-trip produces. We deliberately don't store native BSON Binary here —
156
+ // keeping the on-disk shape textually identical to the sqlite/mysql/postgres
157
+ // adapters means the same value round-trips byte-for-byte across every backend.
158
+ function mongoSerialize(value) {
159
+ return JSON.parse(JSON.stringify(value, BufferJSON.replacer));
160
+ }
161
+ function mongoDeserialize(doc) {
162
+ return JSON.parse(JSON.stringify(doc), BufferJSON.reviver);
163
+ }
164
+
165
+ /**
166
+ * @param {{ url?: string, dbName?: string, client?: import('mongodb').MongoClient, collectionName?: string }} opts
167
+ * @returns {Promise<StoreAdapter>}
168
+ */
169
+ export async function createMongoStoreAdapter(opts = {}) {
170
+ const { MongoClient } = await requireDriver(() => import('mongodb'), 'mongodb');
171
+ let client;
172
+ let ownsConnection = false;
173
+ if (opts.client) {
174
+ client = opts.client;
175
+ } else {
176
+ client = new MongoClient(opts.url || 'mongodb://127.0.0.1:27017');
177
+ ownsConnection = true;
178
+ }
179
+
180
+ let collection;
181
+
182
+ return {
183
+ async init() {
184
+ if (ownsConnection) await client.connect();
185
+ const db = client.db(opts.dbName || 'wa_store');
186
+ collection = db.collection(opts.collectionName || 'wa_store');
187
+ await collection.createIndex({ domain: 1, id: 1 }, { unique: true });
188
+ await collection.createIndex({ domain: 1 });
189
+ },
190
+ async get(domain, id) {
191
+ const doc = await collection.findOne({ domain, id });
192
+ return doc ? mongoDeserialize(doc.value) : undefined;
193
+ },
194
+ async set(domain, id, value) {
195
+ await collection.updateOne({ domain, id }, { $set: { domain, id, value: mongoSerialize(value) } }, { upsert: true });
196
+ },
197
+ async delete(domain, id) {
198
+ await collection.deleteOne({ domain, id });
199
+ },
200
+ async list(domain) {
201
+ const docs = await collection.find({ domain }).toArray();
202
+ return docs.map((d) => [d.id, mongoDeserialize(d.value)]);
203
+ },
204
+ async listDomains(prefix) {
205
+ const filter = prefix ? { domain: { $regex: `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}` } } : {};
206
+ return await collection.distinct('domain', filter);
207
+ },
208
+ async clear(domain) {
209
+ await collection.deleteMany({ domain });
210
+ },
211
+ async close() {
212
+ if (ownsConnection) await client.close();
213
+ }
214
+ };
215
+ }
216
+
217
+ // ============================================================================
218
+ // MySQL adapter (mysql2)
219
+ // ============================================================================
220
+
221
+ const MYSQL_CREATE_TABLE_SQL = `
222
+ CREATE TABLE IF NOT EXISTS wa_store (
223
+ domain VARCHAR(255) NOT NULL,
224
+ id VARCHAR(512) NOT NULL,
225
+ value LONGTEXT NOT NULL,
226
+ PRIMARY KEY (domain, id),
227
+ KEY wa_store_domain_idx (domain)
228
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
229
+ `;
230
+
231
+ /**
232
+ * @param {{ pool?: import('mysql2/promise').Pool } & import('mysql2/promise').PoolOptions} opts
233
+ * @returns {Promise<StoreAdapter>}
234
+ */
235
+ export async function createMysqlStoreAdapter(opts = {}) {
236
+ const mysql = await requireDriver(() => import('mysql2/promise'), 'mysql2');
237
+ let pool;
238
+ let ownsConnection = false;
239
+ if (opts.pool) {
240
+ pool = opts.pool;
241
+ } else {
242
+ pool = mysql.createPool(opts);
243
+ ownsConnection = true;
244
+ }
245
+
246
+ return {
247
+ async init() {
248
+ await pool.query('CREATE TABLE IF NOT EXISTS wa_migrations (id VARCHAR(64) PRIMARY KEY, applied_at BIGINT NOT NULL)');
249
+ const [rows] = await pool.query('SELECT id FROM wa_migrations WHERE id = ?', ['0001_init']);
250
+ if (rows.length === 0) {
251
+ await pool.query(MYSQL_CREATE_TABLE_SQL);
252
+ await pool.query('INSERT INTO wa_migrations (id, applied_at) VALUES (?, ?)', ['0001_init', Date.now()]);
253
+ }
254
+ },
255
+ async get(domain, id) {
256
+ const [rows] = await pool.query('SELECT value FROM wa_store WHERE domain = ? AND id = ?', [domain, id]);
257
+ return rows.length ? JSON.parse(rows[0].value, BufferJSON.reviver) : undefined;
258
+ },
259
+ async set(domain, id, value) {
260
+ await pool.query('INSERT INTO wa_store (domain, id, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)', [
261
+ domain,
262
+ id,
263
+ JSON.stringify(value, BufferJSON.replacer)
264
+ ]);
265
+ },
266
+ async delete(domain, id) {
267
+ await pool.query('DELETE FROM wa_store WHERE domain = ? AND id = ?', [domain, id]);
268
+ },
269
+ async list(domain) {
270
+ const [rows] = await pool.query('SELECT id, value FROM wa_store WHERE domain = ?', [domain]);
271
+ return rows.map((r) => [r.id, JSON.parse(r.value, BufferJSON.reviver)]);
272
+ },
273
+ async listDomains(prefix) {
274
+ const [rows] = prefix
275
+ ? await pool.query('SELECT DISTINCT domain FROM wa_store WHERE domain LIKE ?', [`${prefix.replace(/[%_]/g, (c) => `\\${c}`)}%`])
276
+ : await pool.query('SELECT DISTINCT domain FROM wa_store');
277
+ return rows.map((r) => r.domain);
278
+ },
279
+ async clear(domain) {
280
+ await pool.query('DELETE FROM wa_store WHERE domain = ?', [domain]);
281
+ },
282
+ async close() {
283
+ if (ownsConnection) await pool.end();
284
+ }
285
+ };
286
+ }
287
+
288
+ // ============================================================================
289
+ // PostgreSQL adapter (pg)
290
+ // ============================================================================
291
+
292
+ const POSTGRES_CREATE_TABLE_SQL = `
293
+ CREATE TABLE IF NOT EXISTS wa_store (
294
+ domain TEXT NOT NULL,
295
+ id TEXT NOT NULL,
296
+ value TEXT NOT NULL,
297
+ PRIMARY KEY (domain, id)
298
+ );
299
+ CREATE INDEX IF NOT EXISTS wa_store_domain_idx ON wa_store(domain);
300
+ `;
301
+
302
+ /**
303
+ * @param {{ pool?: import('pg').Pool } & import('pg').PoolConfig} opts
304
+ * @returns {Promise<StoreAdapter>}
305
+ */
306
+ export async function createPostgresStoreAdapter(opts = {}) {
307
+ const mod = await requireDriver(() => import('pg'), 'pg');
308
+ const { Pool } = mod.default ?? mod;
309
+ let pool;
310
+ let ownsConnection = false;
311
+ if (opts.pool) {
312
+ pool = opts.pool;
313
+ } else {
314
+ pool = new Pool(opts);
315
+ ownsConnection = true;
316
+ }
317
+
318
+ return {
319
+ async init() {
320
+ await pool.query('CREATE TABLE IF NOT EXISTS wa_migrations (id TEXT PRIMARY KEY, applied_at BIGINT NOT NULL)');
321
+ const { rows } = await pool.query('SELECT id FROM wa_migrations WHERE id = $1', ['0001_init']);
322
+ if (rows.length === 0) {
323
+ await pool.query(POSTGRES_CREATE_TABLE_SQL);
324
+ await pool.query('INSERT INTO wa_migrations (id, applied_at) VALUES ($1, $2)', ['0001_init', Date.now()]);
325
+ }
326
+ },
327
+ async get(domain, id) {
328
+ const { rows } = await pool.query('SELECT value FROM wa_store WHERE domain = $1 AND id = $2', [domain, id]);
329
+ return rows.length ? JSON.parse(rows[0].value, BufferJSON.reviver) : undefined;
330
+ },
331
+ async set(domain, id, value) {
332
+ await pool.query(
333
+ 'INSERT INTO wa_store (domain, id, value) VALUES ($1, $2, $3) ON CONFLICT (domain, id) DO UPDATE SET value = excluded.value',
334
+ [domain, id, JSON.stringify(value, BufferJSON.replacer)]
335
+ );
336
+ },
337
+ async delete(domain, id) {
338
+ await pool.query('DELETE FROM wa_store WHERE domain = $1 AND id = $2', [domain, id]);
339
+ },
340
+ async list(domain) {
341
+ const { rows } = await pool.query('SELECT id, value FROM wa_store WHERE domain = $1', [domain]);
342
+ return rows.map((r) => [r.id, JSON.parse(r.value, BufferJSON.reviver)]);
343
+ },
344
+ async listDomains(prefix) {
345
+ const { rows } = prefix
346
+ ? await pool.query('SELECT DISTINCT domain FROM wa_store WHERE domain LIKE $1', [`${prefix.replace(/[%_]/g, (c) => `\\${c}`)}%`])
347
+ : await pool.query('SELECT DISTINCT domain FROM wa_store');
348
+ return rows.map((r) => r.domain);
349
+ },
350
+ async clear(domain) {
351
+ await pool.query('DELETE FROM wa_store WHERE domain = $1', [domain]);
352
+ },
353
+ async close() {
354
+ if (ownsConnection) await pool.end();
355
+ }
356
+ };
357
+ }
358
+
359
+ // ============================================================================
360
+ // Redis adapter (ioredis)
361
+ // ============================================================================
362
+
363
+ // Redis has no notion of "distinct domains" the way a SQL/Mongo query does, so
364
+ // we keep an explicit registry: a Set of every domain name that has ever had a
365
+ // key written to it (`wa_store:domains`). Each domain's rows live in their own
366
+ // hash (`wa_store:h:${domain}` -> { id: value }), which keeps per-domain reads
367
+ // (list()/clear()) to a single HGETALL/DEL instead of a keyspace scan.
368
+ const REDIS_DOMAINS_SET_KEY = 'wa_store:domains';
369
+ const redisHashKey = (domain) => `wa_store:h:${domain}`;
370
+
371
+ /**
372
+ * @param {{ url?: string, client?: import('ioredis').Redis } & import('ioredis').RedisOptions} opts
373
+ * @returns {Promise<StoreAdapter>}
374
+ */
375
+ export async function createRedisStoreAdapter(opts = {}) {
376
+ const mod = await requireDriver(() => import('ioredis'), 'ioredis');
377
+ const Redis = mod.default ?? mod;
378
+ let client;
379
+ let ownsConnection = false;
380
+ if (opts.client) {
381
+ client = opts.client;
382
+ } else {
383
+ client = opts.url ? new Redis(opts.url, opts) : new Redis(opts);
384
+ ownsConnection = true;
385
+ }
386
+
387
+ return {
388
+ async init() {
389
+ // Nothing to migrate — Redis is schemaless. This exists purely to
390
+ // satisfy the adapter contract and confirm connectivity early.
391
+ await client.ping();
392
+ },
393
+ async get(domain, id) {
394
+ const raw = await client.hget(redisHashKey(domain), id);
395
+ return raw === null ? undefined : JSON.parse(raw, BufferJSON.reviver);
396
+ },
397
+ async set(domain, id, value) {
398
+ await client.sadd(REDIS_DOMAINS_SET_KEY, domain);
399
+ await client.hset(redisHashKey(domain), id, JSON.stringify(value, BufferJSON.replacer));
400
+ },
401
+ async delete(domain, id) {
402
+ await client.hdel(redisHashKey(domain), id);
403
+ },
404
+ async list(domain) {
405
+ const raw = await client.hgetall(redisHashKey(domain));
406
+ return Object.entries(raw).map(([id, v]) => [id, JSON.parse(v, BufferJSON.reviver)]);
407
+ },
408
+ async listDomains(prefix) {
409
+ const domains = await client.smembers(REDIS_DOMAINS_SET_KEY);
410
+ return prefix ? domains.filter((d) => d.startsWith(prefix)) : domains;
411
+ },
412
+ async clear(domain) {
413
+ await client.del(redisHashKey(domain));
414
+ await client.srem(REDIS_DOMAINS_SET_KEY, domain);
415
+ },
416
+ async close() {
417
+ if (ownsConnection) await client.quit();
418
+ }
419
+ };
420
+ }
421
+
422
+ // ============================================================================
423
+ // makePersistentStore — wraps makeInMemoryStore() with hydration + write-through
424
+ // ============================================================================
425
+
426
+ /**
427
+ * @param {Parameters<typeof makeInMemoryStore>[0] & { adapter: StoreAdapter }} config
428
+ */
429
+ export const makePersistentStore = async (config) => {
430
+ const { adapter, ...inMemConfig } = config;
431
+ if (!adapter) {
432
+ throw new Error('makePersistentStore requires an `adapter` (see createSqliteStoreAdapter / createMongoStoreAdapter / createMysqlStoreAdapter / createPostgresStoreAdapter / createRedisStoreAdapter above)');
433
+ }
434
+
435
+ const store = makeInMemoryStore(inMemConfig);
436
+ await adapter.init();
437
+
438
+ // --- Startup hydration: pull everything from the adapter into the
439
+ // in-memory store before wiring live event write-through, so queries work
440
+ // immediately even before the socket connects. ---
441
+ const hydrate = async () => {
442
+ const chats = (await adapter.list('chats')).map(([, v]) => v);
443
+ if (chats.length) store.chats.upsert(...chats);
444
+
445
+ const contacts = await adapter.list('contacts');
446
+ for (const [id, value] of contacts) store.contacts[id] = value;
447
+
448
+ const groupMetas = await adapter.list('groupMetadata');
449
+ for (const [id, value] of groupMetas) store.groupMetadata[id] = value;
450
+
451
+ const labels = await adapter.list('labels');
452
+ for (const [id, value] of labels) store.labels.upsertById(id, value);
453
+
454
+ const labelAssociations = await adapter.list('labelAssociations');
455
+ if (labelAssociations.length) store.labelAssociations.upsert(...labelAssociations.map(([, v]) => v));
456
+
457
+ const messageDomains = await adapter.listDomains('messages:');
458
+ for (const domain of messageDomains) {
459
+ const jid = domain.slice('messages:'.length);
460
+ const msgs = await adapter.list(domain);
461
+ // makeInMemoryStore() builds per-jid message dictionaries lazily via
462
+ // its own private assertMessageList() closure, which isn't exposed —
463
+ // so hydration mirrors the same array+dict shape it uses internally.
464
+ const array = [];
465
+ const dict = {};
466
+ const list = (store.messages[jid] = {
467
+ array,
468
+ get: (id) => dict[id],
469
+ upsert: (item, mode) => {
470
+ const id = item.key.id || '';
471
+ if (dict[id]) {
472
+ const idx = array.findIndex((i) => (i.key.id || '') === id);
473
+ if (idx >= 0) array[idx] = item;
474
+ } else if (mode === 'append') {
475
+ array.push(item);
476
+ } else {
477
+ array.unshift(item);
478
+ }
479
+ dict[id] = item;
480
+ }
481
+ });
482
+ for (const [, value] of msgs) {
483
+ list.upsert(WAProto.WebMessageInfo.fromObject(value), 'append');
484
+ }
485
+ }
486
+ };
487
+ await hydrate();
488
+
489
+ /** Wire live event write-through. Call this with the same event emitter passed to store.bind(ev). */
490
+ const bind = (ev) => {
491
+ store.bind(ev);
492
+
493
+ ev.on('chats.upsert', async (newChats) => {
494
+ for (const c of newChats) await adapter.set('chats', c.id, store.chats.get(c.id) || c);
495
+ });
496
+ ev.on('chats.update', async (updates) => {
497
+ for (const u of updates) {
498
+ const chat = store.chats.get(u.id);
499
+ if (chat) await adapter.set('chats', u.id, chat);
500
+ }
501
+ });
502
+ ev.on('chats.delete', async (deletions) => {
503
+ for (const id of deletions) await adapter.delete('chats', id);
504
+ });
505
+ ev.on('contacts.upsert', async (contacts) => {
506
+ for (const c of contacts) await adapter.set('contacts', c.id, store.contacts[c.id] || c);
507
+ });
508
+ ev.on('contacts.update', async (updates) => {
509
+ for (const u of updates) {
510
+ const contact = store.contacts[u.id];
511
+ if (contact) await adapter.set('contacts', u.id, contact);
512
+ }
513
+ });
514
+ ev.on('messaging-history.set', async ({ chats: newChats, contacts: newContacts, messages: newMessages }) => {
515
+ for (const c of newChats) await adapter.set('chats', c.id, store.chats.get(c.id) || c);
516
+ for (const c of newContacts) await adapter.set('contacts', c.id, store.contacts[c.id] || c);
517
+ for (const msg of newMessages) {
518
+ const jid = msg.key.remoteJidAlt || msg.key.remoteJid;
519
+ await adapter.set(`messages:${jid}`, msg.key.id || '', msg);
520
+ }
521
+ });
522
+ ev.on('messages.upsert', async ({ messages: newMessages }) => {
523
+ for (const msg of newMessages) {
524
+ const jid = msg.key.remoteJidAlt || msg.key.remoteJid;
525
+ await adapter.set(`messages:${jid}`, msg.key.id || '', msg);
526
+ }
527
+ });
528
+ ev.on('messages.update', async (updates) => {
529
+ for (const { key } of updates) {
530
+ const jid = key.remoteJidAlt || key.remoteJid;
531
+ const msg = store.messages[jid]?.get(key.id);
532
+ if (msg) await adapter.set(`messages:${jid}`, key.id || '', msg);
533
+ }
534
+ });
535
+ ev.on('messages.delete', async (item) => {
536
+ if ('all' in item) {
537
+ await adapter.clear(`messages:${item.jid}`);
538
+ } else {
539
+ const jid = item.keys[0].remoteJidAlt || item.keys[0].remoteJid;
540
+ for (const k of item.keys) await adapter.delete(`messages:${jid}`, k.id || '');
541
+ }
542
+ });
543
+ ev.on('message-receipt.update', async (updates) => {
544
+ for (const { key } of updates) {
545
+ const jid = key.remoteJidAlt || key.remoteJid;
546
+ const msg = store.messages[jid]?.get(key.id);
547
+ if (msg) await adapter.set(`messages:${jid}`, key.id || '', msg);
548
+ }
549
+ });
550
+ ev.on('messages.reaction', async (reactions) => {
551
+ for (const { key } of reactions) {
552
+ const jid = key.remoteJidAlt || key.remoteJid;
553
+ const msg = store.messages[jid]?.get(key.id);
554
+ if (msg) await adapter.set(`messages:${jid}`, key.id || '', msg);
555
+ }
556
+ });
557
+ ev.on('groups.update', async (updates) => {
558
+ for (const u of updates) {
559
+ const meta = store.groupMetadata[u.id];
560
+ if (meta) await adapter.set('groupMetadata', u.id, meta);
561
+ }
562
+ });
563
+ ev.on('group-participants.update', async ({ id }) => {
564
+ const meta = store.groupMetadata[id];
565
+ if (meta) await adapter.set('groupMetadata', id, meta);
566
+ });
567
+ ev.on('labels.edit', async (label) => {
568
+ if (label.deleted) await adapter.delete('labels', label.id);
569
+ else await adapter.set('labels', label.id, label);
570
+ });
571
+ ev.on('labels.association', async ({ type, association }) => {
572
+ const id = labelAssociationId(association);
573
+ if (type === 'add') await adapter.set('labelAssociations', id, association);
574
+ else if (type === 'remove') await adapter.delete('labelAssociations', id);
575
+ });
576
+ };
577
+
578
+ /** fetchGroupMetadata already caches to store.groupMetadata; this also persists that cache entry. */
579
+ const fetchGroupMetadata = async (jid, sock) => {
580
+ const meta = await store.fetchGroupMetadata(jid, sock);
581
+ if (meta) await adapter.set('groupMetadata', jid, meta);
582
+ return meta;
583
+ };
584
+
585
+ return {
586
+ ...store,
587
+ bind,
588
+ fetchGroupMetadata,
589
+ adapter,
590
+ close: () => adapter.close()
591
+ };
592
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Common contract every backend below implements. A flat (domain, id) -> value
3
+ * KV store — the same shape useSqliteAuthState() uses for its `signal_keys`
4
+ * table. Query semantics (ordering, pagination) stay owned by
5
+ * makeInMemoryStore() in JS; adapters are pure storage.
6
+ */
7
+ export interface StoreAdapter {
8
+ /** Create tables/collections/indexes if missing. Idempotent. */
9
+ init(): Promise<void>;
10
+ get(domain: string, id: string): Promise<unknown | undefined>;
11
+ set(domain: string, id: string, value: unknown): Promise<void>;
12
+ delete(domain: string, id: string): Promise<void>;
13
+ /** All (id, value) pairs currently stored under `domain`. */
14
+ list(domain: string): Promise<Array<[string, unknown]>>;
15
+ /** All distinct domain names, optionally filtered by prefix (e.g. `messages:`). */
16
+ listDomains(prefix?: string): Promise<string[]>;
17
+ /** Remove every row under `domain`. */
18
+ clear(domain: string): Promise<void>;
19
+ close(): Promise<void>;
20
+ }
21
+
22
+ export function createSqliteStoreAdapter(opts?: { dbPath?: string; database?: import('better-sqlite3').Database }): Promise<StoreAdapter>;
23
+
24
+ export function createMongoStoreAdapter(opts?: {
25
+ url?: string;
26
+ dbName?: string;
27
+ client?: import('mongodb').MongoClient;
28
+ collectionName?: string;
29
+ }): Promise<StoreAdapter>;
30
+
31
+ export function createMysqlStoreAdapter(
32
+ opts?: { pool?: import('mysql2/promise').Pool } & import('mysql2/promise').PoolOptions
33
+ ): Promise<StoreAdapter>;
34
+
35
+ export function createPostgresStoreAdapter(opts?: { pool?: import('pg').Pool } & import('pg').PoolConfig): Promise<StoreAdapter>;
36
+
37
+ export function createRedisStoreAdapter(
38
+ opts?: { url?: string; client?: import('ioredis').Redis } & import('ioredis').RedisOptions
39
+ ): Promise<StoreAdapter>;
40
+
41
+ export interface MakePersistentStoreConfig {
42
+ adapter: StoreAdapter;
43
+ socket?: any;
44
+ logger?: any;
45
+ chatKey?: any;
46
+ labelAssociationKey?: any;
47
+ }
48
+
49
+ /**
50
+ * Persisted counterpart to makeInMemoryStore(). Same return shape (chats,
51
+ * contacts, messages, groupMetadata, labels, bind, loadMessages, ...) plus
52
+ * `adapter` and an async `close()`. Hydrates from `adapter` on creation, then
53
+ * write-throughs every mutating event alongside the normal in-memory update.
54
+ */
55
+ export function makePersistentStore(config: MakePersistentStoreConfig): Promise<
56
+ ReturnType<typeof import('../Store/make-in-memory-store.js').makeInMemoryStore> & {
57
+ adapter: StoreAdapter;
58
+ close(): Promise<void>;
59
+ }
60
+ >;