nodejs-insta-private-api-mqtt 1.1.4 → 1.1.5

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 CHANGED
@@ -2,24 +2,15 @@ Dear users,
2
2
  First of all, when handling view-once images or videos, please make sure to save the received media in a folder such as /storage/emulated/0/Pictures/, or depending on your device's path. The photos and videos are saved correctly on your phone, but the library currently has some issues with uploading them back to Instagram. This will be resolved as soon as possible.
3
3
  I post many versions of the project because Instagram changes the protocol almost daily, if you like this project leave a star on github https://github.com/Kunboruto20/nodejs-insta-private-api.git
4
4
 
5
- ⚠️ IMPORTANT NOTICE
6
- This is an important announcement regarding the nodejs-insta-private-api library.
7
- The old repository and NPM package were lost and are now associated with a different NPM account.
8
- Starting from today, this NPM account will be the official and active source where this library will be published and maintained.
9
- Please make sure you are using this package from this account only to receive future updates, fixes, and support.
10
5
 
11
- You can subscribe to any Instagram topic in this library visit the bookstore code, it would help me a lot if you like this project and if it helps you!
12
6
 
13
- If you see clones of nodejs-insta-private-api on npm don't download, it could be malicious code and you may have security issues on your Instagram accounts, always use this project or the nodejs-insta-private-api project but that's on the lost account, always use npm install nodejs-insta-private-api-mqtt in your projects
14
-
15
-
16
- # nodejs-insta-private-api-mqtt
7
+ # nodejs-insta-private-api
17
8
 
18
9
  This project implements a complete and production-ready MQTT protocol client for Instagram's real-time messaging infrastructure. Instagram uses MQTT natively for direct messages, notifications, and real-time presence updates. This library replicates that exact implementation, allowing developers to build high-performance bots and automation tools that communicate with Instagram's backend using the same protocol the official app uses.
19
10
 
20
11
  By leveraging MQTT instead of Instagram's REST API, this library achieves sub-500ms message latency, bidirectional real-time communication, and native support for notifications, presence tracking, and thread management. The implementation is reverse-engineered from Instagram's mobile app protocol and tested extensively for reliability and compatibility.
21
12
 
22
- ## Features ( - iOS + Android Full Support)
13
+ ## Features (v5.61.11 - iOS + Android Full Support)
23
14
 
24
15
  - **NEW: FULL iOS SUPPORT** - iPhone 16/15/14/13/12 + iPad Pro/Air device emulation
25
16
  - **NEW: FULL ANDROID SUPPORT** - Samsung, Huawei, Google Pixel, OnePlus, Xiaomi, OPPO
@@ -75,7 +66,7 @@ Requires **Node.js 18 or higher**.
75
66
 
76
67
  ---
77
68
 
78
- ## NEW: Custom Device Emulation ()
69
+ ## NEW: Custom Device Emulation (v5.60.7)
79
70
 
80
71
  **Default Device:** Samsung Galaxy S25 Ultra (Android 15) - used automatically if you don't set a custom device.
81
72
 
@@ -93,7 +84,7 @@ This feature allows you to **choose which phone model Instagram sees** when your
93
84
  The easiest way to set a custom device is using the built-in presets:
94
85
 
95
86
  ```javascript
96
- const { IgApiClient } = require('nodejs-insta-private-api-mqtt');
87
+ const { IgApiClient } = require('nodejs-insta-private-api');
97
88
 
98
89
  const ig = new IgApiClient();
99
90
 
@@ -1004,7 +995,7 @@ realtime.on('message', async (data) => {
1004
995
  ### Extract Media URLs Without Downloading
1005
996
 
1006
997
  ```javascript
1007
- const { extractMediaUrls } = require('nodejs-insta-private-api-mqtt');
998
+ const { extractMediaUrls } = require('nodejs-insta-private-api');
1008
999
 
1009
1000
  realtime.on('message', async (data) => {
1010
1001
  const msg = data.message;
@@ -84,16 +84,14 @@ exports.FBNS = {
84
84
  PACKAGE: 'com.instagram.android',
85
85
  APP_ID: '567310203415052',
86
86
  HOST_NAME_V6: 'mqtt-mini.facebook.com',
87
- CLIENT_CAPABILITIES: 6143,
88
- ENDPOINT_CAPABILITIES: 255,
87
+ CLIENT_CAPABILITIES: 439,
88
+ ENDPOINT_CAPABILITIES: 128,
89
89
  CLIENT_STACK: 3,
90
90
  PUBLISH_FORMAT: 1,
91
91
  };
92
92
  exports.REALTIME = {
93
93
  HOST_NAME_V6: 'edge-mqtt.facebook.com',
94
94
  };
95
- exports.APP_VERSION = '380.0.0.40.94';
96
- exports.APP_VERSION_CODE = '525123456';
97
95
  // TODO: exclude in release
98
96
  /* eslint @typescript-eslint/no-unused-vars: "off" */
99
97
  exports.PossibleTopics = [
@@ -14,14 +14,7 @@ class Request {
14
14
  timeout: 30000,
15
15
  headers: {
16
16
  'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
17
- },
18
- // Modern TLS settings to bypass basic fingerprinting (Strict Instagram Fizz mimicry)
19
- httpsAgent: new (require('https').Agent)({
20
- ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305',
21
- honorCipherOrder: true,
22
- minVersion: 'TLSv1.3', // Instagram 2026 enforces TLS 1.3
23
- maxVersion: 'TLSv1.3'
24
- })
17
+ }
25
18
  });
26
19
  }
