whalibmob 2.0.0
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/README.md +379 -0
- package/cli.js +509 -0
- package/index.js +35 -0
- package/lib/BinaryNode.js +278 -0
- package/lib/Client.js +957 -0
- package/lib/DeviceManager.js +238 -0
- package/lib/MediaService.js +181 -0
- package/lib/Registration.js +475 -0
- package/lib/Store.js +226 -0
- package/lib/constants.js +68 -0
- package/lib/messages/MessageSender.js +548 -0
- package/lib/noise.js +371 -0
- package/lib/proto/MessageProto.js +229 -0
- package/lib/proto.js +152 -0
- package/lib/signal/SenderKey.js +329 -0
- package/lib/signal/SignalProtocol.js +269 -0
- package/lib/signal/SignalStore.js +161 -0
- package/lib/tokens.js +216 -0
- package/package.json +18 -0
package/lib/Client.js
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { NoiseSocket } = require('./noise');
|
|
7
|
+
const { MessageSender, generateMessageId, makeJid } = require('./messages/MessageSender');
|
|
8
|
+
const { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode } = require('./Registration');
|
|
9
|
+
const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = require('./Store');
|
|
10
|
+
const { BinaryNode } = require('./BinaryNode');
|
|
11
|
+
const { SignalProtocol } = require('./signal/SignalProtocol');
|
|
12
|
+
const { DeviceManager } = require('./DeviceManager');
|
|
13
|
+
|
|
14
|
+
const PING_INTERVAL_MS = 25000;
|
|
15
|
+
const KEEPALIVE_INTERVAL = 20000;
|
|
16
|
+
const RECONNECT_BACKOFF = [1000, 2000, 4000, 8000, 15000, 30000];
|
|
17
|
+
|
|
18
|
+
// ─── Key helpers ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function intToBytes(n, byteLen) {
|
|
21
|
+
const buf = Buffer.alloc(byteLen);
|
|
22
|
+
for (let i = byteLen - 1; i >= 0; i--) { buf[i] = n & 0xff; n >>= 8; }
|
|
23
|
+
return buf;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripKeyPrefix(key) {
|
|
27
|
+
if (!key) return Buffer.alloc(32);
|
|
28
|
+
const buf = Buffer.from(key);
|
|
29
|
+
if (buf.length === 33 && buf[0] === 0x05) return buf.slice(1);
|
|
30
|
+
return buf;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findChild(node, desc) {
|
|
34
|
+
if (!node || !Array.isArray(node.content)) return null;
|
|
35
|
+
return node.content.find(c => c && c.description === desc) || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findChildDeep(node, desc) {
|
|
39
|
+
if (!node) return null;
|
|
40
|
+
if (node.description === desc) return node;
|
|
41
|
+
if (!Array.isArray(node.content)) return null;
|
|
42
|
+
for (const child of node.content) {
|
|
43
|
+
const found = findChildDeep(child, desc);
|
|
44
|
+
if (found) return found;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getNodeContent(node) {
|
|
50
|
+
if (!node) return null;
|
|
51
|
+
if (Buffer.isBuffer(node.content)) return node.content;
|
|
52
|
+
if (typeof node.content === 'string') return Buffer.from(node.content);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function intFromBuf(buf) {
|
|
57
|
+
if (!buf) return 0;
|
|
58
|
+
let v = 0;
|
|
59
|
+
for (const b of buf) v = (v << 8) | b;
|
|
60
|
+
return v;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toSignalKey(raw) {
|
|
64
|
+
if (!raw) return null;
|
|
65
|
+
if (raw.length === 33 && raw[0] === 0x05) return Buffer.from(raw);
|
|
66
|
+
if (raw.length === 32) return Buffer.concat([Buffer.from([0x05]), raw]);
|
|
67
|
+
return Buffer.from(raw);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── WhalibmobClient ──────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
class WhalibmobClient extends EventEmitter {
|
|
73
|
+
constructor(opts) {
|
|
74
|
+
super();
|
|
75
|
+
opts = opts || {};
|
|
76
|
+
this._store = null;
|
|
77
|
+
this._socket = null;
|
|
78
|
+
this._sender = null;
|
|
79
|
+
this._signal = null;
|
|
80
|
+
this._devMgr = null;
|
|
81
|
+
this._sessionDir = opts.sessionDir || process.env.HOME + '/.waSession';
|
|
82
|
+
this._pingTimer = null;
|
|
83
|
+
this._keepTimer = null;
|
|
84
|
+
this._connected = false;
|
|
85
|
+
this._reconnecting = false;
|
|
86
|
+
this._reconnectTry = 0;
|
|
87
|
+
this._phoneNumber = null;
|
|
88
|
+
this._mediaConn = null;
|
|
89
|
+
this._pendingIqs = new Map(); // id → resolve fn
|
|
90
|
+
this._pendingAcks = new Map(); // msgId → resolve fn
|
|
91
|
+
this._groupMembers = new Map(); // groupJid → Set<memberJid>
|
|
92
|
+
this._retryPending = new Map(); // msgId → {node, retryCount}
|
|
93
|
+
this._appStateVersions = {}; // collectionName → version (int)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get store() { return this._store; }
|
|
97
|
+
get sender() { return this._sender; }
|
|
98
|
+
get connected() { return this._connected; }
|
|
99
|
+
|
|
100
|
+
// ─── Registration statics ─────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
static async register(phoneNumber, opts) {
|
|
103
|
+
return checkIfRegistered(phoneNumber, opts || {});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static async requestCode(phoneNumber, method, opts) {
|
|
107
|
+
return requestSmsCode(phoneNumber, method || 'sms', opts || {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static async confirmCode(phoneNumber, code, opts) {
|
|
111
|
+
return verifyCode(phoneNumber, code, opts || {});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Connect ──────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
async connect(phoneNumber) {
|
|
117
|
+
return this.init(phoneNumber);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async init(phoneNumber) {
|
|
121
|
+
phoneNumber = String(phoneNumber).replace(/\D/g, '');
|
|
122
|
+
this._phoneNumber = phoneNumber;
|
|
123
|
+
|
|
124
|
+
const sessionFile = path.join(this._sessionDir, `${phoneNumber}.json`);
|
|
125
|
+
this._store = loadStore(sessionFile);
|
|
126
|
+
if (!this._store) {
|
|
127
|
+
throw new Error(`No session for ${phoneNumber}. Run 'wa registration -R <code>' first.`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const signalFile = path.join(this._sessionDir, `${phoneNumber}.signal.json`);
|
|
131
|
+
const skFile = path.join(this._sessionDir, `${phoneNumber}.sk.json`);
|
|
132
|
+
|
|
133
|
+
this._signal = SignalProtocol.fromStore(this._store, signalFile, skFile);
|
|
134
|
+
this._devMgr = new DeviceManager(this);
|
|
135
|
+
|
|
136
|
+
await this._connectSocket();
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async _connectSocket() {
|
|
141
|
+
const socket = new NoiseSocket(this._store);
|
|
142
|
+
this._socket = socket;
|
|
143
|
+
|
|
144
|
+
// Recreate sender with updated socket reference
|
|
145
|
+
this._sender = new MessageSender(this);
|
|
146
|
+
|
|
147
|
+
socket.on('open', (sn) => this._onOpen(sn));
|
|
148
|
+
socket.on('node', node => this._onNode(node));
|
|
149
|
+
socket.on('close', () => this._onClose());
|
|
150
|
+
socket.on('error', err => this.emit('error', err));
|
|
151
|
+
|
|
152
|
+
await socket.connect();
|
|
153
|
+
return socket;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
disconnect() {
|
|
157
|
+
this._reconnecting = false;
|
|
158
|
+
this._reconnectTry = 0;
|
|
159
|
+
this._stopTimers();
|
|
160
|
+
if (this._socket) {
|
|
161
|
+
try { this._socket.close(); } catch (_) {}
|
|
162
|
+
this._socket = null;
|
|
163
|
+
}
|
|
164
|
+
this._connected = false;
|
|
165
|
+
this.emit('close');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Reconnection ─────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
_scheduleReconnect() {
|
|
171
|
+
if (this._reconnecting) return;
|
|
172
|
+
this._reconnecting = true;
|
|
173
|
+
const delay = RECONNECT_BACKOFF[Math.min(this._reconnectTry, RECONNECT_BACKOFF.length - 1)];
|
|
174
|
+
this._reconnectTry++;
|
|
175
|
+
this.emit('reconnecting', { delay, attempt: this._reconnectTry });
|
|
176
|
+
|
|
177
|
+
setTimeout(async () => {
|
|
178
|
+
if (!this._reconnecting) return;
|
|
179
|
+
try {
|
|
180
|
+
await this._connectSocket();
|
|
181
|
+
this._reconnecting = false;
|
|
182
|
+
this._reconnectTry = 0;
|
|
183
|
+
this.emit('reconnected');
|
|
184
|
+
} catch (err) {
|
|
185
|
+
this._reconnecting = false;
|
|
186
|
+
this._scheduleReconnect();
|
|
187
|
+
}
|
|
188
|
+
}, delay);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Connection events ────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
_onOpen(successNode) {
|
|
194
|
+
this._connected = true;
|
|
195
|
+
this._startTimers();
|
|
196
|
+
|
|
197
|
+
// ── Feature 1: Parse <success> node ──────────────────────────────────────
|
|
198
|
+
// Extract ADVSignedDeviceIdentity, platform, and other account info that
|
|
199
|
+
// the server provides on each successful authentication.
|
|
200
|
+
this._parseSuccessNode(successNode);
|
|
201
|
+
|
|
202
|
+
// ── Feature 3 (part A): Send <active> IQ ─────────────────────────────────
|
|
203
|
+
// WhatsApp opens sessions as "passive" — the client must explicitly
|
|
204
|
+
// activate the connection before the server will deliver messages.
|
|
205
|
+
this._sendActiveIq();
|
|
206
|
+
|
|
207
|
+
this._requestMediaConnection();
|
|
208
|
+
this._uploadPreKeys();
|
|
209
|
+
this.emit('connected');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Feature 1: Parse the <success> node ────────────────────────────────────
|
|
213
|
+
//
|
|
214
|
+
// The <success> node from WhatsApp contains:
|
|
215
|
+
// attrs.platform — server-reported client platform
|
|
216
|
+
// attrs.lid — linked-device ID (for multi-device)
|
|
217
|
+
// child <device-identity> — raw ADVSignedDeviceIdentity protobuf bytes
|
|
218
|
+
//
|
|
219
|
+
// ADVSignedDeviceIdentity fields (all bytes / wire type 2):
|
|
220
|
+
// 1: details — ADVDeviceIdentityDetails proto
|
|
221
|
+
// 2: accountSignatureKey — 32-byte public key (we strip this when sending)
|
|
222
|
+
// 3: accountSignature — 64-byte Ed25519 signature
|
|
223
|
+
// 4: deviceSignature — 64-byte Ed25519 signature (empty on primary)
|
|
224
|
+
//
|
|
225
|
+
// We persist the bytes so device_identity nodes can be attached to pkmsg
|
|
226
|
+
// stanzas on every subsequent send — even after a reconnect.
|
|
227
|
+
_parseSuccessNode(node) {
|
|
228
|
+
if (!node) return;
|
|
229
|
+
const attrs = node.attrs || {};
|
|
230
|
+
if (attrs.platform) this._platform = attrs.platform;
|
|
231
|
+
|
|
232
|
+
const devIdNode = findChild(node, 'device-identity');
|
|
233
|
+
if (devIdNode) {
|
|
234
|
+
const bytes = getNodeContent(devIdNode);
|
|
235
|
+
if (bytes && bytes.length > 0) {
|
|
236
|
+
this._store.advIdentity = bytes;
|
|
237
|
+
// Persist to session file so it survives restart
|
|
238
|
+
const sessionFile = path.join(
|
|
239
|
+
this._sessionDir,
|
|
240
|
+
`${this._store.phoneNumber}.json`
|
|
241
|
+
);
|
|
242
|
+
try {
|
|
243
|
+
const { saveStore } = require('./Store');
|
|
244
|
+
saveStore(this._store, sessionFile);
|
|
245
|
+
} catch (_) {}
|
|
246
|
+
}
|
|
247
|
+
} else if (this._store.advIdentity) {
|
|
248
|
+
// Already have it persisted from a previous session — keep using it
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Feature 3 (part A): Activate the connection ────────────────────────────
|
|
253
|
+
//
|
|
254
|
+
// WhatsApp starts every Noise session in "passive" mode. The client must
|
|
255
|
+
// send this IQ to become "active" so the server starts delivering messages.
|
|
256
|
+
_sendActiveIq() {
|
|
257
|
+
if (!this._socket) return;
|
|
258
|
+
const id = this._genMsgId();
|
|
259
|
+
this._socket.sendNode(new BinaryNode('iq', {
|
|
260
|
+
id,
|
|
261
|
+
to: 's.whatsapp.net',
|
|
262
|
+
type: 'set',
|
|
263
|
+
xmlns: 'passive'
|
|
264
|
+
}, [new BinaryNode('active', {}, null)]));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Feature 3 (part B): Handle <ib> dirty-state notifications ──────────────
|
|
268
|
+
//
|
|
269
|
+
// After login the server sends <ib><dirty type="account_sync"…/></ib> to
|
|
270
|
+
// tell the client that its app state is stale. We respond with a sync IQ
|
|
271
|
+
// for each dirty collection. We track the version number for each
|
|
272
|
+
// collection in _appStateVersions so we don't repeatedly request version 0.
|
|
273
|
+
_handleIb(node) {
|
|
274
|
+
const children = Array.isArray(node.content) ? node.content : [];
|
|
275
|
+
|
|
276
|
+
const dirtyTypes = [];
|
|
277
|
+
for (const child of children) {
|
|
278
|
+
if (!child || child.description !== 'dirty') continue;
|
|
279
|
+
const type = child.attrs && child.attrs.type;
|
|
280
|
+
if (type) dirtyTypes.push(type);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (dirtyTypes.length > 0) {
|
|
284
|
+
this._sendAppStateSyncForTypes(dirtyTypes);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.emit('ib', { node });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Feature 3 (part C): Send an app-state-sync IQ ──────────────────────────
|
|
291
|
+
//
|
|
292
|
+
// Maps WhatsApp dirty-type names → the actual collection names used in the
|
|
293
|
+
// sync IQ. We use return_snapshot=true on the first request (version 0) so
|
|
294
|
+
// the server sends a full snapshot; subsequent requests use the stored
|
|
295
|
+
// version and return_snapshot=false (incremental patches only).
|
|
296
|
+
//
|
|
297
|
+
// We do NOT decrypt the patches — that requires the full LTHASH machinery.
|
|
298
|
+
// We DO update our version counters from the server response so that we
|
|
299
|
+
// never re-request the same snapshot twice.
|
|
300
|
+
_sendAppStateSyncForTypes(dirtyTypes) {
|
|
301
|
+
if (!this._socket || !this._connected) return;
|
|
302
|
+
|
|
303
|
+
const COLLECTION_MAP = {
|
|
304
|
+
account_sync: ['critical_block', 'critical_unblock_low', 'regular_low', 'regular_high', 'regular'],
|
|
305
|
+
critical_block: ['critical_block'],
|
|
306
|
+
critical_unblock_low:['critical_unblock_low'],
|
|
307
|
+
regular_low: ['regular_low'],
|
|
308
|
+
regular_high: ['regular_high'],
|
|
309
|
+
regular: ['regular']
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const collections = new Set();
|
|
313
|
+
for (const type of dirtyTypes) {
|
|
314
|
+
const mapped = COLLECTION_MAP[type] || [type];
|
|
315
|
+
for (const c of mapped) collections.add(c);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const id = this._genMsgId();
|
|
319
|
+
const collectionNodes = [...collections].map(name => {
|
|
320
|
+
const version = String(this._appStateVersions[name] || 0);
|
|
321
|
+
return new BinaryNode('collection', {
|
|
322
|
+
name,
|
|
323
|
+
version,
|
|
324
|
+
return_snapshot: version === '0' ? 'true' : 'false'
|
|
325
|
+
}, null);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
this._socket.sendNode(new BinaryNode('iq', {
|
|
329
|
+
id,
|
|
330
|
+
to: 's.whatsapp.net',
|
|
331
|
+
type: 'set',
|
|
332
|
+
xmlns: 'w:sync:app:state'
|
|
333
|
+
}, [new BinaryNode('sync', {}, collectionNodes)]));
|
|
334
|
+
|
|
335
|
+
// When the server replies, update our version numbers so future syncs
|
|
336
|
+
// request incremental patches rather than a full snapshot.
|
|
337
|
+
this._pendingIqs.set(id, (resp) => {
|
|
338
|
+
if (!resp || !Array.isArray(resp.content)) return;
|
|
339
|
+
const syncNode = findChild(resp, 'sync');
|
|
340
|
+
if (!syncNode || !Array.isArray(syncNode.content)) return;
|
|
341
|
+
for (const colNode of syncNode.content) {
|
|
342
|
+
if (!colNode || colNode.description !== 'collection') continue;
|
|
343
|
+
const colAttrs = colNode.attrs || {};
|
|
344
|
+
if (colAttrs.name && colAttrs.version !== undefined) {
|
|
345
|
+
this._appStateVersions[colAttrs.name] = parseInt(colAttrs.version, 10) || 0;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_onClose() {
|
|
352
|
+
this._connected = false;
|
|
353
|
+
this._stopTimers();
|
|
354
|
+
this.emit('disconnected');
|
|
355
|
+
|
|
356
|
+
// Reject all pending IQs / acks
|
|
357
|
+
for (const [, resolve] of this._pendingIqs) {
|
|
358
|
+
resolve(null);
|
|
359
|
+
}
|
|
360
|
+
this._pendingIqs.clear();
|
|
361
|
+
|
|
362
|
+
for (const [, resolve] of this._pendingAcks) {
|
|
363
|
+
resolve({ error: 'disconnected' });
|
|
364
|
+
}
|
|
365
|
+
this._pendingAcks.clear();
|
|
366
|
+
|
|
367
|
+
this._scheduleReconnect();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ─── Timers ───────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
_startTimers() {
|
|
373
|
+
this._pingTimer = setInterval(() => {
|
|
374
|
+
if (this._sender && this._connected) this._sender.ping();
|
|
375
|
+
}, PING_INTERVAL_MS);
|
|
376
|
+
|
|
377
|
+
this._keepTimer = setInterval(() => {
|
|
378
|
+
if (this._socket && this._connected) {
|
|
379
|
+
this._socket.sendNode(new BinaryNode('iq', {
|
|
380
|
+
id: this._genMsgId(),
|
|
381
|
+
to: 's.whatsapp.net',
|
|
382
|
+
type: 'get',
|
|
383
|
+
xmlns: 'w:p'
|
|
384
|
+
}, [new BinaryNode('ping', {}, null)]));
|
|
385
|
+
}
|
|
386
|
+
}, KEEPALIVE_INTERVAL);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_stopTimers() {
|
|
390
|
+
if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
|
|
391
|
+
if (this._keepTimer) { clearInterval(this._keepTimer); this._keepTimer = null; }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Node dispatch ────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
_onNode(node) {
|
|
397
|
+
if (!node || !node.description) return;
|
|
398
|
+
const tag = node.description;
|
|
399
|
+
|
|
400
|
+
if (tag === 'iq') this._handleIq(node);
|
|
401
|
+
else if (tag === 'message') this._handleMessage(node);
|
|
402
|
+
else if (tag === 'receipt') this._handleReceipt(node);
|
|
403
|
+
else if (tag === 'ack') this._handleAck(node);
|
|
404
|
+
else if (tag === 'notification') this._handleNotification(node);
|
|
405
|
+
else if (tag === 'presence') this._handlePresence(node);
|
|
406
|
+
else if (tag === 'call') this._handleCall(node);
|
|
407
|
+
else if (tag === 'ib') this._handleIb(node);
|
|
408
|
+
else if (tag === 'success') this.emit('session_refresh', { node }); // late success (re-auth)
|
|
409
|
+
else if (tag === 'failure') this._handleFailure(node);
|
|
410
|
+
else if (tag === 'stream:error') this.emit('stream_error', { reason: node.attrs && node.attrs.reason });
|
|
411
|
+
else this.emit('node', node);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ─── IQ handling ──────────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
_handleIq(node) {
|
|
417
|
+
const id = node.attrs && node.attrs.id;
|
|
418
|
+
const type = node.attrs && node.attrs.type;
|
|
419
|
+
const xmlns = node.attrs && node.attrs.xmlns;
|
|
420
|
+
|
|
421
|
+
// Resolve pending IQ
|
|
422
|
+
if (id) {
|
|
423
|
+
const handler = this._pendingIqs.get(id);
|
|
424
|
+
if (handler) {
|
|
425
|
+
this._pendingIqs.delete(id);
|
|
426
|
+
handler(node);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Media connection result
|
|
432
|
+
if (type === 'result' && xmlns === 'w:m') {
|
|
433
|
+
this._onMediaConnectionResult(node);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Pre-key upload ack — could also replenish
|
|
437
|
+
if (type === 'result' && xmlns === 'encrypt') {
|
|
438
|
+
this._checkAndReplenishPreKeys();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.emit('iq', { id, type, xmlns, node });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── Message handling ─────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
_handleMessage(node) {
|
|
447
|
+
const attrs = node.attrs || {};
|
|
448
|
+
const from = attrs.from || '';
|
|
449
|
+
const id = attrs.id || '';
|
|
450
|
+
const ts = parseInt(attrs.t || '0', 10);
|
|
451
|
+
const participant = attrs.participant || from;
|
|
452
|
+
|
|
453
|
+
// Group: sender is the group JID, participant is the actual sender
|
|
454
|
+
const isGroup = from.endsWith('@g.us');
|
|
455
|
+
|
|
456
|
+
// Find enc node(s) — could be skmsg for groups, or msg/pkmsg for DMs
|
|
457
|
+
const encNodes = Array.isArray(node.content)
|
|
458
|
+
? node.content.filter(c => c && c.description === 'enc')
|
|
459
|
+
: [];
|
|
460
|
+
|
|
461
|
+
// Find participants node (multi-device DMs, or SKDM distribution in groups)
|
|
462
|
+
const participantsNode = findChild(node, 'participants');
|
|
463
|
+
const myJid = `${this._store.phoneNumber}@s.whatsapp.net`;
|
|
464
|
+
|
|
465
|
+
// For groups: look for skmsg enc node
|
|
466
|
+
const skmsgNode = encNodes.find(n => n.attrs && n.attrs.type === 'skmsg');
|
|
467
|
+
|
|
468
|
+
// For DMs: look for direct enc node or enc in participant targeting us
|
|
469
|
+
let dmEncNode = encNodes.find(n => n.attrs && (n.attrs.type === 'msg' || n.attrs.type === 'pkmsg'));
|
|
470
|
+
|
|
471
|
+
// Multi-device: check participants node for our JID
|
|
472
|
+
if (!dmEncNode && participantsNode && Array.isArray(participantsNode.content)) {
|
|
473
|
+
for (const toNode of participantsNode.content) {
|
|
474
|
+
if (!toNode || toNode.description !== 'to') continue;
|
|
475
|
+
const toJid = toNode.attrs && toNode.attrs.jid;
|
|
476
|
+
if (!toJid) continue;
|
|
477
|
+
// Match our main JID or any of our device JIDs
|
|
478
|
+
const toPhone = toJid.split('@')[0].split(':')[0];
|
|
479
|
+
if (toPhone === this._store.phoneNumber) {
|
|
480
|
+
const enc = findChild(toNode, 'enc');
|
|
481
|
+
if (enc) {
|
|
482
|
+
dmEncNode = enc;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Process SKDM from participants (group messages)
|
|
490
|
+
if (isGroup && participantsNode) {
|
|
491
|
+
this._processSKDMDistribution(from, participant, participantsNode);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (skmsgNode && isGroup) {
|
|
495
|
+
this._decryptGroupMessage({ node, from, participant, id, ts, skmsgNode });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (dmEncNode && this._signal) {
|
|
500
|
+
this._decryptDMMessage({ node, from, id, ts, dmEncNode });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Plain text (no encryption) — rare
|
|
505
|
+
const bodyNode = findChild(node, 'body');
|
|
506
|
+
const text = bodyNode ? (
|
|
507
|
+
Buffer.isBuffer(bodyNode.content)
|
|
508
|
+
? bodyNode.content.toString('utf8')
|
|
509
|
+
: (bodyNode.content || null)
|
|
510
|
+
) : null;
|
|
511
|
+
|
|
512
|
+
this.emit('message', { id, from, participant, ts, text, node });
|
|
513
|
+
this._sendReadReceipt(id, from, participant);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_decryptDMMessage({ node, from, id, ts, dmEncNode }) {
|
|
517
|
+
const encType = dmEncNode.attrs && dmEncNode.attrs.type;
|
|
518
|
+
const cipherBuf = Buffer.isBuffer(dmEncNode.content)
|
|
519
|
+
? dmEncNode.content
|
|
520
|
+
: Buffer.from(dmEncNode.content || '');
|
|
521
|
+
|
|
522
|
+
this._signal.decrypt(from, encType, cipherBuf)
|
|
523
|
+
.then(plaintext => {
|
|
524
|
+
this.emit('message', { id, from, participant: from, ts, plaintext, node });
|
|
525
|
+
this._sendReadReceipt(id, from, from);
|
|
526
|
+
})
|
|
527
|
+
.catch(err => {
|
|
528
|
+
this.emit('decrypt_error', { id, from, err });
|
|
529
|
+
this._sendRetryRequest(id, from, node);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
_decryptGroupMessage({ node, from, participant, id, ts, skmsgNode }) {
|
|
534
|
+
const cipherBuf = Buffer.isBuffer(skmsgNode.content)
|
|
535
|
+
? skmsgNode.content
|
|
536
|
+
: Buffer.from(skmsgNode.content || '');
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const plaintext = this._signal.senderKeyDecrypt(from, participant, cipherBuf);
|
|
540
|
+
this.emit('message', { id, from, participant, ts, plaintext, isGroup: true, node });
|
|
541
|
+
this._sendReadReceipt(id, from, participant);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
this.emit('decrypt_error', { id, from, participant, err });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Process SKDM messages in participants node
|
|
548
|
+
_processSKDMDistribution(groupJid, senderJid, participantsNode) {
|
|
549
|
+
// For incoming group messages, the SKDM for us is in the participants node
|
|
550
|
+
if (!Array.isArray(participantsNode.content)) return;
|
|
551
|
+
for (const toNode of participantsNode.content) {
|
|
552
|
+
if (!toNode || toNode.description !== 'to') continue;
|
|
553
|
+
const toJid = toNode.attrs && toNode.attrs.jid;
|
|
554
|
+
if (!toJid) continue;
|
|
555
|
+
const toPhone = toJid.split('@')[0].split(':')[0];
|
|
556
|
+
if (toPhone !== String(this._store.phoneNumber)) continue;
|
|
557
|
+
|
|
558
|
+
const enc = findChild(toNode, 'enc');
|
|
559
|
+
if (!enc) continue;
|
|
560
|
+
|
|
561
|
+
const encType = enc.attrs && enc.attrs.type;
|
|
562
|
+
const cipherBuf = Buffer.isBuffer(enc.content) ? enc.content : Buffer.from(enc.content || '');
|
|
563
|
+
|
|
564
|
+
// Decrypt SKDM with our Signal session
|
|
565
|
+
this._signal.decrypt(senderJid, encType, cipherBuf)
|
|
566
|
+
.then(skdmBytes => {
|
|
567
|
+
this._signal.processSKDM(groupJid, senderJid, skdmBytes);
|
|
568
|
+
})
|
|
569
|
+
.catch(() => {});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ─── Receipt / ack ────────────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
_handleReceipt(node) {
|
|
576
|
+
const attrs = node.attrs || {};
|
|
577
|
+
this.emit('receipt', {
|
|
578
|
+
id: attrs.id || '',
|
|
579
|
+
from: attrs.from || '',
|
|
580
|
+
type: attrs.type || ''
|
|
581
|
+
});
|
|
582
|
+
// ACK the receipt back to server
|
|
583
|
+
if (this._socket && this._connected) {
|
|
584
|
+
this._socket.sendNode(new BinaryNode('ack', {
|
|
585
|
+
id: attrs.id || '',
|
|
586
|
+
class: 'receipt',
|
|
587
|
+
to: attrs.from || ''
|
|
588
|
+
}, null));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
_handleAck(node) {
|
|
593
|
+
const attrs = node.attrs || {};
|
|
594
|
+
const id = attrs.id || '';
|
|
595
|
+
const cls = attrs.class || '';
|
|
596
|
+
|
|
597
|
+
if (cls === 'message') {
|
|
598
|
+
const handler = this._pendingAcks.get(id);
|
|
599
|
+
if (handler) {
|
|
600
|
+
this._pendingAcks.delete(id);
|
|
601
|
+
handler(attrs);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
_sendReadReceipt(msgId, from, participant) {
|
|
607
|
+
if (!this._socket || !this._connected) return;
|
|
608
|
+
const node = new BinaryNode('receipt', {
|
|
609
|
+
id: msgId,
|
|
610
|
+
type: 'read',
|
|
611
|
+
to: from
|
|
612
|
+
}, null);
|
|
613
|
+
if (participant && participant !== from) {
|
|
614
|
+
node.attrs.participant = participant;
|
|
615
|
+
}
|
|
616
|
+
this._socket.sendNode(node);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ─── Retry request (when decryption fails) ────────────────────────────────
|
|
620
|
+
|
|
621
|
+
_sendRetryRequest(msgId, from, origNode) {
|
|
622
|
+
const pending = this._retryPending.get(msgId);
|
|
623
|
+
const count = pending ? pending.count + 1 : 1;
|
|
624
|
+
if (count > 3) return;
|
|
625
|
+
this._retryPending.set(msgId, { node: origNode, count });
|
|
626
|
+
|
|
627
|
+
if (!this._socket || !this._connected) return;
|
|
628
|
+
this._socket.sendNode(new BinaryNode('receipt', {
|
|
629
|
+
id: msgId,
|
|
630
|
+
type: 'retry',
|
|
631
|
+
to: from,
|
|
632
|
+
t: String(Math.floor(Date.now() / 1000))
|
|
633
|
+
}, [
|
|
634
|
+
new BinaryNode('retry', { count: String(count), id: msgId, t: String(Math.floor(Date.now() / 1000)), v: '1' }, null),
|
|
635
|
+
new BinaryNode('registration', {}, intToBytes(this._store.registrationId, 4))
|
|
636
|
+
]));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ─── Notification handling ────────────────────────────────────────────────
|
|
640
|
+
|
|
641
|
+
_handleNotification(node) {
|
|
642
|
+
const attrs = node.attrs || {};
|
|
643
|
+
const type = attrs.type || '';
|
|
644
|
+
|
|
645
|
+
this.emit('notification', { type, attrs, node });
|
|
646
|
+
|
|
647
|
+
if (type === 'encrypt') {
|
|
648
|
+
// Server tells us pre-keys are running low
|
|
649
|
+
this._checkAndReplenishPreKeys();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (type === 'account_sync') {
|
|
653
|
+
// Account sync notification — may contain device list updates
|
|
654
|
+
const devicesNode = findChildDeep(node, 'devices');
|
|
655
|
+
if (devicesNode) this._processDeviceUpdate(devicesNode);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (type === 'w:gp2') {
|
|
659
|
+
// Group update: member add/remove
|
|
660
|
+
this._processGroupUpdate(node);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Ack the notification
|
|
664
|
+
if (this._socket && this._connected) {
|
|
665
|
+
this._socket.sendNode(new BinaryNode('ack', {
|
|
666
|
+
id: attrs.id || '',
|
|
667
|
+
class: 'notification',
|
|
668
|
+
type: type,
|
|
669
|
+
to: attrs.from || 's.whatsapp.net'
|
|
670
|
+
}, null));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
_processDeviceUpdate(devicesNode) {
|
|
675
|
+
if (!Array.isArray(devicesNode.content)) return;
|
|
676
|
+
for (const deviceNode of devicesNode.content) {
|
|
677
|
+
if (!deviceNode || deviceNode.description !== 'device') continue;
|
|
678
|
+
const jid = deviceNode.attrs && deviceNode.attrs.jid;
|
|
679
|
+
if (jid && this._devMgr) {
|
|
680
|
+
const phone = jid.split('@')[0].split(':')[0];
|
|
681
|
+
const device = parseInt((jid.split(':')[1] || '0').split('@')[0], 10);
|
|
682
|
+
if (!this._devMgr._deviceCache.has(phone)) this._devMgr._deviceCache.set(phone, new Set());
|
|
683
|
+
this._devMgr._deviceCache.get(phone).add(device);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
_processGroupUpdate(node) {
|
|
689
|
+
const groupJid = node.attrs && node.attrs.from;
|
|
690
|
+
if (!groupJid) return;
|
|
691
|
+
if (!this._groupMembers.has(groupJid)) this._groupMembers.set(groupJid, new Set());
|
|
692
|
+
const members = this._groupMembers.get(groupJid);
|
|
693
|
+
|
|
694
|
+
if (!Array.isArray(node.content)) return;
|
|
695
|
+
for (const child of node.content) {
|
|
696
|
+
if (!child) continue;
|
|
697
|
+
if (child.description === 'add' || child.description === 'promote') {
|
|
698
|
+
for (const p of (child.content || [])) {
|
|
699
|
+
if (p && p.description === 'participant' && p.attrs && p.attrs.jid) {
|
|
700
|
+
members.add(p.attrs.jid);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (child.description === 'remove' || child.description === 'demote') {
|
|
705
|
+
for (const p of (child.content || [])) {
|
|
706
|
+
if (p && p.description === 'participant' && p.attrs && p.attrs.jid) {
|
|
707
|
+
members.delete(p.attrs.jid);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ─── Presence ─────────────────────────────────────────────────────────────
|
|
715
|
+
|
|
716
|
+
_handlePresence(node) {
|
|
717
|
+
const attrs = node.attrs || {};
|
|
718
|
+
this.emit('presence', {
|
|
719
|
+
from: attrs.from || '',
|
|
720
|
+
available: attrs.type !== 'unavailable'
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ─── Call ─────────────────────────────────────────────────────────────────
|
|
725
|
+
|
|
726
|
+
_handleCall(node) {
|
|
727
|
+
const attrs = node.attrs || {};
|
|
728
|
+
this.emit('call', { from: attrs.from || '', node });
|
|
729
|
+
// Reject call automatically
|
|
730
|
+
if (this._socket && this._connected) {
|
|
731
|
+
const offer = findChild(node, 'offer');
|
|
732
|
+
const callId = offer && offer.attrs && offer.attrs.call_id;
|
|
733
|
+
if (callId) {
|
|
734
|
+
this._socket.sendNode(new BinaryNode('call', {
|
|
735
|
+
to: attrs.from || '',
|
|
736
|
+
id: this._genMsgId()
|
|
737
|
+
}, [new BinaryNode('reject', { call_id: callId, call_creator: attrs.from || '' }, null)]));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ─── Auth failure (server forces logout during active session) ──────────────
|
|
743
|
+
|
|
744
|
+
_handleFailure(node) {
|
|
745
|
+
const attrs = (node && node.attrs) || {};
|
|
746
|
+
const reason = attrs.reason || attrs.location || 'unknown';
|
|
747
|
+
this.emit('auth_failure', { reason, node });
|
|
748
|
+
// The session is no longer valid — stop reconnecting and disconnect
|
|
749
|
+
this._reconnecting = false;
|
|
750
|
+
this.disconnect();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ─── Media connection ─────────────────────────────────────────────────────
|
|
754
|
+
|
|
755
|
+
_requestMediaConnection() {
|
|
756
|
+
if (!this._socket || !this._connected) return;
|
|
757
|
+
this._socket.sendNode(new BinaryNode('iq', {
|
|
758
|
+
id: this._genMsgId(),
|
|
759
|
+
to: 's.whatsapp.net',
|
|
760
|
+
type: 'set',
|
|
761
|
+
xmlns: 'w:m'
|
|
762
|
+
}, [new BinaryNode('media_conn', {}, null)]));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
_onMediaConnectionResult(node) {
|
|
766
|
+
const connNode = findChild(node, 'media_conn');
|
|
767
|
+
if (!connNode) return;
|
|
768
|
+
const auth = (connNode.attrs && connNode.attrs.auth) || '';
|
|
769
|
+
const hosts = [];
|
|
770
|
+
if (Array.isArray(connNode.content)) {
|
|
771
|
+
for (const child of connNode.content) {
|
|
772
|
+
if (child && child.description === 'host') {
|
|
773
|
+
const hostname = child.attrs && child.attrs.hostname;
|
|
774
|
+
if (hostname) hosts.push(hostname);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
this._mediaConn = { hosts, auth };
|
|
779
|
+
if (this._sender) this._sender.setMediaConnection(hosts, auth);
|
|
780
|
+
this.emit('media_conn', { hosts, auth });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ─── Pre-key upload ───────────────────────────────────────────────────────
|
|
784
|
+
|
|
785
|
+
_uploadPreKeys() {
|
|
786
|
+
if (!this._signal || !this._socket || !this._connected) return;
|
|
787
|
+
|
|
788
|
+
const preKeys = this._signal.getPreKeysForUpload(50);
|
|
789
|
+
const spk = this._signal.getSignedPreKeyForUpload();
|
|
790
|
+
const identKey = this._signal.getIdentityKey();
|
|
791
|
+
if (!preKeys.length || !spk) return;
|
|
792
|
+
|
|
793
|
+
const preKeyNodes = preKeys.map(pk => new BinaryNode('key', {}, [
|
|
794
|
+
new BinaryNode('id', {}, intToBytes(pk.keyId, 3)),
|
|
795
|
+
new BinaryNode('value', {}, stripKeyPrefix(pk.pubKey))
|
|
796
|
+
]));
|
|
797
|
+
|
|
798
|
+
const skNode = new BinaryNode('skey', {}, [
|
|
799
|
+
new BinaryNode('id', {}, intToBytes(spk.keyId, 3)),
|
|
800
|
+
new BinaryNode('value', {}, stripKeyPrefix(spk.keyPair.pubKey)),
|
|
801
|
+
new BinaryNode('signature', {}, spk.signature)
|
|
802
|
+
]);
|
|
803
|
+
|
|
804
|
+
this._socket.sendNode(new BinaryNode('iq', {
|
|
805
|
+
id: this._genMsgId(),
|
|
806
|
+
to: 's.whatsapp.net',
|
|
807
|
+
type: 'set',
|
|
808
|
+
xmlns: 'encrypt'
|
|
809
|
+
}, [
|
|
810
|
+
new BinaryNode('registration', {}, intToBytes(this._store.registrationId, 4)),
|
|
811
|
+
new BinaryNode('type', {}, Buffer.from([5])),
|
|
812
|
+
new BinaryNode('identity', {}, stripKeyPrefix(identKey)),
|
|
813
|
+
new BinaryNode('list', {}, preKeyNodes),
|
|
814
|
+
skNode
|
|
815
|
+
]));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
_checkAndReplenishPreKeys() {
|
|
819
|
+
if (!this._signal) return;
|
|
820
|
+
const count = this._signal.preKeyCount();
|
|
821
|
+
if (count < 20) {
|
|
822
|
+
const newKeys = this._signal.replenishPreKeys();
|
|
823
|
+
if (newKeys.length > 0) this._uploadPreKeys();
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// ─── IQ helper (send + await response) ────────────────────────────────────
|
|
828
|
+
|
|
829
|
+
_sendIq(node) {
|
|
830
|
+
return new Promise((resolve, reject) => {
|
|
831
|
+
const id = node.attrs && node.attrs.id;
|
|
832
|
+
if (!id) return reject(new Error('IQ node has no id'));
|
|
833
|
+
|
|
834
|
+
const timer = setTimeout(() => {
|
|
835
|
+
this._pendingIqs.delete(id);
|
|
836
|
+
resolve(null); // Resolve null on timeout rather than reject
|
|
837
|
+
}, 15000);
|
|
838
|
+
|
|
839
|
+
this._pendingIqs.set(id, (result) => {
|
|
840
|
+
clearTimeout(timer);
|
|
841
|
+
resolve(result);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
this._socket.sendNode(node);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
clearTimeout(timer);
|
|
848
|
+
this._pendingIqs.delete(id);
|
|
849
|
+
reject(err);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// ─── Group metadata ───────────────────────────────────────────────────────
|
|
855
|
+
|
|
856
|
+
async getGroupMetadata(groupJid) {
|
|
857
|
+
const id = this._genMsgId();
|
|
858
|
+
const node = new BinaryNode('iq', {
|
|
859
|
+
id,
|
|
860
|
+
to: groupJid,
|
|
861
|
+
type: 'get',
|
|
862
|
+
xmlns: 'w:g2'
|
|
863
|
+
}, [new BinaryNode('query', { request: 'interactive' }, null)]);
|
|
864
|
+
|
|
865
|
+
const response = await this._sendIq(node);
|
|
866
|
+
if (!response) return null;
|
|
867
|
+
|
|
868
|
+
// Parse members from response and update cache
|
|
869
|
+
const groupNode = findChild(response, 'group');
|
|
870
|
+
if (groupNode && Array.isArray(groupNode.content)) {
|
|
871
|
+
if (!this._groupMembers.has(groupJid)) this._groupMembers.set(groupJid, new Set());
|
|
872
|
+
const members = this._groupMembers.get(groupJid);
|
|
873
|
+
for (const child of groupNode.content) {
|
|
874
|
+
if (child && child.description === 'participant' && child.attrs && child.attrs.jid) {
|
|
875
|
+
members.add(child.attrs.jid);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return response;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ─── Group member helpers ─────────────────────────────────────────────────
|
|
883
|
+
|
|
884
|
+
_getGroupMembers(groupJid) {
|
|
885
|
+
const members = this._groupMembers.get(groupJid);
|
|
886
|
+
return members ? [...members] : [];
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async refreshGroupMembers(groupJid) {
|
|
890
|
+
await this.getGroupMetadata(groupJid);
|
|
891
|
+
return this._getGroupMembers(groupJid);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ─── Message ID generator ─────────────────────────────────────────────────
|
|
895
|
+
|
|
896
|
+
_genMsgId() {
|
|
897
|
+
return crypto.randomBytes(8).toString('hex').toUpperCase();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
901
|
+
|
|
902
|
+
setPresence(available) {
|
|
903
|
+
if (this._sender) this._sender.sendPresence(available);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
markRead(jid, messageIds) {
|
|
907
|
+
if (this._sender) this._sender.markRead(jid, messageIds);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async sendText(to, text, opts) {
|
|
911
|
+
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
912
|
+
return this._sender.sendText(to, text, opts);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async sendImage(to, data, opts) {
|
|
916
|
+
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
917
|
+
return this._sender.sendImage(to, data, opts);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async sendVideo(to, data, opts) {
|
|
921
|
+
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
922
|
+
return this._sender.sendVideo(to, data, opts);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async sendAudio(to, data, opts) {
|
|
926
|
+
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
927
|
+
return this._sender.sendAudio(to, data, opts);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async sendDocument(to, data, opts) {
|
|
931
|
+
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
932
|
+
return this._sender.sendDocument(to, data, opts);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async sendSticker(to, data, opts) {
|
|
936
|
+
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
937
|
+
return this._sender.sendSticker(to, data, opts);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async sendReaction(to, messageId, emoji, opts) {
|
|
941
|
+
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
942
|
+
return this._sender.sendReaction(to, messageId, emoji, opts);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
module.exports = {
|
|
947
|
+
WhalibmobClient,
|
|
948
|
+
checkIfRegistered,
|
|
949
|
+
checkNumberStatus,
|
|
950
|
+
requestSmsCode,
|
|
951
|
+
verifyCode,
|
|
952
|
+
createNewStore,
|
|
953
|
+
saveStore,
|
|
954
|
+
loadStore,
|
|
955
|
+
toSixParts,
|
|
956
|
+
fromSixParts
|
|
957
|
+
};
|