whalibmob 5.5.86 → 5.5.89
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 +65 -4
- package/cli.js +36 -13
- package/lib/Client.js +399 -38
- package/lib/messages/MessageSender.js +43 -6
- package/lib/proto/MessageProto.js +22 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,6 +166,7 @@ npm install -g whalibmob
|
|
|
166
166
|
- [Document Message](#document-message)
|
|
167
167
|
- [Sticker Message](#sticker-message)
|
|
168
168
|
- [Status / Stories](#status--stories)
|
|
169
|
+
- [Status Privacy](#status-privacy)
|
|
169
170
|
- [Send States in Chat](#send-states-in-chat)
|
|
170
171
|
- [Reading Messages](#reading-messages)
|
|
171
172
|
- [Mark Voice Message Played](#mark-voice-message-played)
|
|
@@ -189,6 +190,7 @@ npm install -g whalibmob
|
|
|
189
190
|
- [Privacy](#privacy)
|
|
190
191
|
- [Block / Unblock User](#block--unblock-user)
|
|
191
192
|
- [Get Block List](#get-block-list)
|
|
193
|
+
- [Read Privacy Settings](#read-privacy-settings)
|
|
192
194
|
- [Update Privacy Settings](#update-privacy-settings)
|
|
193
195
|
- [Update Default Disappearing Mode](#update-default-disappearing-mode)
|
|
194
196
|
- [Communities](#communities)
|
|
@@ -605,6 +607,8 @@ connect()
|
|
|
605
607
|
| `chat_read` | `{ jid, read }` | Chat marked read (`read: true`) or unread (`read: false`) |
|
|
606
608
|
| `chat_muted` | `{ jid, muted, until }` | Chat muted or unmuted; `until` is epoch ms (−1 = indefinite) |
|
|
607
609
|
| `chat_pinned` | `{ jid, pinned }` | Chat pinned or unpinned |
|
|
610
|
+
| `blocklist` | `{ action, dhash, prevDhash, changes }` | Block list changed on another device; `changes` is `[{ jid, action }]` |
|
|
611
|
+
| `privacy_settings` | `{ changes, settings }` | Privacy settings changed on another device |
|
|
608
612
|
| `chat_archived` | `{ jid, archived }` | Chat archived or unarchived |
|
|
609
613
|
| `message_starred` | `{ msgId, chatJid, starred }` | Message starred or unstarred |
|
|
610
614
|
| `stream_error` | `{ reason }` | Server sent a fatal stream error |
|
|
@@ -1287,11 +1291,41 @@ await client.sendSticker('919634847671@s.whatsapp.net', './sticker.webp')
|
|
|
1287
1291
|
|
|
1288
1292
|
## Status / Stories
|
|
1289
1293
|
|
|
1294
|
+
A Status is posted to `status@broadcast` and travels the SenderKey path a group
|
|
1295
|
+
message does — one encrypted body plus a key distribution to every recipient.
|
|
1296
|
+
Who those recipients are comes from your status privacy settings, resolved
|
|
1297
|
+
against the contacts in the synced history store.
|
|
1298
|
+
|
|
1290
1299
|
```js
|
|
1291
|
-
//
|
|
1300
|
+
// text
|
|
1292
1301
|
await client.sendStatus('Good morning!')
|
|
1302
|
+
|
|
1303
|
+
// photo, video, voice — same arguments as the matching send* methods
|
|
1304
|
+
await client.sendStatus({ image: './photo.jpg', caption: 'Look at this' })
|
|
1305
|
+
await client.sendStatus({ video: './clip.mp4' })
|
|
1306
|
+
await client.sendStatus({ audio: './voice.ogg' })
|
|
1307
|
+
|
|
1308
|
+
// styled text status
|
|
1309
|
+
await client.sendStatus('Colorful', { backgroundArgb: 0xFF25D366, font: 3 })
|
|
1310
|
+
|
|
1311
|
+
// post to an explicit list instead of the privacy-derived one
|
|
1312
|
+
await client.sendStatus('Hi', { recipients: ['919634847671@s.whatsapp.net'] })
|
|
1293
1313
|
```
|
|
1294
1314
|
|
|
1315
|
+
### Status Privacy
|
|
1316
|
+
|
|
1317
|
+
```js
|
|
1318
|
+
const [def, ...rest] = await client.queryStatusPrivacy()
|
|
1319
|
+
// def = { type: 'contacts' | 'blacklist' | 'whitelist', isDefault, list }
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
`contacts` sends to everyone in your address book, `blacklist` to everyone
|
|
1323
|
+
except `list`, `whitelist` to `list` only. Your own JID is always included so
|
|
1324
|
+
the post reaches your other devices.
|
|
1325
|
+
|
|
1326
|
+
If history has not synced yet the contact list is unknown, and `sendStatus`
|
|
1327
|
+
will say so rather than post to nobody — pass `recipients` in that case.
|
|
1328
|
+
|
|
1295
1329
|
## Send States in Chat
|
|
1296
1330
|
|
|
1297
1331
|
### Reading Messages
|
|
@@ -1457,8 +1491,12 @@ await client.changeGroupPicture('120363000000000000@g.us', fs.readFileSync('./gr
|
|
|
1457
1491
|
### Block / Unblock User
|
|
1458
1492
|
|
|
1459
1493
|
```js
|
|
1460
|
-
|
|
1494
|
+
// both return the updated block list, and throw if the server refuses
|
|
1495
|
+
const blocked = await client.blockContact('919634847671@s.whatsapp.net')
|
|
1461
1496
|
await client.unblockContact('919634847671@s.whatsapp.net')
|
|
1497
|
+
|
|
1498
|
+
// blocking done from the phone arrives as an event
|
|
1499
|
+
client.on('blocklist', ({ changes }) => console.log(changes))
|
|
1462
1500
|
```
|
|
1463
1501
|
|
|
1464
1502
|
### Get Block List
|
|
@@ -1471,17 +1509,40 @@ console.log(list) // [ '919634847671@s.whatsapp.net', ... ]
|
|
|
1471
1509
|
### Update Privacy Settings
|
|
1472
1510
|
|
|
1473
1511
|
```js
|
|
1474
|
-
// type: 'last_seen' | 'profile_picture' | 'status' | 'online' | 'read_receipts'
|
|
1475
|
-
//
|
|
1512
|
+
// type: 'last_seen' | 'profile_picture' | 'status' | 'online' | 'read_receipts'
|
|
1513
|
+
// 'groups_add' | 'call_add' | 'messages' | 'defense' | 'stickers'
|
|
1514
|
+
// value: 'all' | 'contacts' | 'contact_blacklist' | 'contact_allowlist' | 'none'
|
|
1515
|
+
// 'match_last_seen' | 'known' | 'on_standard' | 'off'
|
|
1476
1516
|
|
|
1517
|
+
// returns the settings as they now stand; throws if the server refuses
|
|
1477
1518
|
await client.changePrivacySetting('last_seen', 'contacts')
|
|
1478
1519
|
await client.changePrivacySetting('profile_picture', 'contacts')
|
|
1479
1520
|
await client.changePrivacySetting('status', 'contacts')
|
|
1480
1521
|
await client.changePrivacySetting('online', 'match_last_seen')
|
|
1481
1522
|
await client.changePrivacySetting('read_receipts', 'none')
|
|
1482
1523
|
await client.changePrivacySetting('groups_add', 'contacts')
|
|
1524
|
+
await client.changePrivacySetting('call_add', 'known')
|
|
1483
1525
|
```
|
|
1484
1526
|
|
|
1527
|
+
### Read Privacy Settings
|
|
1528
|
+
|
|
1529
|
+
```js
|
|
1530
|
+
const s = await client.queryPrivacySettings()
|
|
1531
|
+
// { lastSeen, profile, status, online, readReceipts,
|
|
1532
|
+
// groupAdd, callAdd, messages, defense, stickers }
|
|
1533
|
+
// anything the server did not report is null
|
|
1534
|
+
|
|
1535
|
+
await client.queryPrivacySettings({ force: true }) // skip the cache
|
|
1536
|
+
|
|
1537
|
+
// a change made on the phone arrives as an event
|
|
1538
|
+
client.on('privacy_settings', ({ changes, settings }) => console.log(changes))
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
Turning `read_receipts` off changes what read receipts go out: they become
|
|
1542
|
+
`read-self`, which syncs the read state across your own devices without telling
|
|
1543
|
+
the sender. The settings are fetched once after connect so this holds from the
|
|
1544
|
+
first message.
|
|
1545
|
+
|
|
1485
1546
|
### Update Default Disappearing Mode
|
|
1486
1547
|
|
|
1487
1548
|
```js
|
package/cli.js
CHANGED
|
@@ -336,7 +336,8 @@ const HELP = `
|
|
|
336
336
|
/react <jid> <msgId> <emoji> react to a message
|
|
337
337
|
/edit <jid> <msgId> <text> edit a sent message
|
|
338
338
|
/delete <jid> <msgId> [all] delete message (add 'all' for everyone)
|
|
339
|
-
/status <text> post a Status/Story
|
|
339
|
+
/status <text> post a text Status/Story
|
|
340
|
+
/status image|video|audio <file> [caption] post a media Status/Story
|
|
340
341
|
/forward <jid> <text|msgObj> forward text (or decoded msg) with forwarded flag
|
|
341
342
|
/poll <jid> <question> | <opt1> | <opt2> [selectable=N] send a poll
|
|
342
343
|
/location <jid> <lat> <lon> [name] [| address] send a location pin
|
|
@@ -354,11 +355,13 @@ const HELP = `
|
|
|
354
355
|
/name <text> change display name
|
|
355
356
|
/about <text> change own bio / about text
|
|
356
357
|
/photo <file> change own profile picture (JPEG)
|
|
357
|
-
/privacy <type> <value>
|
|
358
|
+
/privacy [<type> <value>] show or change privacy settings
|
|
358
359
|
types: last_seen profile_picture status
|
|
359
360
|
online read_receipts groups_add
|
|
361
|
+
call_add messages defense stickers
|
|
360
362
|
values: all contacts contact_blacklist
|
|
361
|
-
none
|
|
363
|
+
contact_allowlist none known
|
|
364
|
+
match_last_seen on_standard off
|
|
362
365
|
|
|
363
366
|
Contacts
|
|
364
367
|
/whatsapp <phone...> check which numbers have WhatsApp
|
|
@@ -828,8 +831,19 @@ async function handleLine(line) {
|
|
|
828
831
|
case '/status': {
|
|
829
832
|
requireConn();
|
|
830
833
|
const [, ...rest] = p;
|
|
831
|
-
if (!rest.length) {
|
|
832
|
-
|
|
834
|
+
if (!rest.length) {
|
|
835
|
+
fail('usage: /status <text> | /status image|video|audio <file> [caption]');
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
const kind = rest[0] && rest[0].toLowerCase();
|
|
839
|
+
let r;
|
|
840
|
+
if ((kind === 'image' || kind === 'video' || kind === 'audio') && rest[1]) {
|
|
841
|
+
const opts = { [kind]: rest[1] };
|
|
842
|
+
if (rest.length > 2) opts.caption = rest.slice(2).join(' ');
|
|
843
|
+
r = await _client.sendStatus(opts);
|
|
844
|
+
} else {
|
|
845
|
+
r = await _client.sendStatus(rest.join(' '));
|
|
846
|
+
}
|
|
833
847
|
out('status posted ' + (r && r.id ? r.id : ''));
|
|
834
848
|
break;
|
|
835
849
|
}
|
|
@@ -968,10 +982,19 @@ async function handleLine(line) {
|
|
|
968
982
|
case '/privacy': {
|
|
969
983
|
requireConn();
|
|
970
984
|
const [, type, value] = p;
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
out('
|
|
985
|
+
// with no arguments, show what the settings currently are
|
|
986
|
+
if (!type) {
|
|
987
|
+
const s = await _client.queryPrivacySettings({ force: true });
|
|
988
|
+
out(' privacy settings');
|
|
989
|
+
for (const k of Object.keys(s)) out(' ' + k.padEnd(14) + (s[k] || '(unset)'));
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
if (!value) {
|
|
993
|
+
fail('usage: /privacy [<type> <value>]');
|
|
994
|
+
out(' types: last_seen profile_picture status online read_receipts');
|
|
995
|
+
out(' groups_add call_add messages defense stickers');
|
|
996
|
+
out(' values: all contacts contact_blacklist contact_allowlist none');
|
|
997
|
+
out(' match_last_seen known on_standard off');
|
|
975
998
|
break;
|
|
976
999
|
}
|
|
977
1000
|
await _client.changePrivacySetting(type, value);
|
|
@@ -1161,8 +1184,8 @@ async function handleLine(line) {
|
|
|
1161
1184
|
requireConn();
|
|
1162
1185
|
const jid = normalizeJid(p[1]);
|
|
1163
1186
|
if (!jid) { fail('usage: /block <jid>'); break; }
|
|
1164
|
-
await _client.blockContact(jid);
|
|
1165
|
-
out('blocked ' + jid);
|
|
1187
|
+
const after = await _client.blockContact(jid);
|
|
1188
|
+
out('blocked ' + jid + ' (' + after.length + ' blocked in total)');
|
|
1166
1189
|
break;
|
|
1167
1190
|
}
|
|
1168
1191
|
|
|
@@ -1170,8 +1193,8 @@ async function handleLine(line) {
|
|
|
1170
1193
|
requireConn();
|
|
1171
1194
|
const jid = normalizeJid(p[1]);
|
|
1172
1195
|
if (!jid) { fail('usage: /unblock <jid>'); break; }
|
|
1173
|
-
await _client.unblockContact(jid);
|
|
1174
|
-
out('unblocked ' + jid);
|
|
1196
|
+
const left = await _client.unblockContact(jid);
|
|
1197
|
+
out('unblocked ' + jid + ' (' + left.length + ' blocked in total)');
|
|
1175
1198
|
break;
|
|
1176
1199
|
}
|
|
1177
1200
|
|
package/lib/Client.js
CHANGED
|
@@ -30,6 +30,9 @@ const {
|
|
|
30
30
|
HistorySyncType
|
|
31
31
|
} = require('./HistorySyncHandler');
|
|
32
32
|
|
|
33
|
+
// Status posts are messages addressed here.
|
|
34
|
+
const STATUS_BROADCAST_JID = 'status@broadcast';
|
|
35
|
+
|
|
33
36
|
// The two prefixes a call-link token is wrapped in, as WhatsApp Web uses them.
|
|
34
37
|
const CALL_AUDIO_PREFIX = 'https://call.whatsapp.com/voice/';
|
|
35
38
|
const CALL_VIDEO_PREFIX = 'https://call.whatsapp.com/video/';
|
|
@@ -37,6 +40,47 @@ const CALL_VIDEO_PREFIX = 'https://call.whatsapp.com/video/';
|
|
|
37
40
|
// whatsmeow's InviteLinkPrefix — the one prefix group invite codes are wrapped in.
|
|
38
41
|
const INVITE_LINK_PREFIX = 'https://chat.whatsapp.com/';
|
|
39
42
|
|
|
43
|
+
// The names privacy settings actually go by on the wire, and the friendly
|
|
44
|
+
// aliases this library has always accepted for them. whatsmeow's
|
|
45
|
+
// PrivacySettingType constants are the right-hand column.
|
|
46
|
+
const PRIVACY_NAME_ALIASES = {
|
|
47
|
+
last_seen: 'last', last: 'last',
|
|
48
|
+
profile_picture: 'profile', profile_pic: 'profile', profile: 'profile',
|
|
49
|
+
status: 'status',
|
|
50
|
+
online: 'online',
|
|
51
|
+
read_receipts: 'readreceipts', readreceipts: 'readreceipts',
|
|
52
|
+
groups_add: 'groupadd', group_add: 'groupadd', groupadd: 'groupadd',
|
|
53
|
+
call_add: 'calladd', calladd: 'calladd',
|
|
54
|
+
messages: 'messages',
|
|
55
|
+
defense: 'defense',
|
|
56
|
+
stickers: 'stickers'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Wire name → the key it is reported under in the settings object.
|
|
60
|
+
const PRIVACY_WIRE_TO_KEY = {
|
|
61
|
+
last: 'lastSeen',
|
|
62
|
+
profile: 'profile',
|
|
63
|
+
status: 'status',
|
|
64
|
+
online: 'online',
|
|
65
|
+
readreceipts: 'readReceipts',
|
|
66
|
+
groupadd: 'groupAdd',
|
|
67
|
+
calladd: 'callAdd',
|
|
68
|
+
messages: 'messages',
|
|
69
|
+
defense: 'defense',
|
|
70
|
+
stickers: 'stickers'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Reduce a JID to its bare user — whatsmeow's ToNonAD. Blocking, and anything
|
|
74
|
+
// else that names an account rather than one of its devices, wants this form.
|
|
75
|
+
function toNonAdJid(input) {
|
|
76
|
+
const s = String(input || '');
|
|
77
|
+
const at = s.indexOf('@');
|
|
78
|
+
const user = at >= 0 ? s.slice(0, at) : s;
|
|
79
|
+
const server = at >= 0 ? s.slice(at + 1) : 's.whatsapp.net';
|
|
80
|
+
const colon = user.indexOf(':');
|
|
81
|
+
return (colon >= 0 ? user.slice(0, colon) : user) + '@' + server;
|
|
82
|
+
}
|
|
83
|
+
|
|
40
84
|
// Accept a full invite URL or a bare code and return the code.
|
|
41
85
|
function stripInviteUrl(codeOrUrl) {
|
|
42
86
|
return String(codeOrUrl || '')
|
|
@@ -185,6 +229,9 @@ class WhalibmobClient extends EventEmitter {
|
|
|
185
229
|
// Flipped by setOnline(). Delivery receipts sent while this is false carry
|
|
186
230
|
// type="inactive", which the sender's app records but does not render.
|
|
187
231
|
this._sendActiveReceipts = false;
|
|
232
|
+
// Cached privacy settings, as whatsmeow caches them. Warmed after connect
|
|
233
|
+
// and refreshed whenever the server reports a change; markRead reads it.
|
|
234
|
+
this._privacySettings = null;
|
|
188
235
|
if (opts.pino !== undefined) _whaConfigLogger(opts.pino);
|
|
189
236
|
}
|
|
190
237
|
|
|
@@ -325,6 +372,11 @@ class WhalibmobClient extends EventEmitter {
|
|
|
325
372
|
this._requestMediaConnection();
|
|
326
373
|
this._uploadPreKeys();
|
|
327
374
|
|
|
375
|
+
// Warm the privacy settings so read receipts respect them from the first
|
|
376
|
+
// message rather than from whenever something first asks for them.
|
|
377
|
+
this.queryPrivacySettings({ force: true }).catch(err =>
|
|
378
|
+
_whaDbg('[DBG] PRIVACY_FETCH_FAILED ' + (err && err.message)));
|
|
379
|
+
|
|
328
380
|
// ── Reconnect: flush stale device cache + background re-usync ─────────────
|
|
329
381
|
// On first connect _hasConnectedOnce is false — nothing extra to do.
|
|
330
382
|
// On every reconnect (network drop / NAT reset / server restart) we flush the
|
|
@@ -1034,6 +1086,19 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1034
1086
|
this._socket.sendNode(new BinaryNode('ack', attrs, null));
|
|
1035
1087
|
}
|
|
1036
1088
|
|
|
1089
|
+
// A read receipt only tells the sender we read it when read receipts are on.
|
|
1090
|
+
// whatsmeow's MarkRead checks the same setting and downgrades "read" to
|
|
1091
|
+
// "read-self", which marks the message read across our own devices without
|
|
1092
|
+
// reporting it to the other side. Nothing here looked at the setting, so a
|
|
1093
|
+
// user who had turned read receipts off still sent blue ticks.
|
|
1094
|
+
_readReceiptType(base) {
|
|
1095
|
+
const settings = this._privacySettings;
|
|
1096
|
+
if (settings && settings.readReceipts === 'none' && base === 'read') {
|
|
1097
|
+
return 'read-self';
|
|
1098
|
+
}
|
|
1099
|
+
return base;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1037
1102
|
// True when a JID names this account, in either address family.
|
|
1038
1103
|
_isOwnUser(jid) {
|
|
1039
1104
|
if (!jid) return false;
|
|
@@ -1080,7 +1145,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1080
1145
|
if (this.autoRead === false) return;
|
|
1081
1146
|
const node = new BinaryNode('receipt', {
|
|
1082
1147
|
id: msgId,
|
|
1083
|
-
type: 'read',
|
|
1148
|
+
type: this._readReceiptType('read'),
|
|
1084
1149
|
to: fromJid,
|
|
1085
1150
|
t: String(Math.floor(Date.now() / 1000))
|
|
1086
1151
|
}, null);
|
|
@@ -1201,6 +1266,46 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1201
1266
|
|
|
1202
1267
|
// ─── Notification handling ────────────────────────────────────────────────
|
|
1203
1268
|
|
|
1269
|
+
// <privacy><category name="..." value="..."/></privacy> inside an account_sync
|
|
1270
|
+
// notification. The node carries only what changed, so it is folded into the
|
|
1271
|
+
// cached settings rather than replacing them.
|
|
1272
|
+
_handlePrivacySettingsNotification(node) {
|
|
1273
|
+
const changed = this._parsePrivacySettings(node);
|
|
1274
|
+
const merged = Object.assign({}, this._privacySettings || {});
|
|
1275
|
+
const changes = {};
|
|
1276
|
+
for (const key of Object.keys(changed)) {
|
|
1277
|
+
if (changed[key] !== null) { merged[key] = changed[key]; changes[key] = changed[key]; }
|
|
1278
|
+
}
|
|
1279
|
+
this._privacySettings = merged;
|
|
1280
|
+
_whaDbg('[DBG] PRIVACY_CHANGE ' + JSON.stringify(changes));
|
|
1281
|
+
this.emit('privacy_settings', { changes, settings: merged });
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// <blocklist action="..." dhash="..." prev_dhash="..."><item jid=... action=.../></blocklist>
|
|
1285
|
+
// Mirrors whatsmeow's handleBlocklist: the action on the node describes the
|
|
1286
|
+
// whole change, each child names one JID and what happened to it.
|
|
1287
|
+
_handleBlocklistNotification(node) {
|
|
1288
|
+
const attrs = node.attrs || {};
|
|
1289
|
+
const changes = [];
|
|
1290
|
+
if (Array.isArray(node.content)) {
|
|
1291
|
+
for (const child of node.content) {
|
|
1292
|
+
if (!child || !child.attrs || !child.attrs.jid) continue;
|
|
1293
|
+
changes.push({
|
|
1294
|
+
jid: String(child.attrs.jid),
|
|
1295
|
+
action: child.attrs.action ? String(child.attrs.action) : null
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
_whaDbg('[DBG] BLOCKLIST_CHANGE action=' + (attrs.action || '') +
|
|
1300
|
+
' changes=' + changes.map(c => c.action + ':' + c.jid).join(','));
|
|
1301
|
+
this.emit('blocklist', {
|
|
1302
|
+
action: attrs.action ? String(attrs.action) : null,
|
|
1303
|
+
dhash: attrs.dhash ? String(attrs.dhash) : null,
|
|
1304
|
+
prevDhash: attrs.prev_dhash ? String(attrs.prev_dhash) : null,
|
|
1305
|
+
changes
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1204
1309
|
_handleNotification(node) {
|
|
1205
1310
|
const attrs = node.attrs || {};
|
|
1206
1311
|
const type = attrs.type || '';
|
|
@@ -1216,6 +1321,16 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1216
1321
|
// Account sync notification — may contain device list updates
|
|
1217
1322
|
const devicesNode = findChildDeep(node, 'devices');
|
|
1218
1323
|
if (devicesNode) this._processDeviceUpdate(devicesNode);
|
|
1324
|
+
// ...and block list changes. Blocking someone from the phone arrives
|
|
1325
|
+
// here; nothing was watching for it, so the client's idea of who is
|
|
1326
|
+
// blocked only ever changed when it did the blocking itself.
|
|
1327
|
+
const blocklistNode = findChildDeep(node, 'blocklist');
|
|
1328
|
+
if (blocklistNode) this._handleBlocklistNotification(blocklistNode);
|
|
1329
|
+
// ...and privacy setting changes, which arrive the same way. whatsmeow
|
|
1330
|
+
// re-reads the settings on this and raises an event; nothing was watching
|
|
1331
|
+
// the node, so a change made on the phone never reached the cache.
|
|
1332
|
+
const privacyNode = findChildDeep(node, 'privacy');
|
|
1333
|
+
if (privacyNode) this._handlePrivacySettingsNotification(privacyNode);
|
|
1219
1334
|
}
|
|
1220
1335
|
|
|
1221
1336
|
if (type === 'w:gp2') {
|
|
@@ -2297,7 +2412,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2297
2412
|
|
|
2298
2413
|
const attrs = {
|
|
2299
2414
|
id: ids[0],
|
|
2300
|
-
type: opts.type === 'played' ? 'played' : 'read',
|
|
2415
|
+
type: this._readReceiptType(opts.type === 'played' ? 'played' : 'read'),
|
|
2301
2416
|
to: jidStrToObj(chatJid),
|
|
2302
2417
|
t: String(Math.floor(Date.now() / 1000))
|
|
2303
2418
|
};
|
|
@@ -2623,7 +2738,29 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2623
2738
|
return httpGetBuffer(info.url);
|
|
2624
2739
|
}
|
|
2625
2740
|
|
|
2626
|
-
//
|
|
2741
|
+
// Parse the <list> a blocklist query or update comes back with.
|
|
2742
|
+
//
|
|
2743
|
+
// whatsmeow's parseBlocklist walks every child and takes its jid, without
|
|
2744
|
+
// caring what the tag is called, and keeps the dhash off the list itself.
|
|
2745
|
+
// The JIDs are stringified here: the binary decoder hands them back as JID
|
|
2746
|
+
// objects, and while those print correctly they are not strings, so the array
|
|
2747
|
+
// this used to return failed any includes() or === against a JID string —
|
|
2748
|
+
// which is exactly what the documented shape invites you to do.
|
|
2749
|
+
_parseBlocklist(listNode) {
|
|
2750
|
+
const jids = [];
|
|
2751
|
+
if (listNode && Array.isArray(listNode.content)) {
|
|
2752
|
+
for (const child of listNode.content) {
|
|
2753
|
+
if (!child || !child.attrs || !child.attrs.jid) continue;
|
|
2754
|
+
jids.push(String(child.attrs.jid));
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
const dhash = (listNode && listNode.attrs && listNode.attrs.dhash)
|
|
2758
|
+
? String(listNode.attrs.dhash) : null;
|
|
2759
|
+
return { dhash, jids };
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// Query blocked contacts list. Returns an array of JID strings; never throws,
|
|
2763
|
+
// an unreachable server yields an empty list.
|
|
2627
2764
|
async queryBlockList() {
|
|
2628
2765
|
const id = this._genMsgId();
|
|
2629
2766
|
const node = new BinaryNode('iq', {
|
|
@@ -2634,11 +2771,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2634
2771
|
}, null);
|
|
2635
2772
|
const resp = await this._sendIq(node);
|
|
2636
2773
|
if (!resp) return [];
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
return list.content
|
|
2640
|
-
.filter(n => n && n.description === 'item' && n.attrs && n.attrs.jid)
|
|
2641
|
-
.map(n => n.attrs.jid);
|
|
2774
|
+
if (resp.attrs && resp.attrs.type === 'error') return [];
|
|
2775
|
+
return this._parseBlocklist(findChild(resp, 'list')).jids;
|
|
2642
2776
|
}
|
|
2643
2777
|
|
|
2644
2778
|
// Change display name
|
|
@@ -2683,29 +2817,114 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2683
2817
|
return this._sendIq(node);
|
|
2684
2818
|
}
|
|
2685
2819
|
|
|
2686
|
-
//
|
|
2687
|
-
|
|
2688
|
-
//
|
|
2689
|
-
//
|
|
2820
|
+
// ─── Privacy settings ─────────────────────────────────────────────────────
|
|
2821
|
+
|
|
2822
|
+
// Read the current privacy settings.
|
|
2823
|
+
//
|
|
2824
|
+
// Returns { lastSeen, profile, status, online, readReceipts, groupAdd,
|
|
2825
|
+
// callAdd, messages, defense, stickers } — any the server did not
|
|
2826
|
+
// report come back null. Served from cache unless opts.force is set.
|
|
2827
|
+
//
|
|
2828
|
+
// There was no way to read these at all before, which also meant nothing
|
|
2829
|
+
// could act on them; see markRead for what that cost.
|
|
2830
|
+
async queryPrivacySettings(opts) {
|
|
2831
|
+
opts = opts || {};
|
|
2832
|
+
if (this._privacySettings && !opts.force) return this._privacySettings;
|
|
2833
|
+
|
|
2834
|
+
const node = new BinaryNode('iq', {
|
|
2835
|
+
id: this._genMsgId(),
|
|
2836
|
+
xmlns: 'privacy',
|
|
2837
|
+
to: 's.whatsapp.net',
|
|
2838
|
+
type: 'get'
|
|
2839
|
+
}, [new BinaryNode('privacy', {}, null)]);
|
|
2840
|
+
|
|
2841
|
+
const resp = await this._sendIq(node);
|
|
2842
|
+
if (!resp) throw new Error('queryPrivacySettings: no reply from server (IQ timed out)');
|
|
2843
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
2844
|
+
const errNode = findChild(resp, 'error');
|
|
2845
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
2846
|
+
throw new Error('queryPrivacySettings: server rejected the request' +
|
|
2847
|
+
(code ? ' (' + code + ')' : ''));
|
|
2848
|
+
}
|
|
2849
|
+
const privacyNode = findChild(resp, 'privacy');
|
|
2850
|
+
this._privacySettings = this._parsePrivacySettings(privacyNode);
|
|
2851
|
+
return this._privacySettings;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// <privacy><category name="..." value="..."/>...</privacy>
|
|
2855
|
+
_parsePrivacySettings(privacyNode) {
|
|
2856
|
+
const out = {};
|
|
2857
|
+
for (const key of Object.keys(PRIVACY_WIRE_TO_KEY)) out[PRIVACY_WIRE_TO_KEY[key]] = null;
|
|
2858
|
+
if (privacyNode && Array.isArray(privacyNode.content)) {
|
|
2859
|
+
for (const child of privacyNode.content) {
|
|
2860
|
+
if (!child || child.description !== 'category' || !child.attrs) continue;
|
|
2861
|
+
const key = PRIVACY_WIRE_TO_KEY[String(child.attrs.name)];
|
|
2862
|
+
if (key) out[key] = child.attrs.value ? String(child.attrs.value) : null;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return out;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// Change a privacy setting. Returns the settings as they now stand.
|
|
2869
|
+
//
|
|
2870
|
+
// type: 'last_seen' | 'profile_picture' | 'status' | 'online' |
|
|
2871
|
+
// 'read_receipts' | 'groups_add' | 'call_add' | 'messages' |
|
|
2872
|
+
// 'defense' | 'stickers' (the protocol's own spellings work too)
|
|
2873
|
+
// value: 'all' | 'contacts' | 'contact_blacklist' | 'contact_allowlist' |
|
|
2874
|
+
// 'none' | 'match_last_seen' | 'known' | 'on_standard' | 'off'
|
|
2875
|
+
// excluded: JIDs to exclude — see the note below
|
|
2876
|
+
//
|
|
2877
|
+
// The names above are friendly aliases. What goes on the wire is what
|
|
2878
|
+
// whatsmeow sends: last, profile, status, online, readreceipts, groupadd,
|
|
2879
|
+
// calladd, messages, defense, stickers. Four of the six names this used to
|
|
2880
|
+
// put on the wire — last_seen, profile_picture, read_receipts, groups_add —
|
|
2881
|
+
// are not names the server knows, so those settings never changed and the
|
|
2882
|
+
// discarded reply meant it never said so either.
|
|
2690
2883
|
async changePrivacySetting(type, value, excluded) {
|
|
2691
|
-
const
|
|
2884
|
+
const name = PRIVACY_NAME_ALIASES[String(type)];
|
|
2885
|
+
if (!name) {
|
|
2886
|
+
throw new Error('changePrivacySetting: unknown setting "' + type + '" — one of ' +
|
|
2887
|
+
Object.keys(PRIVACY_NAME_ALIASES).join(', '));
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// Neither whatsmeow nor WhatsApp Web puts <user> children on this IQ; the
|
|
2891
|
+
// exclusion list is maintained separately. Kept for callers that pass it,
|
|
2892
|
+
// and only emitted when they do.
|
|
2692
2893
|
const children = [];
|
|
2693
2894
|
if (excluded && excluded.length > 0) {
|
|
2694
2895
|
for (const jid of excluded) {
|
|
2695
|
-
children.push(new BinaryNode('user', { jid: makeJid(jid) }, null));
|
|
2896
|
+
children.push(new BinaryNode('user', { jid: jidStrToObj(toNonAdJid(makeJid(jid))) }, null));
|
|
2696
2897
|
}
|
|
2697
2898
|
}
|
|
2698
|
-
|
|
2699
|
-
const id = this._genMsgId();
|
|
2899
|
+
|
|
2700
2900
|
const node = new BinaryNode('iq', {
|
|
2701
|
-
id,
|
|
2901
|
+
id: this._genMsgId(),
|
|
2702
2902
|
xmlns: 'privacy',
|
|
2703
2903
|
to: 's.whatsapp.net',
|
|
2704
2904
|
type: 'set'
|
|
2705
2905
|
}, [
|
|
2706
|
-
new BinaryNode('privacy', {}, [
|
|
2906
|
+
new BinaryNode('privacy', {}, [
|
|
2907
|
+
new BinaryNode('category', { name, value: String(value) },
|
|
2908
|
+
children.length > 0 ? children : null)
|
|
2909
|
+
])
|
|
2707
2910
|
]);
|
|
2708
|
-
|
|
2911
|
+
|
|
2912
|
+
const resp = await this._sendIq(node);
|
|
2913
|
+
if (!resp) throw new Error('changePrivacySetting: no reply from server (IQ timed out)');
|
|
2914
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
2915
|
+
const errNode = findChild(resp, 'error');
|
|
2916
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
2917
|
+
const text = errNode && errNode.attrs && errNode.attrs.text;
|
|
2918
|
+
throw new Error('changePrivacySetting: server rejected ' + name + '=' + value +
|
|
2919
|
+
(code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
// whatsmeow folds the change into its cached copy and hands the whole thing
|
|
2923
|
+
// back rather than making the caller re-query.
|
|
2924
|
+
const key = PRIVACY_WIRE_TO_KEY[name];
|
|
2925
|
+
this._privacySettings = Object.assign({}, this._privacySettings || {},
|
|
2926
|
+
{ [key]: String(value) });
|
|
2927
|
+
return this._privacySettings;
|
|
2709
2928
|
}
|
|
2710
2929
|
|
|
2711
2930
|
// ──────────────────────────────────────────────────────────────────────────
|
|
@@ -2726,10 +2945,132 @@ class WhalibmobClient extends EventEmitter {
|
|
|
2726
2945
|
return this._sender.deleteMessage(origMsgId, chatJid, fromMe, forEveryone, opts);
|
|
2727
2946
|
}
|
|
2728
2947
|
|
|
2729
|
-
//
|
|
2730
|
-
|
|
2948
|
+
// ─── Status / Stories ─────────────────────────────────────────────────────
|
|
2949
|
+
|
|
2950
|
+
// Who your Status posts go to.
|
|
2951
|
+
//
|
|
2952
|
+
// Returns [{ type, isDefault, list }] with the default first, as whatsmeow's
|
|
2953
|
+
// GetStatusPrivacy does. type is 'contacts', 'blacklist' or 'whitelist'; list
|
|
2954
|
+
// holds the JIDs the last two are built from. A server that has never been
|
|
2955
|
+
// told otherwise answers nothing, which means the default: all contacts.
|
|
2956
|
+
async queryStatusPrivacy() {
|
|
2957
|
+
const node = new BinaryNode('iq', {
|
|
2958
|
+
id: this._genMsgId(),
|
|
2959
|
+
xmlns: 'status',
|
|
2960
|
+
to: 's.whatsapp.net',
|
|
2961
|
+
type: 'get'
|
|
2962
|
+
}, [new BinaryNode('privacy', {}, null)]);
|
|
2963
|
+
|
|
2964
|
+
const resp = await this._sendIq(node);
|
|
2965
|
+
if (!resp || (resp.attrs && resp.attrs.type === 'error')) {
|
|
2966
|
+
return [{ type: 'contacts', isDefault: true, list: [] }];
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
const privacyNode = findChild(resp, 'privacy');
|
|
2970
|
+
const out = [];
|
|
2971
|
+
if (privacyNode && Array.isArray(privacyNode.content)) {
|
|
2972
|
+
for (const listNode of privacyNode.content) {
|
|
2973
|
+
if (!listNode || listNode.description !== 'list' || !listNode.attrs) continue;
|
|
2974
|
+
const entry = {
|
|
2975
|
+
type: listNode.attrs.type ? String(listNode.attrs.type) : 'contacts',
|
|
2976
|
+
isDefault: String(listNode.attrs.default) === 'true',
|
|
2977
|
+
list: []
|
|
2978
|
+
};
|
|
2979
|
+
if (Array.isArray(listNode.content)) {
|
|
2980
|
+
for (const u of listNode.content) {
|
|
2981
|
+
if (u && u.description === 'user' && u.attrs && u.attrs.jid) {
|
|
2982
|
+
entry.list.push(String(u.attrs.jid));
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
// The default is always reported first.
|
|
2987
|
+
if (entry.isDefault) out.unshift(entry); else out.push(entry);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
return out.length ? out : [{ type: 'contacts', isDefault: true, list: [] }];
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
// Resolve the status privacy settings into the JIDs a post actually goes to,
|
|
2994
|
+
// the way whatsmeow's getStatusBroadcastRecipients does: a whitelist is the
|
|
2995
|
+
// list itself, anything else is every known contact minus the blacklist, and
|
|
2996
|
+
// our own JID is added so the post lands on our own devices too.
|
|
2997
|
+
//
|
|
2998
|
+
// Contacts come from the synced history store — this client has no separate
|
|
2999
|
+
// contact database — so a session that has not synced history yet has to be
|
|
3000
|
+
// given the list explicitly.
|
|
3001
|
+
async _getStatusRecipients() {
|
|
3002
|
+
let privacy;
|
|
3003
|
+
try {
|
|
3004
|
+
privacy = (await this.queryStatusPrivacy())[0];
|
|
3005
|
+
} catch (_) {
|
|
3006
|
+
privacy = { type: 'contacts', list: [] };
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
const ownJid = String(this._store.phoneNumber) + '@s.whatsapp.net';
|
|
3010
|
+
let recipients;
|
|
3011
|
+
|
|
3012
|
+
if (privacy.type === 'whitelist') {
|
|
3013
|
+
recipients = privacy.list.slice();
|
|
3014
|
+
} else {
|
|
3015
|
+
const hist = this.getHistoryStore();
|
|
3016
|
+
const contacts = (hist && hist.contacts) ? Object.keys(hist.contacts) : [];
|
|
3017
|
+
// Only real contacts — an entry the history sync created purely to hold a
|
|
3018
|
+
// push name is not somebody you have in your address book.
|
|
3019
|
+
const named = contacts.filter(jid => {
|
|
3020
|
+
const c = hist.contacts[jid];
|
|
3021
|
+
return c && (c.name || c.displayName) && !jid.endsWith('@g.us') &&
|
|
3022
|
+
!jid.endsWith('@broadcast');
|
|
3023
|
+
});
|
|
3024
|
+
const blocked = new Set(privacy.type === 'blacklist' ? privacy.list : []);
|
|
3025
|
+
recipients = named.filter(jid => !blocked.has(jid));
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
if (!recipients.some(j => this._isOwnUser(j))) recipients.push(ownJid);
|
|
3029
|
+
_whaDbg('[DBG] STATUS_RECIPIENTS type=' + privacy.type +
|
|
3030
|
+
' count=' + recipients.length);
|
|
3031
|
+
return recipients;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// Post a Status/Story.
|
|
3035
|
+
//
|
|
3036
|
+
// sendStatus('Good morning!') text
|
|
3037
|
+
// sendStatus({ image: './a.jpg', caption: 'hi' }) photo
|
|
3038
|
+
// sendStatus({ video: './v.mp4' }) video
|
|
3039
|
+
// sendStatus({ audio: './v.ogg' }) voice
|
|
3040
|
+
//
|
|
3041
|
+
// opts (also accepted as fields of the object form):
|
|
3042
|
+
// recipients post to these JIDs instead of the privacy-derived list
|
|
3043
|
+
// backgroundArgb background colour of a text status, e.g. 0xFF25D366
|
|
3044
|
+
// textArgb, font the rest of a text status' styling
|
|
3045
|
+
//
|
|
3046
|
+
// Text posted with a background or font becomes an ExtendedTextMessage, which
|
|
3047
|
+
// is what carries them; plain text stays a plain conversation.
|
|
3048
|
+
async sendStatus(content, opts) {
|
|
2731
3049
|
if (!this._sender || !this._connected) throw new Error('Not connected');
|
|
2732
|
-
|
|
3050
|
+
opts = Object.assign({},
|
|
3051
|
+
(content && typeof content === 'object') ? content : {},
|
|
3052
|
+
opts || {});
|
|
3053
|
+
|
|
3054
|
+
const recipients = opts.recipients || opts.statusJidList ||
|
|
3055
|
+
await this._getStatusRecipients();
|
|
3056
|
+
const sendOpts = Object.assign({}, opts, { _statusRecipients: recipients });
|
|
3057
|
+
|
|
3058
|
+
if (opts.image) return this._sender.sendImage(STATUS_BROADCAST_JID, opts.image, sendOpts);
|
|
3059
|
+
if (opts.video) return this._sender.sendVideo(STATUS_BROADCAST_JID, opts.video, sendOpts);
|
|
3060
|
+
if (opts.audio) return this._sender.sendAudio(STATUS_BROADCAST_JID, opts.audio, sendOpts);
|
|
3061
|
+
|
|
3062
|
+
const text = (typeof content === 'string') ? content : (opts.text || opts.caption || '');
|
|
3063
|
+
const styled = opts.backgroundArgb != null || opts.textArgb != null || opts.font != null;
|
|
3064
|
+
if (!styled) return this._sender.sendText(STATUS_BROADCAST_JID, text, sendOpts);
|
|
3065
|
+
|
|
3066
|
+
const { encodeMessage, encodeExtendedText } = require('./proto/MessageProto');
|
|
3067
|
+
const buf = encodeMessage('extendedText', encodeExtendedText(text, {
|
|
3068
|
+
backgroundArgb: opts.backgroundArgb,
|
|
3069
|
+
textArgb: opts.textArgb,
|
|
3070
|
+
font: opts.font
|
|
3071
|
+
}));
|
|
3072
|
+
return this._sender._sendMessage(STATUS_BROADCAST_JID,
|
|
3073
|
+
generateMessageId(), buf, 'text', sendOpts);
|
|
2733
3074
|
}
|
|
2734
3075
|
|
|
2735
3076
|
// Forward a message
|
|
@@ -3158,32 +3499,52 @@ class WhalibmobClient extends EventEmitter {
|
|
|
3158
3499
|
return this._sendIq(node);
|
|
3159
3500
|
}
|
|
3160
3501
|
|
|
3161
|
-
// Block a contact
|
|
3502
|
+
// Block a contact. Returns the updated block list.
|
|
3162
3503
|
async blockContact(jid) {
|
|
3163
|
-
|
|
3164
|
-
const node = new BinaryNode('iq', {
|
|
3165
|
-
id,
|
|
3166
|
-
xmlns: 'blocklist',
|
|
3167
|
-
to: 's.whatsapp.net',
|
|
3168
|
-
type: 'set'
|
|
3169
|
-
}, [
|
|
3170
|
-
new BinaryNode('item', { action: 'block', jid: makeJid(jid) }, null)
|
|
3171
|
-
]);
|
|
3172
|
-
return this._sendIq(node);
|
|
3504
|
+
return this._updateBlocklist(jid, 'block');
|
|
3173
3505
|
}
|
|
3174
3506
|
|
|
3175
|
-
// Unblock a contact
|
|
3507
|
+
// Unblock a contact. Returns the updated block list.
|
|
3176
3508
|
async unblockContact(jid) {
|
|
3177
|
-
|
|
3509
|
+
return this._updateBlocklist(jid, 'unblock');
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
// The one IQ behind both, as whatsmeow's UpdateBlocklist sends it:
|
|
3513
|
+
//
|
|
3514
|
+
// <iq xmlns="blocklist" type="set" to="s.whatsapp.net">
|
|
3515
|
+
// <item jid="<JID>" action="block|unblock"/>
|
|
3516
|
+
// </iq>
|
|
3517
|
+
//
|
|
3518
|
+
// Three things were wrong with the pair this replaces. The jid went out as a
|
|
3519
|
+
// raw string rather than a binary JID. It was not reduced to its bare user
|
|
3520
|
+
// first, so blocking a contact you happened to name with a device suffix sent
|
|
3521
|
+
// the server a device JID it has nothing to block. And the reply was thrown
|
|
3522
|
+
// away without being looked at — including a type="error" reply — so a block
|
|
3523
|
+
// the server refused came back looking exactly like one that worked.
|
|
3524
|
+
async _updateBlocklist(jid, action) {
|
|
3525
|
+
const target = toNonAdJid(makeJid(jid));
|
|
3178
3526
|
const node = new BinaryNode('iq', {
|
|
3179
|
-
id,
|
|
3527
|
+
id: this._genMsgId(),
|
|
3180
3528
|
xmlns: 'blocklist',
|
|
3181
3529
|
to: 's.whatsapp.net',
|
|
3182
3530
|
type: 'set'
|
|
3183
3531
|
}, [
|
|
3184
|
-
new BinaryNode('item', {
|
|
3532
|
+
new BinaryNode('item', { jid: jidStrToObj(target), action }, null)
|
|
3185
3533
|
]);
|
|
3186
|
-
|
|
3534
|
+
|
|
3535
|
+
const resp = await this._sendIq(node);
|
|
3536
|
+
if (!resp) {
|
|
3537
|
+
throw new Error(action + 'Contact: no reply from server (IQ timed out)');
|
|
3538
|
+
}
|
|
3539
|
+
if (resp.attrs && resp.attrs.type === 'error') {
|
|
3540
|
+
const errNode = findChild(resp, 'error');
|
|
3541
|
+
const code = errNode && errNode.attrs && errNode.attrs.code;
|
|
3542
|
+
const text = errNode && errNode.attrs && errNode.attrs.text;
|
|
3543
|
+
throw new Error(action + 'Contact: server rejected the request' +
|
|
3544
|
+
(code ? ' (' + code + (text ? ' ' + text : '') + ')' : ''));
|
|
3545
|
+
}
|
|
3546
|
+
// The server answers with the whole list as it now stands.
|
|
3547
|
+
return this._parseBlocklist(findChild(resp, 'list')).jids;
|
|
3187
3548
|
}
|
|
3188
3549
|
|
|
3189
3550
|
// Pin / unpin a chat (mobile: local state)
|
|
@@ -37,6 +37,16 @@ function isGroupJid(jid) {
|
|
|
37
37
|
return jid.endsWith('@g.us') || jid.endsWith('@community');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// A Status/Story is posted as a message to status@broadcast, and it travels the
|
|
41
|
+
// SenderKey path a group message does — one skmsg plus a distribution to every
|
|
42
|
+
// recipient — not the 1:1 path. Baileys calls the pair isGroupOrStatus;
|
|
43
|
+
// whatsmeow routes BroadcastServer into sendGroup for the same reason.
|
|
44
|
+
const STATUS_BROADCAST_JID = 'status@broadcast';
|
|
45
|
+
|
|
46
|
+
function isStatusBroadcast(jid) {
|
|
47
|
+
return String(jid) === STATUS_BROADCAST_JID;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function phoneFromJid(jid) {
|
|
41
51
|
const at = jid.indexOf('@');
|
|
42
52
|
const user = at >= 0 ? jid.slice(0, at) : jid;
|
|
@@ -615,7 +625,11 @@ class MessageSender {
|
|
|
615
625
|
|
|
616
626
|
async _sendMessage(toJid, msgId, plaintext, mediaType, options) {
|
|
617
627
|
options = options || {};
|
|
618
|
-
|
|
628
|
+
// status@broadcast is neither a group JID nor a user JID. It used to fall
|
|
629
|
+
// through to the 1:1 path, which took "status" for a phone number, ran a
|
|
630
|
+
// usync for it and tried to open a Signal session with status@s.whatsapp.net
|
|
631
|
+
// — so a Status could not be posted at all.
|
|
632
|
+
if (isGroupJid(toJid) || isStatusBroadcast(toJid)) {
|
|
619
633
|
try {
|
|
620
634
|
return await this._sendGroupMessage(toJid, msgId, plaintext, mediaType, options);
|
|
621
635
|
} catch (err) {
|
|
@@ -1086,9 +1100,27 @@ class MessageSender {
|
|
|
1086
1100
|
const ownJid = `${ownPhone}@s.whatsapp.net`;
|
|
1087
1101
|
const mediaSubtype = options._mediaSubtype || null;
|
|
1088
1102
|
|
|
1103
|
+
const isStatus = isStatusBroadcast(groupJid);
|
|
1104
|
+
|
|
1105
|
+
// A Status has no group metadata; its recipients come from the account's
|
|
1106
|
+
// status privacy settings, or from a list the caller supplied.
|
|
1107
|
+
let members;
|
|
1108
|
+
if (isStatus) {
|
|
1109
|
+
members = options._statusRecipients ||
|
|
1110
|
+
(typeof this._client._getStatusRecipients === 'function'
|
|
1111
|
+
? await this._client._getStatusRecipients()
|
|
1112
|
+
: []);
|
|
1113
|
+
if (!members.length) {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
'Cannot post a Status: no recipients. Sync history so the contact ' +
|
|
1116
|
+
'list is known, or pass { recipients: [...] }.'
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1089
1121
|
// Auto-fetch group metadata if member cache is empty (first send to this group)
|
|
1090
|
-
|
|
1091
|
-
if (members.length === 0 && typeof this._client.getGroupMetadata === 'function') {
|
|
1122
|
+
if (!isStatus) members = this._client._getGroupMembers(groupJid);
|
|
1123
|
+
if (!isStatus && members.length === 0 && typeof this._client.getGroupMetadata === 'function') {
|
|
1092
1124
|
_whaDbg('[DBG] GROUP_SEND auto-fetch metadata for ' + groupJid);
|
|
1093
1125
|
let metaErr = null;
|
|
1094
1126
|
try {
|
|
@@ -1113,8 +1145,13 @@ class MessageSender {
|
|
|
1113
1145
|
}
|
|
1114
1146
|
}
|
|
1115
1147
|
|
|
1116
|
-
// Determine addressing mode for this group (lid = modern groups, pn = legacy)
|
|
1117
|
-
|
|
1148
|
+
// Determine addressing mode for this group (lid = modern groups, pn = legacy).
|
|
1149
|
+
// A Status carries none: whatsmeow sets addressing_mode only for the group
|
|
1150
|
+
// server, and its recipients are ordinary contacts, so everything below
|
|
1151
|
+
// stays in phone space.
|
|
1152
|
+
const groupAddressingMode = isStatus
|
|
1153
|
+
? null
|
|
1154
|
+
: (this._client._groupAddressingMode.get(groupJid) || 'lid');
|
|
1118
1155
|
// A stanza addressed with addressing_mode='lid' must carry participants in
|
|
1119
1156
|
// LID space only. Mixing @s.whatsapp.net targets into it is exactly what
|
|
1120
1157
|
// DeviceManager warns about, and the server answers it with ack 479.
|
|
@@ -1285,10 +1322,10 @@ class MessageSender {
|
|
|
1285
1322
|
to: jidStrToObj(groupJid),
|
|
1286
1323
|
id: msgId,
|
|
1287
1324
|
type: mediaType,
|
|
1288
|
-
addressing_mode: groupAddressingMode,
|
|
1289
1325
|
phash,
|
|
1290
1326
|
t: String(msNow())
|
|
1291
1327
|
};
|
|
1328
|
+
if (groupAddressingMode) stanzaAttrs.addressing_mode = groupAddressingMode;
|
|
1292
1329
|
if (options.edit) stanzaAttrs.edit = String(options.edit);
|
|
1293
1330
|
|
|
1294
1331
|
// Cache the plaintext here too, not just on the DM path. A group recipient
|
|
@@ -53,6 +53,15 @@ function bytes(b) { return b ? Buffer.from(b) : Buffer.alloc(0); }
|
|
|
53
53
|
function str(s) { return s ? Buffer.from(s, 'utf8') : Buffer.alloc(0); }
|
|
54
54
|
function bool(b) { return encodeVarint(b ? 1 : 0); }
|
|
55
55
|
|
|
56
|
+
const WIRE_32 = 5;
|
|
57
|
+
|
|
58
|
+
// fixed32 — four little-endian bytes, wire type 5. ARGB colours use this.
|
|
59
|
+
function fixed32(fieldNum, value) {
|
|
60
|
+
const buf = Buffer.allocUnsafe(4);
|
|
61
|
+
buf.writeUInt32LE((value >>> 0), 0);
|
|
62
|
+
return Buffer.concat([tag(fieldNum, WIRE_32), buf]);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
function encodeDouble(fieldNum, value) {
|
|
57
66
|
const t = tag(fieldNum, WIRE_64);
|
|
58
67
|
const buf = Buffer.allocUnsafe(8);
|
|
@@ -342,8 +351,19 @@ function encodeProtocolMessage(opts) {
|
|
|
342
351
|
|
|
343
352
|
// ─── Extended text message for editing ───────────────────────────────────────
|
|
344
353
|
// ExtendedTextMessage { text=1 } — used when editing a text message
|
|
345
|
-
|
|
346
|
-
|
|
354
|
+
// ExtendedTextMessage { text=1, textArgb=7, backgroundArgb=8, font=9,
|
|
355
|
+
// jpegThumbnail=16, contextInfo=17 }
|
|
356
|
+
//
|
|
357
|
+
// The colour fields are what a text Status is drawn with; both are fixed32
|
|
358
|
+
// (wire type 5), not varints, so they are written little-endian in four bytes.
|
|
359
|
+
function encodeExtendedText(text, opts) {
|
|
360
|
+
opts = opts || {};
|
|
361
|
+
const parts = [field(1, WIRE_LEN, str(text))];
|
|
362
|
+
if (opts.textArgb != null) parts.push(fixed32(7, opts.textArgb));
|
|
363
|
+
if (opts.backgroundArgb != null) parts.push(fixed32(8, opts.backgroundArgb));
|
|
364
|
+
if (opts.font != null) parts.push(field(9, WIRE_VARINT, varint(opts.font)));
|
|
365
|
+
if (opts.contextInfo) parts.push(field(17, WIRE_LEN, encodeContextInfo(opts.contextInfo)));
|
|
366
|
+
return Buffer.concat(parts);
|
|
347
367
|
}
|
|
348
368
|
|
|
349
369
|
// ─── Protobuf decoder ─────────────────────────────────────────────────────────
|