27
20
 
@@ -208,9 +201,6 @@ class Request {
208
201
  'X-IG-Android-ID': this.client.state.deviceId,
209
202
  'Accept-Language': this.client.state.language.replace('_', '-'),
210
203
  'X-FB-HTTP-Engine': 'Liger',
211
- 'X-FB-Client-IP': 'True',
212
- 'X-FB-Server-Cluster': 'True',
213
- 'X-IG-Nav-Chain': 'MainFeed:FeedTimelineFragment:1',
214
204
  'Authorization': this.client.state.authorization,
215
205
  'Host': 'i.instagram.com',
216
206
  'Accept-Encoding': 'gzip, deflate',
@@ -21,10 +21,8 @@ class State {
21
21
  this.language = 'en_US';
22
22
  this.timezoneOffset = String(new Date().getTimezoneOffset() * -60);
23
23
  this.radioType = 'wifi-none';
24
- this.capabilitiesHeader = '3brTvw==';
24
+ this.capabilitiesHeader = '3brTv10=';
25
25
  this.connectionTypeHeader = 'WIFI';
26
- this.clientCapabilities = 6143;
27
- this.endpointCapabilities = 255;
28
26
  this.isLayoutRTL = false;
29
27
  this.adsOptOut = false;
30
28
  this.thumbnailCacheBustingValue = 1000;
@@ -37,12 +35,10 @@ class State {
37
35
 
38
36
  // ===== PLATFORM SUPPORT (iOS + Android) =====
39
37
  this.platform = 'android'; // 'android' or 'ios'
40
- this.androidVersion = '15';
41
- this.androidApiLevel = '35';
42
38
  this.iosVersion = '18.1';
43
- this.iosAppVersion = '411.0.0';
44
- this.iosAppVersionCode = '700123456';
45
- this.iosDeviceModel = 'iPhone17,1'; // iPhone 16 Pro Max
39
+ this.iosAppVersion = '347.0.0.36.89';
40
+ this.iosAppVersionCode = '618023787';
41
+ this.iosDeviceModel = 'iPhone16,2'; // iPhone 15 Pro Max
46
42
  this.iosDeviceName = 'iPhone';
47
43
  this.iosBundleId = 'com.burbn.instagram';
48
44
 
@@ -23,26 +23,12 @@ class MQTToTClient extends mqtts_1.MqttClient {
23
23
  host: options.url,
24
24
  port: 443,
25
25
  proxyOptions: options.socksOptions,
26
- additionalOptions: {
27
- ...options.additionalOptions,
28
- ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
29
- minVersion: 'TLSv1.3',
30
- maxVersion: 'TLSv1.3',
31
- honorCipherOrder: true,
32
- ALPNProtocols: ['h2', 'mqtt']
33
- },
26
+ additionalOptions: options.additionalOptions,
34
27
  })
35
28
  : new mqtts_1.TlsTransport({
36
29
  host: options.url,
37
30
  port: 443,
38
- additionalOptions: {
39
- ...options.additionalOptions,
40
- ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
41
- minVersion: 'TLSv1.3',
42
- maxVersion: 'TLSv1.3',
43
- honorCipherOrder: true,
44
- ALPNProtocols: ['h2', 'mqtt']
45
- },
31
+ additionalOptions: options.additionalOptions,
46
32
  }),
47
33
  });
48
34
  this.mqttotDebug = (msg, ...args) => (0, shared_1.debugChannel)('mqttot')(`${options.url}: ${msg}`, ...args);
@@ -114,7 +100,7 @@ function mqttotConnectFlow(payload, requirePayload) {
114
100
  type: mqtts_1.PacketType.Connect,
115
101
  options: {
116
102
  payload,
117
- keepAlive: 120,
103
+ keepAlive: 60,
118
104
  },
119
105
  }),
120
106
  accept: mqtts_1.isConnAck,
@@ -7,20 +7,7 @@ class MQTToTConnection {
7
7
  this.fbnsConnectionData = connectionData;
8
8
  }
