nodejs-insta-private-api-mqtt 1.2.10 → 1.3.11

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,22 @@ 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 or milliseconds)
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
+
9
25
  class MessageSyncMixin extends mixin_1.Mixin {
10
26
  constructor() {
11
27
  super();
@@ -36,10 +52,17 @@ class MessageSyncMixin extends mixin_1.Mixin {
36
52
  client.mqtt.listen({
37
53
  topic: constants_1.Topics.MESSAGE_SYNC.id,
38
54
  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;
55
+ try {
56
+ const unzipped = await (0, shared_1.tryUnzipAsync)(payload);
57
+ const parsed = constants_1.Topics.MESSAGE_SYNC.parser
58
+ .parseMessage(constants_1.Topics.MESSAGE_SYNC, unzipped)
59
+ .map(msg => msg.data);
60
+ return parsed;
61
+ } catch (err) {
62
+ // If transformer fails, return empty array so handler is tolerant
63
+ console.warn('[MESSAGE_SYNC] transformer parse failed:', err?.message || err);
64
+ return [];
65
+ }
43
66
  },
44
67
  }, data => {
45
68
  this.handleMessageSync(client, data);
@@ -47,9 +70,13 @@ class MessageSyncMixin extends mixin_1.Mixin {
47
70
  } else {
48
71
  console.log(`[MESSAGE_SYNC] mqtt.listen() NOT FOUND - using fallback 'receive' event`);
49
72
  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);
73
+ try {
74
+ if (topic.id === constants_1.Topics.MESSAGE_SYNC.id) {
75
+ const data = messages.map(m => m.data);
76
+ this.handleMessageSync(client, data);
77
+ }
78
+ } catch (err) {
79
+ console.warn('[MESSAGE_SYNC] receive fallback handler error:', err?.message || err);
53
80
  }
54
81
  });
55
82
  }
@@ -67,16 +94,26 @@ class MessageSyncMixin extends mixin_1.Mixin {
67
94
  }
68
95
 
69
96
  if (this.pendingUserFetches.has(userIdStr)) {
70
- return await this.pendingUserFetches.get(userIdStr);
97
+ try {
98
+ return await this.pendingUserFetches.get(userIdStr);
99
+ } catch (e) {
100
+ // if pending fetch failed, continue to fresh attempt
101
+ }
71
102
  }
72
103
 
73
104
  const fetchPromise = (async () => {
74
105
  try {
106
+ // small backoff to avoid immediate burst of parallel requests
107
+ await new Promise(r => setTimeout(r, 120));
75
108
  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;
109
+ try {
110
+ const userInfo = await client.ig.user.info(userIdStr);
111
+ if (userInfo && userInfo.username) {
112
+ this.userCache.set(userIdStr, userInfo.username);
113
+ return userInfo.username;
114
+ }
115
+ } catch (innerErr) {
116
+ // rate-limited or not found - swallow
80
117
  }
81
118
  }
82
119
  } catch (err) {
@@ -96,158 +133,171 @@ class MessageSyncMixin extends mixin_1.Mixin {
96
133
  let content = '';
97
134
  let mediaInfo = '';
98
135
 
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`;
136
+ try {
137
+ switch (itemType) {
138
+ case 'text':
139
+ content = msgValue.text || msgValue.body || '';
140
+ break;
141
+
142
+ case 'media':
143
+ case 'raven_media':
144
+ content = '[PHOTO/VIDEO]';
145
+ if (msgValue.media) {
146
+ const media = msgValue.media;
147
+ if (media.image_versions2) {
148
+ content = '[PHOTO]';
149
+ mediaInfo = ` URL: ${media.image_versions2?.candidates?.[0]?.url || 'N/A'}`;
150
+ } else if (media.video_versions) {
151
+ content = '[VIDEO]';
152
+ mediaInfo = ` Duration: ${media.video_duration || 'N/A'}s`;
153
+ }
115
154
  }
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)}..."`;
155
+ if (msgValue.visual_media) {
156
+ content = '[DISAPPEARING MEDIA]';
144
157
  }
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}"`;
158
+ break;
159
+
160
+ case 'voice_media':
161
+ content = '[VOICE MESSAGE]';
162
+ if (msgValue.voice_media?.media?.audio) {
163
+ const duration = msgValue.voice_media.media.audio.duration || 0;
164
+ content = `[VOICE MESSAGE] Duration: ${duration}ms`;
155
165
  }
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}"`;
166
+ break;
167
+
168
+ case 'animated_media':
169
+ content = '[GIF]';
170
+ if (msgValue.animated_media?.images?.fixed_height?.url) {
171
+ mediaInfo = ` URL: ${msgValue.animated_media.images.fixed_height.url}`;
166
172
  }
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;
173
+ break;
174
+
175
+ case 'media_share':
176
+ content = '[SHARED POST]';
177
+ if (msgValue.media_share) {
178
+ const share = msgValue.media_share;
179
+ content = `[SHARED POST] From: @${share.user?.username || 'unknown'}`;
180
+ if (share.caption?.text) {
181
+ content += ` Caption: "${String(share.caption.text).substring(0, 50)}..."`;
182
+ }
183
+ }
184
+ break;
185
+
186
+ case 'reel_share':
187
+ content = '[SHARED REEL]';
188
+ if (msgValue.reel_share) {
189
+ const reel = msgValue.reel_share;
190
+ content = `[SHARED REEL] From: @${reel.media?.user?.username || 'unknown'}`;
191
+ if (reel.text) {
192
+ content += ` Text: "${reel.text}"`;
193
+ }
194
+ }
195
+ break;
196
+
197
+ case 'story_share':
198
+ content = '[SHARED STORY]';
199
+ if (msgValue.story_share) {
200
+ const story = msgValue.story_share;
201
+ content = `[SHARED STORY] From: @${story.media?.user?.username || 'unknown'}`;
202
+ if (story.message) {
203
+ content += ` Message: "${story.message}"`;
204
+ }
205
+ }
206
+ break;
207
+
208
+ case 'felix_share':
209
+ content = '[SHARED IGTV/VIDEO]';
210
+ if (msgValue.felix_share?.video) {
211
+ content = `[SHARED IGTV] Title: "${msgValue.felix_share.video.title || 'N/A'}"`;
212
+ }
213
+ break;
214
+
215
+ case 'clip':
216
+ content = '[SHARED CLIP]';
217
+ if (msgValue.clip?.clip) {
218
+ content = `[SHARED CLIP] From: @${msgValue.clip.clip.user?.username || 'unknown'}`;
219
+ }
220
+ break;
221
+
222
+ case 'profile':
223
+ content = '[SHARED PROFILE]';
224
+ if (msgValue.profile) {
225
+ content = `[SHARED PROFILE] @${msgValue.profile.username || 'unknown'}`;
226
+ }
227
+ break;
228
+
229
+ case 'location':
230
+ content = '[LOCATION]';
231
+ if (msgValue.location) {
232
+ content = `[LOCATION] ${msgValue.location.name || msgValue.location.address || 'Unknown location'}`;
233
+ }
234
+ break;
235
+
236
+ case 'hashtag':
237
+ content = '[HASHTAG]';
238
+ if (msgValue.hashtag) {
239
+ content = `[HASHTAG] #${msgValue.hashtag.name || 'unknown'}`;
240
+ }
241
+ break;
242
+
243
+ case 'like':
244
+ content = '[LIKE]';
245
+ break;
246
+
247
+ case 'link':
248
+ content = '[LINK]';
249
+ if (msgValue.link) {
250
+ content = `[LINK] ${msgValue.link.text || msgValue.link.link_url || 'N/A'}`;
251
+ }
252
+ break;
253
+
254
+ case 'action_log':
255
+ content = '[ACTION]';
256
+ if (msgValue.action_log) {
257
+ content = `[ACTION] ${msgValue.action_log.description || 'N/A'}`;
258
+ }
259
+ break;
260
+
261
+ case 'placeholder':
262
+ content = '[PLACEHOLDER]';
263
+ if (msgValue.placeholder?.message) {
264
+ content = `[PLACEHOLDER] ${msgValue.placeholder.message}`;
265
+ }
266
+ break;
267
+
268
+ case 'xma':
269
+ case 'xma_media_share':
270
+ content = '[XMA SHARE]';
271
+ if (msgValue.xma_link_url) {
272
+ content = `[XMA SHARE] ${msgValue.xma_link_url}`;
273
+ }
274
+ break;
275
+
276
+ case 'video_call_event':
277
+ content = '[VIDEO CALL EVENT]';
278
+ if (msgValue.video_call_event) {
279
+ content = `[VIDEO CALL] ${msgValue.video_call_event.action || 'event'}`;
280
+ }
281
+ break;
282
+
283
+ default:
284
+ if (msgValue && (msgValue.text || msgValue.body)) {
285
+ content = msgValue.text || msgValue.body;
286
+ } else {
287
+ content = `[${(itemType || 'UNKNOWN').toUpperCase()}]`;
288
+ }
289
+ }
290
+ } catch (e) {
291
+ // defensive fallback
292
+ try {
293
+ if (msgValue && (msgValue.text || msgValue.body)) {
294
+ content = msgValue.text || msgValue.body;
248
295
  } else {
249
296
  content = `[${(itemType || 'UNKNOWN').toUpperCase()}]`;
250
297
  }
298
+ } catch (e2) {
299
+ content = `[${(itemType || 'UNKNOWN').toUpperCase()}]`;
300
+ }
251
301
  }
