whalibmob 5.5.26 → 5.5.28
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/cli.js +29 -12
- package/lib/Client.js +9 -21
- package/lib/Registration.js +25 -1
- package/lib/messages/MessageSender.js +108 -21
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -196,9 +196,10 @@ const HELP = `
|
|
|
196
196
|
/biz <phone|jid> query business profile
|
|
197
197
|
|
|
198
198
|
Registration
|
|
199
|
-
/reg check <phone>
|
|
200
|
-
/reg code <phone> [sms|voice|wa_old]
|
|
201
|
-
/reg
|
|
199
|
+
/reg check <phone> check if number has WhatsApp
|
|
200
|
+
/reg code <phone> [sms|voice|wa_old] request verification code
|
|
201
|
+
/reg code <phone> email <address> request code via email
|
|
202
|
+
/reg confirm <phone> <code> complete registration
|
|
202
203
|
|
|
203
204
|
Connection
|
|
204
205
|
/connect <phone> connect to WhatsApp
|
|
@@ -1153,8 +1154,14 @@ async function handleLine(line) {
|
|
|
1153
1154
|
}
|
|
1154
1155
|
else if (sub === 'code') {
|
|
1155
1156
|
const ph = normalizePhone(p[2]);
|
|
1156
|
-
const method = p[3] || 'sms';
|
|
1157
|
-
|
|
1157
|
+
const method = (p[3] || 'sms').toLowerCase();
|
|
1158
|
+
// email method: /reg code <phone> email <address>
|
|
1159
|
+
const emailAddr = method === 'email' ? (p[4] || '') : '';
|
|
1160
|
+
if (!ph) { fail('usage: /reg code <phone> [sms|voice|wa_old|email <address>]'); break; }
|
|
1161
|
+
if (method === 'email' && !emailAddr) {
|
|
1162
|
+
fail('email method requires an address — usage: /reg code <phone> email <address>');
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1158
1165
|
if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
|
|
1159
1166
|
const sessFile = path.join(_sessDir, `${ph}.json`);
|
|
1160
1167
|
let store = loadStore(sessFile);
|
|
@@ -1173,8 +1180,10 @@ async function handleLine(line) {
|
|
|
1173
1180
|
out(' new keys saved — proceed with code below');
|
|
1174
1181
|
}
|
|
1175
1182
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1183
|
+
const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
|
|
1184
|
+
out('requesting ' + methodLabel + ' code for +' + ph + '...');
|
|
1185
|
+
const codeOpts = method === 'email' ? { email: emailAddr } : {};
|
|
1186
|
+
const r = await requestSmsCode(store, method, codeOpts);
|
|
1178
1187
|
store.codePending = true;
|
|
1179
1188
|
saveStore(store, sessFile);
|
|
1180
1189
|
out(' status ' + (r && r.status));
|
|
@@ -1427,7 +1436,8 @@ usage:
|
|
|
1427
1436
|
|
|
1428
1437
|
options:
|
|
1429
1438
|
--session <dir> session directory (default: ~/.waSession)
|
|
1430
|
-
--method sms | voice | wa_old (default: sms)
|
|
1439
|
+
--method sms | voice | wa_old | email (default: sms)
|
|
1440
|
+
--email <address> email address (required when --method email)
|
|
1431
1441
|
|
|
1432
1442
|
after connecting, type /help for all available commands.
|
|
1433
1443
|
`.trim();
|
|
@@ -1475,9 +1485,14 @@ async function main() {
|
|
|
1475
1485
|
}
|
|
1476
1486
|
|
|
1477
1487
|
if (flags['request-code'] !== undefined) {
|
|
1478
|
-
const ph
|
|
1479
|
-
const method
|
|
1488
|
+
const ph = phone || normalizePhone(pos[0] || '');
|
|
1489
|
+
const method = (flags.method || 'sms').toLowerCase();
|
|
1490
|
+
const emailAddr = flags.email || '';
|
|
1480
1491
|
if (!ph) { fail('phone number required'); process.exit(1); }
|
|
1492
|
+
if (method === 'email' && !emailAddr) {
|
|
1493
|
+
fail('--method email requires --email <address>');
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1481
1496
|
if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
|
|
1482
1497
|
const sessFile = path.join(_sessDir, `${ph}.json`);
|
|
1483
1498
|
let store = loadStore(sessFile);
|
|
@@ -1500,9 +1515,11 @@ async function main() {
|
|
|
1500
1515
|
}
|
|
1501
1516
|
// If store.codePending === true, keys were already accepted by WhatsApp in a
|
|
1502
1517
|
// prior /code request — reuse the exact same store without any /exist call.
|
|
1503
|
-
|
|
1518
|
+
const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
|
|
1519
|
+
out('requesting ' + methodLabel + ' code for +' + ph + '...');
|
|
1504
1520
|
try {
|
|
1505
|
-
const
|
|
1521
|
+
const codeOpts = method === 'email' ? { email: emailAddr } : {};
|
|
1522
|
+
const r = await requestSmsCode(store, method, codeOpts);
|
|
1506
1523
|
store.codePending = true;
|
|
1507
1524
|
saveStore(store, sessFile);
|
|
1508
1525
|
out(' status ' + (r && r.status));
|
package/lib/Client.js
CHANGED
|
@@ -134,7 +134,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
134
134
|
this._appStateVersions = {}; // collectionName → version (int)
|
|
135
135
|
this._sentMsgCache = new Map(); // msgId → {plaintext, toJid, msgId, mediaType, options, recipientPhone}
|
|
136
136
|
this._tcTokenStore = null; // TcTokenStore — loaded in init()
|
|
137
|
-
this._inFlightTcTokenIssuance = new Set(); // dedupe concurrent issuePrivacyTokens
|
|
137
|
+
this._inFlightTcTokenIssuance = new Set(); // dedupe concurrent proactive issuePrivacyTokens per JID
|
|
138
|
+
this._inFlight463Recoveries = new Set(); // dedupe concurrent 463-triggered token issuances per JID (separate from proactive)
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
get store() { return this._store; }
|
|
@@ -789,7 +790,7 @@ class WhalibmobClient extends EventEmitter {
|
|
|
789
790
|
}
|
|
790
791
|
|
|
791
792
|
const fromStr = attrs.from ? String(attrs.from) : '';
|
|
792
|
-
// Extract bare phone number: "
|
|
793
|
+
// Extract bare phone number: "407xxx@s.whatsapp.net" → "407xxxx"
|
|
793
794
|
const recipientPhone = fromStr.split('@')[0].split('.')[0];
|
|
794
795
|
|
|
795
796
|
process.stderr.write('[DBG] RETRY_RECV msgId=' + msgId + ' from=' + fromStr + ' — clearing session + resending\n');
|
|
@@ -832,25 +833,12 @@ class WhalibmobClient extends EventEmitter {
|
|
|
832
833
|
handler(attrs);
|
|
833
834
|
}
|
|
834
835
|
|
|
835
|
-
// ─── Error 463
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
const from = attrs.from ? String(attrs.from) : '';
|
|
842
|
-
process.stderr.write('[DBG] ACK_463 from=' + from + ' id=' + id + '\n');
|
|
843
|
-
if (from && this._tcTokenStore && !this._inFlightTcTokenIssuance.has(from)) {
|
|
844
|
-
this._inFlightTcTokenIssuance.add(from);
|
|
845
|
-
const issueTs = Math.floor(Date.now() / 1000);
|
|
846
|
-
const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
|
|
847
|
-
const tcJid = normalizeJidForTcToken(from);
|
|
848
|
-
this._issuePrivacyTokens(from, issueTs)
|
|
849
|
-
.then(result => this._storeTcTokenFromIqResult(result, tcJid, issueTs))
|
|
850
|
-
.catch(() => {})
|
|
851
|
-
.finally(() => this._inFlightTcTokenIssuance.delete(from));
|
|
852
|
-
}
|
|
853
|
-
}
|
|
836
|
+
// ─── Error 463 ───────────────────────────────────────────────────────
|
|
837
|
+
// 463 recovery is fully handled inside MessageSender._dispatchAndAck:
|
|
838
|
+
// it issues the privacy token then automatically re-sends the message
|
|
839
|
+
// so the caller's promise resolves transparently (no loop).
|
|
840
|
+
// Nothing to do here — the pendingAck handler above already drove the
|
|
841
|
+
// recovery via _inFlight463Recoveries + _issuePrivacyTokens.
|
|
854
842
|
// ────────────────────────────────────────────────────────────────────
|
|
855
843
|
}
|
|
856
844
|
}
|
package/lib/Registration.js
CHANGED
|
@@ -590,13 +590,37 @@ async function assertRegistrationKeys(store, waVersion) {
|
|
|
590
590
|
return false;
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
-
async function requestSmsCode(store, method) {
|
|
593
|
+
async function requestSmsCode(store, method, opts) {
|
|
594
594
|
method = method || 'sms';
|
|
595
|
+
opts = opts || {};
|
|
595
596
|
const _device = getDeviceConfig();
|
|
596
597
|
const waVersion = await fetchWaVersion(_device);
|
|
597
598
|
store.version = waVersion;
|
|
598
599
|
store.device = _device;
|
|
599
600
|
|
|
601
|
+
// Email method: request code via email instead of SMS/voice.
|
|
602
|
+
// Sends method=email + email=<address> in the /code request.
|
|
603
|
+
// No auto-fallback for email — it either works or fails.
|
|
604
|
+
if (method === 'email') {
|
|
605
|
+
const emailAddr = opts.email || '';
|
|
606
|
+
if (!emailAddr) throw new Error('requestSmsCode: email method requires opts.email address');
|
|
607
|
+
const { cc: _regCc } = parsePhone(store.phoneNumber);
|
|
608
|
+
const _regMeta = getCountryMeta(_regCc);
|
|
609
|
+
const extra = [
|
|
610
|
+
'method', 'email',
|
|
611
|
+
'email', emailAddr,
|
|
612
|
+
'sim_mcc', _regMeta.mcc,
|
|
613
|
+
'sim_mnc', _regMeta.mnc,
|
|
614
|
+
'reason', '',
|
|
615
|
+
'cellular_strength', '1'
|
|
616
|
+
];
|
|
617
|
+
const result = await sendRequest('/code', store, waVersion, true, extra);
|
|
618
|
+
const status = result.status;
|
|
619
|
+
if (status === 'ok' || status === 'sent') return result;
|
|
620
|
+
const reason = result.reason || status || '';
|
|
621
|
+
throw new Error(`Registration email error: ${reason} — raw: ${JSON.stringify(result)}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
600
624
|
// Auto-fallback: if the primary method gets no_routes, try the alternate once.
|
|
601
625
|
// sms → wa_old, wa_old → sms, voice → sms
|
|
602
626
|
const fallbackMethod = method === 'wa_old' ? 'sms' : (method === 'sms' ? 'wa_old' : 'sms');
|
|
@@ -814,31 +814,118 @@ class MessageSender {
|
|
|
814
814
|
}
|
|
815
815
|
|
|
816
816
|
// ─── Dispatch and wait for ack ────────────────────────────────────────────
|
|
817
|
+
//
|
|
818
|
+
// Error 463 recovery (mirrors Baileys messages-recv.js inFlight463Recoveries):
|
|
819
|
+
// 1. 463 ack arrives → issue privacy token via _issuePrivacyTokens
|
|
820
|
+
// 2. Wait for token to be stored in _tcTokenStore
|
|
821
|
+
// 3. Re-send the SAME message node with a new msgId — the tctoken node
|
|
822
|
+
// was already attached by _sendDMMessage, so the re-dispatched node
|
|
823
|
+
// carries the same content; the token lookup on re-send will succeed.
|
|
824
|
+
// The re-send uses _isRetry=true to prevent recursive 463 handling.
|
|
825
|
+
// 4. Resolve/reject based on the re-send ack — transparent to the caller.
|
|
826
|
+
//
|
|
827
|
+
// Uses a SEPARATE _inFlight463Recoveries Set (not shared with the proactive
|
|
828
|
+
// _inFlightTcTokenIssuance) so 463 recovery and proactive issuance never
|
|
829
|
+
// block each other. Matches Baileys pattern exactly.
|
|
830
|
+
|
|
831
|
+
_dispatchAndAck(node, msgId, _isRetry) {
|
|
832
|
+
const self = this;
|
|
833
|
+
const client = this._client;
|
|
834
|
+
const socket = this._socket;
|
|
817
835
|
|
|
818
|
-
_dispatchAndAck(node, msgId) {
|
|
819
836
|
return new Promise((resolve, reject) => {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
837
|
+
let _timer = null;
|
|
838
|
+
|
|
839
|
+
const armTimer = (id) => {
|
|
840
|
+
_timer = setTimeout(() => {
|
|
841
|
+
client._pendingAcks.delete(id);
|
|
842
|
+
resolve({ id, status: 'sent' });
|
|
843
|
+
}, 20000);
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const sendNode = (n, id) => {
|
|
847
|
+
armTimer(id);
|
|
848
|
+
client._pendingAcks.set(id, (ackAttrs) => {
|
|
849
|
+
clearTimeout(_timer);
|
|
850
|
+
_timer = null;
|
|
851
|
+
const error = ackAttrs && ackAttrs.error ? String(ackAttrs.error) : '';
|
|
852
|
+
|
|
853
|
+
// ─── Error 463: missing tcToken / reachout timelock ───────────
|
|
854
|
+
// Issue privacy token then automatically re-send once with the
|
|
855
|
+
// token attached. Uses a separate _inFlight463Recoveries Set so
|
|
856
|
+
// concurrent 463 recoveries and proactive issuances don't clash.
|
|
857
|
+
if (error === '463' && !_isRetry) {
|
|
858
|
+
const ackFrom = ackAttrs.from ? String(ackAttrs.from) : '';
|
|
859
|
+
const tcJid = normalizeJidForTcToken(
|
|
860
|
+
ackFrom || String(node.attrs && node.attrs.to || '')
|
|
861
|
+
);
|
|
862
|
+
process.stderr.write(
|
|
863
|
+
'[DBG] ACK_463 from=' + ackFrom + ' id=' + id +
|
|
864
|
+
' tcJid=' + tcJid + ' — issuing tctoken then retrying\n'
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
if (client._inFlight463Recoveries.has(tcJid)) {
|
|
868
|
+
// Another concurrent recovery in progress for this JID —
|
|
869
|
+
// wait ~3 s for it to finish then retry with whatever token
|
|
870
|
+
// is now stored (may or may not succeed).
|
|
871
|
+
process.stderr.write('[DBG] ACK_463 already in-flight for ' + tcJid + ' — retrying after delay\n');
|
|
872
|
+
setTimeout(() => {
|
|
873
|
+
const retryId = crypto.randomBytes(8).toString('hex').toUpperCase();
|
|
874
|
+
const retryNode = new BinaryNode(
|
|
875
|
+
node.description,
|
|
876
|
+
Object.assign({}, node.attrs, { id: retryId }),
|
|
877
|
+
node.content
|
|
878
|
+
);
|
|
879
|
+
self._dispatchAndAck(retryNode, retryId, /* _isRetry */ true)
|
|
880
|
+
.then(r => resolve(r))
|
|
881
|
+
.catch(e => reject(e));
|
|
882
|
+
}, 3000);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
client._inFlight463Recoveries.add(tcJid);
|
|
887
|
+
const issueTs = Math.floor(Date.now() / 1000);
|
|
888
|
+
|
|
889
|
+
client._issuePrivacyTokens(ackFrom || tcJid, issueTs)
|
|
890
|
+
.then(result => client._storeTcTokenFromIqResult(result, tcJid, issueTs))
|
|
891
|
+
.then(() => {
|
|
892
|
+
process.stderr.write('[DBG] ACK_463 token stored for ' + tcJid + ' — retrying send\n');
|
|
893
|
+
const retryId = crypto.randomBytes(8).toString('hex').toUpperCase();
|
|
894
|
+
const retryNode = new BinaryNode(
|
|
895
|
+
node.description,
|
|
896
|
+
Object.assign({}, node.attrs, { id: retryId }),
|
|
897
|
+
node.content
|
|
898
|
+
);
|
|
899
|
+
return self._dispatchAndAck(retryNode, retryId, /* _isRetry */ true);
|
|
900
|
+
})
|
|
901
|
+
.then(r => resolve(r))
|
|
902
|
+
.catch(err => {
|
|
903
|
+
process.stderr.write('[DBG] ACK_463 recovery failed: ' + (err && err.message) + '\n');
|
|
904
|
+
reject(new Error('Send error 463: tctoken recovery failed — ' + (err && err.message)));
|
|
905
|
+
})
|
|
906
|
+
.finally(() => client._inFlight463Recoveries.delete(tcJid));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
// ─────────────────────────────────────────────────────────────
|
|
910
|
+
|
|
911
|
+
if (error) {
|
|
912
|
+
reject(new Error('Send error ' + error + ' for ' + id));
|
|
913
|
+
} else {
|
|
914
|
+
resolve({ id, t: ackAttrs && ackAttrs.t, status: 'sent' });
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
try {
|
|
919
|
+
socket.sendNode(n);
|
|
920
|
+
} catch (err) {
|
|
921
|
+
clearTimeout(_timer);
|
|
922
|
+
_timer = null;
|
|
923
|
+
client._pendingAcks.delete(id);
|
|
924
|
+
reject(err);
|
|
832
925
|
}
|
|
833
|
-
}
|
|
926
|
+
};
|
|
834
927
|
|
|
835
|
-
|
|
836
|
-
this._socket.sendNode(node);
|
|
837
|
-
} catch (err) {
|
|
838
|
-
clearTimeout(timer);
|
|
839
|
-
this._client._pendingAcks.delete(msgId);
|
|
840
|
-
reject(err);
|
|
841
|
-
}
|
|
928
|
+
sendNode(node, msgId);
|
|
842
929
|
});
|
|
843
930
|
}
|
|
844
931
|
|