whalibmob 5.5.93 → 5.6.3
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 +483 -2
- package/cli.js +169 -6
- package/index.js +30 -0
- package/lib/Client.js +553 -13
- package/lib/CompanionPairing.js +247 -0
- package/lib/HistorySyncHandler.js +6 -1
- package/lib/MediaService.js +58 -12
- package/lib/PairingCode.js +198 -0
- package/lib/WebSocketStream.js +98 -0
- package/lib/WebStore.js +117 -0
- package/lib/WebVersion.js +78 -0
- package/lib/constants.js +29 -0
- package/lib/messages/MessageSender.js +26 -7
- package/lib/noise.js +72 -7
- package/lib/signal/SignalProtocol.js +14 -5
- package/lib/webproto.js +275 -0
- package/package.json +3 -2
package/cli.js
CHANGED
|
@@ -436,7 +436,8 @@ const HELP = `
|
|
|
436
436
|
/reg confirm <phone> <code> complete registration
|
|
437
437
|
|
|
438
438
|
Connection
|
|
439
|
-
/connect <phone>
|
|
439
|
+
/connect <phone> [sms|pair] connect to WhatsApp (asks which if unset)
|
|
440
|
+
/pair <phone> [code] link to an existing account by 8-digit code
|
|
440
441
|
/disconnect disconnect
|
|
441
442
|
/reconnect force reconnection
|
|
442
443
|
/session show session info
|
|
@@ -571,7 +572,19 @@ function attachEvents(client) {
|
|
|
571
572
|
|
|
572
573
|
client.on('auth_failure', (f) => {
|
|
573
574
|
_rl && _rl.pause();
|
|
574
|
-
|
|
575
|
+
const how = client._mode === 'web'
|
|
576
|
+
? 'link again with /pair'
|
|
577
|
+
: 're-register with /reg code';
|
|
578
|
+
fail('session revoked: ' + f.reason + ' — ' + how);
|
|
579
|
+
_rl && (_rl.resume(), _rl.prompt(true));
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// The server declined the client, not the session. Nothing to re-register.
|
|
583
|
+
client.on('client_rejected', (r) => {
|
|
584
|
+
_rl && _rl.pause();
|
|
585
|
+
fail('rejected by WhatsApp (' + r.reason +
|
|
586
|
+
(r.location ? ', edge ' + r.location : '') + ')');
|
|
587
|
+
out(' ' + r.message);
|
|
575
588
|
_rl && (_rl.resume(), _rl.prompt(true));
|
|
576
589
|
});
|
|
577
590
|
|
|
@@ -604,6 +617,108 @@ function openShell(prompt) {
|
|
|
604
617
|
|
|
605
618
|
// ─── connect helper ───────────────────────────────────────────────────────────
|
|
606
619
|
|
|
620
|
+
// Which way in: register this number over SMS as the account's own device, or
|
|
621
|
+
// link to an account that already exists the way WhatsApp Web does.
|
|
622
|
+
//
|
|
623
|
+
// Asked only when it is genuinely ambiguous. If exactly one kind of session is
|
|
624
|
+
// already on disk that one wins, and a non-interactive stdin never blocks on a
|
|
625
|
+
// question nobody is there to answer.
|
|
626
|
+
function hasMobileSession(phone) {
|
|
627
|
+
return fs.existsSync(path.join(_sessDir, phone + '.json'));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function hasWebSession(phone) {
|
|
631
|
+
const f = path.join(_sessDir, phone + '.web.json');
|
|
632
|
+
if (!fs.existsSync(f)) return false;
|
|
633
|
+
try {
|
|
634
|
+
const j = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
635
|
+
return !!(j.registered && j.me && j.me.id);
|
|
636
|
+
} catch (_) { return false; }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function askLoginMethod(phone) {
|
|
640
|
+
const mob = hasMobileSession(phone);
|
|
641
|
+
const web = hasWebSession(phone);
|
|
642
|
+
if (web && !mob) return Promise.resolve('pairing');
|
|
643
|
+
if (mob && !web) return Promise.resolve('sms');
|
|
644
|
+
if (!process.stdin.isTTY) return Promise.resolve(mob ? 'sms' : 'pairing');
|
|
645
|
+
|
|
646
|
+
return new Promise(resolve => {
|
|
647
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
648
|
+
out('');
|
|
649
|
+
out(' how do you want to connect?');
|
|
650
|
+
out(' 1) sms register this number as its own device');
|
|
651
|
+
out(' 2) pairing code link to an existing WhatsApp account (8-digit code)');
|
|
652
|
+
rl.question(' sms or pairing code? [1/2] ', (answer) => {
|
|
653
|
+
rl.close();
|
|
654
|
+
const a = String(answer).trim().toLowerCase();
|
|
655
|
+
resolve((a === '2' || a.startsWith('p')) ? 'pairing' : 'sms');
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Link as a companion device.
|
|
661
|
+
//
|
|
662
|
+
// Connect first, then ask for the code: the request rides on the encrypted
|
|
663
|
+
// channel, so there has to be one before there can be a code.
|
|
664
|
+
async function doConnectWeb(phone, opts) {
|
|
665
|
+
opts = opts || {};
|
|
666
|
+
phone = normalizePhone(phone);
|
|
667
|
+
const client = new WhalibmobClient({ sessionDir: _sessDir });
|
|
668
|
+
attachEvents(client);
|
|
669
|
+
|
|
670
|
+
client.on('pair_device', () => { /* QR path — nothing to draw in a terminal */ });
|
|
671
|
+
|
|
672
|
+
client.once('paired', (p) => {
|
|
673
|
+
out('');
|
|
674
|
+
out(' linked as ' + p.jid + (p.lid ? ' (' + p.lid + ')' : ''));
|
|
675
|
+
out(' device slot ' + p.deviceIndex + (p.platform ? ' · primary is ' + p.platform : ''));
|
|
676
|
+
out(' finishing handshake...');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
client.on('history_sync', (r) => {
|
|
680
|
+
out(' history ' + r.syncTypeName +
|
|
681
|
+
' chats=' + r.chats.length +
|
|
682
|
+
' contacts=' + r.contacts.length);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
client.once('connected', () => {
|
|
686
|
+
_client = client;
|
|
687
|
+
_phone = phone;
|
|
688
|
+
out('connected as +' + phone + ' (web / companion)');
|
|
689
|
+
_rl.setPrompt('wa +' + phone + '> ');
|
|
690
|
+
_rl.prompt();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const alreadyLinked = hasWebSession(phone);
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
await client.connectWeb(phone, { syncFullHistory: true });
|
|
697
|
+
|
|
698
|
+
if (!alreadyLinked) {
|
|
699
|
+
const code = await client.requestPairingCode(phone, opts.customCode);
|
|
700
|
+
out('');
|
|
701
|
+
hr();
|
|
702
|
+
out(' pairing code ' + code.slice(0, 4) + '-' + code.slice(4));
|
|
703
|
+
hr();
|
|
704
|
+
out(' on the phone that owns +' + phone + ':');
|
|
705
|
+
out(' WhatsApp → Settings → Linked Devices → Link a device');
|
|
706
|
+
out(' → Link with phone number instead → enter the code above');
|
|
707
|
+
out('');
|
|
708
|
+
out(' the code is valid for a few minutes; waiting...');
|
|
709
|
+
} else {
|
|
710
|
+
out('session already linked — reconnecting');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const keepAlive = setInterval(() => {}, 10000);
|
|
714
|
+
client.once('connected', () => clearInterval(keepAlive));
|
|
715
|
+
client.once('auth_failure', () => clearInterval(keepAlive));
|
|
716
|
+
} catch (e) {
|
|
717
|
+
fail(e.message);
|
|
718
|
+
_rl.prompt();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
607
722
|
async function doConnect(phone) {
|
|
608
723
|
phone = normalizePhone(phone);
|
|
609
724
|
const client = new WhalibmobClient({ sessionDir: _sessDir });
|
|
@@ -676,9 +791,16 @@ async function handleLine(line) {
|
|
|
676
791
|
if (!_client || !_client.store) { fail('not connected'); break; }
|
|
677
792
|
let _meJid = '—';
|
|
678
793
|
try { _meJid = assertMeId(_client.store); } catch (_) {}
|
|
794
|
+
const web = _client._mode === 'web';
|
|
679
795
|
hr();
|
|
680
796
|
kv('phone', _client.store.phoneNumber);
|
|
681
|
-
kv('
|
|
797
|
+
kv('mode', web ? 'web / companion' : 'mobile / primary');
|
|
798
|
+
kv('jid', web && _client.store.me ? _client.store.me.id : _meJid);
|
|
799
|
+
if (web) {
|
|
800
|
+
kv('lid', (_client.store.me && _client.store.me.lid) || '—');
|
|
801
|
+
kv('device', String(_client.store.deviceIndex || 0));
|
|
802
|
+
kv('primary', _client.store.platform || '—');
|
|
803
|
+
}
|
|
682
804
|
kv('name', _client.store.pushName || _client.store.name || '—');
|
|
683
805
|
kv('session', _sessDir);
|
|
684
806
|
hr();
|
|
@@ -687,10 +809,30 @@ async function handleLine(line) {
|
|
|
687
809
|
|
|
688
810
|
case '/connect': {
|
|
689
811
|
const ph = p[1];
|
|
690
|
-
if (!ph) { fail('usage: /connect <phone>'); break; }
|
|
812
|
+
if (!ph) { fail('usage: /connect <phone> [sms|pair]'); break; }
|
|
691
813
|
if (_client && _client.connected) { fail('already connected'); break; }
|
|
814
|
+
const phn = normalizePhone(ph);
|
|
815
|
+
const forced = (p[2] || '').toLowerCase();
|
|
816
|
+
const method = forced === 'pair' || forced === 'pairing' ? 'pairing'
|
|
817
|
+
: forced === 'sms' ? 'sms'
|
|
818
|
+
: await askLoginMethod(phn);
|
|
692
819
|
out('connecting...');
|
|
693
|
-
await
|
|
820
|
+
if (method === 'pairing') await doConnectWeb(phn);
|
|
821
|
+
else await doConnect(phn);
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
case '/pair': {
|
|
826
|
+
const ph = p[1] || _phone;
|
|
827
|
+
if (!ph) {
|
|
828
|
+
fail('usage: /pair <phone> [8-char-code]');
|
|
829
|
+
out(' the phone number is the account you want to link to, digits only');
|
|
830
|
+
out(' example: /pair 40756469325');
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
if (_client && _client.connected) { fail('already connected — /disconnect first'); break; }
|
|
834
|
+
out('connecting...');
|
|
835
|
+
await doConnectWeb(ph, { customCode: p[2] });
|
|
694
836
|
break;
|
|
695
837
|
}
|
|
696
838
|
|
|
@@ -1709,6 +1851,7 @@ whalibmob v${VERSION}
|
|
|
1709
1851
|
usage:
|
|
1710
1852
|
wa open interactive shell
|
|
1711
1853
|
wa connect <phone> connect and open interactive shell
|
|
1854
|
+
wa pair <phone> [code] link to an existing account (8-digit code)
|
|
1712
1855
|
wa listen <phone> connect and listen (stay-alive)
|
|
1713
1856
|
wa registration --request-code <phone> request SMS code
|
|
1714
1857
|
wa registration --register <phone> --code <code>
|
|
@@ -1717,6 +1860,8 @@ usage:
|
|
|
1717
1860
|
|
|
1718
1861
|
options:
|
|
1719
1862
|
--session <dir> session directory (default: ~/.waSession)
|
|
1863
|
+
--sms connect by registering this number over SMS
|
|
1864
|
+
--pair connect by linking to an existing account (8-digit code)
|
|
1720
1865
|
--method sms | voice | wa_old | email (default: sms)
|
|
1721
1866
|
--email <address> email address (required when --method email)
|
|
1722
1867
|
|
|
@@ -1890,9 +2035,27 @@ async function main() {
|
|
|
1890
2035
|
if (cmd === 'connect') {
|
|
1891
2036
|
const phone = normalizePhone(sub || pos[0] || flags.phone || '');
|
|
1892
2037
|
if (!phone) { fail('phone number required'); process.exit(1); }
|
|
2038
|
+
const forced = String(flags.method || '').toLowerCase();
|
|
2039
|
+
const method = flags.pair || forced === 'pair' || forced === 'pairing' ? 'pairing'
|
|
2040
|
+
: flags.sms || forced === 'sms' ? 'sms'
|
|
2041
|
+
: await askLoginMethod(phone);
|
|
1893
2042
|
openShell('wa +' + phone + '> ');
|
|
1894
2043
|
out('connecting to +' + phone + '...');
|
|
1895
|
-
await
|
|
2044
|
+
if (method === 'pairing') await doConnectWeb(phone);
|
|
2045
|
+
else await doConnect(phone);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if (cmd === 'pair') {
|
|
2050
|
+
// `wa pair <phone> [code]` — parseArgs puts the phone in sub, so the
|
|
2051
|
+
// optional code is the first positional that remains.
|
|
2052
|
+
const phone = normalizePhone(sub || pos[0] || flags.phone || '');
|
|
2053
|
+
if (!phone) { fail('phone number required'); process.exit(1); }
|
|
2054
|
+
const custom = (sub ? pos[0] : pos[1]) ||
|
|
2055
|
+
(typeof flags.code === 'string' ? flags.code : undefined);
|
|
2056
|
+
openShell('wa +' + phone + '> ');
|
|
2057
|
+
out('linking +' + phone + ' to an existing WhatsApp account...');
|
|
2058
|
+
await doConnectWeb(phone, { customCode: custom });
|
|
1896
2059
|
return;
|
|
1897
2060
|
}
|
|
1898
2061
|
|
package/index.js
CHANGED
|
@@ -17,6 +17,22 @@ const {
|
|
|
17
17
|
assertMeId,
|
|
18
18
|
initAuthCreds
|
|
19
19
|
} = require('./lib/auth-utils');
|
|
20
|
+
const {
|
|
21
|
+
createNewWebStore, saveWebStore, loadWebStore, webSessionPath
|
|
22
|
+
} = require('./lib/WebStore');
|
|
23
|
+
const {
|
|
24
|
+
generatePairingCode,
|
|
25
|
+
derivePairingCodeKey,
|
|
26
|
+
buildWrappedEphemeralPub,
|
|
27
|
+
decipherLinkPublicKey,
|
|
28
|
+
buildCompanionFinishBundle
|
|
29
|
+
} = require('./lib/PairingCode');
|
|
30
|
+
const { configureSuccessfulPairing } = require('./lib/CompanionPairing');
|
|
31
|
+
const { fetchWaWebVersion } = require('./lib/WebVersion');
|
|
32
|
+
const {
|
|
33
|
+
encodeCompanionRegisterPayload,
|
|
34
|
+
encodeCompanionLoginPayload
|
|
35
|
+
} = require('./lib/webproto');
|
|
20
36
|
const {
|
|
21
37
|
processHistorySyncNotification,
|
|
22
38
|
decodeHistorySyncNotification,
|
|
@@ -53,6 +69,20 @@ module.exports = {
|
|
|
53
69
|
storeFromJson,
|
|
54
70
|
toSixParts,
|
|
55
71
|
fromSixParts,
|
|
72
|
+
// WhatsApp Web (companion) mode — pairing-code linking to an existing account
|
|
73
|
+
createNewWebStore,
|
|
74
|
+
saveWebStore,
|
|
75
|
+
loadWebStore,
|
|
76
|
+
webSessionPath,
|
|
77
|
+
generatePairingCode,
|
|
78
|
+
derivePairingCodeKey,
|
|
79
|
+
buildWrappedEphemeralPub,
|
|
80
|
+
decipherLinkPublicKey,
|
|
81
|
+
buildCompanionFinishBundle,
|
|
82
|
+
configureSuccessfulPairing,
|
|
83
|
+
encodeCompanionRegisterPayload,
|
|
84
|
+
encodeCompanionLoginPayload,
|
|
85
|
+
fetchWaWebVersion,
|
|
56
86
|
// Signal / encryption internals
|
|
57
87
|
SignalProtocol,
|
|
58
88
|
SignalStore,
|