whalibmob 5.14.5 → 5.14.6
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 +36 -3
- package/cli.js +48 -14
- package/lib/Client.js +101 -2
- package/lib/Registration.js +13 -0
- package/lib/Store.js +27 -2
- package/lib/auth-utils.js +3 -1
- package/lib/noise.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -294,6 +294,20 @@ wa registration --request-code 919634847671 --method voice
|
|
|
294
294
|
wa registration --request-code 919634847671 --method wa_old
|
|
295
295
|
```
|
|
296
296
|
|
|
297
|
+
**Set the account's display name while registering** with `--name`. This is the
|
|
298
|
+
name people who have *not* saved your number see next to it — in group
|
|
299
|
+
participant lists, in notifications, and beside your messages. Quote it if it
|
|
300
|
+
contains spaces:
|
|
301
|
+
|
|
302
|
+
```sh
|
|
303
|
+
wa registration --request-code 919634847671 --name "Ricardo Trade"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The name is stored with the session and announced on every connection from then
|
|
307
|
+
on. Registration itself carries no name — the server learns it when the session
|
|
308
|
+
connects — so a number registered without `--name` has none until one is set.
|
|
309
|
+
It can be given at either registration step, and changed later with `/name`.
|
|
310
|
+
|
|
297
311
|
**Registering as Android** is the same command with the platform named. Nothing
|
|
298
312
|
else to prepare — see [what it does behind that one command](#registering-as-android).
|
|
299
313
|
|
|
@@ -1378,6 +1392,10 @@ wa> /reg code 919634847671
|
|
|
1378
1392
|
wa> /reg code 919634847671 voice
|
|
1379
1393
|
wa> /reg code 919634847671 wa_old
|
|
1380
1394
|
|
|
1395
|
+
# register with a display name — what people who have not saved
|
|
1396
|
+
# your number see. Quote it if it has spaces.
|
|
1397
|
+
wa> /reg code 919634847671 --name "Ricardo Trade"
|
|
1398
|
+
|
|
1381
1399
|
# confirm the code received
|
|
1382
1400
|
wa> /reg confirm 919634847671 123456
|
|
1383
1401
|
registered session saved to /home/user/.waSession/919634847671/919634847671.json
|
|
@@ -1542,8 +1560,8 @@ wa> /quit
|
|
|
1542
1560
|
| `/biz <phone\|jid>` | Query business profile of a WhatsApp Business account |
|
|
1543
1561
|
| **Registration** | |
|
|
1544
1562
|
| `/reg check <phone>` | Check if number has WhatsApp |
|
|
1545
|
-
| `/reg code <phone> [method]` | Request verification code |
|
|
1546
|
-
| `/reg confirm <phone> <code
|
|
1563
|
+
| `/reg code <phone> [method] [--name "Name"]` | Request verification code, optionally naming the account |
|
|
1564
|
+
| `/reg confirm <phone> <code> [--name "Name"]` | Complete registration |
|
|
1547
1565
|
| **Connection** | |
|
|
1548
1566
|
| `/connect <phone> [sms\|pair]` | Connect to WhatsApp — asks which method when unset |
|
|
1549
1567
|
| `/pair <phone> [code]` | Link to an existing account by 8-digit pairing code |
|
|
@@ -1578,12 +1596,27 @@ const sessFile = path.join(sessDir, phone + '.json')
|
|
|
1578
1596
|
|
|
1579
1597
|
fs.mkdirSync(sessDir, { recursive: true })
|
|
1580
1598
|
|
|
1581
|
-
|
|
1599
|
+
// `name` is the display name the account registers with — what people who have
|
|
1600
|
+
// not saved the number see. Omit it and the account has no name until one is
|
|
1601
|
+
// set later with client.changeName().
|
|
1602
|
+
const store = createNewStore(phone, { name: 'Ricardo Trade' })
|
|
1582
1603
|
saveStore(store, sessFile)
|
|
1583
1604
|
|
|
1584
1605
|
await requestSmsCode(store, 'sms') // 'sms' | 'voice' | 'wa_old'
|
|
1585
1606
|
```
|
|
1586
1607
|
|
|
1608
|
+
The name is written to the session, not sent to the registration endpoint — the
|
|
1609
|
+
server learns it from the first connection, and from every one after. It can
|
|
1610
|
+
also be passed as an option at either step, which is the way to name a session
|
|
1611
|
+
that already exists:
|
|
1612
|
+
|
|
1613
|
+
```js
|
|
1614
|
+
await requestSmsCode(store, 'sms', { name: 'Ricardo Trade' })
|
|
1615
|
+
await verifyCode(store, '123456', { name: 'Ricardo Trade' })
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
Names are trimmed, collapsed to single spaces and capped at 25 characters.
|
|
1619
|
+
|
|
1587
1620
|
**Step 2 — verify the code**
|
|
1588
1621
|
|
|
1589
1622
|
```js
|
package/cli.js
CHANGED
|
@@ -1152,6 +1152,17 @@ function requireConn() {
|
|
|
1152
1152
|
|
|
1153
1153
|
// ─── tokenizer ────────────────────────────────────────────────────────────────
|
|
1154
1154
|
|
|
1155
|
+
// Pull `--flag value` out of a token list, in place, and hand back the value.
|
|
1156
|
+
// Removing it keeps every positional argument at the index the usage line
|
|
1157
|
+
// promises, whichever end of the command the flag was typed at.
|
|
1158
|
+
function takeFlag(toks, flag) {
|
|
1159
|
+
const i = toks.indexOf(flag);
|
|
1160
|
+
if (i === -1) return null;
|
|
1161
|
+
const value = (i + 1 < toks.length && !toks[i + 1].startsWith('--')) ? toks[i + 1] : null;
|
|
1162
|
+
toks.splice(i, value === null ? 1 : 2);
|
|
1163
|
+
return value;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1155
1166
|
function tokens(line) {
|
|
1156
1167
|
const s = String(line || '');
|
|
1157
1168
|
const t = []; let b = '';
|
|
@@ -2167,6 +2178,10 @@ async function handleLine(line) {
|
|
|
2167
2178
|
// ── registration ───────────────────────────────────────────────────────
|
|
2168
2179
|
|
|
2169
2180
|
case '/reg': {
|
|
2181
|
+
// --name is pulled out before anything reads a position, so the phone
|
|
2182
|
+
// and the code stay where the usage lines say they are no matter where
|
|
2183
|
+
// the flag was typed. A name with spaces needs quoting: --name "A B".
|
|
2184
|
+
const regName = takeFlag(p, '--name');
|
|
2170
2185
|
const sub = p[1] && p[1].toLowerCase();
|
|
2171
2186
|
|
|
2172
2187
|
if (sub === 'check') {
|
|
@@ -2182,7 +2197,12 @@ async function handleLine(line) {
|
|
|
2182
2197
|
const method = (p[3] || 'sms').toLowerCase();
|
|
2183
2198
|
// email method: /reg code <phone> email <address>
|
|
2184
2199
|
const emailAddr = method === 'email' ? (p[4] || '') : '';
|
|
2185
|
-
if (!ph) {
|
|
2200
|
+
if (!ph) {
|
|
2201
|
+
fail('usage: /reg code <phone> [sms|voice|wa_old|email <address>] [--name "Your Name"]');
|
|
2202
|
+
out(' --name sets the display name the account registers with — what people');
|
|
2203
|
+
out(' who have not saved your number see. It can be changed later with /name.');
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2186
2206
|
if (method === 'email' && !emailAddr) {
|
|
2187
2207
|
fail('email method requires an address — usage: /reg code <phone> email <address>');
|
|
2188
2208
|
break;
|
|
@@ -2191,7 +2211,7 @@ async function handleLine(line) {
|
|
|
2191
2211
|
const sessFile = storeFileFor(_sessDir, ph);
|
|
2192
2212
|
let store = loadStore(sessFile);
|
|
2193
2213
|
if (!store) {
|
|
2194
|
-
store = initAuthCreds(ph);
|
|
2214
|
+
store = initAuthCreds(ph, { name: regName });
|
|
2195
2215
|
saveStore(store, sessFile);
|
|
2196
2216
|
} else if (!store.codePending && !store.registered) {
|
|
2197
2217
|
// Only check /exist when keys were never used to request a code.
|
|
@@ -2200,14 +2220,16 @@ async function handleLine(line) {
|
|
|
2200
2220
|
const fresh = await assertRegistrationKeys(store, waVersion);
|
|
2201
2221
|
if (!fresh) {
|
|
2202
2222
|
out(' device keys already registered — generating new keys...');
|
|
2203
|
-
store = initAuthCreds(ph);
|
|
2223
|
+
store = initAuthCreds(ph, { name: regName });
|
|
2204
2224
|
saveStore(store, sessFile);
|
|
2205
2225
|
out(' new keys saved — proceed with code below');
|
|
2206
2226
|
}
|
|
2207
2227
|
}
|
|
2208
2228
|
const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
|
|
2209
2229
|
out('requesting ' + methodLabel + ' code for +' + ph + '...');
|
|
2210
|
-
const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {},
|
|
2230
|
+
const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {},
|
|
2231
|
+
{ onProgress: out, name: regName });
|
|
2232
|
+
if (regName) out(' registering as "' + (store.name || regName) + '"');
|
|
2211
2233
|
const r = await requestSmsCode(store, method, codeOpts);
|
|
2212
2234
|
store.codePending = true;
|
|
2213
2235
|
saveStore(store, sessFile);
|
|
@@ -2218,11 +2240,12 @@ async function handleLine(line) {
|
|
|
2218
2240
|
else if (sub === 'confirm') {
|
|
2219
2241
|
const ph = normalizePhone(p[2]);
|
|
2220
2242
|
const code = p[3];
|
|
2221
|
-
if (!ph || !code) { fail('usage: /reg confirm <phone> <code>'); break; }
|
|
2243
|
+
if (!ph || !code) { fail('usage: /reg confirm <phone> <code> [--name "Your Name"]'); break; }
|
|
2222
2244
|
const file = storeFileFor(_sessDir, ph);
|
|
2223
|
-
const store = loadStore(file) || initAuthCreds(ph);
|
|
2245
|
+
const store = loadStore(file) || initAuthCreds(ph, { name: regName });
|
|
2224
2246
|
out('verifying...');
|
|
2225
|
-
const r = await verifyCode(store, code,
|
|
2247
|
+
const r = await verifyCode(store, code,
|
|
2248
|
+
Object.assign(registrationPrompts(), { onProgress: out, name: regName }));
|
|
2226
2249
|
if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
|
|
2227
2250
|
sessionDirFor(_sessDir, ph, { create: true });
|
|
2228
2251
|
const finalStore = r.store || store;
|
|
@@ -2239,6 +2262,11 @@ async function handleLine(line) {
|
|
|
2239
2262
|
out(' the session is saved under that number');
|
|
2240
2263
|
}
|
|
2241
2264
|
out('registered session saved to ' + savedFile);
|
|
2265
|
+
if (finalStore.name && finalStore.name !== 'User') {
|
|
2266
|
+
out(' name ' + finalStore.name + ' (announced on every connect)');
|
|
2267
|
+
} else {
|
|
2268
|
+
out(' name not set — run /name <text> after connecting');
|
|
2269
|
+
}
|
|
2242
2270
|
out('now run: /connect ' + savedPhone);
|
|
2243
2271
|
} else {
|
|
2244
2272
|
fail('verification failed ' + JSON.stringify(r));
|
|
@@ -2464,8 +2492,8 @@ usage:
|
|
|
2464
2492
|
wa connect <phone> connect and open interactive shell
|
|
2465
2493
|
wa pair <phone> [code] link to an existing account (8-digit code)
|
|
2466
2494
|
wa listen <phone> connect and listen (stay-alive)
|
|
2467
|
-
wa registration --request-code <phone>
|
|
2468
|
-
wa registration --register <phone> --code <code>
|
|
2495
|
+
wa registration --request-code <phone> [--name "Your Name"]
|
|
2496
|
+
wa registration --register <phone> --code <code> [--name "Your Name"]
|
|
2469
2497
|
wa registration --check <phone>
|
|
2470
2498
|
wa apk-material <base.apk> [split.apk ...] read the Android token material
|
|
2471
2499
|
wa apk-material --download fetch that APK from Google Play
|
|
@@ -2490,6 +2518,7 @@ debug:
|
|
|
2490
2518
|
|
|
2491
2519
|
--debug trace without asking (same as WA_DEBUG=1)
|
|
2492
2520
|
--no-debug, -q stay quiet without asking (same as WA_DEBUG=0)
|
|
2521
|
+
--name <text> display name to register with (registration commands only)
|
|
2493
2522
|
--trace-bytes also dump the raw encoded bytes of every stanza
|
|
2494
2523
|
|
|
2495
2524
|
after connecting, type /help for all available commands.
|
|
@@ -2821,10 +2850,11 @@ async function main() {
|
|
|
2821
2850
|
// the base directory stays there, a new one gets a directory of its own.
|
|
2822
2851
|
sessionDirFor(_sessDir, ph, { create: true });
|
|
2823
2852
|
const sessFile = storeFileFor(_sessDir, ph);
|
|
2853
|
+
const regName = typeof flags.name === 'string' ? flags.name : null;
|
|
2824
2854
|
let store = loadStore(sessFile);
|
|
2825
2855
|
if (!store) {
|
|
2826
2856
|
// Brand new — generate fresh keys, save immediately, no need to check /exist
|
|
2827
|
-
store = initAuthCreds(ph);
|
|
2857
|
+
store = initAuthCreds(ph, { name: regName });
|
|
2828
2858
|
saveStore(store, sessFile);
|
|
2829
2859
|
} else if (!store.codePending && !store.registered) {
|
|
2830
2860
|
// Existing store but code was never sent and not registered — check if
|
|
@@ -2835,7 +2865,7 @@ async function main() {
|
|
|
2835
2865
|
const fresh = await assertRegistrationKeys(store, waVersion);
|
|
2836
2866
|
if (!fresh) {
|
|
2837
2867
|
out(' device keys already registered — generating new keys...');
|
|
2838
|
-
store = initAuthCreds(ph);
|
|
2868
|
+
store = initAuthCreds(ph, { name: regName });
|
|
2839
2869
|
saveStore(store, sessFile);
|
|
2840
2870
|
}
|
|
2841
2871
|
}
|
|
@@ -2844,7 +2874,9 @@ async function main() {
|
|
|
2844
2874
|
const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
|
|
2845
2875
|
out('requesting ' + methodLabel + ' code for +' + ph + '...');
|
|
2846
2876
|
try {
|
|
2847
|
-
const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {},
|
|
2877
|
+
const codeOpts = Object.assign(method === 'email' ? { email: emailAddr } : {},
|
|
2878
|
+
{ onProgress: out, name: regName });
|
|
2879
|
+
if (regName) out(' registering as "' + (store.name || regName) + '"');
|
|
2848
2880
|
const r = await requestSmsCode(store, method, codeOpts);
|
|
2849
2881
|
store.codePending = true;
|
|
2850
2882
|
saveStore(store, sessFile);
|
|
@@ -2866,11 +2898,13 @@ async function main() {
|
|
|
2866
2898
|
const code = flags.code;
|
|
2867
2899
|
if (!ph) { fail('phone number required'); process.exit(1); }
|
|
2868
2900
|
if (!code) { fail('--code is required'); process.exit(1); }
|
|
2901
|
+
const regName = typeof flags.name === 'string' ? flags.name : null;
|
|
2869
2902
|
const file = storeFileFor(_sessDir, ph);
|
|
2870
|
-
const store = loadStore(file) || initAuthCreds(ph);
|
|
2903
|
+
const store = loadStore(file) || initAuthCreds(ph, { name: regName });
|
|
2871
2904
|
out('verifying code for +' + ph + '...');
|
|
2872
2905
|
try {
|
|
2873
|
-
const r = await verifyCode(store, code,
|
|
2906
|
+
const r = await verifyCode(store, code,
|
|
2907
|
+
Object.assign(registrationPrompts(), { onProgress: out, name: regName }));
|
|
2874
2908
|
if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
|
|
2875
2909
|
if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
|
|
2876
2910
|
const finalStore = r.store || store;
|
package/lib/Client.js
CHANGED
|
@@ -676,6 +676,19 @@ class WhalibmobClient extends EventEmitter {
|
|
|
676
676
|
saveWebStore(this._store, this._webSessionFile);
|
|
677
677
|
}
|
|
678
678
|
|
|
679
|
+
// Write the session back to disk, whichever kind it is. A change that only
|
|
680
|
+
// lives in memory is a change that is gone at the next start.
|
|
681
|
+
_persistStore() {
|
|
682
|
+
try {
|
|
683
|
+
if (this._mode === 'web') { this._saveWebStore(); return; }
|
|
684
|
+
if (!this._sessionDir || !this._store || !this._store.phoneNumber) return;
|
|
685
|
+
const { saveStore } = require('./Store');
|
|
686
|
+
saveStore(this._store, path.join(this._sessionDir, `${this._store.phoneNumber}.json`));
|
|
687
|
+
} catch (err) {
|
|
688
|
+
_whaDbg('[DBG] STORE persist failed: ' + (err && err.message));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
679
692
|
async _connectWebSocket() {
|
|
680
693
|
const { encodeCompanionRegisterPayload, encodeCompanionLoginPayload } =
|
|
681
694
|
require('./webproto');
|
|
@@ -1279,6 +1292,26 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1279
1292
|
const attrs = node.attrs || {};
|
|
1280
1293
|
// Debug: log the full success node structure
|
|
1281
1294
|
_whaDbg('[DBG] SUCCESS node attrs=' + JSON.stringify(attrs));
|
|
1295
|
+
|
|
1296
|
+
// The one place the server ever tells us what it thinks this account is
|
|
1297
|
+
// called. Nothing else answers that question — there is no request that
|
|
1298
|
+
// reads a push name back — so this is both how the local name is kept
|
|
1299
|
+
// honest and the only way to find out whether a rename was accepted.
|
|
1300
|
+
if (attrs.display_name) {
|
|
1301
|
+
const serverName = String(attrs.display_name);
|
|
1302
|
+
if (this._store && this._store.name !== serverName) {
|
|
1303
|
+
_whaDbg('[DBG] SUCCESS display_name="' + serverName + '" — server disagrees with the ' +
|
|
1304
|
+
'stored name "' + this._store.name + '"; taking the server\'s');
|
|
1305
|
+
this._store.name = serverName;
|
|
1306
|
+
if (!this._store.pushName) this._store.pushName = serverName;
|
|
1307
|
+
this._persistStore();
|
|
1308
|
+
} else {
|
|
1309
|
+
_whaDbg('[DBG] SUCCESS display_name="' + serverName + '" — matches the stored name');
|
|
1310
|
+
}
|
|
1311
|
+
} else {
|
|
1312
|
+
_whaDbg('[DBG] SUCCESS carried no display_name — the server did not say what this ' +
|
|
1313
|
+
'account is called');
|
|
1314
|
+
}
|
|
1282
1315
|
if (Array.isArray(node.content)) {
|
|
1283
1316
|
for (const child of node.content) {
|
|
1284
1317
|
if (child && child.description) {
|
|
@@ -1725,7 +1758,15 @@ class WhalibmobClient extends EventEmitter {
|
|
|
1725
1758
|
}
|
|
1726
1759
|
case 'pushName': {
|
|
1727
1760
|
const name = (a.pushNameSetting && a.pushNameSetting.name) || null;
|
|
1728
|
-
|
|
1761
|
+
// Both fields, and to disk. Only `pushName` was set here, and it is
|
|
1762
|
+
// the one the store does not carry — so a rename made on another
|
|
1763
|
+
// device took effect until the process restarted, and never reached
|
|
1764
|
+
// the handshake, which announces `name`.
|
|
1765
|
+
if (name && this._store) {
|
|
1766
|
+
this._store.name = name;
|
|
1767
|
+
this._store.pushName = name;
|
|
1768
|
+
this._persistStore();
|
|
1769
|
+
}
|
|
1729
1770
|
this.emit('push_name_update', { name, remote: true });
|
|
1730
1771
|
break;
|
|
1731
1772
|
}
|
|
@@ -4546,13 +4587,71 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4546
4587
|
|
|
4547
4588
|
// Change display name
|
|
4548
4589
|
// Sends presence node with new name — immediate, no IQ needed.
|
|
4590
|
+
// The push name lives in two places, and only one of them survives.
|
|
4591
|
+
//
|
|
4592
|
+
// The presence node announces it to whoever is looking right now — it is how
|
|
4593
|
+
// a contact who has not saved the number sees a name at all. But it says
|
|
4594
|
+
// nothing lasting: the server treats it as the state of this connection, so a
|
|
4595
|
+
// name sent only that way is gone by the next login, and the account keeps
|
|
4596
|
+
// whatever the profile actually holds.
|
|
4597
|
+
//
|
|
4598
|
+
// What persists is an app state write, filed under 'setting_pushName' in the
|
|
4599
|
+
// critical_block collection. That is the record the phone and every other
|
|
4600
|
+
// linked device read, and the one the profile is served from.
|
|
4601
|
+
//
|
|
4602
|
+
// Both are sent: the presence so the change shows immediately, the patch so
|
|
4603
|
+
// it is still there tomorrow.
|
|
4604
|
+
//
|
|
4605
|
+
// Returns a promise for the patch that resolves true when it was written and
|
|
4606
|
+
// false when app state was not writable (no sync key shared yet) or the
|
|
4607
|
+
// server refused it. It never rejects, so a caller that ignores the return —
|
|
4608
|
+
// as every caller did when this only sent presence — cannot end up with an
|
|
4609
|
+
// unhandled rejection. The connection check still throws synchronously.
|
|
4549
4610
|
changeName(name) {
|
|
4550
4611
|
if (!this._socket || !this._connected) throw new Error('Not connected');
|
|
4612
|
+
// A presence node carrying only a name is a name republish: it says what
|
|
4613
|
+
// this account is called without also describing whether it is online.
|
|
4614
|
+
// Carrying a type turns it into an availability update that happens to
|
|
4615
|
+
// mention a name, which is a different thing to ask for and is not what a
|
|
4616
|
+
// rename is. The bare form goes first, on its own.
|
|
4617
|
+
this._socket.sendNode(new BinaryNode('presence', { name }, null));
|
|
4618
|
+
|
|
4619
|
+
// And the same name again on an availability node, unconditionally. The
|
|
4620
|
+
// bare form above is what a browser client sends, but the implementation
|
|
4621
|
+
// with the longest production record only ever sends this one, and ties the
|
|
4622
|
+
// server learning your name to it: announce it at least once after
|
|
4623
|
+
// connecting or other people see a placeholder instead. Making it
|
|
4624
|
+
// conditional on already being marked available would leave a session that
|
|
4625
|
+
// renames before going online with only the form that may be ignored.
|
|
4551
4626
|
this._socket.sendNode(new BinaryNode('presence', {
|
|
4552
4627
|
name,
|
|
4553
4628
|
type: 'available'
|
|
4554
4629
|
}, null));
|
|
4555
|
-
|
|
4630
|
+
|
|
4631
|
+
// `name` is the field the handshake reads when it announces this account,
|
|
4632
|
+
// and it is the one the session file carries. Writing only `pushName` left
|
|
4633
|
+
// the store's `name` at its default, so every login announced that default
|
|
4634
|
+
// again and undid the change — the account went on being called "User" no
|
|
4635
|
+
// matter how often the presence went out. Both are set: `name` because the
|
|
4636
|
+
// handshake and the file are keyed off it, `pushName` because the presence
|
|
4637
|
+
// helper reads that one first.
|
|
4638
|
+
if (this._store) {
|
|
4639
|
+
this._store.name = name;
|
|
4640
|
+
this._store.pushName = name;
|
|
4641
|
+
this._persistStore();
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4644
|
+
return this._appStatePatchOrFallback(AppMutations.pushNamePatch(name))
|
|
4645
|
+
.then(synced => {
|
|
4646
|
+
_whaDbg(synced
|
|
4647
|
+
? '[DBG] PUSHNAME persisted in app state: ' + name
|
|
4648
|
+
: '[DBG] PUSHNAME announced but NOT persisted — no app state sync key yet');
|
|
4649
|
+
return synced;
|
|
4650
|
+
})
|
|
4651
|
+
.catch(err => {
|
|
4652
|
+
_whaDbg('[DBG] PUSHNAME app state write failed: ' + (err && err.message));
|
|
4653
|
+
return false;
|
|
4654
|
+
});
|
|
4556
4655
|
}
|
|
4557
4656
|
|
|
4558
4657
|
// Change bio/about text
|
package/lib/Registration.js
CHANGED
|
@@ -1653,9 +1653,21 @@ async function assertRegistrationKeys(store, waVersion) {
|
|
|
1653
1653
|
// bites when the server keeps answering with a different reason each time.
|
|
1654
1654
|
const MAX_CODE_REQUEST_ATTEMPTS = 5;
|
|
1655
1655
|
|
|
1656
|
+
// The display name a registration is carrying, if the caller gave one. It is
|
|
1657
|
+
// never sent to the registration endpoints — those take no name, and adding one
|
|
1658
|
+
// would be inventing a field. It is written to the session, and the handshake
|
|
1659
|
+
// on the first connection is what announces it.
|
|
1660
|
+
function applyRegistrationName(store, opts) {
|
|
1661
|
+
if (!store || !opts || opts.name === undefined || opts.name === null) return;
|
|
1662
|
+
const { normalizePushName } = require('./Store');
|
|
1663
|
+
const clean = normalizePushName(opts.name);
|
|
1664
|
+
if (clean) store.name = clean;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1656
1667
|
async function requestSmsCode(store, method, opts) {
|
|
1657
1668
|
method = method || 'sms';
|
|
1658
1669
|
opts = opts || {};
|
|
1670
|
+
applyRegistrationName(store, opts);
|
|
1659
1671
|
// Before the version is read, since on Android the version comes out of the
|
|
1660
1672
|
// APK this fetches.
|
|
1661
1673
|
const _device = deviceForRegistration(store, opts);
|
|
@@ -1792,6 +1804,7 @@ async function requestSmsCode(store, method, opts) {
|
|
|
1792
1804
|
|
|
1793
1805
|
async function verifyCode(store, code, opts) {
|
|
1794
1806
|
opts = opts || {};
|
|
1807
|
+
applyRegistrationName(store, opts);
|
|
1795
1808
|
// Normally already there from the code request, but a confirmation can be run
|
|
1796
1809
|
// from a fresh shell that never made one.
|
|
1797
1810
|
const _device = deviceForRegistration(store, opts);
|
package/lib/Store.js
CHANGED
|
@@ -49,7 +49,30 @@ function sign(privKey32, message) {
|
|
|
49
49
|
// Store creation
|
|
50
50
|
// ─────────────────────────────────────────────────────────
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// What is safe to announce as a display name. The server takes the value
|
|
53
|
+
// verbatim, so a stray newline or a run of spaces goes out as typed; and 'User'
|
|
54
|
+
// is the placeholder this file writes when no name was given, so accepting it
|
|
55
|
+
// back as a real name would make it indistinguishable from having none.
|
|
56
|
+
const MAX_PUSH_NAME_LENGTH = 25;
|
|
57
|
+
|
|
58
|
+
function normalizePushName(name) {
|
|
59
|
+
if (name === null || name === undefined) return null;
|
|
60
|
+
const clean = String(name).replace(/[\r\n\t]+/g, ' ').trim().replace(/\s{2,}/g, ' ');
|
|
61
|
+
if (!clean || clean === 'User') return null;
|
|
62
|
+
return clean.length > MAX_PUSH_NAME_LENGTH ? clean.slice(0, MAX_PUSH_NAME_LENGTH).trim() : clean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} phoneNumber digits, no +
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* name the display name this account announces to people who have not saved
|
|
69
|
+
* the number. Setting it here is what makes the first connection carry
|
|
70
|
+
* it: the handshake reads this field, and it only announces a name that
|
|
71
|
+
* is not the placeholder below. Left unset, the account stays nameless
|
|
72
|
+
* until something calls changeName().
|
|
73
|
+
*/
|
|
74
|
+
function createNewStore(phoneNumber, opts) {
|
|
75
|
+
opts = opts || {};
|
|
53
76
|
const noiseKeyPair = generateKeyPair();
|
|
54
77
|
const identityKeyPair = generateKeyPair();
|
|
55
78
|
const signedPreKeyPair = generateKeyPair();
|
|
@@ -89,7 +112,7 @@ function createNewStore(phoneNumber) {
|
|
|
89
112
|
backupToken,
|
|
90
113
|
registered: false,
|
|
91
114
|
codePending: false, // true after /code request, cleared on successful /register
|
|
92
|
-
name: 'User',
|
|
115
|
+
name: normalizePushName(opts.name) || 'User',
|
|
93
116
|
version,
|
|
94
117
|
device,
|
|
95
118
|
// ADVSignedDeviceIdentity bytes received from server <success> node.
|
|
@@ -307,6 +330,8 @@ function fromSixParts(sixParts) {
|
|
|
307
330
|
|
|
308
331
|
module.exports = {
|
|
309
332
|
createNewStore,
|
|
333
|
+
normalizePushName,
|
|
334
|
+
MAX_PUSH_NAME_LENGTH,
|
|
310
335
|
saveStore,
|
|
311
336
|
loadStore,
|
|
312
337
|
toSixParts,
|
package/lib/auth-utils.js
CHANGED
|
@@ -666,7 +666,9 @@ function initAuthCreds(phoneNumber, options) {
|
|
|
666
666
|
options = options || {};
|
|
667
667
|
|
|
668
668
|
// The library's own key generation — curve25519, with the right prefixes.
|
|
669
|
-
|
|
669
|
+
// options.name, when given, is the display name the account registers with:
|
|
670
|
+
// it is stored here and announced by the first handshake.
|
|
671
|
+
const store = createNewStore(phoneNumber, { name: options.name });
|
|
670
672
|
|
|
671
673
|
// ── Extra credential fields ──────────────────────────────────────────────────
|
|
672
674
|
// Not required internally, but commonly expected by application code.
|
package/lib/noise.js
CHANGED
|
@@ -546,7 +546,14 @@ class NoiseSocket extends EventEmitter {
|
|
|
546
546
|
const payload = encodeClientPayload({
|
|
547
547
|
username: BigInt(this.store.phoneNumber),
|
|
548
548
|
passive: false,
|
|
549
|
-
|
|
549
|
+
// A session starts out named "User" because the store needs something
|
|
550
|
+
// there, not because the account is called that. Announcing it says the
|
|
551
|
+
// account IS called "User", and the handshake runs on every connect, so
|
|
552
|
+
// a name set while online was overwritten by the placeholder the moment
|
|
553
|
+
// the connection came back. Send nothing until there is a real name —
|
|
554
|
+
// the companion payload has always guarded this the same way.
|
|
555
|
+
pushName: (this.store.registered && this.store.name &&
|
|
556
|
+
this.store.name !== 'User') ? this.store.name : null,
|
|
550
557
|
shortConnect: _reconnect,
|
|
551
558
|
// ConnectType is a closed set — 0 CELLULAR_UNKNOWN, 1 WIFI_UNKNOWN,
|
|
552
559
|
// and 100-112 for the named cellular radios. There is no 3, which is
|