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