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

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,15 +2,24 @@ 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.
5
10
 
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!
6
12
 
7
- # nodejs-insta-private-api
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
8
17
 
9
18
  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.
10
19
 
11
20
  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.
12
21
 
13
- ## Features (v5.61.11 - iOS + Android Full Support)
22
+ ## Features ( - iOS + Android Full Support)
14
23
 
15
24
  - **NEW: FULL iOS SUPPORT** - iPhone 16/15/14/13/12 + iPad Pro/Air device emulation
16
25
  - **NEW: FULL ANDROID SUPPORT** - Samsung, Huawei, Google Pixel, OnePlus, Xiaomi, OPPO
@@ -66,7 +75,7 @@ Requires **Node.js 18 or higher**.
66
75
 
67
76
  ---
68
77
 
69
- ## NEW: Custom Device Emulation (v5.60.7)
78
+ ## NEW: Custom Device Emulation ()
70
79
 
71
80
  **Default Device:** Samsung Galaxy S25 Ultra (Android 15) - used automatically if you don't set a custom device.
72
81
 
@@ -84,7 +93,7 @@ This feature allows you to **choose which phone model Instagram sees** when your
84
93
  The easiest way to set a custom device is using the built-in presets:
85
94
 
86
95
  ```javascript
87
- const { IgApiClient } = require('nodejs-insta-private-api');
96
+ const { IgApiClient } = require('nodejs-insta-private-api-mqtt');
88
97
 
89
98
  const ig = new IgApiClient();
90
99
 
@@ -995,7 +1004,7 @@ realtime.on('message', async (data) => {
995
1004
  ### Extract Media URLs Without Downloading
996
1005
 
997
1006
  ```javascript
998
- const { extractMediaUrls } = require('nodejs-insta-private-api');
1007
+ const { extractMediaUrls } = require('nodejs-insta-private-api-mqtt');
999
1008
 
1000
1009
  realtime.on('message', async (data) => {
1001
1010
  const msg = data.message;
@@ -13,7 +13,11 @@ class Request {
13
13
  baseURL: 'https://i.instagram.com/',
14
14
  timeout: 30000,
15
15
  headers: {
16
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
16
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
17
+ 'Accept-Encoding': 'gzip, deflate',
18
+ 'Connection': 'keep-alive',
19
+ 'Accept': '*/*',
20
+ 'Accept-Language': 'en-US'
17
21
  }
18
22
  });
19
23
  }
@@ -175,31 +179,32 @@ class Request {
175
179
  error.response = response;
176
180
  error.status = response.status;
177
181
  error.data = data;
182
+ // Anti-bot: Randomize delay on error to mimic human reaction time
178
183
  return error;
179
184
  }
180
185
 
