whalibmob 5.5.27 → 5.5.29

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/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 calls per JID
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; }
@@ -832,25 +833,12 @@ class WhalibmobClient extends EventEmitter {
832
833
  handler(attrs);
833
834
  }
834
835
 
835
- // ─── Error 463: reachout timelock / missing tcToken ─────────────────
836
- // The server returns error 463 when a DM is sent without a tctoken node
837
- // and the account has hit the "reaching out" rate limit.
838
- // Recovery: immediately issue a privacy token to the sender and store it
839
- // so future messages carry the <tctoken> node automatically.
840
- if (error === '463') {
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
  }
@@ -1601,7 +1589,7 @@ class WhalibmobClient extends EventEmitter {
1601
1589
  */
1602
1590
  _reissueTcTokenAfterIdentityChange(from) {
1603
1591
  if (!this._tcTokenStore || !from) return;
1604
- const { normalizeJidForTcToken, isTcTokenExpired } = require('./messages/TcTokenStore');
1592
+ const { normalizeJidForTcToken, tcTokenExpired } = require('./messages/TcTokenStore');
1605
1593
 
1606
1594
  void (async () => {
1607
1595
  try {
@@ -1613,7 +1601,7 @@ class WhalibmobClient extends EventEmitter {
1613
1601
 
1614
1602
  const entry = this._tcTokenStore.get(tcJid);
1615
1603
  const senderTs = entry && entry.senderTimestamp;
1616
- if (!senderTs || isTcTokenExpired(senderTs)) {
1604
+ if (!senderTs || tcTokenExpired(senderTs)) {
1617
1605
  // No previous issuance — nothing to re-issue
1618
1606
  return;
1619
1607
  }
@@ -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,135 @@ 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
- const timer = setTimeout(() => {
821
- this._client._pendingAcks.delete(msgId);
822
- // Resolve anyway WA often doesn't ack immediately
823
- resolve({ id: msgId, status: 'sent' });
824
- }, 20000);
825
-
826
- this._client._pendingAcks.set(msgId, (ackAttrs) => {
827
- clearTimeout(timer);
828
- if (ackAttrs && ackAttrs.error) {
829
- reject(new Error('Send error ' + ackAttrs.error + ' for ' + msgId));
830
- } else {
831
- resolve({ id: msgId, t: ackAttrs && ackAttrs.t, status: 'sent' });
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
+ // Verify that real token bytes were received from the server.
893
+ // If the IQ timed out (result=NULL) or the server refused to
894
+ // issue (restricted account), tokenBytes=0 — re-sending would
895
+ // just get another 463 immediately. Reject with a clear message.
896
+ const tcStore = client._tcTokenStore;
897
+ const entry = tcStore && tcStore.get(tcJid);
898
+ const hasToken = entry && entry.token && entry.token.length > 0;
899
+ if (!hasToken) {
900
+ process.stderr.write(
901
+ '[DBG] ACK_463 IQ timeout or server refused — no token bytes stored for ' +
902
+ tcJid + ', giving up (account may be restricted)\n'
903
+ );
904
+ throw new Error(
905
+ 'Send error 463: server did not issue a tctoken for ' + tcJid +
906
+ ' (IQ timeout or account restricted)'
907
+ );
908
+ }
909
+ process.stderr.write('[DBG] ACK_463 token stored for ' + tcJid + ' — retrying send\n');
910
+ const retryId = crypto.randomBytes(8).toString('hex').toUpperCase();
911
+ const retryNode = new BinaryNode(
912
+ node.description,
913
+ Object.assign({}, node.attrs, { id: retryId }),
914
+ node.content
915
+ );
916
+ return self._dispatchAndAck(retryNode, retryId, /* _isRetry */ true);
917
+ })
918
+ .then(r => resolve(r))
919
+ .catch(err => {
920
+ process.stderr.write('[DBG] ACK_463 recovery failed: ' + (err && err.message) + '\n');
921
+ reject(new Error('Send error 463: tctoken recovery failed — ' + (err && err.message)));
922
+ })
923
+ .finally(() => client._inFlight463Recoveries.delete(tcJid));
924
+ return;
925
+ }
926
+ // ─────────────────────────────────────────────────────────────
927
+
928
+ if (error) {
929
+ reject(new Error('Send error ' + error + ' for ' + id));
930
+ } else {
931
+ resolve({ id, t: ackAttrs && ackAttrs.t, status: 'sent' });
932
+ }
933
+ });
934
+
935
+ try {
936
+ socket.sendNode(n);
937
+ } catch (err) {
938
+ clearTimeout(_timer);
939
+ _timer = null;
940
+ client._pendingAcks.delete(id);
941
+ reject(err);
832
942
  }
833
- });
943
+ };
834
944
 
835
- try {
836
- this._socket.sendNode(node);
837
- } catch (err) {
838
- clearTimeout(timer);
839
- this._client._pendingAcks.delete(msgId);
840
- reject(err);
841
- }
945
+ sendNode(node, msgId);
842
946
  });
843
947
  }
844
948
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.27",
3
+ "version": "5.5.29",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",