nodejs-insta-private-api-mqtt 1.3.10 → 1.3.12

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.
@@ -6,6 +6,24 @@ const constants_1 = require("../../constants");
6
6
  const shared_1 = require("../../shared");
7
7
  const mqtts_1 = require("mqtts");
8
8
 
9
+ /**
10
+ * MessageSyncMixin - patched for 2026 (robust parsing + safe username fetch + tolerant timestamp handling)
11
+ *
12
+ * Changes applied:
13
+ * - tolerant parsing for e.value (string / object / already-parsed)
14
+ * - support for several path shapes when extracting thread id
15
+ * - safer timestamp parsing (accepts seconds, milliseconds, microseconds, nanoseconds)
16
+ * - username fetch uses a pending map + small backoff to reduce rush/rate-limit risk
17
+ * - defensive try/catch around JSON.parse and all external calls
18
+ * - keeps original API: apply(client) registers post-connect hook and emits same events
19
+ *
20
+ * Additional change requested:
21
+ * - set message status to 'received' for incoming messages and 'sent' for messages authored by the logged-in account,
22
+ * instead of the previous 'good'.
23
+ *
24
+ * Note: No rate-limiting code is included.
25
+ */
26
+
9
27
  class MessageSyncMixin extends mixin_1.Mixin {
10
28
  constructor() {
11
29
  super();
@@ -36,10 +54,17 @@ class MessageSyncMixin extends mixin_1.Mixin {
36
54
  client.mqtt.listen({
37
55
  topic: constants_1.Topics.MESSAGE_SYNC.id,
38
56
  transformer: async ({ payload }) => {
39
- const parsed = constants_1.Topics.MESSAGE_SYNC.parser
40
- .parseMessage(constants_1.Topics.MESSAGE_SYNC, await (0, shared_1.tryUnzipAsync)(payload))
41
- .map(msg => msg.data);
42
- return parsed;
57
+ try {
58
+ const unzipped = await (0, shared_1.tryUnzipAsync)(payload);
59
+ const parsed = constants_1.Topics.MESSAGE_SYNC.parser
60
+ .parseMessage(constants_1.Topics.MESSAGE_SYNC, unzipped)
61
+ .map(msg => msg.data);
62
+ return parsed;
63
+ } catch (err) {
64
+ // If transformer fails, return empty array so handler is tolerant
65
+ console.warn('[MESSAGE_SYNC] transformer parse failed:', err?.message || err);
66
+ return [];
67
+ }
43
68
  },
44
69
  }, data => {
45
70
  this.handleMessageSync(client, data);
@@ -47,9 +72,13 @@ class MessageSyncMixin extends mixin_1.Mixin {
47
72
  } else {
48
73
  console.log(`[MESSAGE_SYNC] mqtt.listen() NOT FOUND - using fallback 'receive' event`);
49
74
  client.on('receive', (topic, messages) => {
50
- if (topic.id === constants_1.Topics.MESSAGE_SYNC.id) {
51
- const data = messages.map(m => m.data);
52
- this.handleMessageSync(client, data);
75
+ try {
76
+ if (topic.id === constants_1.Topics.MESSAGE_SYNC.id) {
77
+ const data = messages.map(m => m.data);
78
+ this.handleMessageSync(client, data);
79
+ }
80
+ } catch (err) {
81
+ console.warn('[MESSAGE_SYNC] receive fallback handler error:', err?.message || err);
53
82
  }
54
83
  });
55
84
  }
@@ -67,16 +96,26 @@ class MessageSyncMixin extends mixin_1.Mixin {
67
96
  }
68
97
 
69
98
  if (this.pendingUserFetches.has(userIdStr)) {
70
- return await this.pendingUserFetches.get(userIdStr);
99
+ try {
100
+ return await this.pendingUserFetches.get(userIdStr);
101
+ } catch (e) {
102
+ // if pending fetch failed, continue to fresh attempt
103
+ }
71
104
  }
72
105
 
73
106
  const fetchPromise = (async () => {
74
107
  try {
108
+ // small backoff to avoid immediate burst of parallel requests
109
+ await new Promise(r => setTimeout(r, 120));
75
110
  if (client.ig && client.ig.user && client.ig.user.info) {
76
- const userInfo = await client.ig.user.info(userIdStr);
77
- if (userInfo && userInfo.username) {
78
- this.userCache.set(userIdStr, userInfo.username);
79
- return userInfo.username;
111
+ try {
112
+ const userInfo = await client.ig.user.info(userIdStr);
113
+ if (userInfo && userInfo.username) {
114
+ this.userCache.set(userIdStr, userInfo.username);
115
+ return userInfo.username;
116
+ }
117
+ } catch (innerErr) {
118
+ // rate-limited or not found - swallow
80
119
  }
81
120
  }
82
121
  } catch (err) {
@@ -96,158 +135,171 @@ class MessageSyncMixin extends mixin_1.Mixin {
96
135
  let content = '';
97
136
  let mediaInfo = '';
98
137
 
99
- switch (itemType) {
100
- case 'text':
101
- content = msgValue.text || '';
102
- break;
103
-
104
- case 'media':
105
- case 'raven_media':
106
- content = '[PHOTO/VIDEO]';
107
- if (msgValue.media) {
108
- const media = msgValue.media;
109
- if (media.image_versions2) {
110
- content = '[PHOTO]';
111
- mediaInfo = ` URL: ${media.image_versions2?.candidates?.[0]?.url || 'N/A'}`;
112
- } else if (media.video_versions) {
113
- content = '[VIDEO]';
114
- mediaInfo = ` Duration: ${media.video_duration || 'N/A'}s`;
138
+ try {
139
+ switch (itemType) {
140
+ case 'text':
141
+ content = msgValue.text || msgValue.body || '';
142
+ break;
143
+
144
+ case 'media':
145
+ case 'raven_media':
146
+ content = '[PHOTO/VIDEO]';
147
+ if (msgValue.media) {
148
+ const media = msgValue.media;
149
+ if (media.image_versions2) {
150
+ content = '[PHOTO]';
151
+ mediaInfo = ` URL: ${media.image_versions2?.candidates?.[0]?.url || 'N/A'}`;
152
+ } else if (media.video_versions) {
153
+ content = '[VIDEO]';
154
+ mediaInfo = ` Duration: ${media.video_duration || 'N/A'}s`;
155
+ }
115
156
  }
116
- }
117
- if (msgValue.visual_media) {
118
- content = '[DISAPPEARING MEDIA]';
119
- }
120
- break;
121
-
122
- case 'voice_media':
123
- content = '[VOICE MESSAGE]';
124
- if (msgValue.voice_media?.media?.audio) {
125
- const duration = msgValue.voice_media.media.audio.duration || 0;
126
- content = `[VOICE MESSAGE] Duration: ${duration}ms`;
127
- }
128
- break;
129
-
130
- case 'animated_media':
131
- content = '[GIF]';
132
- if (msgValue.animated_media?.images?.fixed_height?.url) {
133
- mediaInfo = ` URL: ${msgValue.animated_media.images.fixed_height.url}`;
134
- }
135
- break;
136
-
137
- case 'media_share':
138
- content = '[SHARED POST]';
139
- if (msgValue.media_share) {
140
- const share = msgValue.media_share;
141
- content = `[SHARED POST] From: @${share.user?.username || 'unknown'}`;
142
- if (share.caption?.text) {
143
- content += ` Caption: "${share.caption.text.substring(0, 50)}..."`;
157
+ if (msgValue.visual_media) {
158
+ content = '[DISAPPEARING MEDIA]';
144
159
  }
145
- }
146
- break;
147
-
148
- case 'reel_share':
149
- content = '[SHARED REEL]';
150
- if (msgValue.reel_share) {
151
- const reel = msgValue.reel_share;
152
- content = `[SHARED REEL] From: @${reel.media?.user?.username || 'unknown'}`;
153
- if (reel.text) {
154
- content += ` Text: "${reel.text}"`;
160
+ break;
161
+
162
+ case 'voice_media':
163
+ content = '[VOICE MESSAGE]';
164
+ if (msgValue.voice_media?.media?.audio) {
165
+ const duration = msgValue.voice_media.media.audio.duration || 0;
166
+ content = `[VOICE MESSAGE] Duration: ${duration}ms`;
155
167
  }
156
- }
157
- break;
158
-
159
- case 'story_share':
160
- content = '[SHARED STORY]';
161
- if (msgValue.story_share) {
162
- const story = msgValue.story_share;
163
- content = `[SHARED STORY] From: @${story.media?.user?.username || 'unknown'}`;
164
- if (story.message) {
165
- content += ` Message: "${story.message}"`;
168
+ break;
169
+
170
+ case 'animated_media':
171
+ content = '[GIF]';
172
+ if (msgValue.animated_media?.images?.fixed_height?.url) {
173
+ mediaInfo = ` URL: ${msgValue.animated_media.images.fixed_height.url}`;
166
174
  }
167
- }
168
- break;
169
-
170
- case 'felix_share':
171
- content = '[SHARED IGTV/VIDEO]';
172
- if (msgValue.felix_share?.video) {
173
- content = `[SHARED IGTV] Title: "${msgValue.felix_share.video.title || 'N/A'}"`;
174
- }
175
- break;
176
-
177
- case 'clip':
178
- content = '[SHARED CLIP]';
179
- if (msgValue.clip?.clip) {
180
- content = `[SHARED CLIP] From: @${msgValue.clip.clip.user?.username || 'unknown'}`;
181
- }
182
- break;
183
-
184
- case 'profile':
185
- content = '[SHARED PROFILE]';
186
- if (msgValue.profile) {
187
- content = `[SHARED PROFILE] @${msgValue.profile.username || 'unknown'}`;
188
- }
189
- break;
190
-
191
- case 'location':
192
- content = '[LOCATION]';
193
- if (msgValue.location) {
194
- content = `[LOCATION] ${msgValue.location.name || msgValue.location.address || 'Unknown location'}`;
195
- }
196
- break;
197
-
198
- case 'hashtag':
199
- content = '[HASHTAG]';
200
- if (msgValue.hashtag) {
201
- content = `[HASHTAG] #${msgValue.hashtag.name || 'unknown'}`;
202
- }
203
- break;
204
-
205
- case 'like':
206
- content = '[LIKE]';
207
- break;
208
-
209
- case 'link':
210
- content = '[LINK]';
211
- if (msgValue.link) {
212
- content = `[LINK] ${msgValue.link.text || msgValue.link.link_url || 'N/A'}`;
213
- }
214
- break;
215
-
216
- case 'action_log':
217
- content = '[ACTION]';
218
- if (msgValue.action_log) {
219
- content = `[ACTION] ${msgValue.action_log.description || 'N/A'}`;
220
- }
221
- break;
222
-
223
- case 'placeholder':
224
- content = '[PLACEHOLDER]';
225
- if (msgValue.placeholder?.message) {
226
- content = `[PLACEHOLDER] ${msgValue.placeholder.message}`;
227
- }
228
- break;
229
-
230
- case 'xma':
231
- case 'xma_media_share':
232
- content = '[XMA SHARE]';
233
- if (msgValue.xma_link_url) {
234
- content = `[XMA SHARE] ${msgValue.xma_link_url}`;
235
- }
236
- break;
237
-
238
- case 'video_call_event':
239
- content = '[VIDEO CALL EVENT]';
240
- if (msgValue.video_call_event) {
241
- content = `[VIDEO CALL] ${msgValue.video_call_event.action || 'event'}`;
242
- }
243
- break;
244
-
245
- default:
246
- if (msgValue.text) {
247
- content = msgValue.text;
175
+ break;
176
+
177
+ case 'media_share':
178
+ content = '[SHARED POST]';
179
+ if (msgValue.media_share) {
180
+ const share = msgValue.media_share;
181
+ content = `[SHARED POST] From: @${share.user?.username || 'unknown'}`;
182
+ if (share.caption?.text) {
183
+ content += ` Caption: "${String(share.caption.text).substring(0, 50)}..."`;
184
+ }
185
+ }
186
+ break;
187
+
188
+ case 'reel_share':
189
+ content = '[SHARED REEL]';
190
+ if (msgValue.reel_share) {
191
+ const reel = msgValue.reel_share;
192
+ content = `[SHARED REEL] From: @${reel.media?.user?.username || 'unknown'}`;
193
+ if (reel.text) {
194
+ content += ` Text: "${reel.text}"`;
195
+ }
196
+ }
197
+ break;
198
+
199
+ case 'story_share':
200
+ content = '[SHARED STORY]';
201
+ if (msgValue.story_share) {
202
+ const story = msgValue.story_share;
203
+ content = `[SHARED STORY] From: @${story.media?.user?.username || 'unknown'}`;
204
+ if (story.message) {
205
+ content += ` Message: "${story.message}"`;
206
+ }
207
+ }
208
+ break;
209
+
210
+ case 'felix_share':
211
+ content = '[SHARED IGTV/VIDEO]';
212
+ if (msgValue.felix_share?.video) {
213
+ content = `[SHARED IGTV] Title: "${msgValue.felix_share.video.title || 'N/A'}"`;
214
+ }
215
+ break;
216
+
217
+ case 'clip':
218
+ content = '[SHARED CLIP]';
219
+ if (msgValue.clip?.clip) {
220
+ content = `[SHARED CLIP] From: @${msgValue.clip.clip.user?.username || 'unknown'}`;
221
+ }
222
+ break;
223
+
224
+ case 'profile':
225
+ content = '[SHARED PROFILE]';
226
+ if (msgValue.profile) {
227
+ content = `[SHARED PROFILE] @${msgValue.profile.username || 'unknown'}`;
228
+ }
229
+ break;
230
+
231
+ case 'location':
232
+ content = '[LOCATION]';
233
+ if (msgValue.location) {
234
+ content = `[LOCATION] ${msgValue.location.name || msgValue.location.address || 'Unknown location'}`;
235
+ }
236
+ break;
237
+
238
+ case 'hashtag':
239
+ content = '[HASHTAG]';
240
+ if (msgValue.hashtag) {
241
+ content = `[HASHTAG] #${msgValue.hashtag.name || 'unknown'}`;
242
+ }
243
+ break;
244
+
245
+ case 'like':
246
+ content = '[LIKE]';
247
+ break;
248
+
249
+ case 'link':
250
+ content = '[LINK]';
251
+ if (msgValue.link) {
252
+ content = `[LINK] ${msgValue.link.text || msgValue.link.link_url || 'N/A'}`;
253
+ }
254
+ break;
255
+
256
+ case 'action_log':
257
+ content = '[ACTION]';
258
+ if (msgValue.action_log) {
259
+ content = `[ACTION] ${msgValue.action_log.description || 'N/A'}`;
260
+ }
261
+ break;
262
+
263
+ case 'placeholder':
264
+ content = '[PLACEHOLDER]';
265
+ if (msgValue.placeholder?.message) {
266
+ content = `[PLACEHOLDER] ${msgValue.placeholder.message}`;
267
+ }
268
+ break;
269
+
270
+ case 'xma':
271
+ case 'xma_media_share':
272
+ content = '[XMA SHARE]';
273
+ if (msgValue.xma_link_url) {
274
+ content = `[XMA SHARE] ${msgValue.xma_link_url}`;
275
+ }
276
+ break;
277
+
278
+ case 'video_call_event':
279
+ content = '[VIDEO CALL EVENT]';
280
+ if (msgValue.video_call_event) {
281
+ content = `[VIDEO CALL] ${msgValue.video_call_event.action || 'event'}`;
282
+ }
283
+ break;
284
+
285
+ default:
286
+ if (msgValue && (msgValue.text || msgValue.body)) {
287
+ content = msgValue.text || msgValue.body;
288
+ } else {
289
+ content = `[${(itemType || 'UNKNOWN').toUpperCase()}]`;
290
+ }
291
+ }
292
+ } catch (e) {
293
+ // defensive fallback
294
+ try {
295
+ if (msgValue && (msgValue.text || msgValue.body)) {
296
+ content = msgValue.text || msgValue.body;
248
297
  } else {
249
298
  content = `[${(itemType || 'UNKNOWN').toUpperCase()}]`;
250
299
  }
300
+ } catch (e2) {
301
+ content = `[${(itemType || 'UNKNOWN').toUpperCase()}]`;
302
+ }
251
303
  }
252
304
 
253
305
  return content + mediaInfo;
@@ -255,6 +307,26 @@ class MessageSyncMixin extends mixin_1.Mixin {
255
307
 
256
308
  formatMessageForConsole(msgData) {
257
309
  const separator = '----------------------------------------';
310
+ // robust timestamp formatting into readable date+time in Europe/Bucharest
311
+ let ts = 'N/A';
312
+ try {
313
+ const parsed = this.parseTimestamp(msgData.timestamp);
314
+ if (parsed) {
315
+ const d = new Date(parsed);
316
+ ts = d.toLocaleString('ro-RO', {
317
+ year: 'numeric',
318
+ month: '2-digit',
319
+ day: '2-digit',
320
+ hour: '2-digit',
321
+ minute: '2-digit',
322
+ second: '2-digit',
323
+ hour12: false,
324
+ timeZone: 'Europe/Bucharest'
325
+ });
326
+ }
327
+ } catch (e) {
328
+ ts = 'N/A';
329
+ }
258
330
  const lines = [
259
331
  '',
260
332
  separator,
@@ -266,14 +338,69 @@ class MessageSyncMixin extends mixin_1.Mixin {
266
338
  `Type: ${msgData.itemType || 'text'}`,
267
339
  `Thread: ${msgData.threadId || 'unknown'}`,
268
340
  `Message ID: ${msgData.messageId || 'unknown'}`,
269
- `Timestamp: ${msgData.timestamp ? new Date(parseInt(msgData.timestamp) / 1000).toISOString() : 'N/A'}`,
270
- `Status: ${msgData.status || 'good'}`,
341
+ `Timestamp: ${ts}`,
342
+ `Status: ${msgData.status || 'unknown'}`,
271
343
  separator,
272
344
  ''
273
345
  ];
274
346
  return lines.join('\n');
275
347
  }
276
348
 
349
+ /**
350
+ * parseTimestamp
351
+ * - accepts numeric strings or numbers in seconds, milliseconds, microseconds, nanoseconds
352
+ * - normalizes to milliseconds
353
+ * - sanity-checks to avoid absurd future dates; returns Date.now() fallback if out of range
354
+ */
355
+ parseTimestamp(ts) {
356
+ try {
357
+ if (ts === undefined || ts === null) return null;
358
+ // if object with .ms or similar, try common fields
359
+ if (typeof ts === 'object') {
360
+ if (ts.ms) return Number(ts.ms);
361
+ if (ts.seconds) return Number(ts.seconds) * 1000;
362
+ if (ts.nano) return Math.floor(Number(ts.nano) / 1e6);
363
+ // fallback to toString
364
+ ts = String(ts);
365
+ }
366
+ let n = Number(ts);
367
+ if (!Number.isFinite(n)) return null;
368
+
369
+ // Heuristics:
370
+ // nanoseconds ~ 1e18+, microseconds ~ 1e15+, milliseconds ~ 1e12, seconds ~ 1e9
371
+ if (n > 1e17) {
372
+ // nanoseconds -> ms
373
+ n = Math.floor(n / 1e6);
374
+ } else if (n > 1e14) {
375
+ // microseconds -> ms
376
+ n = Math.floor(n / 1e3);
377
+ } else if (n > 1e12) {
378
+ // likely already ms (leave)
379
+ n = Math.floor(n);
380
+ } else if (n > 1e9) {
381
+ // seconds -> ms
382
+ n = Math.floor(n * 1000);
383
+ } else if (n > 1e6) {
384
+ // ambiguous (older formats) -> treat as seconds -> ms
385
+ n = Math.floor(n * 1000);
386
+ } else {
387
+ // too small -> invalid
388
+ return null;
389
+ }
390
+
391
+ // sanity range: allow roughly 2010-2036 (ms)
392
+ const min = 1262304000000; // 2010-01-01
393
+ const max = 2114380800000; // 2037-01-01 (safe future upper bound)
394
+ if (!Number.isFinite(n) || n < min || n > max) {
395
+ // fallback to now to avoid huge future years displayed
396
+ return Date.now();
397
+ }
398
+ return n;
399
+ } catch (e) {
400
+ return null;
401
+ }
402
+ }
403
+
277
404
  async handleMessageSync(client, syncData) {
278
405
  if (!syncData || !Array.isArray(syncData)) {
279
406
  console.log(`[MESSAGE_SYNC] No sync data received`);
@@ -281,96 +408,184 @@ class MessageSyncMixin extends mixin_1.Mixin {
281
408
  }
282
409
 
283
410
  for (const element of syncData) {
284
- const data = element.data;
285
-
286
- if (!data) {
287
- client.emit('iris', element);
288
- continue;
289
- }
290
-
291
- delete element.data;
292
-
293
- for (const e of data) {
294
- if (!e.path) {
295
- client.emit('iris', { ...element, ...e });
411
+ try {
412
+ const data = element.data;
413
+
414
+ if (!data) {
415
+ // fallback: emit iris with original element
416
+ client.emit('iris', element);
296
417
  continue;
297
418
  }
298
419
 
299
- if (e.path.startsWith('/direct_v2/threads') && e.value) {
420
+ // ensure element.data removed in downstream parsed message (keeps original behavior)
421
+ delete element.data;
422
+
423
+ for (const e of data) {
300
424
  try {
301
- const msgValue = JSON.parse(e.value);
302
- const threadId = MessageSyncMixin.getThreadIdFromPath(e.path);
303
-
304
- const userId = msgValue.user_id || msgValue.from_user_id || msgValue.sender_id;
305
- const itemType = msgValue.item_type || 'text';
306
-
307
- let username = msgValue.username || null;
308
- if (!username && userId) {
309
- username = await this.getUsernameFromId(client, userId);
425
+ // tolerant handling: e.value may be string, object, null, or already parsed
426
+ let parsedValue = {};
427
+ if (e.value === undefined || e.value === null) {
428
+ parsedValue = {};
429
+ } else if (typeof e.value === 'string') {
430
+ const str = e.value.trim();
431
+ if (str.length === 0) {
432
+ parsedValue = {};
433
+ } else {
434
+ try {
435
+ parsedValue = JSON.parse(str);
436
+ } catch (errJson) {
437
+ // If not JSON, attempt basic fallback (sometimes server sends plain key=value or quoted)
438
+ try {
439
+ // try to safe-evaluate limited forms like a bare object without quotes (rare)
440
+ parsedValue = {};
441
+ } catch (err2) {
442
+ parsedValue = {};
443
+ }
444
+ }
445
+ }
446
+ } else if (typeof e.value === 'object') {
447
+ parsedValue = e.value;
448
+ } else {
449
+ parsedValue = {};
310
450
  }
311
- if (!username) {
312
- username = `user_${userId || 'unknown'}`;
451
+
452
+ // Sometimes the message payload is nested under 'message' or similar
453
+ const msgValue = parsedValue.message || parsedValue.data || parsedValue || {};
454
+
455
+ if (!e.path) {
456
+ // no path means iris-like delta; merge element + e
457
+ client.emit('iris', { ...element, ...e, value: msgValue });
458
+ continue;
313
459
  }
314
460
 
315
- const textContent = this.extractMessageContent(msgValue, itemType);
316
-
317
- const msgData = {
318
- username: username,
319
- userId: userId,
320
- text: textContent,
321
- itemType: itemType,
322
- threadId: threadId,
323
- messageId: msgValue.item_id || msgValue.id,
324
- timestamp: msgValue.timestamp,
325
- status: 'good',
326
- rawData: msgValue
327
- };
328
-
329
- console.log(this.formatMessageForConsole(msgData));
330
-
331
- const parsedMessage = {
332
- ...element,
333
- message: {
334
- path: e.path,
335
- op: e.op,
336
- thread_id: threadId,
337
- ...msgValue,
338
- },
339
- parsed: msgData
340
- };
341
-
342
- client.emit('message', parsedMessage);
461
+ // normalize path check for thread messages
462
+ if ((e.path && e.path.startsWith('/direct_v2/threads')) ||
463
+ (e.path && e.path.startsWith('/direct_v2/inbox/threads')) ||
464
+ (e.path && e.path.indexOf('/direct_v2/threads/') !== -1) ) {
465
+
466
+ if (msgValue && (msgValue.item_type || msgValue.itemType || msgValue.type || msgValue.msg_type)) {
467
+ // determine item type as robustly as possible
468
+ const itemType = msgValue.item_type || msgValue.itemType || msgValue.type || msgValue.msg_type || 'text';
469
+
470
+ // thread id extraction
471
+ const threadId = MessageSyncMixin.getThreadIdFromPath(e.path);
472
+
473
+ // user id resolution: try many possible fields
474
+ const userId = msgValue.user_id || msgValue.from_user_id || msgValue.sender_id || msgValue.userId || msgValue.senderId || null;
475
+
476
+ // username resolution: prefer embedded username, otherwise fetch
477
+ let username = msgValue.username || msgValue.from_username || null;
478
+ if (!username && userId) {
479
+ try {
480
+ username = await this.getUsernameFromId(client, userId);
481
+ } catch (ux) {
482
+ username = null;
483
+ }
484
+ }
485
+ if (!username) {
486
+ username = `user_${userId || 'unknown'}`;
487
+ }
488
+
489
+ const textContent = this.extractMessageContent(msgValue, itemType);
490
+
491
+ const messageId = msgValue.item_id || msgValue.id || msgValue.client_context || msgValue.client_context_id || msgValue.message_id || msgValue.messageId || null;
492
+ const timestamp = msgValue.timestamp || msgValue.ts || msgValue.client_time || null;
493
+
494
+ // determine status based on whether message author is the logged-in account
495
+ let status = 'received';
496
+ try {
497
+ const ownId = client?.ig?.state?.cookieUserId || client?.ig?.state?.userId || null;
498
+ if (ownId && userId && String(userId) === String(ownId)) {
499
+ status = 'sent';
500
+ } else {
501
+ status = 'received';
502
+ }
503
+ } catch (stErr) {
504
+ status = 'received';
505
+ }
506
+
507
+ const msgData = {
508
+ username: username,
509
+ userId: userId,
510
+ text: textContent,
511
+ itemType: itemType,
512
+ threadId: threadId,
513
+ messageId: messageId,
514
+ timestamp: timestamp,
515
+ status: status,
516
+ rawData: msgValue
517
+ };
518
+
519
+ // console output (keeps original formatted block)
520
+ try {
521
+ console.log(this.formatMessageForConsole(msgData));
522
+ } catch (eLog) {
523
+ // don't let logging break processing
524
+ }
525
+
526
+ const parsedMessage = {
527
+ ...element,
528
+ message: {
529
+ path: e.path,
530
+ op: e.op,
531
+ thread_id: threadId,
532
+ ...msgValue,
533
+ },
534
+ parsed: msgData
535
+ };
536
+
537
+ client.emit('message', parsedMessage);
538
+ continue;
539
+ } // end if msgValue has item_type
540
+ } // end if path matches threads
343
541
 
344
- } catch (err) {
345
- console.log(`[MESSAGE_SYNC] Parse error: ${err.message}`);
346
- }
347
- } else {
348
- try {
349
- const updateValue = e.value ? JSON.parse(e.value) : {};
350
- client.emit('threadUpdate', {
351
- ...element,
352
- meta: {
353
- path: e.path,
354
- op: e.op,
355
- thread_id: MessageSyncMixin.getThreadIdFromPath(e.path),
356
- },
357
- update: updateValue,
358
- });
359
- } catch (err) {
360
- console.log(`[MESSAGE_SYNC] Thread update parse error: ${err.message}`);
542
+ // If not a thread message, emit as threadUpdate or iris depending on payload
543
+ try {
544
+ const updateValue = e.value ? (typeof e.value === 'string' ? (() => {
545
+ try { return JSON.parse(e.value); } catch { return e.value; }
546
+ })() : e.value) : {};
547
+ client.emit('threadUpdate', {
548
+ ...element,
549
+ meta: {
550
+ path: e.path,
551
+ op: e.op,
552
+ thread_id: MessageSyncMixin.getThreadIdFromPath(e.path),
553
+ },
554
+ update: updateValue,
555
+ });
556
+ } catch (errUpdate) {
557
+ client.emit('iris', { ...element, ...e, value: parsedValue });
558
+ }
559
+ } catch (inner) {
560
+ console.log(`[MESSAGE_SYNC] element handling error: ${inner?.message || inner}`);
361
561
  }
362
562
  }
563
+ } catch (outer) {
564
+ console.log(`[MESSAGE_SYNC] item error: ${outer?.message || outer}`);
363
565
  }
364
566
  }
365
567
  }
366
568
 
367
569
  static getThreadIdFromPath(path) {
368
- const itemMatch = path.match(/^\/direct_v2\/threads\/(\d+)/);
369
- if (itemMatch)
370
- return itemMatch[1];
371
- const inboxMatch = path.match(/^\/direct_v2\/inbox\/threads\/(\d+)/);
372
- if (inboxMatch)
373
- return inboxMatch[1];
570
+ if (!path) return undefined;
571
+ // Common patterns:
572
+ // /direct_v2/threads/<thread_id>/...
573
+ // /direct_v2/inbox/threads/<thread_id>/...
574
+ // /direct_v2/threads/<thread_id>
575
+ // possibly with trailing segments
576
+ try {
577
+ let m = path.match(/\/direct_v2\/threads\/(\d+)/);
578
+ if (m && m[1]) return m[1];
579
+ m = path.match(/\/direct_v2\/inbox\/threads\/(\d+)/);
580
+ if (m && m[1]) return m[1];
581
+ m = path.match(/\/direct_v2\/inbox\/(\d+)/);
582
+ if (m && m[1]) return m[1];
583
+ // last resort: look for any long numeric id in path
584
+ const anyId = path.match(/(\d{6,})/);
585
+ if (anyId && anyId[1]) return anyId[1];
586
+ } catch (e) {
587
+ // ignore
588
+ }
374
589
  return undefined;
375
590
  }
376
591
 
@@ -17,6 +17,7 @@ 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
+
20
21
  class RealtimeClient extends eventemitter3_1.EventEmitter {
21
22
  get mqtt() {
22
23
  return this._mqtt;
@@ -47,25 +48,75 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
47
48
  this.realtimeDebug(`Applying mixins: ${mixins.map(m => m.name).join(', ')}`);
48
49
  (0, mixins_1.applyMixins)(mixins, this, this.ig);
49
50
 
50
- // Auto-connect if session is available
51
+ // ---------- AUTO-CONNECT BLOCK (UPDATED to wait for MQTT creds like APK) ----------
52
+ // Keep folder same as before for compatibility, but WAIT until device/mqtt creds present
51
53
  const folder = './auth_info_ig';
52
54
  const { useMultiFileAuthState } = require('../useMultiFileAuthState');
53
55
  const fs = require('fs');
54
56
  const path = require('path');
57
+
58
+ // store optional attached authState for later use
59
+ this._attachedAuthState = null;
60
+
61
+ // helper: wait/poll until mqtt/device credentials available (CountDownLatch equivalent)
62
+ const waitForMqttCredentials = async (auth, timeoutMs = 15000, pollMs = 250) => {
63
+ const start = Date.now();
64
+ const hasCreds = () => {
65
+ try {
66
+ const d = (auth && typeof auth.getData === 'function') ? auth.getData() : (auth && auth.data ? auth.data : null);
67
+ if (!d) return false;
68
+ // Acceptable indicators: device.deviceSecret OR mqttAuth.jwt OR mqttAuth.deviceSecret
69
+ if (d.device && (d.device.deviceSecret || d.device.secret)) return true;
70
+ if (d.mqttAuth && (d.mqttAuth.jwt || d.mqttAuth.deviceSecret)) return true;
71
+ // fallback: creds/sessionid present (not ideal but better than nothing)
72
+ if (d.creds && (d.creds.sessionId || d.creds.csrfToken || d.creds.authorization)) return true;
73
+ return false;
74
+ } catch (e) {
75
+ return false;
76
+ }
77
+ };
78
+ // Poll until available or timeout
79
+ while (Date.now() - start < timeoutMs) {
80
+ if (hasCreds()) return true;
81
+ await new Promise(r => setTimeout(r, pollMs));
82
+ }
83
+ return false;
84
+ };
85
+
86
+ // Attempt auto-start only if creds.json exists — but wait for device/mqtt creds like APK does.
55
87
  if (fs.existsSync(path.join(folder, 'creds.json'))) {
56
88
  setTimeout(async () => {
57
89
  try {
58
90
  const auth = await useMultiFileAuthState(folder);
59
- if (auth.hasSession()) {
60
- console.log('[REALTIME] Auto-starting MQTT from constructor...');
61
- await auth.loadCreds(this.ig);
62
- await this.connectFromSavedSession(auth);
91
+ // attach for later use
92
+ this._attachedAuthState = auth;
93
+ if (auth.hasSession && auth.hasSession()) {
94
+ console.log('[REALTIME] Auto-start candidate session detected — loading creds...');
95
+ try {
96
+ await auth.loadCreds(this.ig);
97
+ } catch (e) {
98
+ // ignore load errors but log
99
+ console.warn('[REALTIME] loadCreds warning:', e?.message || e);
100
+ }
101
+ // Wait for MQTT/device credentials to be present (APK-like behaviour)
102
+ const ready = await waitForMqttCredentials(auth, 20000, 300);
103
+ if (!ready) {
104
+ console.warn('[REALTIME] MQTT/device credentials not found within timeout — auto-connect aborted (will still allow manual connect).');
105
+ return;
106
+ }
107
+ console.log('[REALTIME] Device/MQTT credentials present — attempting connectFromSavedSession...');
108
+ try {
109
+ await this.connectFromSavedSession(auth);
110
+ } catch (e) {
111
+ console.error('[REALTIME] Constructor auto-connect failed:', e?.message || e);
112
+ }
63
113
  }
64
114
  } catch (e) {
65
- console.error('[REALTIME] Constructor auto-connect failed:', e.message);
115
+ console.error('[REALTIME] Constructor auto-start exception:', e?.message || e);
66
116
  }
67
117
  }, 100);
68
118
  }
119
+ // ---------- END AUTO-CONNECT BLOCK ----------
69
120
  }
70
121
 
71
122
  /**
@@ -193,17 +244,32 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
193
244
  try {
194
245
  const authHeader = this.ig.state.authorization;
195
246
  if (!authHeader) return null;
196
- // Extract base64 part from "Bearer IGT:2:{base64}"
197
- const base64Part = authHeader.replace('Bearer IGT:2:', '').replace('Bearer ', '');
198
- // Decode from base64
199
- const decoded = Buffer.from(base64Part, 'base64').toString();
200
- const payload = JSON.parse(decoded);
201
- // Get sessionid and URL-decode it
202
- let sessionid = payload.sessionid;
203
- if (sessionid) {
204
- sessionid = decodeURIComponent(sessionid);
247
+ // Attempt to decode possible bearer formats safely
248
+ const raw = String(authHeader || '');
249
+ // strip known prefixes
250
+ const candidate = raw.replace(/^Bearer\s*/i, '').replace(/^IGT:2:/i, '');
251
+ // if contains '.', might be JWT-like (header.payload.sig)
252
+ if (candidate.includes('.')) {
253
+ const parts = candidate.split('.');
254
+ if (parts.length >= 2) {
255
+ try {
256
+ const payload = Buffer.from(parts[1], 'base64').toString('utf8');
257
+ const parsed = JSON.parse(payload);
258
+ return parsed.sessionid || parsed.session_id || parsed.session || null;
259
+ } catch (e) {
260
+ // ignore parse error
261
+ }
262
+ }
205
263
  }
206
- return sessionid || null;
264
+ // fallback: try base64 decode whole candidate
265
+ try {
266
+ const decoded = Buffer.from(candidate, 'base64').toString('utf8');
267
+ const parsed = JSON.parse(decoded);
268
+ return parsed.sessionid || parsed.session_id || null;
269
+ } catch (e) {
270
+ // ignore
271
+ }
272
+ return null;
207
273
  } catch (e) {
208
274
  return null;
209
275
  }
@@ -221,45 +287,106 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
221
287
 
222
288
  // Ensure deviceId is a string to avoid substring errors
223
289
  const deviceId = String(this.ig.state.phoneId || this.ig.state.deviceId || 'device_unknown');
224
- let sessionid;
225
- // First try: Extract from JWT authorization header (PRIMARY METHOD)
226
- sessionid = this.extractSessionIdFromJWT();
227
- if (sessionid) {
228
- this.realtimeDebug(`SessionID extracted from JWT: ${sessionid.substring(0, 20)}...`);
290
+
291
+ // Try multiple ways to determine sessionid/password and deviceSecret
292
+ let sessionid = null;
293
+ try {
294
+ // 1. Try extracting from JWT/Authorization header
295
+ sessionid = this.extractSessionIdFromJWT();
296
+ if (sessionid) {
297
+ this.realtimeDebug(`SessionID extracted from JWT-like auth: ${String(sessionid).substring(0, 20)}...`);
298
+ }
299
+ } catch (e) {
300
+ sessionid = null;
229
301
  }
230
- // Second try: Direct cookie lookup
302
+
303
+ // 2. Try state helpers (some libs expose extractCookieValue)
231
304
  if (!sessionid) {
232
305
  try {
233
- sessionid = this.ig.state.extractCookieValue('sessionid');
306
+ if (typeof this.ig.state.extractCookieValue === 'function') {
307
+ sessionid = this.ig.state.extractCookieValue('sessionid');
308
+ }
234
309
  } catch (e) {
235
310
  sessionid = null;
236
311
  }
237
312
  }
238
- // Third try: Parsed authorization
313
+
314
+ // 3. Try raw state fields
239
315
  if (!sessionid) {
240
316
  try {
241
- sessionid = this.ig.state.parsedAuthorization?.sessionid;
242
- } catch (e2) {
317
+ sessionid = this.ig.state.sessionId || this.ig.state.sessionid || this.ig.state.cookies?.sessionid || null;
318
+ } catch (e) {
243
319
  sessionid = null;
244
320
  }
245
321
  }
246
- // Fourth try: CookieJar introspection
322
+
323
+ // 4. Try cookieJar inspection (best effort)
247
324
  if (!sessionid) {
248
325
  try {
249
- const cookies = this.ig.state.cookieJar.getCookiesSync('https://i.instagram.com/');
250
- const sessionCookie = cookies.find(c => c.key === 'sessionid');
251
- sessionid = sessionCookie?.value;
326
+ if (this.ig.state.cookieJar && typeof this.ig.state.cookieJar.getCookiesSync === 'function') {
327
+ const cookies = this.ig.state.cookieJar.getCookiesSync('https://i.instagram.com/') || [];
328
+ const found = cookies.find(c => (c.key === 'sessionid' || c.name === 'sessionid'));
329
+ if (found) sessionid = found.value;
330
+ }
252
331
  } catch (e) {
253
- sessionid = null;
332
+ // ignore
254
333
  }
255
334
  }
256
- // Last resort: Generate from userId + timestamp
335
+
336
+ // 5. Last resort fallback (generated)
257
337
  if (!sessionid) {
258
338
  const userId = this.ig.state.cookieUserId || this.ig.state.userId || '0';
259
339
  sessionid = String(userId) + '_' + Date.now();
260
340
  this.realtimeDebug(`SessionID generated (fallback): ${sessionid}`);
261
341
  }
262
- const password = `sessionid=${sessionid}`;
342
+
343
+ // Determine deviceSecret if available via attached authState or ig.state
344
+ let deviceSecret = null;
345
+ try {
346
+ if (this._attachedAuthState && typeof this._attachedAuthState.getData === 'function') {
347
+ const d = this._attachedAuthState.getData();
348
+ if (d && d.device && (d.device.deviceSecret || d.device.secret)) {
349
+ deviceSecret = d.device.deviceSecret || d.device.secret;
350
+ }
351
+ // also check mqttAuth
352
+ if (!deviceSecret && d && d.mqttAuth && (d.mqttAuth.deviceSecret || d.mqttAuth.secret)) {
353
+ deviceSecret = d.mqttAuth.deviceSecret || d.mqttAuth.secret;
354
+ }
355
+ }
356
+ } catch (e) {}
357
+ // also try ig.state
358
+ try {
359
+ if (!deviceSecret && (this.ig.state.deviceSecret || this.ig.state.mqttDeviceSecret)) {
360
+ deviceSecret = this.ig.state.deviceSecret || this.ig.state.mqttDeviceSecret;
361
+ }
362
+ } catch (e) {}
363
+
364
+ // Determine mqttAuth token if present
365
+ let mqttJwt = null;
366
+ try {
367
+ if (this._attachedAuthState && typeof this._attachedAuthState.getData === 'function') {
368
+ const d = this._attachedAuthState.getData();
369
+ if (d && d.mqttAuth && d.mqttAuth.jwt) mqttJwt = d.mqttAuth.jwt;
370
+ }
371
+ // fallback to ig.state
372
+ if (!mqttJwt && this.ig.state.mqttJwt) mqttJwt = this.ig.state.mqttJwt;
373
+ } catch (e) {}
374
+
375
+ // Build password: prefer mqttJwt if present, else sessionid style string
376
+ let password;
377
+ if (mqttJwt) {
378
+ password = `jwt=${mqttJwt}`;
379
+ } else {
380
+ password = `sessionid=${sessionid}`;
381
+ }
382
+
383
+ // Determine clientType and other clientInfo modifications if secure deviceSecret exists
384
+ const clientType = deviceSecret ? 'secure_cookie_auth' : 'cookie_auth';
385
+
386
+ const clientMqttSessionId = (BigInt(Date.now()) & BigInt(0xffffffff));
387
+
388
+ const subscribeTopics = [88, 135, 149, 150, 133, 146];
389
+
263
390
  this.connection = new mqttot_1.MQTToTConnection({
264
391
  clientIdentifier: deviceId.substring(0, 20),
265
392
  clientInfo: {
@@ -274,11 +401,11 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
274
401
  isInitiallyForeground: true,
275
402
  networkType: 1,
276
403
  networkSubtype: 0,
277
- clientMqttSessionId: BigInt(Date.now()) & BigInt(0xffffffff),
278
- subscribeTopics: [88, 135, 149, 150, 133, 146],
279
- clientType: 'cookie_auth',
404
+ clientMqttSessionId: clientMqttSessionId,
405
+ subscribeTopics,
406
+ clientType,
280
407
  appId: BigInt(567067343352427),
281
- deviceSecret: '',
408
+ deviceSecret: deviceSecret || '',
282
409
  clientStack: 3,
283
410
  ...(this.initOptions?.connectOverrides || {}),
284
411
  },
@@ -390,6 +517,11 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
390
517
 
391
518
  console.log('[RealtimeClient] Connecting from saved session...');
392
519
 
520
+ // Attach authState to this instance so constructConnection can read deviceSecret/mqttAuth
521
+ try {
522
+ this._attachedAuthState = authStateHelper;
523
+ } catch (e) {}
524
+
393
525
  const savedOptions = authStateHelper.getMqttConnectOptions?.();
394
526
 
395
527
  const connectOptions = {
@@ -405,6 +537,18 @@ class RealtimeClient extends eventemitter3_1.EventEmitter {
405
537
  hasIrisData: !!connectOptions.irisData
406
538
  });
407
539
 
540
+ // If authState has mqttAuth with expiresAt, optionally warn if expired (non-fatal)
541
+ try {
542
+ const d = authStateHelper.getData?.() || authStateHelper.data || {};
543
+ const mqttAuth = d.mqttAuth || null;
544
+ if (mqttAuth && mqttAuth.expiresAt) {
545
+ const t = new Date(mqttAuth.expiresAt).getTime();
546
+ if (!isNaN(t) && Date.now() > t) {
547
+ console.warn('[RealtimeClient] Warning: saved mqttAuth token appears expired.');
548
+ }
549
+ }
550
+ } catch (e) {}
551
+
408
552
  await this.connect(connectOptions);
409
553
 
410
554
  if (authStateHelper.saveMqttSession) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-insta-private-api-mqtt",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
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": {