whalibmob 5.6.8 → 5.6.9

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.
Files changed (3) hide show
  1. package/cli.js +17 -0
  2. package/lib/Client.js +104 -19
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -580,6 +580,23 @@ function attachEvents(client) {
580
580
  notConnected();
581
581
  });
582
582
 
583
+ // Someone else opened a session for this number. Reconnecting would fight
584
+ // them for it, so the client stands down and says so.
585
+ client.on('connection_replaced', (r) => {
586
+ _rl && _rl.pause();
587
+ fail('connection replaced');
588
+ out(' ' + r.message);
589
+ _rl && _rl.resume();
590
+ notConnected();
591
+ });
592
+
593
+ // A stanza whose shape we mishandled. Not fatal — the connection kept going.
594
+ client.on('node_error', (e) => {
595
+ _rl && _rl.pause();
596
+ fail('could not handle a <' + e.tag + '>: ' + (e.err && e.err.message));
597
+ _rl && (_rl.resume(), _rl.prompt(true));
598
+ });
599
+
583
600
  // The server declined the client, not the session. Nothing to re-register.
584
601
  client.on('client_rejected', (r) => {
585
602
  _rl && _rl.pause();
package/lib/Client.js CHANGED
@@ -1019,6 +1019,29 @@ class WhalibmobClient extends EventEmitter {
1019
1019
  // We do NOT decrypt the patches — that requires the full LTHASH machinery.
1020
1020
  // We DO update our version counters from the server response so that we
1021
1021
  // never re-request the same snapshot twice.
1022
+ /**
1023
+ * Acknowledge a dirty bit so the server stops announcing it.
1024
+ *
1025
+ * `<ib><dirty type="..."/></ib>` is the server saying something changed while
1026
+ * we were away. Whatever the client does about it, the bit has to be cleared
1027
+ * or the same announcement arrives on every connection.
1028
+ *
1029
+ * @param {string} type the dirty type, e.g. 'groups'
1030
+ * @param {number|string} [fromTs] the timestamp the announcement carried
1031
+ */
1032
+ _cleanDirtyBits(type, fromTs) {
1033
+ if (!this._socket || !this._connected) return;
1034
+ const attrs = { type: String(type) };
1035
+ if (fromTs) attrs.timestamp = String(fromTs);
1036
+ _whaDbg('[DBG] CLEAN_DIRTY type=' + type + (fromTs ? ' t=' + fromTs : ''));
1037
+ this._socket.sendNode(new BinaryNode('iq', {
1038
+ id: this._genMsgId(),
1039
+ to: 's.whatsapp.net',
1040
+ type: 'set',
1041
+ xmlns: 'urn:xmpp:whatsapp:dirty'
1042
+ }, [new BinaryNode('clean', attrs, null)]));
1043
+ }
1044
+
1022
1045
  _sendAppStateSyncForTypes(dirtyTypes) {
1023
1046
  if (!this._socket || !this._connected) return;
1024
1047
 
@@ -1037,7 +1060,11 @@ class WhalibmobClient extends EventEmitter {
1037
1060
  for (const type of dirtyTypes) {
1038
1061
  const mapped = COLLECTION_MAP[type];
1039
1062
  if (!mapped) {
1040
- _whaDbg('[DBG] ignoring unknown dirty type: ' + type);
1063
+ // "groups" is not an app-state collection, but it is still a dirty bit
1064
+ // the server expects to be cleared. Ignoring it meant the server
1065
+ // re-announced it on every single connection, forever.
1066
+ _whaDbg('[DBG] DIRTY type=' + type + ' — not an app-state collection, clearing the bit');
1067
+ this._cleanDirtyBits(type);
1041
1068
  continue;
1042
1069
  }
1043
1070
  for (const c of mapped) collections.add(c);
@@ -1134,18 +1161,31 @@ class WhalibmobClient extends EventEmitter {
1134
1161
  try { _whaDbg('[DBG] IQ_ERROR content=' + JSON.stringify(node.content)); } catch(_) {}
1135
1162
  }
1136
1163
 
1137
- if (tag === 'iq') this._handleIq(node);
1138
- else if (tag === 'message') this._handleMessage(node);
1139
- else if (tag === 'receipt') this._handleReceipt(node);
1140
- else if (tag === 'ack') this._handleAck(node);
1141
- else if (tag === 'notification') this._handleNotification(node);
1142
- else if (tag === 'presence') this._handlePresence(node);
1143
- else if (tag === 'call') this._handleCall(node);
1144
- else if (tag === 'ib') this._handleIb(node);
1145
- else if (tag === 'success') this.emit('session_refresh', { node }); // late success (re-auth)
1146
- else if (tag === 'failure') this._handleFailure(node);
1147
- else if (tag === 'stream:error') this._handleStreamError(node);
1148
- else this.emit('node', node);
1164
+ // One malformed stanza must not take the connection down with it.
1165
+ //
1166
+ // A throw here propagates out through the socket's frame reader, which
1167
+ // reports it as a socket error — so a single unhandled shape in one
1168
+ // notification looked like the transport failing, and every stanza queued
1169
+ // behind it in the same offline batch was lost. Contain it: log the stanza
1170
+ // that did it, tell the caller, carry on reading.
1171
+ try {
1172
+ if (tag === 'iq') this._handleIq(node);
1173
+ else if (tag === 'message') this._handleMessage(node);
1174
+ else if (tag === 'receipt') this._handleReceipt(node);
1175
+ else if (tag === 'ack') this._handleAck(node);
1176
+ else if (tag === 'notification') this._handleNotification(node);
1177
+ else if (tag === 'presence') this._handlePresence(node);
1178
+ else if (tag === 'call') this._handleCall(node);
1179
+ else if (tag === 'ib') this._handleIb(node);
1180
+ else if (tag === 'success') this.emit('session_refresh', { node }); // late success (re-auth)
1181
+ else if (tag === 'failure') this._handleFailure(node);
1182
+ else if (tag === 'stream:error') this._handleStreamError(node);
1183
+ else this.emit('node', node);
1184
+ } catch (err) {
1185
+ _whaDbg('[DBG] NODE_HANDLER_ERR tag=' + tag + ' err=' + (err && err.message) +
1186
+ ' attrs=' + JSON.stringify(node.attrs || {}));
1187
+ this.emit('node_error', { tag, node, err });
1188
+ }
1149
1189
  }
1150
1190
 
1151
1191
  // A stream error ends the connection; the reconnect in _onClose picks it up.
@@ -1157,16 +1197,55 @@ class WhalibmobClient extends EventEmitter {
1157
1197
  _handleStreamError(node) {
1158
1198
  const attrs = (node && node.attrs) || {};
1159
1199
  const code = attrs.code ? String(attrs.code) : null;
1160
- const reason = attrs.reason || code || 'unknown';
1161
1200
 
1162
1201
  if (code === '515') {
1163
1202
  _whaDbg('[DBG] STREAM_ERROR 515 — restart required after pairing, reconnecting');
1164
- this.emit('restart_required', { reason });
1203
+ this.emit('restart_required', { reason: '515' });
1204
+ return;
1205
+ }
1206
+
1207
+ // Most stream errors carry no code at all — the reason is a child element,
1208
+ // and for <conflict> the type attribute on it says which kind. Reading only
1209
+ // the attributes reported every one of these as "code=null reason=unknown",
1210
+ // which is the same message for a session someone else took over and a
1211
+ // session that was deleted.
1212
+ const child = Array.isArray(node.content) && node.content[0] ? node.content[0] : null;
1213
+ const childTag = child && child.description ? child.description : null;
1214
+ const conflictType = childTag === 'conflict' && child.attrs
1215
+ ? String(child.attrs.type || '') : null;
1216
+
1217
+ if (conflictType === 'replaced') {
1218
+ // Another client opened a session for this account and the server handed
1219
+ // it over. Reconnecting immediately just takes it back, and the two ends
1220
+ // trade the session until one of them is stopped — so this one stands
1221
+ // down and says why.
1222
+ _whaDbg('[DBG] STREAM_ERROR conflict/replaced — another client took the session');
1223
+ this._fatal = true;
1224
+ this.emit('connection_replaced', {
1225
+ message: 'Another client connected with this number and took over the ' +
1226
+ 'session. Reconnecting would take it back and the two would ' +
1227
+ 'keep displacing each other, so this connection has stopped. ' +
1228
+ 'Close the other client, then connect again.'
1229
+ });
1230
+ this.disconnect();
1231
+ return;
1232
+ }
1233
+
1234
+ if (conflictType === 'device_removed') {
1235
+ // The companion was unlinked from the phone. Same meaning as a 401.
1236
+ _whaDbg('[DBG] STREAM_ERROR conflict/device_removed — unlinked');
1237
+ this._fatal = true;
1238
+ this.emit('auth_failure', {
1239
+ reason: '401', location: null, loggedOut: true, node
1240
+ });
1241
+ this.disconnect();
1165
1242
  return;
1166
1243
  }
1167
1244
 
1168
- _whaDbg('[DBG] STREAM_ERROR code=' + code + ' reason=' + reason);
1169
- this.emit('stream_error', { reason, code });
1245
+ const reason = attrs.reason || conflictType || childTag || code || 'unknown';
1246
+ _whaDbg('[DBG] STREAM_ERROR code=' + code + ' child=' + childTag +
1247
+ ' conflict=' + conflictType + ' reason=' + reason);
1248
+ this.emit('stream_error', { reason, code, conflictType, childTag, node });
1170
1249
  }
1171
1250
 
1172
1251
  // ─── IQ handling ──────────────────────────────────────────────────────────
@@ -1956,7 +2035,13 @@ class WhalibmobClient extends EventEmitter {
1956
2035
  // Server push: a contact's linked device list changed (they linked or
1957
2036
  // unlinked a tablet, desktop, etc.). Invalidate that phone's cache entry
1958
2037
  // so the next send triggers a fresh usync IQ and reaches all their devices.
1959
- const fromJid = attrs.from || '';
2038
+ // BinaryNode decodes an AD-encoded JID into an object, not a string, so
2039
+ // this has to be stringified before it can be split. It was not, and a
2040
+ // <notification type="devices"> from a LID-addressed contact threw
2041
+ // "fromJid.split is not a function" out of the node handler — taking the
2042
+ // whole dispatch down with it, so every notification behind it in the
2043
+ // offline queue was dropped too.
2044
+ const fromJid = String(attrs.from || '');
1960
2045
  const fromPhone = fromJid.split('@')[0].split(':')[0];
1961
2046
  if (fromPhone && this._devMgr) {
1962
2047
  this._devMgr._dcDel([fromPhone]);
@@ -1980,7 +2065,7 @@ class WhalibmobClient extends EventEmitter {
1980
2065
  // Server push: a contact re-registered WhatsApp (new identity key / new phone).
1981
2066
  // Their old Signal sessions are no longer valid — clear them and the device
1982
2067
  // cache so the next send builds a fresh pkmsg session from scratch.
1983
- const fromJid = attrs.from || '';
2068
+ const fromJid = String(attrs.from || '');
1984
2069
  const fromPhone = fromJid.split('@')[0].split(':')[0];
1985
2070
  if (fromPhone && this._devMgr) {
1986
2071
  this._devMgr._dcDel([fromPhone]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.6.8",
3
+ "version": "5.6.9",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",