whalibmob 5.5.83 → 5.5.86
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 +37 -0
- package/cli.js +38 -9
- package/lib/Client.js +566 -95
- package/lib/MediaService.js +3 -0
- package/lib/MediaThumbnail.js +394 -0
- package/lib/messages/MessageSender.js +112 -44
- package/lib/proto/MessageProto.js +110 -10
- package/package.json +5 -2
package/lib/Client.js
CHANGED
|
@@ -21,7 +21,7 @@ const { NodeCache } = require('@cacheable/node-cache');
|
|
|
21
21
|
// Group metadata cache lifetime — matches DeviceManager's device cache.
|
|
22
22
|
const GROUP_META_CACHE_TTL = '5m';
|
|
23
23
|
const { SignalProtocol } = require('./signal/SignalProtocol');
|
|
24
|
-
const { DeviceManager }
|
|
24
|
+
const { DeviceManager, jidStrToObj } = require('./DeviceManager');
|
|
25
25
|
const {
|
|
26
26
|
processHistorySyncNotification,
|
|
27
27
|
decodeHistorySyncNotification,
|
|
@@ -30,6 +30,22 @@ const {
|
|
|
30
30
|
HistorySyncType
|
|
31
31
|
} = require('./HistorySyncHandler');
|
|
32
32
|
|
|
33
|
+
// The two prefixes a call-link token is wrapped in, as WhatsApp Web uses them.
|
|
34
|
+
const CALL_AUDIO_PREFIX = 'https://call.whatsapp.com/voice/';
|
|
35
|
+
const CALL_VIDEO_PREFIX = 'https://call.whatsapp.com/video/';
|
|
36
|
+
|
|
37
|
+
// whatsmeow's InviteLinkPrefix — the one prefix group invite codes are wrapped in.
|
|
38
|
+
const INVITE_LINK_PREFIX = 'https://chat.whatsapp.com/';
|
|
39
|
+
|
|
40
|
+
// Accept a full invite URL or a bare code and return the code.
|
|
41
|
+
function stripInviteUrl(codeOrUrl) {
|
|
42
|
+
return String(codeOrUrl || '')
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/^https?:\/\/chat\.whatsapp\.com\//i, '')
|
|
45
|
+
.replace(/[?#].*$/, '')
|
|
46
|
+
.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
const PING_INTERVAL_MS = 25000;
|
|
34
50
|
const KEEPALIVE_INTERVAL = 20000;
|
|
35
51
|
const RECONNECT_BACKOFF = [1000, 2000, 4000, 8000, 15000, 30000];
|
|
@@ -163,6 +179,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
163
179
|
this._tcTokenStore = null; // TcTokenStore — loaded in init()
|
|
164
180
|
this._inFlightTcTokenIssuance = new Set(); // dedupe concurrent proactive issuePrivacyTokens per JID
|
|
165
181
|
this._inFlight463Recoveries = new Set(); // dedupe concurrent 463-triggered token issuances per JID (separate from proactive)
|
|
182
|
+
// Blue-tick every message as it arrives. Set to false for whatsmeow's model,
|
|
183
|
+
// where read is only ever sent by an explicit markRead() call.
|
|
184
|
+
this.autoRead = opts.autoRead !== false;
|
|
185
|
+
// Flipped by setOnline(). Delivery receipts sent while this is false carry
|
|
186
|
+
// type="inactive", which the sender's app records but does not render.
|
|
187
|
+
this._sendActiveReceipts = false;
|
|
166
188
|
if (opts.pino !== undefined) _whaConfigLogger(opts.pino);
|
|
167
189
|
}
|
|
168
190
|
|
|
@@ -667,6 +689,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
667
689
|
|
|
668
690
|
if (skmsgNode && isGroup) {
|
|
669
691
|
this._sendMessageAck(id, fromRaw, partRaw);
|
|
692
|
+
this._sendDeliveryReceipt(id, fromRaw, partRaw);
|
|
670
693
|
// SKDM must be processed (awaited) before we attempt skmsg decryption,
|
|
671
694
|
// because the first message from a sender always bundles both together.
|
|
672
695
|
if (participantsNode) {
|
|
@@ -678,6 +701,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
678
701
|
|
|
679
702
|
if (dmEncNode && this._signal) {
|
|
680
703
|
this._sendMessageAck(id, fromRaw, partRaw);
|
|
704
|
+
this._sendDeliveryReceipt(id, fromRaw, partRaw);
|
|
681
705
|
this._decryptDMMessage({ node, from, participant, fromRaw, partRaw, senderPn, id, ts, dmEncNode });
|
|
682
706
|
return;
|
|
683
707
|
}
|
|
@@ -690,6 +714,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
690
714
|
) : null;
|
|
691
715
|
|
|
692
716
|
this._sendMessageAck(id, fromRaw, partRaw);
|
|
717
|
+
this._sendDeliveryReceipt(id, fromRaw, partRaw);
|
|
693
718
|
this.emit('message', { id, from, participant, ts, text, node });
|
|
694
719
|
this._sendReadReceipt(id, fromRaw, partRaw);
|
|
695
720
|
}
|
|
@@ -1009,15 +1034,55 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1009
1034
|
this._socket.sendNode(new BinaryNode('ack', attrs, null));
|
|
1010
1035
|
}
|
|
1011
1036
|
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1037
|
+
// True when a JID names this account, in either address family.
|
|
1038
|
+
_isOwnUser(jid) {
|
|
1039
|
+
if (!jid) return false;
|
|
1040
|
+
const user = String(jid).split('@')[0].split(':')[0];
|
|
1041
|
+
if (!user) return false;
|
|
1042
|
+
if (user === String(this._store && this._store.phoneNumber)) return true;
|
|
1043
|
+
const ownLid = this._myLid ? String(this._myLid).split('@')[0].split(':')[0] : null;
|
|
1044
|
+
return !!ownLid && user === ownLid;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Delivery receipt — the sender's two grey ticks.
|
|
1048
|
+
//
|
|
1049
|
+
// Distinct from the <ack> above: the ack tells the server we took the stanza,
|
|
1050
|
+
// the receipt tells the sender it reached a device. whatsmeow sends both for
|
|
1051
|
+
// every incoming message (sendAck + sendMessageReceipt) and leaves the blue
|
|
1052
|
+
// ticks to an explicit MarkRead. whalibmob only ever sent the ack and then
|
|
1053
|
+
// jumped straight to type="read", so a sender never saw the grey stage.
|
|
1054
|
+
//
|
|
1055
|
+
// A client that has not marked itself available sends this with
|
|
1056
|
+
// type="inactive": the sender's app records the delivery but does not render
|
|
1057
|
+
// it, which is how WhatsApp Web behaves in the background.
|
|
1058
|
+
_sendDeliveryReceipt(msgId, fromJid, partJid) {
|
|
1059
|
+
if (!this._socket || !this._connected) return;
|
|
1060
|
+
const attrs = { id: msgId, to: fromJid };
|
|
1061
|
+
if (partJid && String(partJid) !== String(fromJid)) attrs.participant = partJid;
|
|
1062
|
+
// A message that came from one of our own other devices is receipted with
|
|
1063
|
+
// type="sender", never as a delivery to ourselves.
|
|
1064
|
+
if (this._isOwnUser(partJid || fromJid)) {
|
|
1065
|
+
attrs.type = 'sender';
|
|
1066
|
+
} else if (!this._sendActiveReceipts) {
|
|
1067
|
+
attrs.type = 'inactive';
|
|
1068
|
+
}
|
|
1069
|
+
this._socket.sendNode(new BinaryNode('receipt', attrs, null));
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Read receipt — the sender's two blue ticks.
|
|
1073
|
+
//
|
|
1074
|
+
// Sent automatically after a message decodes unless autoRead is turned off, in
|
|
1075
|
+
// which case marking a chat read is up to the caller via markRead(). Turning
|
|
1076
|
+
// it off gives whatsmeow's model, where read is always explicit.
|
|
1014
1077
|
// fromJid / partJid are raw JID objects (AD_JID encoding for correct routing).
|
|
1015
1078
|
_sendReadReceipt(msgId, fromJid, partJid) {
|
|
1016
1079
|
if (!this._socket || !this._connected) return;
|
|
1080
|
+
if (this.autoRead === false) return;
|
|
1017
1081
|
const node = new BinaryNode('receipt', {
|
|
1018
1082
|
id: msgId,
|
|
1019
1083
|
type: 'read',
|
|
1020
|
-
to: fromJid
|
|
1084
|
+
to: fromJid,
|
|
1085
|
+
t: String(Math.floor(Date.now() / 1000))
|
|
1021
1086
|
}, null);
|
|
1022
1087
|
if (partJid && String(partJid) !== String(fromJid)) {
|
|
1023
1088
|
node.attrs.participant = partJid;
|
|
@@ -1331,6 +1396,20 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1331
1396
|
|
|
1332
1397
|
_handleCall(node) {
|
|
1333
1398
|
const attrs = node.attrs || {};
|
|
1399
|
+
|
|
1400
|
+
// A <call> can also be the answer to something we asked for — creating a
|
|
1401
|
+
// call link, for one. Those come back on the same tag with the id we sent,
|
|
1402
|
+
// so they have to be handed to the waiting caller and not treated as an
|
|
1403
|
+
// incoming call to reject.
|
|
1404
|
+
if (attrs.id) {
|
|
1405
|
+
const handler = this._pendingIqs.get(attrs.id);
|
|
1406
|
+
if (handler) {
|
|
1407
|
+
this._pendingIqs.delete(attrs.id);
|
|
1408
|
+
handler(node);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1334
1413
|
this.emit('call', { from: attrs.from || '', node });
|
|
1335
1414
|
// Reject call automatically
|
|
1336
1415
|
if (this._socket && this._connected) {
|
|
@@ -1523,8 +1602,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1523
1602
|
return { jid: pJid, role: pRole };
|
|
1524
1603
|
});
|
|
1525
1604
|
|
|
1526
|
-
// description
|
|
1605
|
+
// description — the id is the "topic id" the server wants echoed back as
|
|
1606
|
+
// prev= on the next edit, so it has to survive the parse.
|
|
1527
1607
|
const descNode = children.find(n => n && n.description === 'description');
|
|
1608
|
+
const descriptionId = (descNode && descNode.attrs && descNode.attrs.id)
|
|
1609
|
+
? String(descNode.attrs.id) : null;
|
|
1528
1610
|
let description = null;
|
|
1529
1611
|
if (descNode) {
|
|
1530
1612
|
const bodyNode = Array.isArray(descNode.content)
|
|
@@ -1563,6 +1645,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1563
1645
|
subjectTime: parseInt(a.s_t, 10) || 0,
|
|
1564
1646
|
subjectBy: a.s_o || null,
|
|
1565
1647
|
description,
|
|
1648
|
+
descriptionId,
|
|
1566
1649
|
ephemeral,
|
|
1567
1650
|
onlyAdminsSend: announce,
|
|
1568
1651
|
onlyAdminsEdit: restrict,
|
|
@@ -2182,12 +2265,54 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2182
2265
|
|
|
2183
2266
|
// ─── Public API ───────────────────────────────────────────────────────────
|
|
2184
2267
|
|
|
2268
|
+
// Alias kept from the original API — setOnline is the implementation.
|
|
2185
2269
|
setPresence(available) {
|
|
2186
|
-
|
|
2270
|
+
return this.setOnline(available);
|
|
2187
2271
|
}
|
|
2188
2272
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2273
|
+
// Mark messages as read — the sender's two blue ticks.
|
|
2274
|
+
//
|
|
2275
|
+
// jid chat the messages are in (the group JID for a group)
|
|
2276
|
+
// messageIds one id or an array of ids, all from the SAME sender
|
|
2277
|
+
// opts { participant, type } — participant is who sent them and is
|
|
2278
|
+
// required in a group; type may be 'read' (default) or 'played'
|
|
2279
|
+
// to mark a voice note as listened to.
|
|
2280
|
+
//
|
|
2281
|
+
// Shaped the way whatsmeow's MarkRead builds it: the FIRST id on the receipt
|
|
2282
|
+
// and the rest as <item> children of a <list> node. whalibmob used the last id
|
|
2283
|
+
// and put the items straight into <receipt>, which is not a shape the server
|
|
2284
|
+
// reads back — the extra ids were silently ignored, so only one message per
|
|
2285
|
+
// call was ever marked. The participant was missing entirely, so a group read
|
|
2286
|
+
// never told the server whose message had been read.
|
|
2287
|
+
markRead(jid, messageIds, opts) {
|
|
2288
|
+
if (!this._socket || !this._connected) return;
|
|
2289
|
+
const ids = (Array.isArray(messageIds) ? messageIds : [messageIds])
|
|
2290
|
+
.map(String).filter(Boolean);
|
|
2291
|
+
if (ids.length === 0) return;
|
|
2292
|
+
|
|
2293
|
+
opts = opts || {};
|
|
2294
|
+
const chatJid = makeJid(jid);
|
|
2295
|
+
const isGroup = chatJid.endsWith('@g.us') || chatJid.endsWith('@community')
|
|
2296
|
+
|| chatJid.endsWith('@broadcast');
|
|
2297
|
+
|
|
2298
|
+
const attrs = {
|
|
2299
|
+
id: ids[0],
|
|
2300
|
+
type: opts.type === 'played' ? 'played' : 'read',
|
|
2301
|
+
to: jidStrToObj(chatJid),
|
|
2302
|
+
t: String(Math.floor(Date.now() / 1000))
|
|
2303
|
+
};
|
|
2304
|
+
// Only group and broadcast receipts carry a participant; on a 1:1 chat the
|
|
2305
|
+
// `to` already identifies the sender.
|
|
2306
|
+
if (isGroup && opts.participant) {
|
|
2307
|
+
attrs.participant = jidStrToObj(makeJid(opts.participant));
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
const children = ids.length > 1
|
|
2311
|
+
? [new BinaryNode('list', {}, ids.slice(1).map(
|
|
2312
|
+
id => new BinaryNode('item', { id }, null)))]
|
|
2313
|
+
: null;
|
|
2314
|
+
|
|
2315
|
+
this._socket.sendNode(new BinaryNode('receipt', attrs, children));
|
|
2191
2316
|
}
|
|
2192
2317
|
|
|
2193
2318
|
async sendText(to, text, opts) {
|
|
@@ -2229,43 +2354,89 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2229
2354
|
// PRESENCE
|
|
2230
2355
|
// ──────────────────────────────────────────────────────────────────────────
|
|
2231
2356
|
|
|
2232
|
-
//
|
|
2357
|
+
// Our own JID in the family a chat is addressed with — the LID when the chat
|
|
2358
|
+
// is LID-addressed, the phone JID otherwise. chatstate carries a `from`, and
|
|
2359
|
+
// it has to be in the same space as the `to` beside it.
|
|
2360
|
+
_ownJidFor(chatJid) {
|
|
2361
|
+
const str = String(chatJid || '');
|
|
2362
|
+
if (str.endsWith('@lid') && this._myLid) {
|
|
2363
|
+
return String(this._myLid).split('@')[0].split(':')[0] + '@lid';
|
|
2364
|
+
}
|
|
2365
|
+
return String(this._store && this._store.phoneNumber) + '@s.whatsapp.net';
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// Set global online/offline status.
|
|
2369
|
+
//
|
|
2370
|
+
// whatsmeow refuses to send this without a push name (ErrNoPushName): the
|
|
2371
|
+
// server takes the display name from this node, and sending it empty makes
|
|
2372
|
+
// every other user see "-" instead of a name. Going available also flips the
|
|
2373
|
+
// client into sending active delivery receipts — see _sendDeliveryReceipt.
|
|
2374
|
+
//
|
|
2375
|
+
// Returns true when the node went out, false when it was skipped.
|
|
2233
2376
|
setOnline(available) {
|
|
2234
|
-
if (!this._socket || !this._connected) return;
|
|
2235
|
-
const name = (this._store && this._store.pushName) || '';
|
|
2377
|
+
if (!this._socket || !this._connected) return false;
|
|
2378
|
+
const name = (this._store && (this._store.pushName || this._store.name)) || '';
|
|
2379
|
+
if (!name) {
|
|
2380
|
+
_whaDbg('[DBG] PRESENCE skipped: no push name set — other users would see "-"');
|
|
2381
|
+
return false;
|
|
2382
|
+
}
|
|
2383
|
+
this._sendActiveReceipts = !!available;
|
|
2236
2384
|
this._socket.sendNode(new BinaryNode('presence', {
|
|
2237
2385
|
name,
|
|
2238
2386
|
type: available ? 'available' : 'unavailable'
|
|
2239
2387
|
}, null));
|
|
2388
|
+
return true;
|
|
2240
2389
|
}
|
|
2241
2390
|
|
|
2242
|
-
// Set typing/recording presence in a specific chat
|
|
2391
|
+
// Set typing/recording presence in a specific chat.
|
|
2243
2392
|
// presence: 'composing' | 'paused' | 'recording'
|
|
2393
|
+
//
|
|
2394
|
+
// whatsmeow's SendChatPresence puts both `from` (our own JID) and `to` on the
|
|
2395
|
+
// chatstate node and expresses recording as composing with media="audio" —
|
|
2396
|
+
// there is no separate recording state in the protocol. The `from` was missing
|
|
2397
|
+
// here, and both JIDs were raw strings, which the server ignores for @lid
|
|
2398
|
+
// chats, so the indicator never showed for a LID-addressed contact.
|
|
2244
2399
|
setChatPresence(chatJid, presence) {
|
|
2245
2400
|
if (!this._socket || !this._connected) return;
|
|
2246
|
-
const jid
|
|
2401
|
+
const jid = makeJid(chatJid);
|
|
2402
|
+
const attrs = {
|
|
2403
|
+
from: jidStrToObj(this._ownJidFor(jid)),
|
|
2404
|
+
to: jidStrToObj(jid)
|
|
2405
|
+
};
|
|
2247
2406
|
if (presence === 'composing' || presence === 'recording') {
|
|
2248
|
-
const
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
new BinaryNode('composing', attrs, null)
|
|
2407
|
+
const stateAttrs = presence === 'recording' ? { media: 'audio' } : {};
|
|
2408
|
+
this._socket.sendNode(new BinaryNode('chatstate', attrs, [
|
|
2409
|
+
new BinaryNode('composing', stateAttrs, null)
|
|
2252
2410
|
]));
|
|
2253
2411
|
} else {
|
|
2254
|
-
this._socket.sendNode(new BinaryNode('chatstate',
|
|
2412
|
+
this._socket.sendNode(new BinaryNode('chatstate', attrs, [
|
|
2255
2413
|
new BinaryNode('paused', {}, null)
|
|
2256
2414
|
]));
|
|
2257
2415
|
}
|
|
2258
2416
|
}
|
|
2259
2417
|
|
|
2260
|
-
// Subscribe to presence updates for contacts
|
|
2418
|
+
// Subscribe to presence updates for contacts.
|
|
2419
|
+
//
|
|
2420
|
+
// whatsmeow attaches the contact's privacy token when it has one; without it
|
|
2421
|
+
// the server may decline to send their presence. It also notes that the server
|
|
2422
|
+
// only forwards presence to clients that have marked themselves available, so
|
|
2423
|
+
// call setOnline(true) first.
|
|
2261
2424
|
subscribeToPresence(jids) {
|
|
2262
2425
|
if (!this._socket || !this._connected) return;
|
|
2263
2426
|
const arr = Array.isArray(jids) ? jids : [jids];
|
|
2264
|
-
|
|
2427
|
+
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
2428
|
+
for (const raw of arr) {
|
|
2429
|
+
const jid = makeJid(raw);
|
|
2430
|
+
const entry = this._tcTokenStore && this._tcTokenStore.get(
|
|
2431
|
+
normalizeJidForTcToken(jid)
|
|
2432
|
+
);
|
|
2433
|
+
const children = (entry && entry.token && entry.token.length)
|
|
2434
|
+
? [new BinaryNode('tctoken', {}, entry.token)]
|
|
2435
|
+
: null;
|
|
2265
2436
|
this._socket.sendNode(new BinaryNode('presence', {
|
|
2266
|
-
to:
|
|
2437
|
+
to: jidStrToObj(jid),
|
|
2267
2438
|
type: 'subscribe'
|
|
2268
|
-
},
|
|
2439
|
+
}, children));
|
|
2269
2440
|
}
|
|
2270
2441
|
}
|
|
2271
2442
|
|
|
@@ -2359,22 +2530,97 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2359
2530
|
}
|
|
2360
2531
|
|
|
2361
2532
|
// Query profile picture URL
|
|
2362
|
-
|
|
2533
|
+
// Profile picture URL, or null when there is none.
|
|
2534
|
+
// Unchanged signature; queryPictureInfo has the rest of what the server sends.
|
|
2535
|
+
async queryPicture(jid, opts) {
|
|
2536
|
+
const info = await this.queryPictureInfo(jid, opts);
|
|
2537
|
+
return (info && info.url) || null;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// Full profile picture info, the way whatsmeow's GetProfilePictureInfo returns
|
|
2541
|
+
// it: { id, url, type, directPath, hash }, or null when the user has no
|
|
2542
|
+
// picture or it has not changed since opts.existingId.
|
|
2543
|
+
//
|
|
2544
|
+
// opts: { preview, existingId, isCommunity }
|
|
2545
|
+
// preview ask for the small thumbnail instead of the full image
|
|
2546
|
+
// existingId the id you already hold — the server answers 304 and this
|
|
2547
|
+
// returns null when it still matches
|
|
2548
|
+
// isCommunity resolve a community's picture, which goes through w:g2
|
|
2549
|
+
//
|
|
2550
|
+
// The target was a raw string before, which the server drops for an @lid, and
|
|
2551
|
+
// the reply's status codes were not read at all: a 204 (no picture set) has no
|
|
2552
|
+
// url attribute, so it came back as null with no way to tell it apart from a
|
|
2553
|
+
// failed query.
|
|
2554
|
+
async queryPictureInfo(jid, opts) {
|
|
2555
|
+
opts = opts || {};
|
|
2363
2556
|
const jidFull = makeJid(jid);
|
|
2364
|
-
const
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2557
|
+
const attrs = {
|
|
2558
|
+
query: 'url',
|
|
2559
|
+
type: opts.preview ? 'preview' : 'image'
|
|
2560
|
+
};
|
|
2561
|
+
if (opts.existingId) attrs.id = String(opts.existingId);
|
|
2562
|
+
if (opts.inviteCode) attrs.invite = String(opts.inviteCode);
|
|
2563
|
+
|
|
2564
|
+
let iqAttrs, content;
|
|
2565
|
+
if (opts.isCommunity) {
|
|
2566
|
+
attrs.parent_group_jid = jidStrToObj(jidFull);
|
|
2567
|
+
iqAttrs = { id: this._genMsgId(), xmlns: 'w:g2', to: jidStrToObj(jidFull), type: 'get' };
|
|
2568
|
+
content = [new BinaryNode('pictures', {}, [new BinaryNode('picture', attrs, null)])];
|
|
2569
|
+
} else {
|
|
2570
|
+
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
2571
|
+
const entry = this._tcTokenStore &&
|
|
2572
|
+
this._tcTokenStore.get(normalizeJidForTcToken(jidFull));
|
|
2573
|
+
const picChildren = (entry && entry.token && entry.token.length)
|
|
2574
|
+
? [new BinaryNode('tctoken', {}, entry.token)]
|
|
2575
|
+
: null;
|
|
2576
|
+
iqAttrs = {
|
|
2577
|
+
id: this._genMsgId(),
|
|
2578
|
+
xmlns: 'w:profile:picture',
|
|
2579
|
+
to: 's.whatsapp.net',
|
|
2580
|
+
target: jidStrToObj(jidFull),
|
|
2581
|
+
type: 'get'
|
|
2582
|
+
};
|
|
2583
|
+
content = [new BinaryNode('picture', attrs, picChildren)];
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
const resp = await this._sendIq(new BinaryNode('iq', iqAttrs, content));
|
|
2375
2587
|
if (!resp) return null;
|
|
2376
|
-
|
|
2377
|
-
|
|
2588
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
2589
|
+
const errNode = findChild(resp, 'error');
|
|
2590
|
+
const code = errNode && errNode.attrs && String(errNode.attrs.code);
|
|
2591
|
+
// 404 = no picture set, 401/403 = their privacy settings hide it. Neither
|
|
2592
|
+
// is a failure worth throwing over.
|
|
2593
|
+
if (code === '404' || code === '401' || code === '403') return null;
|
|
2594
|
+
throw new Error('queryPictureInfo: server rejected the request' +
|
|
2595
|
+
(code ? ' (' + code + ')' : ''));
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
const wrapper = opts.isCommunity ? findChild(resp, 'pictures') : resp;
|
|
2599
|
+
const pic = wrapper ? findChild(wrapper, 'picture') : null;
|
|
2600
|
+
if (!pic || !pic.attrs) return null;
|
|
2601
|
+
|
|
2602
|
+
const status = pic.attrs.status ? parseInt(pic.attrs.status, 10) : 0;
|
|
2603
|
+
if (status === 304 || status === 204) return null; // unchanged / not set
|
|
2604
|
+
|
|
2605
|
+
return {
|
|
2606
|
+
id: pic.attrs.id ? String(pic.attrs.id) : null,
|
|
2607
|
+
url: pic.attrs.url ? String(pic.attrs.url) : null,
|
|
2608
|
+
type: pic.attrs.type ? String(pic.attrs.type) : null,
|
|
2609
|
+
directPath: pic.attrs.direct_path ? String(pic.attrs.direct_path) : null,
|
|
2610
|
+
hash: pic.attrs.hash
|
|
2611
|
+
? Buffer.from(String(pic.attrs.hash), 'base64')
|
|
2612
|
+
: null
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// Download a profile picture and hand back the JPEG bytes, or null when the
|
|
2617
|
+
// user has none. Profile pictures are served unencrypted, unlike message
|
|
2618
|
+
// media, so this is a plain fetch of the URL the query returns.
|
|
2619
|
+
async downloadProfilePicture(jid, opts) {
|
|
2620
|
+
const info = await this.queryPictureInfo(jid, opts);
|
|
2621
|
+
if (!info || !info.url) return null;
|
|
2622
|
+
const { httpGetBuffer } = require('./MediaService');
|
|
2623
|
+
return httpGetBuffer(info.url);
|
|
2378
2624
|
}
|
|
2379
2625
|
|
|
2380
2626
|
// Query blocked contacts list
|
|
@@ -3036,24 +3282,125 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3036
3282
|
return this._chatStates[key];
|
|
3037
3283
|
}
|
|
3038
3284
|
|
|
3285
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3286
|
+
// CALL LINKS
|
|
3287
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
3288
|
+
|
|
3289
|
+
// Ask the server for a call link that can be shared in a chat, so the other
|
|
3290
|
+
// side joins by tapping it instead of being rung.
|
|
3291
|
+
//
|
|
3292
|
+
// type 'audio' (default) or 'video'
|
|
3293
|
+
// opts { startTime } — seconds since the epoch, to schedule the call
|
|
3294
|
+
//
|
|
3295
|
+
// Returns { token, link, type }. The stanza is the one WhatsApp Web sends:
|
|
3296
|
+
// <call to="@call"><link_create media="..."/></call>, answered with the token
|
|
3297
|
+
// on a link_create of its own.
|
|
3298
|
+
async createCallLink(type, opts) {
|
|
3299
|
+
opts = opts || {};
|
|
3300
|
+
const media = type === 'video' ? 'video' : 'audio';
|
|
3301
|
+
const id = this._genMsgId();
|
|
3302
|
+
const children = opts.startTime
|
|
3303
|
+
? [new BinaryNode('event', { start_time: String(opts.startTime) }, null)]
|
|
3304
|
+
: null;
|
|
3305
|
+
|
|
3306
|
+
const resp = await this._sendIq(new BinaryNode('call', { id, to: '@call' }, [
|
|
3307
|
+
new BinaryNode('link_create', { media }, children)
|
|
3308
|
+
]));
|
|
3309
|
+
|
|
3310
|
+
if (!resp) throw new Error('createCallLink: no reply from server (timed out)');
|
|
3311
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
3312
|
+
const errNode = findChild(resp, 'error');
|
|
3313
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
3314
|
+
throw new Error('createCallLink: server rejected the request' +
|
|
3315
|
+
(code ? ' (' + code + ')' : ''));
|
|
3316
|
+
}
|
|
3317
|
+
const created = findChild(resp, 'link_create');
|
|
3318
|
+
const token = created && created.attrs && created.attrs.token;
|
|
3319
|
+
if (!token) throw new Error('createCallLink: server reply carried no token');
|
|
3320
|
+
|
|
3321
|
+
const prefix = media === 'video' ? CALL_VIDEO_PREFIX : CALL_AUDIO_PREFIX;
|
|
3322
|
+
return { token: String(token), link: prefix + token, type: media };
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// Create a call link and send it as a message in one step.
|
|
3326
|
+
// Returns the send result with the link attached.
|
|
3327
|
+
async sendCallLink(to, type, opts) {
|
|
3328
|
+
opts = opts || {};
|
|
3329
|
+
const { link } = await this.createCallLink(type, opts);
|
|
3330
|
+
const text = opts.text ? (opts.text + ' ' + link) : link;
|
|
3331
|
+
const res = await this.sendText(to, text, opts);
|
|
3332
|
+
return Object.assign({}, res, { link });
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3039
3335
|
// ──────────────────────────────────────────────────────────────────────────
|
|
3040
3336
|
// GROUP OPERATIONS
|
|
3041
3337
|
// ──────────────────────────────────────────────────────────────────────────
|
|
3042
3338
|
|
|
3043
3339
|
// Create a group
|
|
3044
3340
|
// participants: array of phone numbers or JIDs
|
|
3341
|
+
// Create a group.
|
|
3342
|
+
//
|
|
3343
|
+
// opts: { ephemeralSeconds, announce, locked, joinApprovalRequired,
|
|
3344
|
+
// memberAddMode, parent, linkedParentJid }
|
|
3345
|
+
//
|
|
3346
|
+
// Built the way whatsmeow's CreateGroup does: every participant is a binary
|
|
3347
|
+
// JID (a raw string is dropped for an @lid), a LID participant also carries
|
|
3348
|
+
// the phone number the server matches it against, and each carries our privacy
|
|
3349
|
+
// token so the invite is not treated as an anonymous reach-out. The setting
|
|
3350
|
+
// nodes are siblings of the participants inside <create>.
|
|
3045
3351
|
async createGroup(subject, participants, opts) {
|
|
3046
3352
|
opts = opts || {};
|
|
3047
3353
|
const id = this._genMsgId();
|
|
3354
|
+
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
3048
3355
|
// participant nodes MUST use attr `jid`, not `value`
|
|
3049
3356
|
const partNodes = (participants || []).map(p => {
|
|
3050
|
-
const jid
|
|
3051
|
-
|
|
3357
|
+
const jid = makeJid(p);
|
|
3358
|
+
const attrs = { jid: jidStrToObj(jid) };
|
|
3359
|
+
if (jid.endsWith('@lid')) {
|
|
3360
|
+
const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
|
|
3361
|
+
if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
|
|
3362
|
+
}
|
|
3363
|
+
const entry = this._tcTokenStore &&
|
|
3364
|
+
this._tcTokenStore.get(normalizeJidForTcToken(jid));
|
|
3365
|
+
const children = (entry && entry.token && entry.token.length)
|
|
3366
|
+
? [new BinaryNode('privacy', {}, entry.token)]
|
|
3367
|
+
: null;
|
|
3368
|
+
return new BinaryNode('participant', attrs, children);
|
|
3052
3369
|
});
|
|
3370
|
+
|
|
3371
|
+
const settingNodes = [];
|
|
3372
|
+
if (opts.parent) {
|
|
3373
|
+
settingNodes.push(new BinaryNode('parent', {
|
|
3374
|
+
default_membership_approval_mode:
|
|
3375
|
+
opts.defaultMembershipApprovalMode || 'request_required'
|
|
3376
|
+
}, null));
|
|
3377
|
+
} else if (opts.linkedParentJid) {
|
|
3378
|
+
settingNodes.push(new BinaryNode('linked_parent', {
|
|
3379
|
+
jid: jidStrToObj(makeJid(opts.linkedParentJid))
|
|
3380
|
+
}, null));
|
|
3381
|
+
}
|
|
3382
|
+
if (opts.locked) settingNodes.push(new BinaryNode('locked', {}, null));
|
|
3383
|
+
if (opts.announce) settingNodes.push(new BinaryNode('announcement', {}, null));
|
|
3384
|
+
if (opts.ephemeralSeconds) {
|
|
3385
|
+
// whatsmeow sends trigger="1" alongside the expiration; without it the
|
|
3386
|
+
// server accepts the group but leaves disappearing messages off.
|
|
3387
|
+
settingNodes.push(new BinaryNode('ephemeral', {
|
|
3388
|
+
expiration: String(opts.ephemeralSeconds),
|
|
3389
|
+
trigger: '1'
|
|
3390
|
+
}, null));
|
|
3391
|
+
}
|
|
3392
|
+
if (opts.joinApprovalRequired) {
|
|
3393
|
+
settingNodes.push(new BinaryNode('membership_approval_mode', {}, [
|
|
3394
|
+
new BinaryNode('group_join', { state: 'on' }, null)
|
|
3395
|
+
]));
|
|
3396
|
+
}
|
|
3397
|
+
if (opts.memberAddMode) {
|
|
3398
|
+
settingNodes.push(new BinaryNode('member_add_mode', {},
|
|
3399
|
+
Buffer.from(String(opts.memberAddMode), 'utf8')));
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3053
3402
|
const baseAttrs = { subject, key: String(Math.floor(Date.now() / 1000)) };
|
|
3054
|
-
const createChildren =
|
|
3055
|
-
? [...partNodes, new BinaryNode('ephemeral', { expiration: String(opts.ephemeralSeconds) }, null)]
|
|
3056
|
-
: partNodes;
|
|
3403
|
+
const createChildren = [...partNodes, ...settingNodes];
|
|
3057
3404
|
const node = new BinaryNode('iq', {
|
|
3058
3405
|
id,
|
|
3059
3406
|
xmlns: 'w:g2',
|
|
@@ -3124,14 +3471,31 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3124
3471
|
async _groupParticipantAction(groupJid, action, jids) {
|
|
3125
3472
|
const gJid = makeJid(groupJid);
|
|
3126
3473
|
const arr = Array.isArray(jids) ? jids : [jids];
|
|
3127
|
-
const
|
|
3128
|
-
|
|
3129
|
-
|
|
3474
|
+
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
3475
|
+
// Same shape whatsmeow's UpdateGroupParticipants builds: binary JIDs, and on
|
|
3476
|
+
// an add, the phone number behind a LID plus our privacy token for them.
|
|
3477
|
+
const participantNodes = arr.map(j => {
|
|
3478
|
+
const jid = makeJid(j);
|
|
3479
|
+
const attrs = { jid: jidStrToObj(jid) };
|
|
3480
|
+
let children = null;
|
|
3481
|
+
if (action === 'add') {
|
|
3482
|
+
if (jid.endsWith('@lid')) {
|
|
3483
|
+
const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
|
|
3484
|
+
if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
|
|
3485
|
+
}
|
|
3486
|
+
const entry = this._tcTokenStore &&
|
|
3487
|
+
this._tcTokenStore.get(normalizeJidForTcToken(jid));
|
|
3488
|
+
if (entry && entry.token && entry.token.length) {
|
|
3489
|
+
children = [new BinaryNode('privacy', {}, entry.token)];
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return new BinaryNode('participant', attrs, children);
|
|
3493
|
+
});
|
|
3130
3494
|
const id = this._genMsgId();
|
|
3131
3495
|
const node = new BinaryNode('iq', {
|
|
3132
3496
|
id,
|
|
3133
3497
|
xmlns: 'w:g2',
|
|
3134
|
-
to: gJid,
|
|
3498
|
+
to: jidStrToObj(gJid),
|
|
3135
3499
|
type: 'set'
|
|
3136
3500
|
}, [
|
|
3137
3501
|
new BinaryNode(action, {}, participantNodes)
|
|
@@ -3161,19 +3525,38 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3161
3525
|
}
|
|
3162
3526
|
|
|
3163
3527
|
// Change group description
|
|
3164
|
-
// description: string or null to remove
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3528
|
+
// description: string or null/'' to remove
|
|
3529
|
+
//
|
|
3530
|
+
// whatsmeow's SetGroupTopic sends prev=<current topic id> so the server can
|
|
3531
|
+
// reject an edit made against a description someone else has already replaced.
|
|
3532
|
+
// Without it the change is refused. The current id is looked up from the group
|
|
3533
|
+
// metadata when the caller does not supply one.
|
|
3534
|
+
async changeGroupDescription(groupJid, description, opts) {
|
|
3535
|
+
opts = opts || {};
|
|
3536
|
+
const gJid = makeJid(groupJid);
|
|
3537
|
+
const id = this._genMsgId();
|
|
3538
|
+
const isDelete = description == null || description === '';
|
|
3539
|
+
|
|
3540
|
+
let prevId = opts.prevId;
|
|
3541
|
+
if (prevId === undefined) {
|
|
3542
|
+
try {
|
|
3543
|
+
const meta = await this.getGroupMetadata(gJid);
|
|
3544
|
+
prevId = meta && meta.descriptionId;
|
|
3545
|
+
} catch (_) { prevId = null; }
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
const descAttrs = { id: opts.newId || this._genMsgId() };
|
|
3549
|
+
if (prevId) descAttrs.prev = prevId;
|
|
3550
|
+
let children = [new BinaryNode('body', {}, Buffer.from(String(description), 'utf8'))];
|
|
3551
|
+
if (isDelete) {
|
|
3552
|
+
descAttrs.delete = 'true';
|
|
3553
|
+
children = null;
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3173
3556
|
const node = new BinaryNode('iq', {
|
|
3174
3557
|
id,
|
|
3175
3558
|
xmlns: 'w:g2',
|
|
3176
|
-
to:
|
|
3559
|
+
to: jidStrToObj(gJid),
|
|
3177
3560
|
type: 'set'
|
|
3178
3561
|
}, [
|
|
3179
3562
|
new BinaryNode('description', descAttrs, children)
|
|
@@ -3181,6 +3564,61 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3181
3564
|
return this._sendIq(node);
|
|
3182
3565
|
}
|
|
3183
3566
|
|
|
3567
|
+
// ─── Group settings ───────────────────────────────────────────────────────
|
|
3568
|
+
//
|
|
3569
|
+
// Both are a single empty node whose TAG carries the state, exactly as
|
|
3570
|
+
// whatsmeow's SetGroupAnnounce / SetGroupLocked send them — there is no
|
|
3571
|
+
// attribute form of these.
|
|
3572
|
+
|
|
3573
|
+
// announce=true → only admins can send messages
|
|
3574
|
+
async setGroupAnnounce(groupJid, announce) {
|
|
3575
|
+
return this._groupSettingIq(groupJid, announce ? 'announcement' : 'not_announcement');
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
// locked=true → only admins can edit group info (subject, description, icon)
|
|
3579
|
+
async setGroupLocked(groupJid, locked) {
|
|
3580
|
+
return this._groupSettingIq(groupJid, locked ? 'locked' : 'unlocked');
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
// mode=true → new members must be approved by an admin
|
|
3584
|
+
async setGroupJoinApprovalMode(groupJid, mode) {
|
|
3585
|
+
return this._groupSettingIq(groupJid, 'membership_approval_mode', [
|
|
3586
|
+
new BinaryNode('group_join', { state: mode ? 'on' : 'off' }, null)
|
|
3587
|
+
]);
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
// mode: 'all_member_add' | 'admin_add'
|
|
3591
|
+
async setGroupMemberAddMode(groupJid, mode) {
|
|
3592
|
+
return this._groupSettingIq(groupJid, 'member_add_mode',
|
|
3593
|
+
Buffer.from(String(mode), 'utf8'));
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
// content is passed to the setting node verbatim: null for the flag settings
|
|
3597
|
+
// whose state lives in the tag, child nodes for membership_approval_mode, raw
|
|
3598
|
+
// bytes for member_add_mode.
|
|
3599
|
+
async _groupSettingIq(groupJid, tag, content) {
|
|
3600
|
+
const node = new BinaryNode('iq', {
|
|
3601
|
+
id: this._genMsgId(),
|
|
3602
|
+
xmlns: 'w:g2',
|
|
3603
|
+
to: jidStrToObj(makeJid(groupJid)),
|
|
3604
|
+
type: 'set'
|
|
3605
|
+
}, [
|
|
3606
|
+
new BinaryNode(tag, {}, content === undefined ? null : content)
|
|
3607
|
+
]);
|
|
3608
|
+
const resp = await this._sendIq(node);
|
|
3609
|
+
if (!resp) throw new Error(tag + ': no reply from server (IQ timed out)');
|
|
3610
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
3611
|
+
const errNode = findChild(resp, 'error');
|
|
3612
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
3613
|
+
const text = errNode && errNode.attrs && errNode.attrs.text;
|
|
3614
|
+
throw new Error(tag + ': server rejected the request' +
|
|
3615
|
+
(code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
|
|
3616
|
+
}
|
|
3617
|
+
// The group's own view of these flags is now stale.
|
|
3618
|
+
this.invalidateGroupMetadata(groupJid);
|
|
3619
|
+
return resp;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3184
3622
|
// Change group picture
|
|
3185
3623
|
// buf: Buffer with JPEG image data, or null to remove
|
|
3186
3624
|
async changeGroupPicture(groupJid, buf) {
|
|
@@ -3197,17 +3635,63 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3197
3635
|
return this._sendIq(node);
|
|
3198
3636
|
}
|
|
3199
3637
|
|
|
3200
|
-
// Get group invite link
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3638
|
+
// Get the group's invite link.
|
|
3639
|
+
//
|
|
3640
|
+
// reset=true revokes the current link and returns a freshly minted one — the
|
|
3641
|
+
// same IQ with type="set" instead of "get", which is how whatsmeow's
|
|
3642
|
+
// GetGroupInviteLink handles its reset flag.
|
|
3643
|
+
async queryGroupInviteLink(groupJid, reset) {
|
|
3644
|
+
const code = await this._queryGroupInviteCode(groupJid, reset);
|
|
3645
|
+
return code ? INVITE_LINK_PREFIX + code : null;
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
// Revoke the current invite link and return the replacement.
|
|
3649
|
+
async revokeGroupInviteLink(groupJid) {
|
|
3650
|
+
return this.queryGroupInviteLink(groupJid, true);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// Join a group from an invite link or bare code.
|
|
3654
|
+
// Returns the JID of the group that was joined. When the group requires admin
|
|
3655
|
+
// approval the server answers with a membership_approval_request instead, and
|
|
3656
|
+
// that JID is returned — the join is pending, not complete.
|
|
3657
|
+
async joinGroupWithLink(inviteCodeOrUrl) {
|
|
3658
|
+
const code = stripInviteUrl(inviteCodeOrUrl);
|
|
3659
|
+
const node = new BinaryNode('iq', {
|
|
3660
|
+
id: this._genMsgId(),
|
|
3661
|
+
xmlns: 'w:g2',
|
|
3662
|
+
to: 'g.us',
|
|
3663
|
+
type: 'set'
|
|
3664
|
+
}, [new BinaryNode('invite', { code }, null)]);
|
|
3665
|
+
|
|
3666
|
+
const resp = await this._sendIq(node);
|
|
3667
|
+
if (!resp) throw new Error('joinGroupWithLink: no reply from server (IQ timed out)');
|
|
3668
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
3669
|
+
const errNode = findChild(resp, 'error');
|
|
3670
|
+
const codeAttr = errNode && errNode.attrs && errNode.attrs.code;
|
|
3671
|
+
// whatsmeow maps these two specifically: 410 is a revoked link, 406 an
|
|
3672
|
+
// invalid one. Anything else is passed through as the server worded it.
|
|
3673
|
+
const reason = String(codeAttr) === '410' ? 'the invite link has been revoked'
|
|
3674
|
+
: String(codeAttr) === '406' ? 'the invite link is not valid'
|
|
3675
|
+
: 'server rejected the request' +
|
|
3676
|
+
(codeAttr ? ' (' + codeAttr + ')' : '');
|
|
3677
|
+
throw new Error('joinGroupWithLink: ' + reason);
|
|
3678
|
+
}
|
|
3679
|
+
const pending = findChild(resp, 'membership_approval_request');
|
|
3680
|
+
if (pending && pending.attrs && pending.attrs.jid) {
|
|
3681
|
+
return { jid: String(pending.attrs.jid), pendingApproval: true };
|
|
3682
|
+
}
|
|
3683
|
+
const grp = findChild(resp, 'group');
|
|
3684
|
+
if (!grp || !grp.attrs || !grp.attrs.jid) {
|
|
3685
|
+
throw new Error('joinGroupWithLink: server reply contained no <group> node');
|
|
3686
|
+
}
|
|
3687
|
+
return { jid: String(grp.attrs.jid), pendingApproval: false };
|
|
3204
3688
|
}
|
|
3205
3689
|
|
|
3206
3690
|
// queryGroupInviteInfo — fetch group metadata from an invite link code
|
|
3207
3691
|
// Returns parsed group metadata (same shape as getGroupMetadata) or null.
|
|
3208
3692
|
// inviteCode is the bare code (no URL prefix needed; URL also accepted).
|
|
3209
3693
|
async queryGroupInviteInfo(inviteCodeOrUrl) {
|
|
3210
|
-
const code =
|
|
3694
|
+
const code = stripInviteUrl(inviteCodeOrUrl);
|
|
3211
3695
|
const id = this._genMsgId();
|
|
3212
3696
|
const node = new BinaryNode('iq', {
|
|
3213
3697
|
id,
|
|
@@ -3224,13 +3708,13 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3224
3708
|
return this._parseGroupNode(grp);
|
|
3225
3709
|
}
|
|
3226
3710
|
|
|
3227
|
-
async _queryGroupInviteCode(groupJid) {
|
|
3711
|
+
async _queryGroupInviteCode(groupJid, reset) {
|
|
3228
3712
|
const id = this._genMsgId();
|
|
3229
3713
|
const node = new BinaryNode('iq', {
|
|
3230
3714
|
id,
|
|
3231
3715
|
xmlns: 'w:g2',
|
|
3232
|
-
to: makeJid(groupJid),
|
|
3233
|
-
type: 'get'
|
|
3716
|
+
to: jidStrToObj(makeJid(groupJid)),
|
|
3717
|
+
type: reset ? 'set' : 'get'
|
|
3234
3718
|
}, [
|
|
3235
3719
|
new BinaryNode('invite', {}, null)
|
|
3236
3720
|
]);
|
|
@@ -3241,34 +3725,24 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3241
3725
|
}
|
|
3242
3726
|
|
|
3243
3727
|
// Revoke group invite
|
|
3728
|
+
// Revoke the current invite link. Returns the replacement link.
|
|
3729
|
+
//
|
|
3730
|
+
// This threw the reply away, so a caller had no way to learn the new code and
|
|
3731
|
+
// had to query the link again. It is the same IQ revokeGroupInviteLink sends.
|
|
3244
3732
|
async revokeGroupInvite(groupJid) {
|
|
3245
|
-
|
|
3246
|
-
const node = new BinaryNode('iq', {
|
|
3247
|
-
id,
|
|
3248
|
-
xmlns: 'w:g2',
|
|
3249
|
-
to: makeJid(groupJid),
|
|
3250
|
-
type: 'set'
|
|
3251
|
-
}, [
|
|
3252
|
-
new BinaryNode('invite', {}, null)
|
|
3253
|
-
]);
|
|
3254
|
-
return this._sendIq(node);
|
|
3733
|
+
return this.revokeGroupInviteLink(groupJid);
|
|
3255
3734
|
}
|
|
3256
3735
|
|
|
3257
|
-
// Join group by invite code
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
]);
|
|
3268
|
-
const resp = await this._sendIq(node);
|
|
3269
|
-
if (!resp) return null;
|
|
3270
|
-
const grp = findChild(resp, 'group');
|
|
3271
|
-
return (grp && grp.attrs && (grp.attrs.jid || grp.attrs.value)) || null;
|
|
3736
|
+
// Join a group by invite code or full link. Returns the group JID, or null.
|
|
3737
|
+
//
|
|
3738
|
+
// Accepts a full https://chat.whatsapp.com/... link as well as a bare code —
|
|
3739
|
+
// it used to pass whatever it was given straight through as the code, so a
|
|
3740
|
+
// pasted link was rejected. A group that gates joins on admin approval answers
|
|
3741
|
+
// with membership_approval_request rather than group; that JID is returned too
|
|
3742
|
+
// (use joinGroupWithLink when you need to tell the two apart).
|
|
3743
|
+
async acceptGroupInvite(inviteCodeOrUrl) {
|
|
3744
|
+
const r = await this.joinGroupWithLink(inviteCodeOrUrl).catch(() => null);
|
|
3745
|
+
return r ? r.jid : null;
|
|
3272
3746
|
}
|
|
3273
3747
|
|
|
3274
3748
|
// Change group settings
|
|
@@ -3297,13 +3771,10 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3297
3771
|
default:
|
|
3298
3772
|
throw new Error('Unknown group setting: ' + setting);
|
|
3299
3773
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
type: 'set'
|
|
3305
|
-
}, [body]);
|
|
3306
|
-
return this._sendIq(node);
|
|
3774
|
+
// Routed through the shared helper so a refusal from the server (not an
|
|
3775
|
+
// admin, not a member) surfaces as an error instead of resolving null, and
|
|
3776
|
+
// so the group's cached metadata does not keep reporting the old flags.
|
|
3777
|
+
return this._groupSettingIq(groupJid, body.description, body.content);
|
|
3307
3778
|
}
|
|
3308
3779
|
|
|
3309
3780
|
// Query participants pending approval
|