whalibmob 5.7.3 → 5.9.1
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 +533 -143
- package/cli.js +213 -55
- package/index.js +13 -1
- package/lib/Client.js +899 -177
- package/lib/GroupParticipant.js +141 -0
- package/lib/MediaService.js +28 -1
- package/lib/appstate/AppStateStore.js +100 -0
- package/lib/appstate/AppStateSync.js +292 -0
- package/lib/appstate/LTHash.js +119 -0
- package/lib/appstate/Mutations.js +106 -0
- package/lib/appstate/SyncdProto.js +263 -0
- package/lib/messages/MessageSender.js +32 -0
- package/lib/proto/MessageProto.js +75 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@ npm install -g whalibmob
|
|
|
97
97
|
- [Pin / Unpin](#pin--unpin)
|
|
98
98
|
- [Archive / Unarchive](#archive--unarchive)
|
|
99
99
|
- [Star / Unstar a Message](#star--unstar-a-message-cli)
|
|
100
|
+
- [Sync App State (CLI)](#cli-app-state)
|
|
100
101
|
- [Disappearing Messages](#cli-disappearing-messages)
|
|
101
102
|
- [Default Disappearing Timer](#default-disappearing-timer)
|
|
102
103
|
- [Block / Unblock](#block--unblock)
|
|
@@ -118,9 +119,10 @@ npm install -g whalibmob
|
|
|
118
119
|
- [List Group Participants](#list-group-participants)
|
|
119
120
|
- [Pending Join Requests](#pending-join-requests)
|
|
120
121
|
- [Approve / Reject Join Requests](#approve--reject-join-requests)
|
|
122
|
+
- [Personal Invitations (CLI)](#cli-personal-invitations)
|
|
121
123
|
- [Group Settings](#group-settings)
|
|
122
124
|
- [Community Commands](#community-commands-cli)
|
|
123
|
-
- [Newsletter / Channel Commands](#newsletter
|
|
125
|
+
- [Newsletter / Channel Commands](#newsletter--channel-commands)
|
|
124
126
|
- [Business Profile Command](#business-profile-command-cli)
|
|
125
127
|
- [Registration Commands (in-shell)](#registration-commands-in-shell)
|
|
126
128
|
- [Connection Commands (in-shell)](#connection-commands-in-shell)
|
|
@@ -201,6 +203,7 @@ npm install -g whalibmob
|
|
|
201
203
|
- [Mark a Chat Read / Unread](#mark-a-chat-read--unread)
|
|
202
204
|
- [Pin / Unpin a Chat](#pin--unpin-a-chat)
|
|
203
205
|
- [Star / Unstar a Message](#star--unstar-a-message)
|
|
206
|
+
- [Reading Changes Made Elsewhere](#app-state-sync)
|
|
204
207
|
- [Disappearing Messages](#disappearing-messages)
|
|
205
208
|
- [User Queries](#user-queries)
|
|
206
209
|
- [Check If a Number Has WhatsApp](#check-if-a-number-has-whatsapp)
|
|
@@ -244,6 +247,7 @@ npm install -g whalibmob
|
|
|
244
247
|
- [Query Metadata](#query-metadata)
|
|
245
248
|
- [Get Request Join List](#get-request-join-list)
|
|
246
249
|
- [Approve / Reject Request Join](#approve--reject-request-join)
|
|
250
|
+
- [Personal Invitations](#personal-invitations)
|
|
247
251
|
- [Toggle Ephemeral in Group](#toggle-ephemeral-in-group)
|
|
248
252
|
- [WhatsApp IDs](#whatsapp-ids)
|
|
249
253
|
- [Transport](#transport)
|
|
@@ -549,6 +553,8 @@ History arrives in chunks over the first minute or so after linking, largest fir
|
|
|
549
553
|
| `<phone>.web.signal.json` | Signal sessions and pre-keys for the linked device |
|
|
550
554
|
| `<phone>.web.sk.json` | group SenderKeys |
|
|
551
555
|
| `<phone>.web.tctoken.json` | privacy tokens |
|
|
556
|
+
| `<phone>.web.appState.json` | app-state version and hash per collection |
|
|
557
|
+
| `<phone>.appStateKeys.json` | app-state sync keys shared by the phone |
|
|
552
558
|
| `<phone>.web.history.json` | synced chats, contacts, push names, LID↔PN mappings |
|
|
553
559
|
| `<phone>.web.messages.json` | flat map of message id → message metadata |
|
|
554
560
|
|
|
@@ -988,7 +994,7 @@ try {
|
|
|
988
994
|
|
|
989
995
|
### `initAuthCreds`
|
|
990
996
|
|
|
991
|
-
Creates a fresh credential store for the given phone number. Functionally equivalent to `createNewStore` but also initialises the
|
|
997
|
+
Creates a fresh credential store for the given phone number. Functionally equivalent to `createNewStore` but also initialises the extra fields the library expects for account sync: `nextPreKeyId`, `firstUnuploadedPreKeyId`, `accountSyncCounter`, `accountSettings`, and `advSecretKey`.
|
|
992
998
|
|
|
993
999
|
```js
|
|
994
1000
|
const { initAuthCreds, saveStore } = require('whalibmob')
|
|
@@ -1093,13 +1099,25 @@ connect()
|
|
|
1093
1099
|
| `group_update` | `{ type, groupJid, actor, participants, subject, timestamp }` | Member added / removed / promoted / demoted, subject or settings changed |
|
|
1094
1100
|
| `notification` | node object | Group or contact update notification |
|
|
1095
1101
|
| `call` | `{ from }` | Incoming call event |
|
|
1096
|
-
| `chat_read` | `{ jid, read }` | Chat marked read (`read: true`) or unread (`read: false`) |
|
|
1097
|
-
| `chat_muted` | `{ jid, muted, until }` | Chat muted or unmuted; `until` is epoch ms (−1 = indefinite) |
|
|
1098
|
-
| `chat_pinned` | `{ jid, pinned }` | Chat pinned or unpinned |
|
|
1102
|
+
| `chat_read` | `{ jid, read, remote?, synced? }` | Chat marked read (`read: true`) or unread (`read: false`) |
|
|
1103
|
+
| `chat_muted` | `{ jid, muted, until, remote?, synced? }` | Chat muted or unmuted; `until` is epoch ms (−1 = indefinite) |
|
|
1104
|
+
| `chat_pinned` | `{ jid, pinned, remote?, synced? }` | Chat pinned or unpinned |
|
|
1099
1105
|
| `blocklist` | `{ action, dhash, prevDhash, changes }` | Block list changed on another device; `changes` is `[{ jid, action }]` |
|
|
1100
1106
|
| `privacy_settings` | `{ changes, settings }` | Privacy settings changed on another device |
|
|
1101
|
-
| `chat_archived` | `{ jid, archived }` | Chat archived or unarchived |
|
|
1102
|
-
| `message_starred` | `{ msgId, chatJid, starred }` | Message starred or unstarred |
|
|
1107
|
+
| `chat_archived` | `{ jid, archived, remote?, synced? }` | Chat archived or unarchived |
|
|
1108
|
+
| `message_starred` | `{ msgId, chatJid, starred, fromMe?, remote?, synced? }` | Message starred or unstarred |
|
|
1109
|
+
| `chat_removed` | `{ jid, kind, remote }` | A chat was cleared or deleted on another device |
|
|
1110
|
+
| `contact_update` | `{ jid, name, firstName, lid, username, removed, remote }` | A contact was renamed or removed elsewhere |
|
|
1111
|
+
| `push_name_update` | `{ name, remote }` | Your own display name changed on another device |
|
|
1112
|
+
| `app_state_sync` | `{ collections, applied }` | An app-state sync finished; see [Reading Changes Made Elsewhere](#app-state-sync) |
|
|
1113
|
+
| `app_state_mutation` | `{ collection, index, action, removed }` | An app-state change this library does not model |
|
|
1114
|
+
| `app_state_key_missing` | `{ collection, keyId }` | App state cannot be read until your phone shares this key |
|
|
1115
|
+
| `app_state_keys` | `{ keys }` | Your phone shared app-state sync keys; a sync starts automatically |
|
|
1116
|
+
|
|
1117
|
+
`remote: true` on a chat event means the change was made on your phone or another
|
|
1118
|
+
linked device rather than by this session. Your own calls carry `synced` instead,
|
|
1119
|
+
saying whether the change reached app state — see
|
|
1120
|
+
[Modifying Chats](#modifying-chats).
|
|
1103
1121
|
| `stream_error` | `{ reason }` | Server sent a fatal stream error |
|
|
1104
1122
|
| `decrypt_error` | `{ id, from, participant, err }` | Failed to decrypt an incoming message |
|
|
1105
1123
|
| `session_refresh` | `{ node }` | Late re-authentication success; Signal session refreshed |
|
|
@@ -1161,6 +1179,10 @@ The `decoded` object shape per message type:
|
|
|
1161
1179
|
// Contact (vCard)
|
|
1162
1180
|
{ type: 'contact', displayName: string, vcard: string }
|
|
1163
1181
|
|
|
1182
|
+
// Personal invitation into a group — see Personal Invitations
|
|
1183
|
+
{ type: 'groupInvite', groupJid: string, inviteCode: string, inviteExpiration: number,
|
|
1184
|
+
groupName: string, jpegThumbnail: Buffer|null, caption: string, isCommunity: boolean }
|
|
1185
|
+
|
|
1164
1186
|
// Protocol (revoke, ephemeral, etc.)
|
|
1165
1187
|
{ type: 'protocol', subtype: string }
|
|
1166
1188
|
```
|
|
@@ -1269,7 +1291,8 @@ The library automatically writes these files to `sessionDir` per account. You do
|
|
|
1269
1291
|
|---|---|
|
|
1270
1292
|
| `<phone>.history.json` | Chats, contacts, push names, LID↔PN mappings, tcTokens |
|
|
1271
1293
|
| `<phone>.messages.json` | Flat map of `msgId → message metadata` |
|
|
1272
|
-
| `<phone>.appStateKeys.json` | App-state sync keys
|
|
1294
|
+
| `<phone>.appStateKeys.json` | App-state sync keys, shared by your primary device |
|
|
1295
|
+
| `<phone>.appState.json` | Per-collection app-state version, hash and index map |
|
|
1273
1296
|
| `<phone>.tctoken.json` | Trusted-contact token store (tcToken per contact JID) |
|
|
1274
1297
|
|
|
1275
1298
|
### Reading the History Store
|
|
@@ -1380,6 +1403,9 @@ Token storage uses the **LID JID** of the contact (e.g. `112345678901234@lid`) a
|
|
|
1380
1403
|
| Persist chats / contacts / push names | ✅ | Written to `<phone>.history.json` |
|
|
1381
1404
|
| Persist message metadata | ✅ | Written to `<phone>.messages.json` |
|
|
1382
1405
|
| Persist app-state sync keys | ✅ | Written to `<phone>.appStateKeys.json` |
|
|
1406
|
+
| Sync app state when the server says it changed | ✅ | Pins, archives, mutes, stars, contact names |
|
|
1407
|
+
| Verify app-state MACs and LT hash | ✅ | A collection that drifts is re-read from a snapshot |
|
|
1408
|
+
| Persist app-state versions across restarts | ✅ | Written to `<phone>.appState.json` |
|
|
1383
1409
|
| Seed tcTokens into memory on connect | ✅ | Prevents error 463 on first send after reconnect |
|
|
1384
1410
|
| Attach tcToken to every outbound DM | ✅ | |
|
|
1385
1411
|
| Issue fresh tcTokens after each send | ✅ | Once per 7-day bucket per contact |
|
|
@@ -1422,115 +1448,64 @@ await client.init('919634847671')
|
|
|
1422
1448
|
|
|
1423
1449
|
## Receiving Media
|
|
1424
1450
|
|
|
1425
|
-
When a media message arrives, `msg.decoded`
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
**Decryption uses two steps:**
|
|
1429
|
-
1. HKDF-SHA256 expands `mediaKey` into IV, cipher key, and MAC key.
|
|
1430
|
-
2. AES-256-CBC decrypts the ciphertext; a 10-byte HMAC-SHA256 MAC is verified first.
|
|
1451
|
+
When a media message arrives, `msg.decoded` carries the CDN location and the
|
|
1452
|
+
`mediaKey` the file is encrypted under. `client.downloadMedia()` fetches it and
|
|
1453
|
+
hands back the plaintext bytes:
|
|
1431
1454
|
|
|
1432
1455
|
```js
|
|
1433
|
-
const
|
|
1434
|
-
const https = require('https')
|
|
1435
|
-
const http = require('http')
|
|
1436
|
-
const fs = require('fs')
|
|
1437
|
-
const path = require('path')
|
|
1438
|
-
|
|
1439
|
-
// HKDF info strings per media type
|
|
1440
|
-
const MEDIA_HKDF_INFO = {
|
|
1441
|
-
image: 'WhatsApp Image Keys',
|
|
1442
|
-
video: 'WhatsApp Video Keys',
|
|
1443
|
-
audio: 'WhatsApp Audio Keys',
|
|
1444
|
-
voice: 'WhatsApp Audio Keys',
|
|
1445
|
-
document: 'WhatsApp Document Keys',
|
|
1446
|
-
sticker: 'WhatsApp Image Keys',
|
|
1447
|
-
}
|
|
1456
|
+
const fs = require('fs')
|
|
1448
1457
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
const expanded = Buffer.from(crypto.hkdfSync('sha256', mediaKey, Buffer.alloc(0), info, 112))
|
|
1452
|
-
return {
|
|
1453
|
-
iv: expanded.slice(0, 16),
|
|
1454
|
-
cipherKey: expanded.slice(16, 48),
|
|
1455
|
-
macKey: expanded.slice(48, 80),
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
+
const EXT = { image: '.jpg', video: '.mp4', audio: '.ogg', voice: '.ogg',
|
|
1459
|
+
sticker: '.webp', document: '' }
|
|
1458
1460
|
|
|
1459
|
-
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
const fileMac = encrypted.slice(-10)
|
|
1463
|
-
|
|
1464
|
-
// Verify MAC
|
|
1465
|
-
const hmac = crypto.createHmac('sha256', macKey)
|
|
1466
|
-
hmac.update(iv)
|
|
1467
|
-
hmac.update(ciphertext)
|
|
1468
|
-
const computed = hmac.digest().slice(0, 10)
|
|
1469
|
-
if (!computed.equals(fileMac)) throw new Error('MAC mismatch — corrupt file or wrong key')
|
|
1470
|
-
|
|
1471
|
-
// Decrypt
|
|
1472
|
-
const decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, iv)
|
|
1473
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
1474
|
-
}
|
|
1461
|
+
client.on('message', async (msg) => {
|
|
1462
|
+
const d = msg.decoded
|
|
1463
|
+
if (!d || !d.mediaKey) return
|
|
1475
1464
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
req.on('error', reject)
|
|
1487
|
-
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')) })
|
|
1488
|
-
})
|
|
1489
|
-
}
|
|
1465
|
+
try {
|
|
1466
|
+
const bytes = await client.downloadMedia(d)
|
|
1467
|
+
const name = d.fileName || (msg.id + (EXT[d.type] || ''))
|
|
1468
|
+
fs.writeFileSync(name, bytes)
|
|
1469
|
+
console.log('saved', d.type, 'to', name)
|
|
1470
|
+
} catch (e) {
|
|
1471
|
+
console.error('media download failed:', e.message)
|
|
1472
|
+
}
|
|
1473
|
+
})
|
|
1474
|
+
```
|
|
1490
1475
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1476
|
+
It works the same in both modes, and that is the point: the CDN applies a
|
|
1477
|
+
browser check on the way **down** as well as on the way up, so a companion has to
|
|
1478
|
+
identify itself as one here too. Doing this by hand means knowing that, and
|
|
1479
|
+
knowing which HKDF key name each media type derives from — a voice note derives
|
|
1480
|
+
from the PTT keys and a GIF from the video ones, not from their own. Get either
|
|
1481
|
+
wrong and the download is refused or the decryption yields garbage.
|
|
1496
1482
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1483
|
+
The whole message object is accepted as well as its `decoded` half, so
|
|
1484
|
+
`client.downloadMedia(msg)` does the same thing.
|
|
1499
1485
|
|
|
1500
|
-
|
|
1501
|
-
fs.mkdirSync('./media', { recursive: true })
|
|
1502
|
-
fs.writeFileSync(outPath, decrypted)
|
|
1503
|
-
return outPath
|
|
1504
|
-
}
|
|
1505
|
-
```
|
|
1486
|
+
**Verifying the file**
|
|
1506
1487
|
|
|
1507
|
-
|
|
1488
|
+
Pass `{ verify: true }` to check the download against the message's
|
|
1489
|
+
`fileEncSha256` before decrypting it. The MAC already proves the plaintext was
|
|
1490
|
+
not tampered with; this catches a truncated or substituted download earlier, and
|
|
1491
|
+
names that failure separately from a decryption one.
|
|
1508
1492
|
|
|
1509
1493
|
```js
|
|
1510
|
-
const
|
|
1494
|
+
const bytes = await client.downloadMedia(d, { verify: true })
|
|
1495
|
+
```
|
|
1511
1496
|
|
|
1512
|
-
|
|
1513
|
-
const d = msg.decoded
|
|
1514
|
-
if (!d) return
|
|
1497
|
+
**What happens underneath**
|
|
1515
1498
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1499
|
+
1. The encrypted blob is fetched from `url`, or from `directPath` when the
|
|
1500
|
+
message carries no absolute URL.
|
|
1501
|
+
2. HKDF-SHA256 expands `mediaKey` into an IV, a cipher key and a MAC key, using
|
|
1502
|
+
the info string for that media type.
|
|
1503
|
+
3. The trailing 10-byte HMAC-SHA256 is verified, then AES-256-CBC decrypts the
|
|
1504
|
+
rest.
|
|
1519
1505
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (MEDIA_TYPES.has(d.type) && d.url && d.mediaKey) {
|
|
1525
|
-
try {
|
|
1526
|
-
const filePath = await downloadAndDecrypt(msg.id, d.type, d.url, d.mediaKey, { fileName: d.fileName })
|
|
1527
|
-
console.log('saved', d.type, 'to', filePath)
|
|
1528
|
-
} catch (e) {
|
|
1529
|
-
console.error('media download failed:', e.message)
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
})
|
|
1533
|
-
```
|
|
1506
|
+
`downloadMedia` throws with the reason rather than returning empty: a message
|
|
1507
|
+
with no media, no CDN location, an unsupported type, or a file that does not
|
|
1508
|
+
match the message all say so.
|
|
1534
1509
|
|
|
1535
1510
|
## Sending Messages
|
|
1536
1511
|
|
|
@@ -1850,40 +1825,156 @@ client.setChatPresence('919634847671@s.whatsapp.net', 'paused') // stopped
|
|
|
1850
1825
|
|
|
1851
1826
|
## Modifying Chats
|
|
1852
1827
|
|
|
1828
|
+
Pinning, archiving, muting, marking read and starring are **app state**. That is
|
|
1829
|
+
WhatsApp's own synchronised settings store — the same one your phone writes to —
|
|
1830
|
+
so a change made here shows up on the phone and on every other linked device,
|
|
1831
|
+
and survives reinstalling.
|
|
1832
|
+
|
|
1833
|
+
Each of these is `async`, sends a patch, and waits for the server to accept it.
|
|
1834
|
+
The local view only moves once the change is actually stored, and they throw if
|
|
1835
|
+
the server refuses.
|
|
1836
|
+
|
|
1837
|
+
They return a boolean: **whether the change reached app state**, and so whether
|
|
1838
|
+
other devices will see it.
|
|
1839
|
+
|
|
1840
|
+
```js
|
|
1841
|
+
const synced = await client.pinChat('919634847671@s.whatsapp.net')
|
|
1842
|
+
if (!synced) console.log('pinned here, but your phone will not know')
|
|
1843
|
+
```
|
|
1844
|
+
|
|
1845
|
+
> [!IMPORTANT]
|
|
1846
|
+
> **App state needs a key, and where that key comes from depends on how you
|
|
1847
|
+
> connected.**
|
|
1848
|
+
>
|
|
1849
|
+
> A **linked session** (pairing code) is a companion. Your phone shares an app
|
|
1850
|
+
> state key with it automatically, shortly after linking — so everything on this
|
|
1851
|
+
> page works, in both directions.
|
|
1852
|
+
>
|
|
1853
|
+
> An **SMS session** *is* the primary device. Nobody shares a key with it,
|
|
1854
|
+
> because it is the device that would create one. Unless you have linked a
|
|
1855
|
+
> companion to it, there is no app state to read or write.
|
|
1856
|
+
>
|
|
1857
|
+
> Check with `client.canSyncAppState()`.
|
|
1858
|
+
>
|
|
1859
|
+
> When there is no key, these calls **do not throw**. `muteChat`, `unmuteChat`
|
|
1860
|
+
> and `markChatRead` fall back to the request a primary device sends for itself,
|
|
1861
|
+
> which is what this library did before app state existed. `pinChat`,
|
|
1862
|
+
> `archiveChat` and `starMessage` have no such request, so they update this
|
|
1863
|
+
> session only. Either way the return value is `false`, which is how you tell.
|
|
1864
|
+
|
|
1853
1865
|
### Archive / Unarchive a Chat
|
|
1854
1866
|
|
|
1855
1867
|
```js
|
|
1856
|
-
client.archiveChat('919634847671@s.whatsapp.net')
|
|
1857
|
-
client.unarchiveChat('919634847671@s.whatsapp.net')
|
|
1868
|
+
await client.archiveChat('919634847671@s.whatsapp.net')
|
|
1869
|
+
await client.unarchiveChat('919634847671@s.whatsapp.net')
|
|
1858
1870
|
```
|
|
1859
1871
|
|
|
1860
1872
|
### Mute / Unmute a Chat
|
|
1861
1873
|
|
|
1862
1874
|
```js
|
|
1863
|
-
await client.muteChat('919634847671@s.whatsapp.net', 8 * 60 * 60 * 1000) //
|
|
1864
|
-
await client.muteChat('919634847671@s.whatsapp.net', 0)
|
|
1875
|
+
await client.muteChat('919634847671@s.whatsapp.net', 8 * 60 * 60 * 1000) // 8 hours
|
|
1876
|
+
await client.muteChat('919634847671@s.whatsapp.net', 0) // until unmuted
|
|
1865
1877
|
await client.unmuteChat('919634847671@s.whatsapp.net')
|
|
1866
1878
|
```
|
|
1867
1879
|
|
|
1868
1880
|
### Mark a Chat Read / Unread
|
|
1869
1881
|
|
|
1882
|
+
This is the chat's own unread badge. To send read receipts (blue ticks) for
|
|
1883
|
+
particular messages, use `markRead()` instead.
|
|
1884
|
+
|
|
1870
1885
|
```js
|
|
1871
|
-
await client.markChatRead('919634847671@s.whatsapp.net')
|
|
1872
|
-
client.markChatUnread('919634847671@s.whatsapp.net')
|
|
1886
|
+
await client.markChatRead('919634847671@s.whatsapp.net')
|
|
1887
|
+
await client.markChatUnread('919634847671@s.whatsapp.net')
|
|
1873
1888
|
```
|
|
1874
1889
|
|
|
1875
1890
|
### Pin / Unpin a Chat
|
|
1876
1891
|
|
|
1877
1892
|
```js
|
|
1878
|
-
client.pinChat('919634847671@s.whatsapp.net')
|
|
1879
|
-
client.unpinChat('919634847671@s.whatsapp.net')
|
|
1893
|
+
await client.pinChat('919634847671@s.whatsapp.net')
|
|
1894
|
+
await client.unpinChat('919634847671@s.whatsapp.net')
|
|
1880
1895
|
```
|
|
1881
1896
|
|
|
1882
1897
|
### Star / Unstar a Message
|
|
1883
1898
|
|
|
1899
|
+
The third argument says whether the message being starred is one you sent. It is
|
|
1900
|
+
part of how the star is filed, so getting it wrong stars a different message.
|
|
1901
|
+
|
|
1884
1902
|
```js
|
|
1885
|
-
client.starMessage('MSGID123', '919634847671@s.whatsapp.net')
|
|
1886
|
-
client.unstarMessage('MSGID123', '919634847671@s.whatsapp.net')
|
|
1903
|
+
await client.starMessage('MSGID123', '919634847671@s.whatsapp.net', true) // yours
|
|
1904
|
+
await client.unstarMessage('MSGID123', '919634847671@s.whatsapp.net', false) // theirs
|
|
1905
|
+
```
|
|
1906
|
+
|
|
1907
|
+
<a id="app-state-sync"></a>
|
|
1908
|
+
|
|
1909
|
+
### Reading Changes Made Elsewhere
|
|
1910
|
+
|
|
1911
|
+
The traffic runs both ways. When you pin a chat on your phone, mute a group from
|
|
1912
|
+
another linked device, or rename a contact, that change is waiting in app state
|
|
1913
|
+
for this session to pick up.
|
|
1914
|
+
|
|
1915
|
+
`syncAppState()` fetches it. It is called for you whenever the server says
|
|
1916
|
+
something has moved — so with a listener attached you generally never need to
|
|
1917
|
+
call it by hand. On an SMS session with no companions linked there is nothing to
|
|
1918
|
+
fetch, and it reports `waitingForKeys` instead.
|
|
1919
|
+
|
|
1920
|
+
```js
|
|
1921
|
+
client.on('chat_pinned', (u) => u.remote && console.log('pinned elsewhere:', u.jid))
|
|
1922
|
+
client.on('chat_archived', (u) => u.remote && console.log('archived elsewhere:', u.jid))
|
|
1923
|
+
client.on('chat_muted', (u) => u.remote && console.log('muted elsewhere:', u.jid, u.until))
|
|
1924
|
+
client.on('chat_read', (u) => u.remote && console.log('read elsewhere:', u.jid))
|
|
1925
|
+
client.on('message_starred', (u) => u.remote && console.log('starred elsewhere:', u.msgId))
|
|
1926
|
+
client.on('contact_update', (u) => console.log('contact renamed:', u.jid, u.name))
|
|
1927
|
+
client.on('push_name_update',(u) => console.log('your display name is now', u.name))
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
`remote: true` marks a change as somebody else's doing. Your own calls emit the
|
|
1931
|
+
same events without it — they carry `synced` instead — so a listener can tell the
|
|
1932
|
+
two apart and avoid echoing a change back where it came from.
|
|
1933
|
+
|
|
1934
|
+
To pull on demand:
|
|
1935
|
+
|
|
1936
|
+
```js
|
|
1937
|
+
// everything
|
|
1938
|
+
const r = await client.syncAppState()
|
|
1939
|
+
console.log(r.applied, 'change(s)')
|
|
1940
|
+
|
|
1941
|
+
// or just one part of it
|
|
1942
|
+
await client.syncAppState(['regular_low'])
|
|
1943
|
+
|
|
1944
|
+
// re-read everything from scratch, discarding what we hold
|
|
1945
|
+
await client.syncAppState(null, { snapshot: true })
|
|
1946
|
+
```
|
|
1947
|
+
|
|
1948
|
+
The result reports each collection separately:
|
|
1949
|
+
|
|
1950
|
+
```js
|
|
1951
|
+
{
|
|
1952
|
+
applied: 3,
|
|
1953
|
+
collections: {
|
|
1954
|
+
regular_low: { version: 41, applied: 3, skipped: 0, snapshot: false, macOk: true }
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
The five collections are `critical_block`, `critical_unblock_low`,
|
|
1960
|
+
`regular_high`, `regular_low` and `regular`. Which one a setting lives in is
|
|
1961
|
+
WhatsApp's choice, not yours — the methods above already file each change where
|
|
1962
|
+
it belongs.
|
|
1963
|
+
|
|
1964
|
+
**When it repairs itself.** Each collection carries a running hash that has to
|
|
1965
|
+
keep agreeing with the server's. If it stops — a patch went missing, or one
|
|
1966
|
+
could not be decrypted — the incremental history is no longer trustworthy, so
|
|
1967
|
+
that collection is thrown away and re-read whole. This happens on its own, once
|
|
1968
|
+
per sync, and shows up as `snapshot: true` in the result.
|
|
1969
|
+
|
|
1970
|
+
**Events for anything not modelled here.** WhatsApp tracks more in app state than
|
|
1971
|
+
this library turns into methods. Rather than dropping those, they are emitted
|
|
1972
|
+
raw, so it is at least visible that something happened:
|
|
1973
|
+
|
|
1974
|
+
```js
|
|
1975
|
+
client.on('app_state_mutation', ({ collection, index, action, removed }) => {
|
|
1976
|
+
console.log('unhandled app state change', index)
|
|
1977
|
+
})
|
|
1887
1978
|
```
|
|
1888
1979
|
|
|
1889
1980
|
### Disappearing Messages
|
|
@@ -1974,9 +2065,17 @@ const fs = require('fs')
|
|
|
1974
2065
|
await client.changeProfilePicture(fs.readFileSync('./avatar.jpg'))
|
|
1975
2066
|
|
|
1976
2067
|
// change a group's picture (you must be admin)
|
|
1977
|
-
|
|
2068
|
+
// returns the new picture id, or 'remove' when the picture was taken down
|
|
2069
|
+
const picId = await client.changeGroupPicture('120363000000000000@g.us',
|
|
2070
|
+
fs.readFileSync('./group.jpg'))
|
|
2071
|
+
|
|
2072
|
+
// pass null to remove the current picture
|
|
2073
|
+
await client.changeGroupPicture('120363000000000000@g.us', null)
|
|
1978
2074
|
```
|
|
1979
2075
|
|
|
2076
|
+
`changeGroupPicture` throws when the server refuses — `406` for an image that is
|
|
2077
|
+
not a JPEG it will take, `403` when you are not an admin of that group.
|
|
2078
|
+
|
|
1980
2079
|
## Privacy
|
|
1981
2080
|
|
|
1982
2081
|
### Block / Unblock User
|
|
@@ -2063,15 +2162,55 @@ console.log('members', group.participants.map(p => p.jid))
|
|
|
2063
2162
|
|
|
2064
2163
|
### Add / Remove or Demote / Promote
|
|
2065
2164
|
|
|
2165
|
+
Each of these returns one result per participant — the ones that went through
|
|
2166
|
+
and the ones that did not. The server decides every participant separately, so a
|
|
2167
|
+
call that half worked tells you which half and why.
|
|
2168
|
+
|
|
2066
2169
|
```js
|
|
2067
2170
|
const groupJid = '120363000000000000@g.us'
|
|
2068
2171
|
|
|
2069
|
-
await client.addGroupParticipants(groupJid,
|
|
2172
|
+
const results = await client.addGroupParticipants(groupJid, [
|
|
2173
|
+
'919634847671@s.whatsapp.net',
|
|
2174
|
+
'12345678901@s.whatsapp.net'
|
|
2175
|
+
])
|
|
2176
|
+
|
|
2177
|
+
for (const r of results) {
|
|
2178
|
+
if (r.ok) console.log('added', r.jid)
|
|
2179
|
+
else console.log('failed', r.jid, r.status, r.needsInvite ? '(invite instead)' : '')
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2070
2182
|
await client.removeGroupParticipants(groupJid, ['919634847671@s.whatsapp.net'])
|
|
2071
2183
|
await client.promoteGroupParticipants(groupJid, ['919634847671@s.whatsapp.net'])
|
|
2072
2184
|
await client.demoteGroupParticipants(groupJid, ['919634847671@s.whatsapp.net'])
|
|
2073
2185
|
```
|
|
2074
2186
|
|
|
2187
|
+
Each result looks like this:
|
|
2188
|
+
|
|
2189
|
+
```js
|
|
2190
|
+
{
|
|
2191
|
+
jid: '919634847671@s.whatsapp.net',
|
|
2192
|
+
status: '403', // '200' when the action went through
|
|
2193
|
+
error: 403, // null on success
|
|
2194
|
+
ok: false, // getter: error == null
|
|
2195
|
+
admin: null, // 'admin' | 'superadmin' | null
|
|
2196
|
+
phoneNumber: '919634847671@s.whatsapp.net',
|
|
2197
|
+
lid: '112713111982325@lid', // when the server told us one
|
|
2198
|
+
displayName: null,
|
|
2199
|
+
// Only on a refused add: the code a personal invitation is built from.
|
|
2200
|
+
addRequest: { code: 'AbCdEfGh', expiration: 1790000000 },
|
|
2201
|
+
needsInvite: true // getter: true when addRequest holds a code
|
|
2202
|
+
}
|
|
2203
|
+
```
|
|
2204
|
+
|
|
2205
|
+
The common error codes are `403` (their privacy settings do not allow it),
|
|
2206
|
+
`404` (not on WhatsApp), `408` (not a member), `409` (already a member) and
|
|
2207
|
+
`401` (you are not allowed to do this).
|
|
2208
|
+
|
|
2209
|
+
> [!TIP]
|
|
2210
|
+
> A result object stringifies to its JID, so `results.join(', ')` and
|
|
2211
|
+
> `String(results[0])` read exactly as they did when these methods returned a
|
|
2212
|
+
> plain list of JID strings.
|
|
2213
|
+
|
|
2075
2214
|
### Change Subject
|
|
2076
2215
|
|
|
2077
2216
|
```js
|
|
@@ -2165,27 +2304,87 @@ for (const g of groups) {
|
|
|
2165
2304
|
|
|
2166
2305
|
```js
|
|
2167
2306
|
const meta = await client.getGroupMetadata('120363000000000000@g.us')
|
|
2168
|
-
// returns: { jid, subject, creation, creator, subjectTime, subjectBy,
|
|
2169
|
-
// description, ephemeral, onlyAdminsSend, onlyAdminsEdit, participants[] }
|
|
2170
2307
|
console.log(meta.subject, meta.participants.length + ' members')
|
|
2171
2308
|
```
|
|
2172
2309
|
|
|
2310
|
+
```js
|
|
2311
|
+
{
|
|
2312
|
+
jid: '120363000000000000@g.us',
|
|
2313
|
+
subject: 'My Group',
|
|
2314
|
+
size: 57, // the server's own count
|
|
2315
|
+
creation: 1705315800,
|
|
2316
|
+
creator: '919634847671@s.whatsapp.net',
|
|
2317
|
+
subjectTime: 1705315900,
|
|
2318
|
+
subjectBy: '919634847671@s.whatsapp.net',
|
|
2319
|
+
description: 'Group description here',
|
|
2320
|
+
descriptionId: 'DESC1', // echoed back as `prev` on the next edit
|
|
2321
|
+
descriptionBy: '919634847671@s.whatsapp.net',
|
|
2322
|
+
descriptionByPn: '919634847671@s.whatsapp.net',
|
|
2323
|
+
descriptionTime: 1705315950,
|
|
2324
|
+
ephemeral: 86400, // 0 when disappearing messages are off
|
|
2325
|
+
onlyAdminsSend: false,
|
|
2326
|
+
onlyAdminsEdit: false,
|
|
2327
|
+
joinApprovalMode: true, // new members need an admin's approval
|
|
2328
|
+
memberAddMode: 'admin_add', // 'admin_add' | 'all_member_add' | null
|
|
2329
|
+
isCommunity: false,
|
|
2330
|
+
isCommunityAnnounce: false,
|
|
2331
|
+
defaultMembershipApprovalMode: null, // communities only
|
|
2332
|
+
linkedParent: null, // the community this group belongs to
|
|
2333
|
+
isIncognito: false, // members' phone numbers hidden from each other
|
|
2334
|
+
isSuspended: false, // the group has been taken down
|
|
2335
|
+
notify: 'My Group',
|
|
2336
|
+
creatorPn: '919634847671@s.whatsapp.net',
|
|
2337
|
+
creatorUsername: null,
|
|
2338
|
+
creatorCountry: 'IN',
|
|
2339
|
+
subjectByPn: '919634847671@s.whatsapp.net',
|
|
2340
|
+
subjectByUsername: null,
|
|
2341
|
+
participantVersion: 'PV1', // bumped when the member list changes
|
|
2342
|
+
announceVersion: 'AV1', // bumped when the announce flag changes
|
|
2343
|
+
addressingMode: 'lid', // 'lid' | 'pn'
|
|
2344
|
+
participants: [
|
|
2345
|
+
{
|
|
2346
|
+
jid: '112713111982325@lid',
|
|
2347
|
+
role: 'admin', // 'admin' | 'superadmin' | 'member'
|
|
2348
|
+
isAdmin: true,
|
|
2349
|
+
isSuperAdmin: false,
|
|
2350
|
+
phoneNumber: '919634847671@s.whatsapp.net',
|
|
2351
|
+
lid: '112713111982325@lid',
|
|
2352
|
+
displayName: null,
|
|
2353
|
+
username: null
|
|
2354
|
+
}
|
|
2355
|
+
]
|
|
2356
|
+
}
|
|
2357
|
+
```
|
|
2358
|
+
|
|
2359
|
+
Both addresses are filled in on every participant whichever way round the server
|
|
2360
|
+
named them, so you never have to resolve a LID by hand to know who somebody is.
|
|
2361
|
+
|
|
2173
2362
|
### Get Request Join List
|
|
2174
2363
|
|
|
2175
2364
|
```js
|
|
2176
2365
|
const pending = await client.queryGroupPendingParticipants('120363000000000000@g.us')
|
|
2177
|
-
|
|
2366
|
+
|
|
2367
|
+
for (const r of pending) {
|
|
2368
|
+
console.log(r.jid, 'asked at', new Date(r.requestedAt * 1000).toISOString())
|
|
2369
|
+
}
|
|
2178
2370
|
```
|
|
2179
2371
|
|
|
2372
|
+
Each entry is `{ jid, requestedAt }` — `requestedAt` is unix seconds, or `0` when
|
|
2373
|
+
the server did not say. Like the participant results, an entry stringifies to its
|
|
2374
|
+
JID.
|
|
2375
|
+
|
|
2180
2376
|
### Approve / Reject Request Join
|
|
2181
2377
|
|
|
2182
|
-
The second parameter is a boolean: `true` to approve, `false` to reject.
|
|
2378
|
+
The second parameter is a boolean: `true` to approve, `false` to reject. The
|
|
2379
|
+
return value is the same list of per-participant results the add/remove calls
|
|
2380
|
+
give you.
|
|
2183
2381
|
|
|
2184
2382
|
```js
|
|
2185
2383
|
// approve join requests
|
|
2186
|
-
await client.approveGroupParticipants('120363000000000000@g.us', true, [
|
|
2384
|
+
const done = await client.approveGroupParticipants('120363000000000000@g.us', true, [
|
|
2187
2385
|
'919634847671@s.whatsapp.net'
|
|
2188
2386
|
])
|
|
2387
|
+
console.log(done.filter(r => !r.ok)) // whoever could not be let in, and why
|
|
2189
2388
|
|
|
2190
2389
|
// reject join requests
|
|
2191
2390
|
await client.approveGroupParticipants('120363000000000000@g.us', false, [
|
|
@@ -2193,6 +2392,82 @@ await client.approveGroupParticipants('120363000000000000@g.us', false, [
|
|
|
2193
2392
|
])
|
|
2194
2393
|
```
|
|
2195
2394
|
|
|
2395
|
+
### Personal Invitations
|
|
2396
|
+
|
|
2397
|
+
An invite link is public — anyone holding it can join. A personal invitation is
|
|
2398
|
+
the other kind: minted for one named person, and the only way into a group for
|
|
2399
|
+
somebody whose privacy settings stop them from being added outright.
|
|
2400
|
+
|
|
2401
|
+
The whole flow starts with a refused add. When the server turns a participant
|
|
2402
|
+
away for that reason it hands back a code, which travels to them as a message
|
|
2403
|
+
they can tap.
|
|
2404
|
+
|
|
2405
|
+
```js
|
|
2406
|
+
const groupJid = '120363000000000000@g.us'
|
|
2407
|
+
|
|
2408
|
+
// add whoever can be added, and invite whoever cannot — in one call
|
|
2409
|
+
const results = await client.addGroupParticipantsOrInvite(groupJid, [
|
|
2410
|
+
'919634847671@s.whatsapp.net',
|
|
2411
|
+
'12345678901@s.whatsapp.net'
|
|
2412
|
+
])
|
|
2413
|
+
|
|
2414
|
+
for (const r of results) {
|
|
2415
|
+
if (r.ok) console.log('added', r.jid)
|
|
2416
|
+
else if (r.invited) console.log('invited', r.jid)
|
|
2417
|
+
else console.log('failed', r.jid, r.status, r.inviteError || '')
|
|
2418
|
+
}
|
|
2419
|
+
```
|
|
2420
|
+
|
|
2421
|
+
Or drive it yourself, if you want to decide who gets an invitation:
|
|
2422
|
+
|
|
2423
|
+
```js
|
|
2424
|
+
const results = await client.addGroupParticipants(groupJid, [
|
|
2425
|
+
'919634847671@s.whatsapp.net'
|
|
2426
|
+
])
|
|
2427
|
+
|
|
2428
|
+
for (const r of results.filter(x => x.needsInvite)) {
|
|
2429
|
+
await client.sendGroupInvite(r.jid, groupJid,
|
|
2430
|
+
r.addRequest.code, r.addRequest.expiration,
|
|
2431
|
+
{ caption: 'Come join us' })
|
|
2432
|
+
}
|
|
2433
|
+
```
|
|
2434
|
+
|
|
2435
|
+
`sendGroupInvite(to, groupJid, code, expiration, opts)` accepts
|
|
2436
|
+
`{ groupName, caption, jpegThumbnail, isCommunity, id, contextInfo }`. The group
|
|
2437
|
+
name is filled in from the group's own metadata when you do not supply one.
|
|
2438
|
+
|
|
2439
|
+
On the receiving side, an invitation arrives as an ordinary `message` event whose
|
|
2440
|
+
`decoded.type` is `'groupInvite'`:
|
|
2441
|
+
|
|
2442
|
+
```js
|
|
2443
|
+
client.on('message', async (msg) => {
|
|
2444
|
+
const d = msg.decoded
|
|
2445
|
+
if (!d || d.type !== 'groupInvite') return
|
|
2446
|
+
|
|
2447
|
+
// look before you leap — this does not join anything
|
|
2448
|
+
const info = await client.queryGroupInviteMessageInfo(
|
|
2449
|
+
d.groupJid, msg.participant || msg.from, d.inviteCode, d.inviteExpiration)
|
|
2450
|
+
console.log(info.subject, info.size + ' members')
|
|
2451
|
+
|
|
2452
|
+
// and accept it
|
|
2453
|
+
const jid = await client.acceptGroupInviteMessage(
|
|
2454
|
+
d.groupJid, msg.participant || msg.from, d.inviteCode, d.inviteExpiration)
|
|
2455
|
+
console.log('joined', jid)
|
|
2456
|
+
})
|
|
2457
|
+
```
|
|
2458
|
+
|
|
2459
|
+
A decoded invitation carries `{ groupJid, inviteCode, inviteExpiration,
|
|
2460
|
+
groupName, jpegThumbnail, caption, isCommunity }`.
|
|
2461
|
+
|
|
2462
|
+
To withdraw an invitation you sent before it is used:
|
|
2463
|
+
|
|
2464
|
+
```js
|
|
2465
|
+
await client.revokeGroupInviteForParticipant(groupJid, '919634847671@s.whatsapp.net')
|
|
2466
|
+
```
|
|
2467
|
+
|
|
2468
|
+
An expired or already-spent invitation throws rather than resolving to nothing,
|
|
2469
|
+
so the two cases are easy to tell apart.
|
|
2470
|
+
|
|
2196
2471
|
### Toggle Ephemeral in Group
|
|
2197
2472
|
|
|
2198
2473
|
```js
|
|
@@ -2780,6 +3055,19 @@ wa> /contact about 919634847671@s.whatsapp.net
|
|
|
2780
3055
|
|
|
2781
3056
|
### Chat Management Commands
|
|
2782
3057
|
|
|
3058
|
+
On a **linked** (pairing-code) session these write to app state, so a change here
|
|
3059
|
+
reaches your phone and every other linked device.
|
|
3060
|
+
|
|
3061
|
+
On an **SMS** session this device is the primary and there is no app state key
|
|
3062
|
+
unless you have linked a companion to it. `/mute`, `/unmute` and `/read` still
|
|
3063
|
+
send the request a primary makes for itself; `/pin`, `/archive` and `/star`
|
|
3064
|
+
update this session only. The command says which happened:
|
|
3065
|
+
|
|
3066
|
+
```sh
|
|
3067
|
+
wa> /pin 919634847671@s.whatsapp.net
|
|
3068
|
+
pinned (this session only — no app state key)
|
|
3069
|
+
```
|
|
3070
|
+
|
|
2783
3071
|
#### Mark Read / Unread
|
|
2784
3072
|
|
|
2785
3073
|
```sh
|
|
@@ -2816,11 +3104,52 @@ wa> /unarchive 919634847671@s.whatsapp.net
|
|
|
2816
3104
|
|
|
2817
3105
|
#### Star / Unstar a Message (CLI)
|
|
2818
3106
|
|
|
3107
|
+
Add `me` when the message is one you sent — it is part of how the star is filed,
|
|
3108
|
+
so leaving it off on your own message stars the wrong thing.
|
|
3109
|
+
|
|
2819
3110
|
```sh
|
|
2820
|
-
wa> /star 919634847671@s.whatsapp.net 3EB0ABCDEF123456
|
|
3111
|
+
wa> /star 919634847671@s.whatsapp.net 3EB0ABCDEF123456 me
|
|
2821
3112
|
wa> /unstar 919634847671@s.whatsapp.net 3EB0ABCDEF123456
|
|
2822
3113
|
```
|
|
2823
3114
|
|
|
3115
|
+
<a id="cli-app-state"></a>
|
|
3116
|
+
|
|
3117
|
+
#### Sync App State
|
|
3118
|
+
|
|
3119
|
+
Pulls in pins, archives, mutes, stars and contact names changed on your phone or
|
|
3120
|
+
another linked device. This happens on its own whenever the server says
|
|
3121
|
+
something moved; the command is for pulling on demand.
|
|
3122
|
+
|
|
3123
|
+
```sh
|
|
3124
|
+
wa> /appstate
|
|
3125
|
+
syncing app state...
|
|
3126
|
+
──────────────────────────────────────────────────
|
|
3127
|
+
critical_block v3 0 change(s)
|
|
3128
|
+
critical_unblock_low v18 2 change(s)
|
|
3129
|
+
regular_high v7 0 change(s)
|
|
3130
|
+
regular_low v41 3 change(s)
|
|
3131
|
+
regular v2 0 change(s)
|
|
3132
|
+
total 5 change(s) applied
|
|
3133
|
+
──────────────────────────────────────────────────
|
|
3134
|
+
|
|
3135
|
+
# just one part of it
|
|
3136
|
+
wa> /appstate regular_low
|
|
3137
|
+
|
|
3138
|
+
# throw away what we hold and re-read everything
|
|
3139
|
+
wa> /appstate --snapshot
|
|
3140
|
+
```
|
|
3141
|
+
|
|
3142
|
+
Changes that arrive on their own are printed as they land:
|
|
3143
|
+
|
|
3144
|
+
```sh
|
|
3145
|
+
pinned 919634847671@s.whatsapp.net
|
|
3146
|
+
muted 120363000000000000@g.us until 2026-08-01T09:00:00.000Z
|
|
3147
|
+
contact 12345678901@s.whatsapp.net → Ion
|
|
3148
|
+
```
|
|
3149
|
+
|
|
3150
|
+
If your phone has not yet shared a sync key with this session, the command says
|
|
3151
|
+
so — leave WhatsApp open on the phone for a moment and try again.
|
|
3152
|
+
|
|
2824
3153
|
#### CLI Disappearing Messages
|
|
2825
3154
|
|
|
2826
3155
|
| Duration | Seconds |
|
|
@@ -2900,13 +3229,17 @@ left 120363000000000000@g.us
|
|
|
2900
3229
|
|
|
2901
3230
|
```sh
|
|
2902
3231
|
# add participants
|
|
2903
|
-
wa> /group add 120363000000000000@g.us 919634847671@s.whatsapp.net
|
|
3232
|
+
wa> /group add 120363000000000000@g.us 919634847671@s.whatsapp.net 12345678901@s.whatsapp.net
|
|
3233
|
+
added 919634847671@s.whatsapp.net
|
|
3234
|
+
failed 12345678901@s.whatsapp.net — their privacy settings do not allow it (403) · can be invited instead
|
|
2904
3235
|
|
|
2905
3236
|
# remove participants
|
|
2906
3237
|
wa> /group remove 120363000000000000@g.us 919634847671@s.whatsapp.net
|
|
3238
|
+
removed 919634847671@s.whatsapp.net
|
|
2907
3239
|
```
|
|
2908
3240
|
|
|
2909
|
-
Multiple participants can be listed, separated by spaces.
|
|
3241
|
+
Multiple participants can be listed, separated by spaces. Every participant is
|
|
3242
|
+
reported on its own line, because the server decides each one separately.
|
|
2910
3243
|
|
|
2911
3244
|
#### Promote / Demote Admins
|
|
2912
3245
|
|
|
@@ -2938,7 +3271,7 @@ Reads the image from disk and sets it as the group's profile picture. You must b
|
|
|
2938
3271
|
|
|
2939
3272
|
```sh
|
|
2940
3273
|
wa> /group photo 120363000000000000@g.us ./group-logo.jpg
|
|
2941
|
-
group picture updated
|
|
3274
|
+
group picture updated id=1705315800
|
|
2942
3275
|
```
|
|
2943
3276
|
|
|
2944
3277
|
#### Get Invite Link
|
|
@@ -2995,17 +3328,34 @@ wa> /group invite-info "https://chat.whatsapp.com/AbCdEfGhIjKlMnOpQrStUv"
|
|
|
2995
3328
|
|
|
2996
3329
|
```sh
|
|
2997
3330
|
wa> /group meta 120363000000000000@g.us
|
|
2998
|
-
jid
|
|
2999
|
-
subject
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3331
|
+
jid 120363000000000000@g.us
|
|
3332
|
+
subject My Group
|
|
3333
|
+
creator 919634847671@s.whatsapp.net
|
|
3334
|
+
created 2024-01-15T10:30:00.000Z
|
|
3335
|
+
description Group description here
|
|
3336
|
+
ephemeral off
|
|
3337
|
+
only admins send no
|
|
3338
|
+
only admins edit yes
|
|
3339
|
+
join approval required
|
|
3340
|
+
who can add admins only
|
|
3341
|
+
size 3
|
|
3342
|
+
participants (3)
|
|
3343
|
+
112713111982325@lid (919634847671@s.whatsapp.net) [admin]
|
|
3344
|
+
229063524376784@lid (12345678901@s.whatsapp.net)
|
|
3345
|
+
98765432109@s.whatsapp.net
|
|
3007
3346
|
```
|
|
3008
3347
|
|
|
3348
|
+
A participant addressed by LID is shown with the phone number behind it when
|
|
3349
|
+
the server sends one. Two more lines appear only when they apply:
|
|
3350
|
+
|
|
3351
|
+
```sh
|
|
3352
|
+
suspended yes — this group has been taken down
|
|
3353
|
+
incognito yes — phone numbers are hidden
|
|
3354
|
+
```
|
|
3355
|
+
|
|
3356
|
+
A suspended group answers every send with a refusal and nothing else, so it is
|
|
3357
|
+
worth checking here before hunting for the cause elsewhere.
|
|
3358
|
+
|
|
3009
3359
|
#### List All Groups
|
|
3010
3360
|
|
|
3011
3361
|
Fetches all groups you are a member of and prints a numbered list:
|
|
@@ -3024,9 +3374,9 @@ Lists all participants of a group with their roles:
|
|
|
3024
3374
|
|
|
3025
3375
|
```sh
|
|
3026
3376
|
wa> /group participants 120363000000000000@g.us
|
|
3027
|
-
|
|
3028
|
-
919634847671@s.whatsapp.net [admin]
|
|
3029
|
-
12345678901@s.whatsapp.net
|
|
3377
|
+
My Group (3 participants)
|
|
3378
|
+
112713111982325@lid (919634847671@s.whatsapp.net) [admin]
|
|
3379
|
+
229063524376784@lid (12345678901@s.whatsapp.net)
|
|
3030
3380
|
98765432109@s.whatsapp.net
|
|
3031
3381
|
```
|
|
3032
3382
|
|
|
@@ -3037,8 +3387,8 @@ Lists users who have requested to join a group (only visible when `approve_parti
|
|
|
3037
3387
|
```sh
|
|
3038
3388
|
wa> /group pending 120363000000000000@g.us
|
|
3039
3389
|
pending (2)
|
|
3040
|
-
919634847671@s.whatsapp.net
|
|
3041
|
-
12345678901@s.whatsapp.net
|
|
3390
|
+
919634847671@s.whatsapp.net 2026-07-20T09:12:00.000Z
|
|
3391
|
+
12345678901@s.whatsapp.net 2026-07-21T14:03:20.000Z
|
|
3042
3392
|
```
|
|
3043
3393
|
|
|
3044
3394
|
#### Approve / Reject Join Requests
|
|
@@ -3046,14 +3396,52 @@ wa> /group pending 120363000000000000@g.us
|
|
|
3046
3396
|
```sh
|
|
3047
3397
|
# approve one or more pending members
|
|
3048
3398
|
wa> /group approve 120363000000000000@g.us 919634847671@s.whatsapp.net
|
|
3049
|
-
approved 919634847671@s.whatsapp.net
|
|
3399
|
+
approved 919634847671@s.whatsapp.net
|
|
3050
3400
|
|
|
3051
3401
|
# reject one or more pending members
|
|
3052
3402
|
wa> /group reject 120363000000000000@g.us 919634847671@s.whatsapp.net
|
|
3053
|
-
rejected 919634847671@s.whatsapp.net
|
|
3403
|
+
rejected 919634847671@s.whatsapp.net
|
|
3404
|
+
```
|
|
3405
|
+
|
|
3406
|
+
Multiple JIDs can be listed, separated by spaces. Anyone the server would not let
|
|
3407
|
+
through is listed separately with the reason.
|
|
3408
|
+
|
|
3409
|
+
<a id="cli-personal-invitations"></a>
|
|
3410
|
+
|
|
3411
|
+
#### Personal Invitations
|
|
3412
|
+
|
|
3413
|
+
For someone whose privacy settings do not let them be added to a group directly,
|
|
3414
|
+
`add-invite` adds whoever it can and sends the rest a personal invitation:
|
|
3415
|
+
|
|
3416
|
+
```sh
|
|
3417
|
+
wa> /group add-invite 120363000000000000@g.us 919634847671@s.whatsapp.net 12345678901@s.whatsapp.net
|
|
3418
|
+
added 919634847671@s.whatsapp.net
|
|
3419
|
+
failed 12345678901@s.whatsapp.net — their privacy settings do not allow it (403) · can be invited instead · invitation sent
|
|
3420
|
+
```
|
|
3421
|
+
|
|
3422
|
+
An invitation that arrives for you shows the command that accepts it:
|
|
3423
|
+
|
|
3424
|
+
```sh
|
|
3425
|
+
message from 919634847671@s.whatsapp.net
|
|
3426
|
+
id 3EB0A1B2C3D4
|
|
3427
|
+
type group invitation My Group
|
|
3428
|
+
accept with /group accept-invite 120363000000000000@g.us 919634847671@s.whatsapp.net AbCdEfGh 1790000000
|
|
3054
3429
|
```
|
|
3055
3430
|
|
|
3056
|
-
|
|
3431
|
+
```sh
|
|
3432
|
+
# look at the group without joining it
|
|
3433
|
+
wa> /group preview-invite 120363000000000000@g.us 919634847671@s.whatsapp.net AbCdEfGh 1790000000
|
|
3434
|
+
|
|
3435
|
+
# join
|
|
3436
|
+
wa> /group accept-invite 120363000000000000@g.us 919634847671@s.whatsapp.net AbCdEfGh 1790000000
|
|
3437
|
+
joined 120363000000000000@g.us
|
|
3438
|
+
|
|
3439
|
+
# send one by hand
|
|
3440
|
+
wa> /group send-invite 120363000000000000@g.us 12345678901@s.whatsapp.net AbCdEfGh 1790000000
|
|
3441
|
+
|
|
3442
|
+
# take one back before it is used
|
|
3443
|
+
wa> /group revoke-invite 120363000000000000@g.us 12345678901@s.whatsapp.net
|
|
3444
|
+
```
|
|
3057
3445
|
|
|
3058
3446
|
#### Group Settings
|
|
3059
3447
|
|
|
@@ -3255,8 +3643,10 @@ wa> /quit
|
|
|
3255
3643
|
| `/unpin <jid>` | Unpin a chat |
|
|
3256
3644
|
| `/archive <jid>` | Archive a chat |
|
|
3257
3645
|
| `/unarchive <jid>` | Unarchive a chat |
|
|
3258
|
-
| `/star <jid> <msgId
|
|
3259
|
-
| `/unstar <jid> <msgId
|
|
3646
|
+
| `/star <jid> <msgId> [me]` | Star a message (`me` if you sent it) |
|
|
3647
|
+
| `/unstar <jid> <msgId> [me]` | Unstar a message |
|
|
3648
|
+
| `/appstate [collection...]` | Pull pins/archives/mutes/stars from your phone |
|
|
3649
|
+
| `/appstate --snapshot` | Re-read all app state from scratch |
|
|
3260
3650
|
| `/ephemeral <jid> <seconds>` | Set disappearing messages timer for a chat |
|
|
3261
3651
|
| `/ephemeral-default <seconds>` | Set global default ephemeral timer for new chats |
|
|
3262
3652
|
| `/block <jid>` | Block a contact |
|