@vanzxy/baileys 1.6.3 → 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.
- package/NOTICE.md +50 -0
- package/lib/Utils/A2UI.js +217 -0
- package/lib/Utils/MessageBuilder.js +290 -40
- package/lib/Utils/MessageBuilder_d.ts +45 -0
- package/lib/Utils/PersistentStore.js +592 -0
- package/lib/Utils/PersistentStore_d.ts +60 -0
- package/lib/Utils/anti-delete.d.ts +68 -0
- package/lib/Utils/anti-delete.js +185 -0
- package/lib/Utils/auto-reply.d.ts +47 -0
- package/lib/Utils/auto-reply.js +155 -0
- package/lib/Utils/button-helper-utils.js +314 -0
- package/lib/Utils/button-sender.js +817 -0
- package/lib/Utils/chat-history-helpers.d.ts +21 -0
- package/lib/Utils/chat-history-helpers.js +71 -0
- package/lib/Utils/index.d.ts +11 -0
- package/lib/Utils/index.js +16 -0
- package/lib/Utils/media-messages.d.ts +18 -0
- package/lib/Utils/media-messages.js +71 -0
- package/lib/Utils/media-set.d.ts +13 -0
- package/lib/Utils/media-set.js +165 -0
- package/lib/Utils/message-kind.js +139 -0
- package/lib/Utils/message-search.d.ts +44 -0
- package/lib/Utils/message-search.js +174 -0
- package/lib/Utils/scheduling.d.ts +42 -0
- package/lib/Utils/scheduling.js +140 -0
- package/lib/Utils/status.d.ts +50 -0
- package/lib/Utils/status.js +108 -0
- package/lib/Utils/stickerpack.d.ts +51 -0
- package/lib/Utils/stickerpack.js +276 -0
- package/lib/Utils/templates.d.ts +76 -0
- package/lib/Utils/templates.js +151 -0
- package/lib/Utils/use-sqlite-auth-state.js +28 -1
- package/lib/Utils/vcard.d.ts +58 -0
- package/lib/Utils/vcard.js +94 -0
- package/lib/VoIP/audio-feeder.d.ts +15 -0
- package/lib/VoIP/audio-feeder.js +132 -0
- package/lib/VoIP/index.js +277 -0
- package/lib/VoIP/relay-transport.d.ts +43 -0
- package/lib/VoIP/relay-transport.js +559 -0
- package/lib/VoIP/signaling.js +624 -0
- package/lib/VoIP/types.d.ts +69 -0
- package/lib/VoIP/types.js +17 -0
- package/lib/VoIP/wasm-engine.d.ts +103 -0
- package/lib/VoIP/wasm-engine.js +1214 -0
- package/lib/VoIP/worker-bootstrap.js +1042 -0
- package/lib/WABinary/generic-utils.js +8 -0
- package/lib/assets/wasm/loader.js +5 -0
- package/lib/assets/wasm/whatsapp.wasm +0 -0
- package/lib/assets/wasm/worker-modules.js +273 -0
- package/lib/index.js +4 -0
- package/package.json +22 -1
|
@@ -104,9 +104,27 @@ export class Button extends BaseBuilder {
|
|
|
104
104
|
static menu?: (client: any, options?: Record<string, any>) => Button;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
export class RowBuilder {
|
|
108
|
+
constructor();
|
|
109
|
+
buttons: Record<string, any>[];
|
|
110
|
+
button(displayText: string, buttonId?: string): this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class CardBuilder {
|
|
114
|
+
constructor(client: any);
|
|
115
|
+
title(text: string): this;
|
|
116
|
+
text(text: string): this;
|
|
117
|
+
image(path: string | Buffer, options?: Record<string, any>): this;
|
|
118
|
+
button(displayText: string, buttonId?: string): this;
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
export class ButtonV2 extends BaseBuilder {
|
|
108
122
|
constructor(client: any);
|
|
109
123
|
addButton(displayText: string, buttonId?: string): this;
|
|
124
|
+
/** Alias for addButton(). */
|
|
125
|
+
button(displayText: string, buttonId?: string): this;
|
|
126
|
+
/** Fluent row helper — group buttons via a callback instead of chaining addButton(). */
|
|
127
|
+
row(cb: (row: RowBuilder) => void): this;
|
|
110
128
|
addRawButton(obj: Record<string, any>): this;
|
|
111
129
|
setThumbnail(path: string | Buffer): this;
|
|
112
130
|
setMedia(obj: Record<string, any>): this;
|
|
@@ -115,6 +133,30 @@ export class ButtonV2 extends BaseBuilder {
|
|
|
115
133
|
send(jid: string, options?: Record<string, any> & { viewOnce?: boolean }): Promise<any>;
|
|
116
134
|
}
|
|
117
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Legacy `templateMessage` / `hydratedFourRowTemplate` builder — WA's Generation-1
|
|
138
|
+
* button protocol (predates the nativeFlow format Button/ButtonV2 use). Capped at
|
|
139
|
+
* 3 buttons (quickReply/url/call only), no interactive list/flow support.
|
|
140
|
+
*/
|
|
141
|
+
export class ButtonV3 extends BaseBuilder {
|
|
142
|
+
constructor(client: any);
|
|
143
|
+
/** Load an existing templateMessage (e.g. from a fetched/quoted message) for editing. */
|
|
144
|
+
loadFrom(msg: Record<string, any>): this;
|
|
145
|
+
setImage(path: string | Buffer, options?: Record<string, any>): this;
|
|
146
|
+
setVideo(path: string | Buffer, options?: Record<string, any>): this;
|
|
147
|
+
setDocument(path: string | Buffer, options?: Record<string, any>): this;
|
|
148
|
+
setMedia(obj: Record<string, any>): this;
|
|
149
|
+
clearButtons(): this;
|
|
150
|
+
/** Max 3 buttons — throws past the limit. */
|
|
151
|
+
addButton(hydratedButton: Record<string, any>): this;
|
|
152
|
+
addReply(display_text?: string, id?: string): this;
|
|
153
|
+
addUrl(display_text?: string, url?: string, options?: Record<string, any>): this;
|
|
154
|
+
addCall(display_text?: string, phone_number?: string): this;
|
|
155
|
+
toTemplate(): Promise<Record<string, any>>;
|
|
156
|
+
build(jid: string, options?: Record<string, any>): Promise<Record<string, any>>;
|
|
157
|
+
send(jid: string, options?: Record<string, any>): Promise<any>;
|
|
158
|
+
}
|
|
159
|
+
|
|
118
160
|
export class Carousel extends BaseBuilder {
|
|
119
161
|
constructor(client: any);
|
|
120
162
|
/** WhatsApp caps carousels at this many cards (10); addCard() throws past it. */
|
|
@@ -224,6 +266,9 @@ export class AIRich extends BaseBuilder {
|
|
|
224
266
|
// MessageBuilder.js). All four are structurally identical to AIRich.
|
|
225
267
|
export { AIRich as AIVanzxy, AIRich as LeafRich, AIRich as VanzxyAI, AIRich as VanzxyRich };
|
|
226
268
|
|
|
269
|
+
/** `class ORich extends AIRich {}` — plain re-export alias, no additional members. */
|
|
270
|
+
export class ORich extends AIRich {}
|
|
271
|
+
|
|
227
272
|
export class Toolkit {
|
|
228
273
|
static extractIE(text: string, options?: AddTextOptions): Record<string, any>;
|
|
229
274
|
static getMp4Duration(buffer: Buffer, options?: { silent?: boolean }): Promise<number>;
|
|
@@ -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
|
+
};
|