9
9
  toThrift() {
10
- // Fix: Ensure clientInfo exists and populate it
11
- if (!this.fbnsConnectionData.clientInfo) {
12
- this.fbnsConnectionData.clientInfo = {};
13
- }
14
- this.fbnsConnectionData.clientInfo.clientCapabilities = this.fbnsConnectionData.clientInfo.clientCapabilities || this.fbnsConnectionData.clientCapabilities || 6143;
15
- this.fbnsConnectionData.clientInfo.endpointCapabilities = this.fbnsConnectionData.clientInfo.endpointCapabilities || this.fbnsConnectionData.endpointCapabilities || 255;
16
-
17
- // Fix: Remove root-level properties that don't have descriptors in the root thriftConfig
18
- // to avoid "Descriptor for ... not found" errors
19
- const dataToWrite = Object.assign({}, this.fbnsConnectionData);
20
- delete dataToWrite.clientCapabilities;
21
- delete dataToWrite.endpointCapabilities;
22
-
23
- return (0, thrift_1.thriftWriteFromObject)(dataToWrite, MQTToTConnection.thriftConfig);
10
+ return (0, thrift_1.thriftWriteFromObject)(this.fbnsConnectionData, MQTToTConnection.thriftConfig);
24
11
  }
25
12
  toString() {
26
13
  return this.toThrift().toString();
@@ -17,6 +17,9 @@ const error_handler_1 = require("./features/error-handler");
17
17
  const gap_handler_1 = require("./features/gap-handler");
18
18
  const enhanced_direct_commands_1 = require("./commands/enhanced.direct.commands");
19
19
  const presence_typing_mixin_1 = require("./mixins/presence-typing.mixin");
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
20
23
  class RealtimeClient extends eventemitter3_1.EventEmitter {
21
24
  get mqtt() {
22
25
  return this._mqtt;
@@ -35,7 +38,20 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
35
38
  this.emitWarning = (e) => this.emit('warning', e);
36
39
  this.ig = ig;
37
40
  this.threads = new Map();
38
-
41
+
42
+ // === New: observability & metrics ===
43
+ this.metrics = {
44
+ reconnectCount: 0,
45
+ messagesReceived: 0,
46
+ parseErrors: 0,
47
+ lastConnectTime: null,
48
+ backoffAttempts: 0,
49
+ };
50
+
51
+ // Session state persistence file (changeable)
52
+ this._sessionStateFile = path.join('.', '.mqtt_session_state.json');
53
+ this._sessionState = this._loadSessionState();
54
+
39
55
  this.irisHandshake = new iris_handshake_1.IrisHandshake(this);
40
56
  this.skywalkerProtocol = new skywalker_protocol_1.SkywalkerProtocol(this);
41
57
  this.presenceManager = new presence_manager_1.PresenceManager(this);
@@ -43,21 +59,52 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
43
59
  this.errorHandler = new error_handler_1.ErrorHandler(this);
44
60
  this.gapHandler = new gap_handler_1.GapHandler(this);
45
61
  this.directCommands = new enhanced_direct_commands_1.EnhancedDirectCommands(this);
46
-
62
+
47
63
  this.realtimeDebug(`Applying mixins: ${mixins.map(m => m.name).join(', ')}`);
48
64
  (0, mixins_1.applyMixins)(mixins, this, this.ig);
65
+
66
+ // Periodic metrics logging (only if not disabled)
67
+ this._metricsInterval = setInterval(() => {
68
+ try {
69
+ if (this.metrics) {
70
+ this.realtimeDebug('METRICS', JSON.stringify(this.metrics));
71
+ }
72
+ } catch (e) { }
73
+ }, 60 * 1000); // every 60s
74
+ }
75
+
76
+ // -------------------- Session state persistence --------------------
77
+ _loadSessionState() {
78
+ try {
79
+ if (fs.existsSync(this._sessionStateFile)) {
80
+ const raw = fs.readFileSync(this._sessionStateFile, 'utf8');
81
+ return JSON.parse(raw || '{}');
82
+ }
83
+ } catch (e) {
84
+ // ignore
85
+ }
86
+ return {};
87
+ }
88
+ _saveSessionState() {
89
+ try {
90
+ fs.writeFileSync(this._sessionStateFile, JSON.stringify(this._sessionState || {}, null, 2), 'utf8');
91
+ } catch (e) {
92
+ this.realtimeDebug('Failed to save session state:', e?.message || e);
93
+ }
49
94
  }
50
95
 
96
+ // ------------------------------------------------------------------
97
+
51
98
  /**
52
99
  * Start Real-Time Listener with Auto-Inbox Fetch + MQTT
53
100
  */
54
101
  async startRealTimeListener(options = {}) {
55
102
  try {
56
103
  console.log('[REALTIME] Starting Real-Time Listener...');
57
-
104
+
58
105
  console.log('[REALTIME] Fetching inbox (IRIS data)...');
59
106
  const inboxData = await this.ig.direct.getInbox();
60
-
107
+
61
108
  console.log('[REALTIME] Connecting to MQTT with IRIS subscription...');
62
109
  await this.connect({
63
110
  graphQlSubs: [
@@ -70,15 +117,15 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
70
117
  ],
71
118
  irisData: inboxData
72
119
  });
73
-
120
+
74
121
  console.log('[REALTIME] MQTT Connected with IRIS');
75
122
  console.log('----------------------------------------');
76
123
  console.log('[REALTIME] Real-Time Listener ACTIVE');
77
124
  console.log('[REALTIME] Waiting for messages...');
78
125
  console.log('----------------------------------------');
79
-
126
+
80
127
  this._setupMessageHandlers();
81
-
128
+
82
129
  return { success: true };
83
130
  } catch (error) {
84
131
  console.error('[REALTIME] Failed:', error.message);
@@ -90,46 +137,92 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
90
137
  * Setup automatic message handlers
91
138
  */
92
139
  _setupMessageHandlers() {
140
+ // handle raw mqtt 'message' events (parsed by this._parseMessage)
93
141
  this.on('message', (data) => {
94
- const msg = this._parseMessage(data);
95
- if (msg) {
96
- this.emit('message_live', msg);
142
+ const parsed = this._parseMessage(data);
143
+ // emit raw version always for debugging/compatibility
144
+ if (parsed && parsed._rawMessage) {
145
+ this.emit('message_raw', parsed._rawMessage);
146
+ }
147
+ // normalize and emit only if it's a real message
148
+ const normalized = this._normalizeToYowsupLike(parsed);
149
+ if (normalized) {
150
+ this.metrics.messagesReceived++;
151
+ // emit cleaned payload as the primary "live" message (Yowsup-like)
152
+ this.emit('message_live', normalized);
153
+ } else {
154
+ // not a real message -> no message_live; keep raw available
97
155
  }
98
156
  });
99
157
 
158
+ // handle iris events (graph/iris)
100
159
  this.on('iris', (data) => {
101
- const msg = this._parseIrisMessage(data);
102
- if (msg) {
103
- this.emit('message_live', msg);
160
+ const parsed = this._parseIrisMessage(data);
161
+ if (parsed && parsed._rawIris) {
162
+ this.emit('message_raw', parsed._rawIris);
163
+ }
164
+ const normalized = this._normalizeToYowsupLike(parsed);
165
+ if (normalized) {
166
+ this.metrics.messagesReceived++;
167
+ this.emit('message_live', normalized);
168
+ } else {
169
+ // not a message event
104
170
  }
105
171
  });
106
172
  }
107
173
 
108
174
  /**
109
175
  * Parse direct message
176
+ * Returns an object that always contains the original raw as _rawMessage when available
110
177
  */
111
178
  _parseMessage(data) {
112
179
  try {
113
180
  const msg = data.message;
114
181
  if (!msg) return null;
115
-
182
+
116
183
  if (data.parsed) {
117
- return data.parsed;
184
+ // Keep mqttString/mqtt for backwards compatibility in parsed cases
185
+ const base = Object.assign({}, data.parsed);
186
+ base.mqttString = `Mqtt client: [${base.username || (base.userId ? 'user_' + base.userId : 'unknown')}] ${base.text || ''}`;
187
+ base.mqtt = {
188
+ from: base.username || base.userId,
189
+ body: base.text || '',
190
+ timestamp: base.timestamp || Date.now()
191
+ };
192
+ base.status = 'received'; // changed from 'good'
193
+ base._rawMessage = msg;
194
+ return base;
118
195
  }
119
-
196
+
120
197
  const threadInfo = this.threads.get(msg.thread_id);
121
- return {
198
+ const username = msg.username || msg.from_username || `user_${msg.user_id || 'unknown'}`;
199
+ const text = msg.text || msg.body || '';
200
+
201
+ // Provide mqtt-like representations
202
+ const mqttString = `Mqtt client: [${username}] ${text}`;
203
+ const mqtt = {
204
+ from: username,
205
+ body: text,
206
+ timestamp: msg.timestamp || Date.now()
207
+ };
208
+
209
+ const out = {
122
210
  id: msg.item_id || msg.id,
123
211
  userId: msg.user_id || msg.from_user_id,
124
- username: msg.username || msg.from_username || `user_${msg.user_id || 'unknown'}`,
125
- text: msg.text || msg.body || '',
212
+ username,
213
+ text,
126
214
  itemType: msg.item_type || 'text',
127
215
  thread: threadInfo?.title || `Thread ${msg.thread_id}`,
128
216
  thread_id: msg.thread_id,
129
217
  timestamp: msg.timestamp,
130
218
  isGroup: threadInfo?.isGroup,
131
- status: 'good'
219
+ status: 'received', // replaced 'good'
220
+ mqttString,
221
+ mqtt,
222
+ _rawMessage: msg
132
223
  };
224
+
225
+ return out;
133
226
  } catch (e) {
134
227
  return null;
135
228
  }
@@ -141,22 +234,125 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
141
234
  _parseIrisMessage(data) {
142
235
  try {
143
236
  if (data.event !== 'message_create' && !data.type?.includes('message')) {
144
- return null;
237
+ // still return raw for potential debug, but normalized filter will drop it
238
+ return { _rawIris: data };
145
239
  }
146
-
147
- return {
240
+
241
+ const username = data.username || data.from_username || `user_${data.user_id || 'unknown'}`;
242
+ const text = data.text || '';
243
+
244
+ const mqttString = `Mqtt client: [${username}] ${text}`;
245
+ const mqtt = {
246
+ from: username,
247
+ body: text,
248
+ timestamp: data.timestamp || Date.now()
249
+ };
250
+
251
+ const out = {
148
252
  id: data.item_id || data.id,
149
253
  userId: data.user_id || data.from_user_id,
150
- username: data.username || data.from_username || `user_${data.user_id || 'unknown'}`,
151
- text: data.text || '',
254
+ username,
255
+ text,
152
256
  itemType: data.item_type || 'text',
153
257
  thread_id: data.thread_id,
154
258
  timestamp: data.timestamp,
155
- status: 'good'
259
+ status: 'received', // replaced 'good'
260
+ mqttString,
261
+ mqtt,
262
+ _rawIris: data
156
263
  };
264
+
265
+ return out;
157
266
  } catch (e) {
267
+ return { _rawIris: data };
268
+ }
269
+ }
270
+
271
+ /**
272
+ * NORMALIZE to Yowsup-like full payload (but branded "Mqtt client")
273
+ * Returns normalized object or null if the parsed input is NOT a real message
274
+ *
275
+ * Normalized schema (Yowsup-like full payload):
276
+ * {
277
+ * client: 'Mqtt client',
278
+ * from: 'username',
279
+ * fromId: 'numeric id',
280
+ * body: 'text or description',
281
+ * timestamp: 1700000000000,
282
+ * type: 'text'|'image'|'video'|'voice'|'sticker'|'unknown',
283
+ * thread_id: '...',
284
+ * id: 'message id',
285
+ * raw: { original raw object }
286
+ * }
287
+ */
288
+ _normalizeToYowsupLike(parsed) {
289
+ if (!parsed) return null;
290
+
291
+ // get raw object for detection
292
+ const raw = parsed._rawMessage || parsed._rawIris || parsed;
293
+
294
+ // Heuristics to decide if this is a "real message" (text/media/voice)
295
+ // Accept if:
296
+ // - parsed.text or parsed.body present and non-empty
297
+ // - or raw contains visual_media/media fields
298
+ // - or itemType indicates media
299
+ const hasText = Boolean(parsed.text && String(parsed.text).trim().length > 0);
300
+ const hasBodyField = Boolean(parsed.body && String(parsed.body).trim().length > 0);
301
+ const hasMediaField = Boolean(
302
+ (raw && (raw.visual_media || raw.media || raw.media_share || raw.items || raw.has_media)) ||
303
+ (parsed.itemType && String(parsed.itemType).toLowerCase() !== 'text')
304
+ );
305
+
306
+ if (!hasText && !hasBodyField && !hasMediaField) {
307
+ // Not a message that Yowsup would surface — drop from message_live
158
308
  return null;
159
309
  }
310
+
311
+ // Determine type
312
+ let type = 'text';
313
+ try {
314
+ if (hasMediaField) {
315
+ // try to deduce image/video/voice from raw fields if possible
316
+ const rl = raw || {};
317
+ if (rl.item_type && String(rl.item_type).toLowerCase().includes('video')) type = 'video';
318
+ else if (rl.item_type && String(rl.item_type).toLowerCase().includes('voice')) type = 'voice';
319
+ else if (rl.media && Array.isArray(rl.media)) {
320
+ // naive check for media types in array
321
+ const m0 = rl.media[0] || {};
322
+ if (m0.media_type === 2 || String(m0.media_type) === '2') type = 'video';
323
+ else type = 'image';
324
+ } else if (rl.visual_media) {
325
+ type = 'image';
326
+ } else {
327
+ type = 'unknown';
328
+ }
329
+ } else {
330
+ type = 'text';
331
+ }
332
+ } catch (e) {
333
+ type = 'unknown';
334
+ }
335
+
336
+ // body selection
337
+ const body = hasText ? String(parsed.text).trim() : (hasBodyField ? String(parsed.body).trim() : (type !== 'text' ? `[${type.toUpperCase()}]` : ''));
338
+
339
+ // timestamp fallback
340
+ const ts = parsed.timestamp ? Number(parsed.timestamp) : (Date.now());
341
+
342
+ // Build normalized object (Yowsup-like full payload)
343
+ const normalized = {
344
+ client: 'Mqtt client',
345
+ from: parsed.username || parsed.from || (parsed.userId ? `user_${parsed.userId}` : 'unknown'),
346
+ fromId: String(parsed.userId || (raw && (raw.from_user_id || raw.user_id)) || 'unknown'),
347
+ body: body,
348
+ timestamp: ts,
349
+ type,
350
+ thread_id: parsed.thread_id || parsed.thread || (raw && (raw.thread_id || raw.thread)) || null,
351
+ id: parsed.id || (raw && (raw.item_id || raw.id)) || null,
352
+ raw: raw
353
+ };
354
+
355
+ return normalized;
160
356
  }
161
357
 
162
358
  setInitOptions(initOptions) {
@@ -230,6 +426,11 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
230
426
  this.realtimeDebug(`SessionID generated (fallback): ${sessionid}`);
231
427
  }
232
428
  const password = `sessionid=${sessionid}`;
429
+
430
+ // Use persisted clientMqttSessionId if present (session continuity)
431
+ const persistedClientMqttSessionId = this._sessionState?.clientMqttSessionId ? BigInt(this._sessionState.clientMqttSessionId) : null;
432
+ const clientMqttSessionIdValue = persistedClientMqttSessionId ?? (BigInt(Date.now()) & BigInt(0xffffffff));
433
+
233
434
  this.connection = new mqttot_1.MQTToTConnection({
234
435
  clientIdentifier: deviceId.substring(0, 20),
235
436
  clientInfo: {
@@ -244,7 +445,7 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
244
445
  isInitiallyForeground: true,
245
446
  networkType: 1,
246
447
  networkSubtype: 0,
247
- clientMqttSessionId: BigInt(Date.now()) & BigInt(0xffffffff),
448
+ clientMqttSessionId: clientMqttSessionIdValue,
248
449
  subscribeTopics: [88, 135, 149, 150, 133, 146],
249
450
  clientType: 'cookie_auth',
250
451
  appId: BigInt(567067343352427),
@@ -266,50 +467,123 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
266
467
  'Accept-Language': this.ig.state.language.replace('_', '-'),
267
468
  },
268
469
  });
470
+
471
+ // update persisted state with chosen session id
472
+ try {
473
+ this._sessionState = this._sessionState || {};
474
+ this._sessionState.clientMqttSessionId = String(clientMqttSessionIdValue);
475
+ this._saveSessionState();
476
+ } catch (e) {
477
+ // ignore
478
+ }
269
479
  }
270
480
  async connect(options) {
271
481
  this.setInitOptions(options);
272
482
  this.constructConnection();
273
483
  const { MQTToTClient } = require("../mqttot");
274
484
  const { compressDeflate } = require("../shared");
275
-
276
- this._mqtt = new MQTToTClient({
277
- url: 'edge-mqtt.facebook.com',
278
- payloadProvider: async () => {
279
- return await compressDeflate(this.connection.toThrift());
280
- },
281
- autoReconnect: true,
282
- requirePayload: true,
283
- });
284
-
285
- await this._mqtt.connect();
286
-
485
+
486
+ // ----------- Enhanced connect with exponential backoff & cooldown -----------
487
+ const maxAttempts = (this.initOptions?.maxConnectAttempts) || 5;
488
+ let attempt = 0;
489
+ let lastError = null;
490
+ while (attempt < maxAttempts) {
491
+ attempt++;
492
+ try {
493
+ this.realtimeDebug(`Attempting MQTT connect (attempt ${attempt}/${maxAttempts})`);
494
+ this._mqtt = new MQTToTClient({
495
+ url: 'edge-mqtt.facebook.com',
496
+ payloadProvider: async () => {
497
+ return await compressDeflate(this.connection.toThrift());
498
+ },
499
+ autoReconnect: true,
500
+ requirePayload: true,
501
+ // pass through any socks/proxy options if provided
502
+ socksOptions: this.initOptions?.socksOptions,
503
+ additionalOptions: this.initOptions?.additionalOptions
504
+ });
505
+
506
+ await this._mqtt.connect();
507
+
508
+ // mark metrics
509
+ this.metrics.reconnectCount++;
510
+ this.metrics.lastConnectTime = Date.now();
511
+ this.metrics.backoffAttempts += (attempt - 1);
512
+
513
+ // If we reach here, connected successfully
514
+ break;
515
+ } catch (err) {
516
+ lastError = err;
517
+ this.realtimeDebug(`MQTT connect attempt ${attempt} failed: ${err?.message || err}`);
518
+
519
+ // exponential backoff with jitter
520
+ const base = 1000; // 1s
521
+ const delayMs = Math.min(30000, base * Math.pow(2, attempt - 1)) + Math.floor(Math.random() * 250);
522
+ this.realtimeDebug(`Waiting ${delayMs}ms before next connect attempt...`);
523
+ await (0, shared_1.delay)(delayMs);
524
+
525
+ // cooldown behavior: if too many attempts, wait longer
526
+ if (attempt >= maxAttempts) {
527
+ this.realtimeDebug('Max connect attempts reached, giving up for now.');
528
+ }
529
+ }
530
+ }
531
+ if (!this._mqtt) {
532
+ // fail with the last error
533
+ throw lastError || new Error('Failed to create MQTT client');
534
+ }
535
+ // ----------------------------------------------------------------------------
536
+
287
537
  this.commands = new commands_1.Commands(this._mqtt);
288
-
538
+
289
539
  this.emit('connected');
290
-
540
+
541
+ // Enhanced message handling: try tolerant parse, log unknown fields, fallback raw
291
542
  this._mqtt.on('message', async (msg) => {
292
543
  const topicMap = this.mqtt?.topicMap;
293
544
  const topic = topicMap?.get(msg.topic);
294
-
545
+
295
546
  if (topic && topic.parser && !topic.noParse) {
296
547
  try {
297
548
  const unzipped = await (0, shared_1.tryUnzipAsync)(msg.payload);
298
- const parsedMessages = topic.parser.parseMessage(topic, unzipped);
549
+ let parsedMessages;
550
+ try {
551
+ parsedMessages = topic.parser.parseMessage(topic, unzipped);
552
+ } catch (parseErr) {
553
+ // Tolerant parsing: log and attempt safe fallback
554
+ this.metrics.parseErrors++;
555
+ this.realtimeDebug('Parse error (topic parser) - attempting tolerant fallback:', parseErr?.message || parseErr);
556
+
557
+ // Attempt to decode as JSON if possible (some thrift->json conversions may be present in tooling)
558
+ try {
559
+ const s = unzipped.toString('utf8');
560
+ parsedMessages = JSON.parse(s);
561
+ this.realtimeDebug('Fallback parsed as JSON.');
562
+ } catch (jsonErr) {
563
+ // Last resort: emit raw payload (preserve for debugging)
564
+ this.realtimeDebug('Fallback JSON parse failed; emitting raw payload.');
565
+ this.emit('receiveRaw', { topic: msg.topic, payload: unzipped, original: msg });
566
+ return;
567
+ }
568
+ }
569
+
299
570
  this.emit('receive', topic, Array.isArray(parsedMessages) ? parsedMessages : [parsedMessages]);
300
- } catch(e) {
301
- // Silent parse error
571
+ } catch (e) {
572
+ // Silent parse error but count & log minimal info
573
+ this.metrics.parseErrors++;
574
+ this.realtimeDebug('Silent parse/unzip error in message handler:', e?.message || e);
302
575
  }
303
576
  } else {
304
577
  try {
305
578
  await (0, shared_1.tryUnzipAsync)(msg.payload);
306
579
  this.emit('receiveRaw', msg);
307
- } catch(e) {
580
+ } catch (e) {
308
581
  // Silent decompress error
309
582
  }
310
583
  }
311
584
  });
312
585
  this._mqtt.on('error', this.emitError);
586
+
313
587
  await (0, shared_1.delay)(100);
314
588
  if (this.initOptions.graphQlSubs && this.initOptions.graphQlSubs.length > 0) {
315
589
  await this.graphQlSubscribe(this.initOptions.graphQlSubs);
@@ -340,12 +614,23 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
340
614
  url: '/api/v1/direct_v2/threads/get_most_recent_message/',
341
615
  method: 'POST',
342
616
  });
343
- } catch(e) {
617
+ } catch (e) {
344
618
  // Silent force fetch error
345
619
  }
346
620
  } catch (error) {
347
621
  // Silent inbox fetch error - MQTT still listening
348
622
  }
623
+
624
+ // Persist last used clientMqttSessionId and optionally last seq_id if present in initOptions
625
+ try {
626
+ const clientMqttSessionId = this.connection?.toThrift()?.clientInfo?.clientMqttSessionId || this._sessionState?.clientMqttSessionId;
627
+ if (clientMqttSessionId) {
628
+ this._sessionState = this._sessionState || {};
629
+ this._sessionState.clientMqttSessionId = String(clientMqttSessionId);
630
+ this._saveSessionState();
631
+ }
632
+ } catch (e) { /* ignore */ }
633
+
349
634
  this._setupMessageHandlers();
350
635
  }
351
636
  /**
@@ -361,7 +646,7 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
361
646
  console.log('[RealtimeClient] Connecting from saved session...');
362
647
 
363
648
  const savedOptions = authStateHelper.getMqttConnectOptions?.();
364
-
649
+
365
650
  const connectOptions = {
366
651
  graphQlSubs: options.graphQlSubs || savedOptions?.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
367
652
  skywalkerSubs: options.skywalkerSubs || savedOptions?.skywalkerSubs || ['presence_subscribe', 'typing_subscribe'],
@@ -375,8 +660,12 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
375
660
  hasIrisData: !!connectOptions.irisData
376
661
  });
377
662
 
663
+ // merge any persisted mqtt connect options into initOptions for continuity
664
+ this.initOptions = { ...(this.initOptions || {}), ...(savedOptions || {}) };
665
+
378
666
  await this.connect(connectOptions);
379
667
 
668
+ // Save session state into auth helper if supported
380
669
  if (authStateHelper.saveMqttSession) {
381
670
  try {
382
671
  await authStateHelper.saveMqttSession(this);
@@ -386,6 +675,11 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
386
675
  }
387
676
  }
388
677
 
678
+ // Also persist locally
679
+ try {
680
+ if (this._sessionState) this._saveSessionState();
681
+ } catch (e) { }
682
+
389
683
  return this;
390
684
  }
391
685
  /**
@@ -395,13 +689,21 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
395
689
  async saveSession(authStateHelper) {
396
690
  if (!authStateHelper || !authStateHelper.saveMqttSession) {
397
691
  console.warn('[RealtimeClient] No authStateHelper provided');
692
+ // still save local state
693
+ try { this._saveSessionState(); } catch (e) { }
398
694
  return false;
399
695
  }
400
696
  await authStateHelper.saveMqttSession(this);
697
+ // also persist locally
698
+ try { this._saveSessionState(); } catch (e) { }
401
699
  return true;
402
700
  }
403
701
  disconnect() {
404
702
  this.safeDisconnect = true;
703
+ // clear metrics interval when shutting down gracefully
704
+ try {
705
+ if (this._metricsInterval) clearInterval(this._metricsInterval);
706
+ } catch (e) { }
405
707
  return this.mqtt?.disconnect() ?? Promise.resolve();
406
708
  }
407
709
  graphQlSubscribe(sub) {
@@ -435,6 +737,13 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
435
737
  throw new mqtts_1.IllegalStateError('connect() must be called before irisSubscribe()');
436
738
  }
437
739
  this.realtimeDebug(`Iris Sub to: seqId: ${seq_id}, snapshot: ${snapshot_at_ms}`);
740
+ // persist last seq_id for continuity
741
+ try {
742
+ this._sessionState = this._sessionState || {};
743
+ if (seq_id) this._sessionState.last_seq_id = seq_id;
744
+ if (snapshot_at_ms) this._sessionState.last_snapshot_at_ms = snapshot_at_ms;
745
+ this._saveSessionState();
746
+ } catch (e) { /* ignore */ }
438
747
  return this.commands.updateSubscriptions({
439
748
  topic: constants_1.Topics.IRIS_SUB,
440
749
  data: {
@@ -444,6 +753,68 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
444
753
  },
445
754
  });
446
755
  }
756
+
757
+ // ---------------- Dynamic subscription helpers ----------------
758
+ async addGraphQlSub(sub) {
759
+ try {
760
+ const current = this.initOptions.graphQlSubs || [];
761
+ if (!current.includes(sub)) {
762
+ current.push(sub);
763
+ this.initOptions.graphQlSubs = current;
764
+ if (this.commands) {
765
+ await this.graphQlSubscribe(current);
766
+ }
767
+ }
768
+ return true;
769
+ } catch (e) {
770
+ this.realtimeDebug('addGraphQlSub error:', e?.message || e);
771
+ return false;
772
+ }
773
+ }
774
+ async removeGraphQlSub(sub) {
775
+ try {
776
+ const current = (this.initOptions.graphQlSubs || []).filter(s => s !== sub);
777
+ this.initOptions.graphQlSubs = current;
778
+ if (this.commands) {
779
+ await this.graphQlSubscribe(current);
780
+ }
781
+ return true;
782
+ } catch (e) {
783
+ this.realtimeDebug('removeGraphQlSub error:', e?.message || e);
784
+ return false;
785
+ }
786
+ }
787
+ async addSkywalkerSub(sub) {
788
+ try {
789
+ const current = this.initOptions.skywalkerSubs || [];
790
+ if (!current.includes(sub)) {
791
+ current.push(sub);
792
+ this.initOptions.skywalkerSubs = current;
793
+ if (this.commands) {
794
+ await this.skywalkerSubscribe(current);
795
+ }
796
+ }
797
+ return true;
798
+ } catch (e) {
799
+ this.realtimeDebug('addSkywalkerSub error:', e?.message || e);
800
+ return false;
801
+ }
802
+ }
803
+ async removeSkywalkerSub(sub) {
804
+ try {
805
+ const current = (this.initOptions.skywalkerSubs || []).filter(s => s !== sub);
806
+ this.initOptions.skywalkerSubs = current;
807
+ if (this.commands) {
808
+ await this.skywalkerSubscribe(current);
809
+ }
810
+ return true;
811
+ } catch (e) {
812
+ this.realtimeDebug('removeSkywalkerSub error:', e?.message || e);
813
+ return false;
814
+ }
815
+ }
816
+ // ----------------------------------------------------------------
817
+
447
818
  }
448
819
  exports.RealtimeClient = RealtimeClient;
449
820
  //# sourceMappingURL=realtime.client.js.map
@@ -21,10 +21,10 @@ function createFbnsUserAgent(ig) {
21
21
  FBBD: 'Android',
22
22
  FBPN: 'com.instagram.android',
23
23
  FBDV: deviceName.trim(),
24
- FBSV: ig.state.androidVersion,
24
+ FBSV: androidVersion.split('/')[1],
25
25
  FBLR: '0',
26
26
  FBBK: '1',
27
- FBCA: 'arm64-v8a:armeabi-v7a:armeabi',
27
+ FBCA: 'x86:armeabi-v7a',
28
28
  };
29
29
  return `[${Object.entries(params)
30
30
  .map(p => p.join('/'))
@@ -32,7 +32,7 @@ function createFbnsUserAgent(ig) {
32
32
  }
33
33
  exports.createFbnsUserAgent = createFbnsUserAgent;
34
34
  function compressDeflate(data) {
35
- return deflatePromise(data, { level: 6, windowBits: -15 });
35
+ return deflatePromise(data, { level: 9 });
36
36
  }
37
37
  exports.compressDeflate = compressDeflate;
38
38
  function unzipAsync(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-insta-private-api-mqtt",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Complete Instagram MQTT protocol with FULL iOS + Android support. 33 device presets (21 iOS + 12 Android). iPhone 16/15/14/13/12, iPad Pro, Samsung, Pixel, Huawei. Real-time DM messaging, view-once media extraction, sub-500ms latency.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {