nodejs-insta-private-api-mqtt 1.3.49 → 1.3.51

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.
@@ -10,6 +10,18 @@ const mqtts_1 = require("../mqtt-shim");
10
10
  const errors_1 = require("../errors");
11
11
  const eventemitter3_1 = require("eventemitter3");
12
12
  const fbns_utilities_1 = require("./fbns.utilities");
13
+
14
+ /**
15
+ * FbnsClient
16
+ *
17
+ * Lightweight wrapper around MQTToTClient to handle FBNS-specific connection
18
+ * payloads, registration, message handling and safe disconnect behavior.
19
+ *
20
+ * NOTE: This file contains a small but important robustness change:
21
+ * when receiving an empty CONNACK payload we no longer force a disconnect.
22
+ * Instagram sometimes returns a successful CONNACK with an empty payload;
23
+ * treating that as a fatal error causes unnecessary disconnect loops.
24
+ */
13
25
  class FbnsClient extends eventemitter3_1.EventEmitter {
14
26
  get auth() {
15
27
  return this._auth;
@@ -24,6 +36,7 @@ class FbnsClient extends eventemitter3_1.EventEmitter {
24
36
  this.safeDisconnect = false;
25
37
  this._auth = new fbns_device_auth_1.FbnsDeviceAuth(this.ig);
26
38
  }
39
+
27
40
  buildConnection() {
28
41
  this.fbnsDebug('Constructing connection');
29
42
  this.conn = new mqttot_1.MQTToTConnection({
@@ -51,9 +64,25 @@ class FbnsClient extends eventemitter3_1.EventEmitter {
51
64
  password: this._auth.password,
52
65
  });
53
66
  }
67
+
68
+ /**
69
+ * Connect to FBNS (Firebase/FBNS equivalent over Instagram MQTT)
70
+ *
71
+ * Options:
72
+ * - enableTrace
73
+ * - autoReconnect
74
+ * - socksOptions
75
+ * - additionalTlsOptions
76
+ *
77
+ * Important robustness changes:
78
+ * - Do not force disconnect on empty CONNACK payload; log and continue.
79
+ * - Use clean:false to prefer session resume on reconnect (less aggressive).
80
+ */
54
81
  async connect({ enableTrace, autoReconnect, socksOptions, additionalTlsOptions, } = {}) {
55
82
  this.fbnsDebug('Connecting to FBNS...');
83
+ // Ensure auth info is up-to-date
56
84
  this.auth.update();
85
+
57
86
  this.client = new mqttot_1.MQTToTClient({
58
87
  url: constants_1.FBNS.HOST_NAME_V6,
59
88
  payloadProvider: () => {
@@ -69,54 +98,91 @@ class FbnsClient extends eventemitter3_1.EventEmitter {
69
98
  socksOptions,
70
99
  additionalOptions: additionalTlsOptions,
71
100
  });
101
+
102
+ // Re-emit warnings/errors to outer listeners
72
103
  this.client.on('warning', w => this.emit('warning', w));
73
104
  this.client.on('error', e => this.emit('error', e));
105
+
106
+ // If disconnect happens and safeDisconnect is false, emit error to upper layer
74
107
  this.client.on('disconnect', reason => this.safeDisconnect
75
108
  ? this.emit('disconnect', reason && JSON.stringify(reason))
76
109
  : this.emit('error', new errors_1.ClientDisconnectedError(`MQTToTClient got disconnected. Reason: ${reason && JSON.stringify(reason)}`)));
110
+
111
+ // Listen for FBNS messages and transform them accordingly
77
112
  this.client.listen(constants_1.FbnsTopics.FBNS_MESSAGE.id, msg => this.handleMessage(msg));
78
113
  this.client.listen({
79
114
  topic: constants_1.FbnsTopics.FBNS_EXP_LOGGING.id,
80
115
  transformer: async (msg) => JSON.parse((await (0, shared_1.tryUnzipAsync)(msg.payload)).toString()),
81
116
  }, msg => this.emit('logging', msg));
82
117
  this.client.listen(constants_1.FbnsTopics.PP.id, msg => this.emit('pp', msg.payload.toString()));
118
+
119
+ // Handle connect event from the underlying mqtt client
83
120
  this.client.on('connect', async (res) => {
84
121
  if (!this.client) {
85
122
  throw new mqtts_1.IllegalStateError('No client registered but an event was received');
86
123
  }
124
+
87
125
  this.fbnsDebug('Connected to MQTT');
126
+
127
+ // IMPORTANT: Instagram sometimes returns a valid CONNACK with an empty payload.
128
+ // Historically this library treated that as an error and disconnected.
129
+ // That behavior causes unnecessary reconnect loops. Instead, log and continue.
88
130
  if (!res.payload?.length) {
89
- this.fbnsDebug(`Received empty connect packet. Reason: ${res.errorName}; Try resetting your fbns state!`);
90
- this.emit('error', new errors_1.EmptyPacketError('Received empty connect packet. Try resetting your fbns state!'));
91
- await this.client.disconnect();
92
- return;
131
+ this.fbnsDebug(`Received empty connect packet. Reason: ${res.errorName} (continuing without disconnect)`);
132
+ // Emit a warning but do not disconnect.
133
+ this.emit('warning', new errors_1.EmptyPacketError('Received empty connect packet (continuing).'));
134
+ // NOTE: we do not return here because some flows expect the registration step below.
135
+ // If you prefer to skip registration on empty payload, uncomment the next line:
136
+ // return;
137
+ } else {
138
+ // If payload present, read auth as before
139
+ try {
140
+ const payload = res.payload.toString('utf8');
141
+ this.fbnsDebug(`Received auth: ${payload}`);
142
+ this._auth.read(payload);
143
+ this.emit('auth', this.auth);
144
+ } catch (e) {
145
+ this.fbnsDebug(`Failed to parse connect payload: ${e?.message || e}`);
146
+ this.emit('error', e);
147
+ // do not force disconnect here - let reconnect logic handle transient issues
148
+ }
149
+ }
150
+
151
+ // Continue with registration attempt if possible.
152
+ try {
153
+ await this.client.mqttotPublish({
154
+ topic: constants_1.FbnsTopics.FBNS_REG_REQ.id,
155
+ payload: Buffer.from(JSON.stringify({
156
+ pkg_name: constants_1.INSTAGRAM_PACKAGE_NAME,
157
+ appid: this.ig.state.fbAnalyticsApplicationId,
158
+ }), 'utf8'),
159
+ qosLevel: 1,
160
+ });
161
+ } catch (e) {
162
+ // Log registration publish error but don't necessarily abort connection entirely here
163
+ this.fbnsDebug(`FBNS_REG_REQ publish failed: ${e?.message || e}`);
164
+ this.emit('warning', e);
93
165
  }
94
- const payload = res.payload.toString('utf8');
95
- this.fbnsDebug(`Received auth: ${payload}`);
96
- this._auth.read(payload);
97
- this.emit('auth', this.auth);
98
- await this.client.mqttotPublish({
99
- topic: constants_1.FbnsTopics.FBNS_REG_REQ.id,
100
- payload: Buffer.from(JSON.stringify({
101
- pkg_name: constants_1.INSTAGRAM_PACKAGE_NAME,
102
- appid: this.ig.state.fbAnalyticsApplicationId,
103
- }), 'utf8'),
104
- qosLevel: 1,
105
- });
106
- // this.buildConnection(); ?
107
166
  });
167
+
168
+ // Establish connection with conservative options (prefer session resume)
108
169
  await this.client
109
170
  .connect({
110
- keepAlive: 60,
111
- protocolLevel: 3,
112
- clean: true,
113
- connectDelay: 60 * 1000,
114
- })
171
+ keepAlive: 60,
172
+ protocolLevel: 3,
173
+ // Use clean: false to allow session resume where the broker supports it.
174
+ // If you have issues with stale state, consider setting clean: true.
175
+ clean: false,
176
+ connectDelay: 60 * 1000,
177
+ })
115
178
  .catch(e => {
116
- this.fbnsDebug(`Connection failed: ${e}`);
117
- throw e;
118
- });
179
+ this.fbnsDebug(`Connection failed: ${e}`);
180
+ throw e;
181
+ });
182
+
183
+ // Subscribe to FBNS message topic and wait for register response
119
184
  await this.client.subscribe({ topic: constants_1.FbnsTopics.FBNS_MESSAGE.id });
185
+
120
186
  const msg = await (0, shared_1.listenOnce)(this.client, constants_1.FbnsTopics.FBNS_REG_RESP.id);
121
187
  const data = await (0, shared_1.tryUnzipAsync)(msg.payload);
122
188
  const payload = data.toString('utf8');
@@ -136,6 +202,7 @@ class FbnsClient extends eventemitter3_1.EventEmitter {
136
202
  throw e;
137
203
  }
138
204
  }
205
+
139
206
  disconnect() {
140
207
  this.safeDisconnect = true;
141
208
  if (!this.client) {
@@ -143,6 +210,7 @@ class FbnsClient extends eventemitter3_1.EventEmitter {
143
210
  }
144
211
  return this.client.disconnect();
145
212
  }
213
+
146
214
  async handleMessage(msg) {
147
215
  const payload = JSON.parse((await (0, shared_1.tryUnzipAsync)(msg.payload)).toString('utf8'));
148
216
  if ((0, shared_1.notUndefined)(payload.fbpushnotif)) {
@@ -156,6 +224,7 @@ class FbnsClient extends eventemitter3_1.EventEmitter {
156
224
  this.emit('message', payload);
157
225
  }
158
226
  }
227
+
159
228
  async sendPushRegister(token) {
160
229
  const { body } = await this.ig.request.send({
161
230
  url: `/api/v1/push/register/`,
@@ -1,23 +1,35 @@
1
1
  'use strict';
2
- // Instagram version: 412.0.0.35.87
2
+ // Instagram version marker (adjust as you like)
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.mqttotConnectFlow = exports.MQTToTClient = void 0;
5
- // Export the Instagram version so it can be used programmatically
6
5
  exports.INSTAGRAM_VERSION = '420.0.0.42.95';
6
+
7
7
  const shared_1 = require("../shared");
8
8
  const mqttot_connect_request_packet_1 = require("./mqttot.connect.request.packet");
9
- // *** Change: import the external mqtts package instead of a local mqtt-shim ***
9
+ // Use external mqtts package (must be installed in your project)
10
10
  const mqtts_1 = require("mqtts");
11
11
  const errors_1 = require("../errors");
12
12
  const mqttot_connect_response_packet_1 = require("./mqttot.connect.response.packet");
13
13
 
14
14
  /**
15
15
  * MQTToTClient
16
- * - Subclasses the external mqtts.MqttClient implementation.
17
- * - Provides Instagram-specific connect/publish helpers (Thrift payload, debug).
18
- * - Keeps compatibility with options used by the original library.
16
+ * - Subclasses external mqtts.MqttClient
17
+ * - Adds Instagram-specific helpers (connect flow, compressed publish)
18
+ * - Adds keepalive (PING) and robust reconnect with exponential backoff
19
+ *
20
+ * Comments/notes are inline. This file is ready to paste into your project.
19
21
  */
20
22
  class MQTToTClient extends mqtts_1.MqttClient {
23
+ /**
24
+ * @param {Object} options
25
+ * options:
26
+ * - url: broker host
27
+ * - socksOptions: optional proxy options
28
+ * - autoReconnect: boolean
29
+ * - payloadProvider: async function returning connection payload (Thrift blob)
30
+ * - requirePayload: boolean (legacy behavior)
31
+ * - additionalOptions: transport options passthrough
32
+ */
21
33
  constructor(options) {
22
34
  super({
23
35
  autoReconnect: options.autoReconnect,
@@ -42,13 +54,73 @@ class MQTToTClient extends mqtts_1.MqttClient {
42
54
  additionalOptions: options.additionalOptions,
43
55
  }),
44
56
  });
45
- // small debug helper that prefixes messages with the broker url
46
- this.mqttotDebug = (msg, ...args) => (0, shared_1.debugChannel)('mqttot')(`${options.url}: ${msg}`, ...args);
57
+
58
+ // Save options for reconnect attempts
59
+ this._options = options || {};
60
+ // Debug helper prefixed with broker url
61
+ this.mqttotDebug = (msg, ...args) => (0, shared_1.debugChannel)('mqttot')(`${this._options.url}: ${msg}`, ...args);
47
62
  this.connectPayloadProvider = options.payloadProvider;
48
63
  this.mqttotDebug(`Creating client`);
64
+ // Register listeners (errors, disconnect, pingresps, etc.)
49
65
  this.registerListeners();
50
66
  this.requirePayload = options.requirePayload;
67
+
68
+ // Keepalive: periodically send PINGREQ to avoid idle timeouts on Instagram
69
+ // Default interval 10-15 minutes; adjust if you need more/less aggressive keepalive.
70
+ // Use a relatively long interval to avoid being rate-limited, but short enough to stay online.
71
+ this._keepaliveMs = (typeof options.keepaliveMs === 'number') ? options.keepaliveMs : (10 * 60 * 1000); // 10 min default
72
+ this._startKeepalive();
51
73
  }
74
+
75
+ /**
76
+ * Start the periodic keepalive ping. Wrapped so we can restart/clear safely.
77
+ */
78
+ _startKeepalive() {
79
+ try {
80
+ if (this._keepaliveTimer) clearInterval(this._keepaliveTimer);
81
+ this._keepaliveTimer = setInterval(() => {
82
+ try {
83
+ // Defensive: only call ping if function exists
84
+ if (typeof this.ping === 'function') {
85
+ this.mqttotDebug('Sending PINGREQ (keepalive)');
86
+ // ping() may be sync or return a promise; wrap in try/catch
87
+ const res = this.ping();
88
+ if (res && typeof res.then === 'function') {
89
+ res.catch((e) => this.mqttotDebug(`Ping promise rejected: ${e?.message || e}`));
90
+ }
91
+ } else {
92
+ // As a fallback: write a zero-length control packet if library exposes low-level send
93
+ this.mqttotDebug('ping() not available on client - keepalive skipped');
94
+ }
95
+ } catch (e) {
96
+ this.mqttotDebug(`Ping error: ${e?.message || e}`);
97
+ }
98
+ }, this._keepaliveMs);
99
+ } catch (e) {
100
+ this.mqttotDebug(`Keepalive setup error: ${e?.message || e}`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Stop keepalive timer (call on explicit close/disconnect)
106
+ */
107
+ _stopKeepalive() {
108
+ try {
109
+ if (this._keepaliveTimer) {
110
+ clearInterval(this._keepaliveTimer);
111
+ this._keepaliveTimer = null;
112
+ }
113
+ } catch (e) {
114
+ // ignore
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Register event listeners on the underlying mqtt client to handle:
120
+ * - errors / warnings
121
+ * - disconnects -> attempt reconnection with exponential backoff
122
+ * - pingresp events for diagnostics
123
+ */
52
124
  registerListeners() {
53
125
  const printErrorOrWarning = (type) => (e) => {
54
126
  if (typeof e === 'string') {
@@ -58,47 +130,106 @@ class MQTToTClient extends mqtts_1.MqttClient {
58
130
  this.mqttotDebug(`${type}: ${e.message}\n\tStack: ${e.stack}`);
59
131
  }
60
132
  };
61
- // Attach basic diagnostics so errors/warnings are visible via debugChannel
133
+
134
+ // Attach diagnostics
62
135
  this.on('error', printErrorOrWarning('Error'));
63
136
  this.on('warning', printErrorOrWarning('Warning'));
64
- this.on('disconnect', e => this.mqttotDebug(`Disconnected. ${e}`));
137
+
138
+ // Listen to ping responses if the library emits them
139
+ this.on('pingresp', () => {
140
+ this.mqttotDebug('Received PINGRESP (keepalive ok)');
141
+ });
142
+
143
+ // On disconnect: try to reconnect with exponential backoff.
144
+ // This avoids immediate tight reconnect loops and gives the server time.
145
+ this.on('disconnect', async (reason) => {
146
+ try {
147
+ this.mqttotDebug(`Disconnected. Reason: ${reason}`);
148
+ // Stop keepalive while disconnected
149
+ this._stopKeepalive();
150
+
151
+ // If autoReconnect option is false, do not attempt manual reconnect
152
+ if (this._options && this._options.autoReconnect === false) {
153
+ this.mqttotDebug('autoReconnect disabled; will not attempt reconnect.');
154
+ return;
155
+ }
156
+
157
+ // Exponential backoff parameters
158
+ let delay = 2000; // start with 2s
159
+ const maxAttempts = 8; // 2s,4s,8s,... up to ~512s
160
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
161
+ try {
162
+ this.mqttotDebug(`Reconnect attempt #${attempt + 1} (delay ${delay}ms)`);
163
+ // Attempt to reconnect; call this.connect which will fetch new payload as needed
164
+ // Use saved options; this.connect will call connectPayloadProvider internally
165
+ await this.connect(this._options);
166
+ this.mqttotDebug('Reconnected successfully');
167
+ // restart keepalive after reconnect
168
+ this._startKeepalive();
169
+ return;
170
+ } catch (err) {
171
+ this.mqttotDebug(`Reconnect attempt #${attempt + 1} failed: ${err?.message || err}`);
172
+ // wait before next attempt
173
+ await new Promise(r => setTimeout(r, delay));
174
+ // exponential backoff
175
+ delay = Math.min(delay * 2, 5 * 60 * 1000); // cap to 5 minutes
176
+ }
177
+ }
178
+ this.mqttotDebug('Exceeded reconnect attempts; giving up until next disconnect event triggers it again.');
179
+ } catch (e) {
180
+ this.mqttotDebug(`Error in disconnect handler: ${e?.message || e}`);
181
+ }
182
+ });
65
183
  }
184
+
185
+ /**
186
+ * connect override
187
+ * - Waits for connect payload from provider before calling parent connect
188
+ */
66
189
  async connect(options) {
67
190
  // Acquire the payload (Thrift serialized connection blob) before connecting.
68
- this.connectPayload = await this.connectPayloadProvider();
191
+ if (typeof this.connectPayloadProvider === 'function') {
192
+ try {
193
+ this.connectPayload = await this.connectPayloadProvider();
194
+ } catch (e) {
195
+ this.mqttotDebug(`connectPayloadProvider failed: ${e?.message || e}`);
196
+ throw e;
197
+ }
198
+ } else {
199
+ this.mqttotDebug('No connectPayloadProvider provided; proceeding without payload');
200
+ this.connectPayload = null;
201
+ }
202
+ // Call super.connect to establish connection
69
203
  return super.connect(options);
70
204
  }
205
+
206
+ /**
207
+ * Return an Instagram-flavored connect flow function for the mqtts client.
208
+ * If payload is missing but CONNACK indicates success, accept it (robustness).
209
+ */
71
210
  getConnectFlow() {
72
211
  if (!this.connectPayload) {
73
212
  throw new mqtts_1.IllegalStateError('Called getConnectFlow() before calling connect()');
74
213
  }
75
214
  return mqttotConnectFlow(this.connectPayload, this.requirePayload);
76
215
  }
216
+
77
217
  /**
78
- * Compresses the payload and publishes it.
79
- * NOTE: Always publishes with qosLevel: 0 to avoid puback/retry instability.
80
- * Rationale: Instagram/edge MQTT sometimes behaves inconsistently with QoS1 (missing PUBACKs,
81
- * delayed acks, etc.). For stability we force QoS 0 here so the library avoids waiting on ACKs
82
- * that may never arrive and causing reconnect loops.
83
- *
84
- * @param {MqttMessage} message
85
- * @returns {Promise}
218
+ * Compresses payload using shared.compressDeflate and publishes with QoS 0.
219
+ * QoS 0 forced for Instagram edge stability (avoid PUBACK waits causing reconnect loops).
86
220
  */
87
221
  async mqttotPublish(message) {
88
- this.mqttotDebug(`Publishing ${message.payload.byteLength}bytes to topic ${message.topic}`);
222
+ this.mqttotDebug(`Publishing ${message.payload.byteLength || message.payload.length} bytes to topic ${message.topic}`);
223
+ const compressed = await (0, shared_1.compressDeflate)(message.payload);
89
224
  return await this.publish({
90
225
  topic: message.topic,
91
- payload: await (0, shared_1.compressDeflate)(message.payload),
226
+ payload: compressed,
92
227
  qosLevel: 0, // FORCED: QoS 0 for stability on Instagram edge
93
228
  });
94
229
  }
230
+
95
231
  /**
96
- * Special listener for specific topics with transformers
97
- * This helper attaches a transformer that converts raw message payloads into
98
- * structured data before calling the provided handler.
99
- *
100
- * @param {Object} config - { topic, transformer }
101
- * @param {Function} handler - Callback to handle transformed data
232
+ * Helper to listen for a specific topic and run transformer before calling handler.
102
233
  */
103
234
  listen(config, handler) {
104
235
  this.mqttotDebug(`[LISTEN] Setting up listener on topic ${config.topic} with transformer`);
@@ -108,23 +239,36 @@ class MQTToTClient extends mqtts_1.MqttClient {
108
239
  const data = await config.transformer({ payload: msg.payload });
109
240
  handler(data);
110
241
  } catch (e) {
111
- this.mqttotDebug(`Error in transformer for topic ${config.topic}: ${e.message}`);
242
+ this.mqttotDebug(`Error in transformer for topic ${config.topic}: ${e?.message || e}`);
112
243
  this.emit('error', e);
113
244
  }
114
245
  }
115
246
  });
116
247
  }
248
+
249
+ /**
250
+ * Clean shutdown helper: stop keepalive & close
251
+ */
252
+ async gracefulClose() {
253
+ try {
254
+ this._stopKeepalive();
255
+ if (typeof super.close === 'function') {
256
+ // some libs provide close() or end()
257
+ await super.close();
258
+ } else if (typeof super.end === 'function') {
259
+ await super.end();
260
+ }
261
+ } catch (e) {
262
+ this.mqttotDebug(`Error during gracefulClose: ${e?.message || e}`);
263
+ }
264
+ }
117
265
  }
118
266
  exports.MQTToTClient = MQTToTClient;
119
267
 
120
268
  /**
121
- * NOTE: changed acceptance logic for CONNACK payload:
122
- * - Historically the connect flow expected a payload in CONNACK (requirePayload = true),
123
- * but Instagram/edge sometimes replies with an empty payload even when connection is valid.
124
- * - To avoid noisy 'CONNACK: no payload (payloadExpected)' errors and unnecessary disconnect logs,
125
- * we now treat any successful CONNACK (packet.isSuccess) as success regardless of payload.
126
- *
127
- * If you *do* need to enforce payload presence for some reason, revert this block to check requirePayload.
269
+ * mqttotConnectFlow
270
+ * - Returns a flow object that the mqtts client uses to perform CONNECT/CONNACK handshake.
271
+ * - Changed behavior: treat CONNACK success as success even if payload missing (robustness).
128
272
  */
129
273
  function mqttotConnectFlow(payload, requirePayload) {
130
274
  return (success, error) => ({
@@ -138,7 +282,7 @@ function mqttotConnectFlow(payload, requirePayload) {
138
282
  accept: mqtts_1.isConnAck,
139
283
  next: (packet) => {
140
284
  if (packet.isSuccess) {
141
- // *** CHANGED: accept success even if payload is empty ***
285
+ // Accept success even if payload is empty to avoid noisy errors
142
286
  success(packet);
143
287
  }
144
288
  else {