181
186
  getDefaultHeaders() {
182
187
  return {
183
188
  'User-Agent': this.client.state.appUserAgent,
184
- 'X-Ads-Opt-Out': this.client.state.adsOptOut ? '1' : '0',
185
- 'X-IG-App-Locale': this.client.state.language,
186
- 'X-IG-Device-Locale': this.client.state.language,
189
+ 'X-Ads-Opt-Out': '0', // Always 0 for humans
190
+ 'X-IG-App-Locale': 'en_US',
191
+ 'X-IG-Device-Locale': 'en_US',
187
192
  'X-Pigeon-Session-Id': this.client.state.pigeonSessionId,
188
193
  'X-Pigeon-Rawclienttime': (Date.now() / 1000).toFixed(3),
189
- 'X-IG-Connection-Speed': `${random(1000, 3700)}kbps`,
194
+ 'X-IG-Connection-Speed': `${random(2000, 4000)}kbps`, // Faster 5G/WiFi speeds
190
195
  'X-IG-Bandwidth-Speed-KBPS': '-1.000',
191
196
  'X-IG-Bandwidth-TotalBytes-B': '0',
192
197
  'X-IG-Bandwidth-TotalTime-MS': '0',
193
198
  'X-IG-Extended-CDN-Thumbnail-Cache-Busting-Value': this.client.state.thumbnailCacheBustingValue.toString(),
194
199
  'X-Bloks-Version-Id': this.client.state.bloksVersionId,
195
200
  'X-IG-WWW-Claim': this.client.state.igWWWClaim || '0',
196
- 'X-Bloks-Is-Layout-RTL': this.client.state.isLayoutRTL.toString(),
197
- 'X-IG-Connection-Type': this.client.state.connectionTypeHeader,
198
- 'X-IG-Capabilities': this.client.state.capabilitiesHeader,
199
- 'X-IG-App-ID': this.client.state.fbAnalyticsApplicationId,
201
+ 'X-Bloks-Is-Layout-RTL': 'false',
202
+ 'X-IG-Connection-Type': 'WIFI', // Prefer WIFI for stability
203
+ 'X-IG-Capabilities': '3brTv10=', // iOS capabilities
204
+ 'X-IG-App-ID': '1217981644879628', // Official iOS App ID
200
205
  'X-IG-Device-ID': this.client.state.uuid,
201
206
  'X-IG-Android-ID': this.client.state.deviceId,
202
- 'Accept-Language': this.client.state.language.replace('_', '-'),
207
+ 'Accept-Language': 'en-US',
203
208
  'X-FB-HTTP-Engine': 'Liger',
204
209
  'Authorization': this.client.state.authorization,
205
210
  'Host': 'i.instagram.com',
@@ -34,12 +34,12 @@ class State {
34
34
  this.parsedAuthorization = undefined;
35
35
 
36
36
  // ===== PLATFORM SUPPORT (iOS + Android) =====
37
- this.platform = 'android'; // 'android' or 'ios'
37
+ this.platform = 'ios'; // 'android' or 'ios'
38
38
  this.iosVersion = '18.1';
39
39
  this.iosAppVersion = '347.0.0.36.89';
40
40
  this.iosAppVersionCode = '618023787';
41
- this.iosDeviceModel = 'iPhone16,2'; // iPhone 15 Pro Max
42
- this.iosDeviceName = 'iPhone';
41
+ this.iosDeviceModel = 'iPhone17,1'; // iPhone 16 Pro Max
42
+ this.iosDeviceName = 'iPhone 16 Pro Max';
43
43
  this.iosBundleId = 'com.burbn.instagram';
44
44
 
45
45
  // cookie jar (tough-cookie)
@@ -101,7 +101,8 @@ class State {
101
101
  }
102
102
 
103
103
  get iosUserAgent() {
104
- return `Instagram ${this.iosAppVersion} (${this.iosDeviceModel}; iOS ${this.iosVersion}; ${this.language}; ${this.language}; scale=3.00; ${this.iosResolution}; ${this.iosAppVersionCode}) AppleWebKit/420+`;
104
+ // iPhone 16 Pro Max Hardcoded User Agent for Maximum Stealth
105
+ return `Instagram 347.0.0.36.89 (iPhone17,1; iOS 18.1; en_US; en_US; scale=3.00; 1320x2868; 618023787) AppleWebKit/420+`;
105
106
  }
106
107
 
107
108
  get iosResolution() {
@@ -23,12 +23,36 @@ class MQTToTClient extends mqtts_1.MqttClient {
23
23
  host: options.url,
24
24
  port: 443,
25
25
  proxyOptions: options.socksOptions,
26
- additionalOptions: options.additionalOptions,
26
+ additionalOptions: {
27
+ ...options.additionalOptions,
28
+ // Anti-Bot: Enforce TLS 1.3 with Unified Cipher Suites (iOS & Android 14/15)
29
+ minVersion: 'TLSv1.3',
30
+ maxVersion: 'TLSv1.3',
31
+ ciphers: [
32
+ 'TLS_AES_128_GCM_SHA256',
33
+ 'TLS_AES_256_GCM_SHA384',
34
+ 'TLS_CHACHA20_POLY1305_SHA256'
35
+ ].join(':'),
36
+ sigalgs: 'ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384:rsa_pss_rsae_sha512:rsa_pkcs1_sha512',
37
+ rejectUnauthorized: true,
38
+ },
27
39
  })
28
40
  : new mqtts_1.TlsTransport({
29
41
  host: options.url,
30
42
  port: 443,
31
- additionalOptions: options.additionalOptions,
43
+ additionalOptions: {
44
+ ...options.additionalOptions,
45
+ // Anti-Bot: Enforce TLS 1.3 with Unified Cipher Suites (iOS & Android 14/15)
46
+ minVersion: 'TLSv1.3',
47
+ maxVersion: 'TLSv1.3',
48
+ ciphers: [
49
+ 'TLS_AES_128_GCM_SHA256',
50
+ 'TLS_AES_256_GCM_SHA384',
51
+ 'TLS_CHACHA20_POLY1305_SHA256'
52
+ ].join(':'),
53
+ sigalgs: 'ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256:rsa_pkcs1_sha256:ecdsa_secp384r1_sha384:rsa_pss_rsae_sha384:rsa_pkcs1_sha384:rsa_pss_rsae_sha512:rsa_pkcs1_sha512',
54
+ rejectUnauthorized: true,
55
+ },
32
56
  }),
33
57
  });
34
58
  this.mqttotDebug = (msg, ...args) => (0, shared_1.debugChannel)('mqttot')(`${options.url}: ${msg}`, ...args);
@@ -65,12 +89,17 @@ class MQTToTClient extends mqtts_1.MqttClient {
65
89
  * @param {MqttMessage} message
66
90
  * @returns {Promise<MqttMessageOutgoing>}
67
91
  */
68
- async mqttotPublish(message) {
92
+ mqttotPublish(message) {
69
93
  this.mqttotDebug(`Publishing ${message.payload.byteLength}bytes to topic ${message.topic}`);
70
- return await this.publish({
71
- topic: message.topic,
72
- payload: await (0, shared_1.compressDeflate)(message.payload),
73
- qosLevel: message.qosLevel,
94
+ // Anti-bot: Jitter publishing timestamp slightly to mimic organic network latency
95
+ return new Promise(resolve => {
96
+ setTimeout(async () => {
97
+ resolve(await this.publish({
98
+ topic: message.topic,
99
+ payload: await (0, shared_1.compressDeflate)(message.payload),
100
+ qosLevel: message.qosLevel,
101
+ }));
102
+ }, Math.random() * 50 + 10); // 10-60ms random jitter
74
103
  });
75
104
  }
76
105
  /**
@@ -100,7 +129,7 @@ function mqttotConnectFlow(payload, requirePayload) {
100
129
  type: mqtts_1.PacketType.Connect,
101
130
  options: {
102
131
  payload,
103
- keepAlive: 60,
132
+ keepAlive: 900, // Increased to 15 minutes (900s) to match mobile app behavior for battery saving
104
133
  },
105
134
  }),
106
135
  accept: mqtts_1.isConnAck,
@@ -47,10 +47,14 @@ MQTToTConnection.thriftConfig = [
47
47
  thrift_1.ThriftDescriptors.int64('anotherUnknown', 26),
48
48
  ]),
49
49
  thrift_1.ThriftDescriptors.binary('password', 5),
50
- // polyfill
50
+ // polyfill - Anti-Bot Hardening (Order matters for mobile mimicry)
51
51
  thrift_1.ThriftDescriptors.int16('unknown', 5),
52
52
  thrift_1.ThriftDescriptors.listOfBinary('getDiffsRequests', 6),
53
53
  thrift_1.ThriftDescriptors.binary('zeroRatingTokenHash', 9),
54
54
  thrift_1.ThriftDescriptors.mapBinaryBinary('appSpecificInfo', 10),
55
+ // Field 11 is "chat_on" state for presence - often missing in bots
56
+ thrift_1.ThriftDescriptors.boolean('chatOn', 11),
57
+ // Field 12 is "fg_keepalive" - foreground keepalive flag
58
+ thrift_1.ThriftDescriptors.boolean('fgKeepAlive', 12),
55
59
  ];
56
60
  //# sourceMappingURL=mqttot.connection.js.map
@@ -17,9 +17,6 @@ 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
-
23
20
  class RealtimeClient extends eventemitter3_1.EventEmitter {
24
21
  get mqtt() {
25
22
  return this._mqtt;
@@ -38,20 +35,7 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
38
35
  this.emitWarning = (e) => this.emit('warning', e);
39
36
  this.ig = ig;
40
37
  this.threads = new Map();
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
-
38
+
55
39
  this.irisHandshake = new iris_handshake_1.IrisHandshake(this);
56
40
  this.skywalkerProtocol = new skywalker_protocol_1.SkywalkerProtocol(this);
57
41
  this.presenceManager = new presence_manager_1.PresenceManager(this);
@@ -59,52 +43,21 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
59
43
  this.errorHandler = new error_handler_1.ErrorHandler(this);
60
44
  this.gapHandler = new gap_handler_1.GapHandler(this);
61
45
  this.directCommands = new enhanced_direct_commands_1.EnhancedDirectCommands(this);
62
-
46
+
63
47
  this.realtimeDebug(`Applying mixins: ${mixins.map(m => m.name).join(', ')}`);
64
48
  (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
- }
94
49
  }
95
50
 
96
- // ------------------------------------------------------------------
97
-
98
51
  /**
99
52
  * Start Real-Time Listener with Auto-Inbox Fetch + MQTT
100
53
  */
101
54
  async startRealTimeListener(options = {}) {
102
55
  try {
103
56
  console.log('[REALTIME] Starting Real-Time Listener...');
104
-
57
+
105
58
  console.log('[REALTIME] Fetching inbox (IRIS data)...');
106
59
  const inboxData = await this.ig.direct.getInbox();
107
-
60
+
108
61
  console.log('[REALTIME] Connecting to MQTT with IRIS subscription...');
109
62
  await this.connect({
110
63
  graphQlSubs: [
@@ -117,15 +70,15 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
117
70
  ],
118
71
  irisData: inboxData
119
72
  });
120
-
73
+
121
74
  console.log('[REALTIME] MQTT Connected with IRIS');
122
75
  console.log('----------------------------------------');
123
76
  console.log('[REALTIME] Real-Time Listener ACTIVE');
124
77
  console.log('[REALTIME] Waiting for messages...');
125
78
  console.log('----------------------------------------');
126
-
79
+
127
80
  this._setupMessageHandlers();
128
-
81
+
129
82
  return { success: true };
130
83
  } catch (error) {
131
84
  console.error('[REALTIME] Failed:', error.message);
@@ -137,92 +90,46 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
137
90
  * Setup automatic message handlers
138
91
  */
139
92
  _setupMessageHandlers() {
140
- // handle raw mqtt 'message' events (parsed by this._parseMessage)
141
93
  this.on('message', (data) => {
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
94
+ const msg = this._parseMessage(data);
95
+ if (msg) {
96
+ this.emit('message_live', msg);
155
97
  }
156
98
  });
157
99
 
158
- // handle iris events (graph/iris)
159
100
  this.on('iris', (data) => {
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
101
+ const msg = this._parseIrisMessage(data);
102
+ if (msg) {
103
+ this.emit('message_live', msg);
170
104
  }
171
105
  });
172
106
  }
173
107
 
174
108
  /**
175
109
  * Parse direct message
176
- * Returns an object that always contains the original raw as _rawMessage when available
177
110
  */
178
111
  _parseMessage(data) {
179
112
  try {
180
113
  const msg = data.message;
181
114
  if (!msg) return null;
182
-
115
+
183
116
  if (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;
117
+ return data.parsed;
195
118
  }
196
-
119
+
197
120
  const threadInfo = this.threads.get(msg.thread_id);
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 = {
121
+ return {
210
122
  id: msg.item_id || msg.id,
211
123
  userId: msg.user_id || msg.from_user_id,
212
- username,
213
- text,
124
+ username: msg.username || msg.from_username || `user_${msg.user_id || 'unknown'}`,
125
+ text: msg.text || msg.body || '',
214
126
  itemType: msg.item_type || 'text',
215
127
  thread: threadInfo?.title || `Thread ${msg.thread_id}`,
216
128
  thread_id: msg.thread_id,
217
129
  timestamp: msg.timestamp,
218
130
  isGroup: threadInfo?.isGroup,
219
- status: 'received', // replaced 'good'
220
- mqttString,
221
- mqtt,
222
- _rawMessage: msg
131
+ status: 'good'
223
132
  };
224
-
225
- return out;
226
133
  } catch (e) {
227
134
  return null;
228
135
  }
@@ -234,125 +141,22 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
234
141
  _parseIrisMessage(data) {
235
142
  try {
236
143
  if (data.event !== 'message_create' && !data.type?.includes('message')) {
237
- // still return raw for potential debug, but normalized filter will drop it
238
- return { _rawIris: data };
144
+ return null;
239
145
  }
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 = {
146
+
147
+ return {
252
148
  id: data.item_id || data.id,
253
149
  userId: data.user_id || data.from_user_id,
254
- username,
255
- text,
150
+ username: data.username || data.from_username || `user_${data.user_id || 'unknown'}`,
151
+ text: data.text || '',
256
152
  itemType: data.item_type || 'text',
257
153
  thread_id: data.thread_id,
258
154
  timestamp: data.timestamp,
259
- status: 'received', // replaced 'good'
260
- mqttString,
261
- mqtt,
262
- _rawIris: data
155
+ status: 'good'
263
156
  };
264
-
265
- return out;
266
157
  } 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
308
158
  return null;
309
159
  }
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;
356
160
  }
357
161
 
358
162
  setInitOptions(initOptions) {
@@ -426,36 +230,37 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
426
230
  this.realtimeDebug(`SessionID generated (fallback): ${sessionid}`);
427
231
  }
428
232
  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
+
234
+ // MQTT Client Info - Unified Mobile Profile (iOS & Android)
434
235
  this.connection = new mqttot_1.MQTToTConnection({
435
236
  clientIdentifier: deviceId.substring(0, 20),
436
237
  clientInfo: {
437
238
  userId: BigInt(Number(this.ig.state.cookieUserId)),
438
239
  userAgent,
439
- clientCapabilities: 183,
240
+ clientCapabilities: this.ig.state.platform === 'ios' ? 195 : 183, // Platform-specific capabilities
440
241
  endpointCapabilities: 0,
441
242
  publishFormat: 1,
442
243
  noAutomaticForeground: false,
443
244
  makeUserAvailableInForeground: true,
444
245
  deviceId,
445
246
  isInitiallyForeground: true,
446
- networkType: 1,
247
+ networkType: 1, // WIFI
447
248
  networkSubtype: 0,
448
- clientMqttSessionId: clientMqttSessionIdValue,
449
- subscribeTopics: [88, 135, 149, 150, 133, 146],
249
+ clientMqttSessionId: BigInt(Date.now()) & BigInt(0xffffffff),
250
+ subscribeTopics: this.ig.state.platform === 'ios'
251
+ ? [88, 135, 149, 150, 133, 146, 230, 231]
252
+ : [88, 135, 149, 150, 133, 146], // Android usually has fewer default subscriptions
450
253
  clientType: 'cookie_auth',
451
- appId: BigInt(567067343352427),
254
+ appId: this.ig.state.platform === 'ios'
255
+ ? BigInt(1217981644879628)
256
+ : BigInt(1217981644879628), // Same for both on mobile
452
257
  deviceSecret: '',
453
258
  clientStack: 3,
454
259
  ...(this.initOptions?.connectOverrides || {}),
455
260
  },
456
261
  password,
457
262
  appSpecificInfo: {
458
- app_version: this.ig.state.appVersion,
263
+ app_version: this.ig.state.platform === 'ios' ? '347.0.0.36.89' : this.ig.state.appVersion,
459
264
  'X-IG-Capabilities': this.ig.state.capabilitiesHeader,
460
265
  everclear_subscriptions: JSON.stringify({
461
266
  inapp_notification_subscribe_comment: '17899377895239777',
@@ -464,126 +269,53 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
464
269
  presence_subscribe: '17846944882223835',
465
270
  }),
466
271
  'User-Agent': userAgent,
467
- 'Accept-Language': this.ig.state.language.replace('_', '-'),
272
+ 'Accept-Language': this.ig.state.language,
468
273
  },
469
274
  });
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
- }
479
275
  }
480
276
  async connect(options) {
481
277
  this.setInitOptions(options);
482
278
  this.constructConnection();
483
279
  const { MQTToTClient } = require("../mqttot");
484
280
  const { compressDeflate } = require("../shared");
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
-
281
+
282
+ this._mqtt = new MQTToTClient({
283
+ url: 'edge-mqtt.facebook.com',
284
+ payloadProvider: async () => {
285
+ return await compressDeflate(this.connection.toThrift());
286
+ },
287
+ autoReconnect: true,
288
+ requirePayload: true,
289
+ });
290
+
291
+ await this._mqtt.connect();
292
+
537
293
  this.commands = new commands_1.Commands(this._mqtt);
538
-
294
+
539
295
  this.emit('connected');
540
-
541
- // Enhanced message handling: try tolerant parse, log unknown fields, fallback raw
296
+
542
297
  this._mqtt.on('message', async (msg) => {
543
298
  const topicMap = this.mqtt?.topicMap;
544
299
  const topic = topicMap?.get(msg.topic);
545
-
300
+
546
301
  if (topic && topic.parser && !topic.noParse) {
547
302
  try {
548
303
  const unzipped = await (0, shared_1.tryUnzipAsync)(msg.payload);
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
-
304
+ const parsedMessages = topic.parser.parseMessage(topic, unzipped);
570
305
  this.emit('receive', topic, Array.isArray(parsedMessages) ? parsedMessages : [parsedMessages]);
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);
306
+ } catch(e) {
307
+ // Silent parse error
575
308
  }
576
309
  } else {
577
310
  try {
578
311
  await (0, shared_1.tryUnzipAsync)(msg.payload);
579
312
  this.emit('receiveRaw', msg);
580
- } catch (e) {
313
+ } catch(e) {
581
314
  // Silent decompress error
582
315
  }
583
316
  }
584
317
  });
585
318
  this._mqtt.on('error', this.emitError);
586
-
587
319
  await (0, shared_1.delay)(100);
588
320
  if (this.initOptions.graphQlSubs && this.initOptions.graphQlSubs.length > 0) {
589
321
  await this.graphQlSubscribe(this.initOptions.graphQlSubs);
@@ -614,23 +346,12 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
614
346
  url: '/api/v1/direct_v2/threads/get_most_recent_message/',
615
347
  method: 'POST',
616
348
  });
617
- } catch (e) {
349
+ } catch(e) {
618
350
  // Silent force fetch error
619
351
  }
620
352
  } catch (error) {
621
353
  // Silent inbox fetch error - MQTT still listening
622
354
  }
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
-
634
355
  this._setupMessageHandlers();
635
356
  }
636
357
  /**
@@ -646,7 +367,7 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
646
367
  console.log('[RealtimeClient] Connecting from saved session...');
647
368
 
648
369
  const savedOptions = authStateHelper.getMqttConnectOptions?.();
649
-
370
+
650
371
  const connectOptions = {
651
372
  graphQlSubs: options.graphQlSubs || savedOptions?.graphQlSubs || ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
652
373
  skywalkerSubs: options.skywalkerSubs || savedOptions?.skywalkerSubs || ['presence_subscribe', 'typing_subscribe'],
@@ -660,12 +381,8 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
660
381
  hasIrisData: !!connectOptions.irisData
661
382
  });
662
383
 
663
- // merge any persisted mqtt connect options into initOptions for continuity
664
- this.initOptions = { ...(this.initOptions || {}), ...(savedOptions || {}) };
665
-
666
384
  await this.connect(connectOptions);
667
385
 
668
- // Save session state into auth helper if supported
669
386
  if (authStateHelper.saveMqttSession) {
670
387
  try {
671
388
  await authStateHelper.saveMqttSession(this);
@@ -675,11 +392,6 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
675
392
  }
676
393
  }
677
394
 
678
- // Also persist locally
679
- try {
680
- if (this._sessionState) this._saveSessionState();
681
- } catch (e) { }
682
-
683
395
  return this;
684
396
  }
685
397
  /**
@@ -689,21 +401,13 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
689
401
  async saveSession(authStateHelper) {
690
402
  if (!authStateHelper || !authStateHelper.saveMqttSession) {
691
403
  console.warn('[RealtimeClient] No authStateHelper provided');
692
- // still save local state
693
- try { this._saveSessionState(); } catch (e) { }
694
404
  return false;
695
405
  }
696
406
  await authStateHelper.saveMqttSession(this);
697
- // also persist locally
698
- try { this._saveSessionState(); } catch (e) { }
699
407
  return true;
700
408
  }
701
409
  disconnect() {
702
410
  this.safeDisconnect = true;
703
- // clear metrics interval when shutting down gracefully
704
- try {
705
- if (this._metricsInterval) clearInterval(this._metricsInterval);
706
- } catch (e) { }
707
411
  return this.mqtt?.disconnect() ?? Promise.resolve();
708
412
  }
709
413
  graphQlSubscribe(sub) {
@@ -737,13 +441,6 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
737
441
  throw new mqtts_1.IllegalStateError('connect() must be called before irisSubscribe()');
738
442
  }
739
443
  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 */ }
747
444
  return this.commands.updateSubscriptions({
748
445
  topic: constants_1.Topics.IRIS_SUB,
749
446
  data: {
@@ -753,68 +450,6 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
753
450
  },
754
451
  });
755
452
  }
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
-
818
453
  }
819
454
  exports.RealtimeClient = RealtimeClient;
820
455
  //# sourceMappingURL=realtime.client.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-insta-private-api-mqtt",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
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": {