252
302
 
253
303
  return content + mediaInfo;
@@ -255,6 +305,16 @@ class MessageSyncMixin extends mixin_1.Mixin {
255
305
 
256
306
  formatMessageForConsole(msgData) {
257
307
  const separator = '----------------------------------------';
308
+ // robust timestamp formatting
309
+ let ts = 'N/A';
310
+ try {
311
+ if (msgData.timestamp) {
312
+ const t = this.parseTimestamp(msgData.timestamp);
313
+ if (t) ts = new Date(t).toISOString();
314
+ }
315
+ } catch (e) {
316
+ ts = 'N/A';
317
+ }
258
318
  const lines = [
259
319
  '',
260
320
  separator,
@@ -266,14 +326,31 @@ class MessageSyncMixin extends mixin_1.Mixin {
266
326
  `Type: ${msgData.itemType || 'text'}`,
267
327
  `Thread: ${msgData.threadId || 'unknown'}`,
268
328
  `Message ID: ${msgData.messageId || 'unknown'}`,
269
- `Timestamp: ${msgData.timestamp ? new Date(parseInt(msgData.timestamp) / 1000).toISOString() : 'N/A'}`,
270
- `Status: ${msgData.status || 'good'}`,
329
+ `Timestamp: ${ts}`,
330
+ `Status: ${msgData.status || 'unknown'}`,
271
331
  separator,
272
332
  ''
273
333
  ];
274
334
  return lines.join('\n');
275
335
  }
276
336
 
337
+ parseTimestamp(ts) {
338
+ // Accept numeric seconds or milliseconds, or numeric-like string
339
+ try {
340
+ if (ts === undefined || ts === null) return null;
341
+ const n = Number(ts);
342
+ if (isNaN(n)) return null;
343
+ // heuristics: if > 10^12 -> already ms; if between 10^9 .. 10^12 -> probably seconds -> convert
344
+ if (n > 1e12) return n; // ms
345
+ if (n > 1e9) return n * 1000; // sec -> ms
346
+ if (n > 1e6) return n * 1000; // fallback seconds-ish
347
+ // otherwise treat as ms fallback
348
+ return n;
349
+ } catch (e) {
350
+ return null;
351
+ }
352
+ }
353
+
277
354
  async handleMessageSync(client, syncData) {
278
355
  if (!syncData || !Array.isArray(syncData)) {
279
356
  console.log(`[MESSAGE_SYNC] No sync data received`);
@@ -281,96 +358,184 @@ class MessageSyncMixin extends mixin_1.Mixin {
281
358
  }
282
359
 
283
360
  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 });
361
+ try {
362
+ const data = element.data;
363
+
364
+ if (!data) {
365
+ // fallback: emit iris with original element
366
+ client.emit('iris', element);
296
367
  continue;
297
368
  }
298
369
 
299
- if (e.path.startsWith('/direct_v2/threads') && e.value) {
370
+ // ensure element.data removed in downstream parsed message (keeps original behavior)
371
+ delete element.data;
372
+
373
+ for (const e of data) {
300
374
  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);
375
+ // tolerant handling: e.value may be string, object, null, or already parsed
376
+ let parsedValue = {};
377
+ if (e.value === undefined || e.value === null) {
378
+ parsedValue = {};
379
+ } else if (typeof e.value === 'string') {
380
+ const str = e.value.trim();
381
+ if (str.length === 0) {
382
+ parsedValue = {};
383
+ } else {
384
+ try {
385
+ parsedValue = JSON.parse(str);
386
+ } catch (errJson) {
387
+ // If not JSON, attempt basic fallback (sometimes server sends plain key=value or quoted)
388
+ try {
389
+ // try to safe-evaluate limited forms like a bare object without quotes (rare)
390
+ parsedValue = {};
391
+ } catch (err2) {
392
+ parsedValue = {};
393
+ }
394
+ }
395
+ }
396
+ } else if (typeof e.value === 'object') {
397
+ parsedValue = e.value;
398
+ } else {
399
+ parsedValue = {};
310
400
  }
311
- if (!username) {
312
- username = `user_${userId || 'unknown'}`;
401
+
402
+ // Sometimes the message payload is nested under 'message' or similar
403
+ const msgValue = parsedValue.message || parsedValue.data || parsedValue || {};
404
+
405
+ if (!e.path) {
406
+ // no path means iris-like delta; merge element + e
407
+ client.emit('iris', { ...element, ...e, value: msgValue });
408
+ continue;
313
409
  }
314
410
 
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);
411
+ // normalize path check for thread messages
412
+ if ((e.path && e.path.startsWith('/direct_v2/threads')) ||
413
+ (e.path && e.path.startsWith('/direct_v2/inbox/threads')) ||
414
+ (e.path && e.path.indexOf('/direct_v2/threads/') !== -1) ) {
415
+
416
+ if (msgValue && (msgValue.item_type || msgValue.itemType || msgValue.type || msgValue.msg_type)) {
417
+ // determine item type as robustly as possible
418
+ const itemType = msgValue.item_type || msgValue.itemType || msgValue.type || msgValue.msg_type || 'text';
419
+
420
+ // thread id extraction
421
+ const threadId = MessageSyncMixin.getThreadIdFromPath(e.path);
422
+
423
+ // user id resolution: try many possible fields
424
+ const userId = msgValue.user_id || msgValue.from_user_id || msgValue.sender_id || msgValue.userId || msgValue.senderId || null;
425
+
426
+ // username resolution: prefer embedded username, otherwise fetch
427
+ let username = msgValue.username || msgValue.from_username || null;
428
+ if (!username && userId) {
429
+ try {
430
+ username = await this.getUsernameFromId(client, userId);
431
+ } catch (ux) {
432
+ username = null;
433
+ }
434
+ }
435
+ if (!username) {
436
+ username = `user_${userId || 'unknown'}`;
437
+ }
438
+
439
+ const textContent = this.extractMessageContent(msgValue, itemType);
440
+
441
+ const messageId = msgValue.item_id || msgValue.id || msgValue.client_context || msgValue.client_context_id || msgValue.message_id || msgValue.messageId || null;
442
+ const timestamp = msgValue.timestamp || msgValue.ts || msgValue.client_time || null;
443
+
444
+ // determine status based on whether message author is the logged-in account
445
+ let status = 'received';
446
+ try {
447
+ const ownId = client?.ig?.state?.cookieUserId || client?.ig?.state?.userId || null;
448
+ if (ownId && userId && String(userId) === String(ownId)) {
449
+ status = 'sent';
450
+ } else {
451
+ status = 'received';
452
+ }
453
+ } catch (stErr) {
454
+ status = 'received';
455
+ }
456
+
457
+ const msgData = {
458
+ username: username,
459
+ userId: userId,
460
+ text: textContent,
461
+ itemType: itemType,
462
+ threadId: threadId,
463
+ messageId: messageId,
464
+ timestamp: timestamp,
465
+ status: status,
466
+ rawData: msgValue
467
+ };
468
+
469
+ // console output (keeps original formatted block)
470
+ try {
471
+ console.log(this.formatMessageForConsole(msgData));
472
+ } catch (eLog) {
473
+ // don't let logging break processing
474
+ }
475
+
476
+ const parsedMessage = {
477
+ ...element,
478
+ message: {
479
+ path: e.path,
480
+ op: e.op,
481
+ thread_id: threadId,
482
+ ...msgValue,
483
+ },
484
+ parsed: msgData
485
+ };
486
+
487
+ client.emit('message', parsedMessage);
488
+ continue;
489
+ } // end if msgValue has item_type
490
+ } // end if path matches threads
343
491
 
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}`);
492
+ // If not a thread message, emit as threadUpdate or iris depending on payload
493
+ try {
494
+ const updateValue = e.value ? (typeof e.value === 'string' ? (() => {
495
+ try { return JSON.parse(e.value); } catch { return e.value; }
496
+ })() : e.value) : {};
497
+ client.emit('threadUpdate', {
498
+ ...element,
499
+ meta: {
500
+ path: e.path,
501
+ op: e.op,
502
+ thread_id: MessageSyncMixin.getThreadIdFromPath(e.path),
503
+ },
504
+ update: updateValue,
505
+ });
506
+ } catch (errUpdate) {
507
+ client.emit('iris', { ...element, ...e, value: parsedValue });
508
+ }
509
+ } catch (inner) {
510
+ console.log(`[MESSAGE_SYNC] element handling error: ${inner?.message || inner}`);
361
511
  }
362
512
  }
513
+ } catch (outer) {
514
+ console.log(`[MESSAGE_SYNC] item error: ${outer?.message || outer}`);
363
515
  }
364
516
  }
365
517
  }
366
518
 
367
519
  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];
520
+ if (!path) return undefined;
521
+ // Common patterns:
522
+ // /direct_v2/threads/<thread_id>/...
523
+ // /direct_v2/inbox/threads/<thread_id>/...
524
+ // /direct_v2/threads/<thread_id>
525
+ // possibly with trailing segments
526
+ try {
527
+ let m = path.match(/\/direct_v2\/threads\/(\d+)/);
528
+ if (m && m[1]) return m[1];
529
+ m = path.match(/\/direct_v2\/inbox\/threads\/(\d+)/);
530
+ if (m && m[1]) return m[1];
531
+ m = path.match(/\/direct_v2\/inbox\/(\d+)/);
532
+ if (m && m[1]) return m[1];
533
+ // last resort: look for any long numeric id in path
534
+ const anyId = path.match(/(\d{6,})/);
535
+ if (anyId && anyId[1]) return anyId[1];
536
+ } catch (e) {
537
+ // ignore
538
+ }
374
539
  return undefined;
375
540
  }
376
541