nodejs-insta-private-api-mqtt 1.3.14 → 1.3.16

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.
@@ -14,6 +14,57 @@ class EnhancedDirectCommands {
14
14
  this.enhancedDebug = (0, shared_1.debugChannel)('realtime', 'enhanced-commands');
15
15
  }
16
16
 
17
+ // ---------- HELPERS ----------
18
+ async _publishMqttCommand(commandObj) {
19
+ // helper: compress + publish to SEND_MESSAGE topic
20
+ const mqtt = this.realtimeClient.mqtt || this.realtimeClient._mqtt;
21
+ if (!mqtt || typeof mqtt.publish !== 'function') {
22
+ throw new Error('MQTT client not available');
23
+ }
24
+ const json = JSON.stringify(commandObj);
25
+ const { compressDeflate } = shared_1;
26
+ const payload = await compressDeflate(json);
27
+ this.enhancedDebug(`Publishing to MQTT topic ${constants_1.Topics.SEND_MESSAGE.id} payload=${json}`);
28
+ const result = await mqtt.publish({
29
+ topic: constants_1.Topics.SEND_MESSAGE.id,
30
+ qosLevel: 1,
31
+ payload: payload,
32
+ });
33
+ return result;
34
+ }
35
+
36
+ /**
37
+ * Publish a media reference via MQTT (send_item with upload_id)
38
+ * This tells the server: "I have upload_id X, attach it to thread Y"
39
+ */
40
+ async sendMediaViaMqtt({ threadId, uploadId, clientContext, text, mediaId }) {
41
+ this.enhancedDebug(`sendMediaViaMqtt: thread=${threadId} upload_id=${uploadId}`);
42
+ if (!threadId) throw new Error('threadId is required');
43
+ if (!uploadId) throw new Error('uploadId is required');
44
+
45
+ const ctx = clientContext || (0, uuid_1.v4)();
46
+ const command = {
47
+ action: 'send_item',
48
+ thread_id: threadId,
49
+ item_type: 'media',
50
+ upload_id: String(uploadId),
51
+ client_context: ctx,
52
+ timestamp: Date.now(),
53
+ };
54
+ // optional fields if present
55
+ if (typeof text === 'string' && text.length > 0) {
56
+ // Instagram sometimes keeps a caption in the media message; include as text
57
+ command.text = text;
58
+ }
59
+ if (mediaId) {
60
+ // sometimes media_id is sent along, include if present
61
+ command.media_id = mediaId;
62
+ }
63
+
64
+ return this._publishMqttCommand(command);
65
+ }
66
+
67
+ // ---------- TEXT / BASIC COMMANDS (unchanged behavior) ----------
17
68
  /**
18
69
  * Send text via MQTT with proper payload format
19
70
  */
@@ -502,47 +553,108 @@ class EnhancedDirectCommands {
502
553
  }
503
554
 
504
555
  /**
505
- * Send media (image/video) via MQTT
556
+ * Send media (image/video)
557
+ *
558
+ * Behavior:
559
+ * - if mediaBuffer provided -> perform rupload (REST) to get upload_id, then send MQTT send_item with upload_id (no REST broadcast)
560
+ * - if only uploadId provided -> send MQTT send_item with upload_id
506
561
  */
507
- async sendMedia({ text, mediaId, threadId, clientContext, uploadId }) {
508
- this.enhancedDebug(`Sending media ${mediaId} (uploadId: ${uploadId}) to ${threadId} via MQTT`);
509
-
562
+ async sendMedia({ text, mediaId, threadId, clientContext, uploadId, mediaBuffer, mimeType = 'image/jpeg', duration = 0, width = 720, height = 1280 }) {
563
+ this.enhancedDebug(`sendMedia called for thread ${threadId} (will rupload if buffer provided)`);
510
564
  try {
511
- const mqtt = this.realtimeClient.mqtt || this.realtimeClient._mqtt;
512
- if (!mqtt || typeof mqtt.publish !== 'function') {
513
- throw new Error('MQTT client not available');
565
+ if (!threadId) {
566
+ throw new Error('threadId is required');
514
567
  }
515
-
516
- const ctx = clientContext || (0, uuid_1.v4)();
517
- const command = {
518
- action: 'send_item',
519
- thread_id: threadId,
520
- item_type: 'media',
521
- media_id: mediaId,
522
- upload_id: uploadId || mediaId,
523
- text: text || '',
524
- timestamp: Date.now(),
525
- client_context: ctx,
526
- };
527
-
528
- // Log payload for debugging
529
- this.enhancedDebug(`Payload: ${JSON.stringify(command)}`);
530
-
531
- const json = JSON.stringify(command);
532
- const { compressDeflate } = shared_1;
533
- const payload = await compressDeflate(json);
534
-
535
- this.enhancedDebug(`Publishing media to MQTT topic ${constants_1.Topics.SEND_MESSAGE.id}`);
536
- const result = await mqtt.publish({
537
- topic: constants_1.Topics.SEND_MESSAGE.id,
538
- qosLevel: 1,
539
- payload: payload,
568
+
569
+ const ig = this.realtimeClient.ig;
570
+ if (!ig || !ig.request) {
571
+ throw new Error('Instagram client not available. Make sure you are logged in.');
572
+ }
573
+
574
+ let serverUploadId = uploadId || null;
575
+ const isVideo = mimeType && mimeType.startsWith('video/');
576
+
577
+ if (mediaBuffer) {
578
+ if (!Buffer.isBuffer(mediaBuffer) || mediaBuffer.length === 0) {
579
+ throw new Error('mediaBuffer must be a non-empty Buffer when provided');
580
+ }
581
+
582
+ // generate upload id and object name
583
+ serverUploadId = serverUploadId || Date.now().toString();
584
+ const objectName = `${(0, uuid_1.v4)()}.${isVideo ? 'mp4' : (mimeType === 'image/png' ? 'png' : 'jpg')}`;
585
+
586
+ const ruploadParams = isVideo
587
+ ? {
588
+ upload_id: serverUploadId,
589
+ media_type: 2,
590
+ xsharing_user_ids: JSON.stringify([]),
591
+ upload_media_duration_ms: Math.round(duration * 1000),
592
+ upload_media_width: width,
593
+ upload_media_height: height,
594
+ }
595
+ : {
596
+ upload_id: serverUploadId,
597
+ media_type: 1,
598
+ image_compression: '{"lib_name":"moz","lib_version":"3.1.m","quality":"80"}',
599
+ xsharing_user_ids: JSON.stringify([]),
600
+ is_clips_media: false,
601
+ };
602
+
603
+ const uploadHeaders = {
604
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
605
+ 'Content-Type': mimeType,
606
+ 'X-Entity-Type': mimeType,
607
+ 'X-Entity-Length': String(mediaBuffer.length),
608
+ 'Content-Length': String(mediaBuffer.length),
609
+ };
610
+
611
+ if (isVideo) {
612
+ uploadHeaders['X_FB_VIDEO_WATERFALL_ID'] = (0, uuid_1.v4)();
613
+ uploadHeaders['Offset'] = '0';
614
+ } else {
615
+ uploadHeaders['X_FB_PHOTO_WATERFALL_ID'] = (0, uuid_1.v4)();
616
+ }
617
+
618
+ const uploadUrl = isVideo ? `/rupload_igvideo/${objectName}` : `/rupload_igphoto/${objectName}`;
619
+
620
+ this.enhancedDebug(`Uploading media to ${uploadUrl} (${mediaBuffer.length} bytes)`);
621
+ try {
622
+ const uploadResponse = await ig.request.send({
623
+ url: uploadUrl,
624
+ method: 'POST',
625
+ headers: uploadHeaders,
626
+ body: mediaBuffer,
627
+ });
628
+
629
+ if (uploadResponse && typeof uploadResponse === 'object' && uploadResponse.upload_id) {
630
+ serverUploadId = uploadResponse.upload_id;
631
+ }
632
+ this.enhancedDebug(`✅ Media uploaded (serverUploadId=${serverUploadId})`);
633
+ } catch (uploadErr) {
634
+ this.enhancedDebug(`Upload error: ${uploadErr.message}`);
635
+ throw new Error(`Media upload failed: ${uploadErr.message}`);
636
+ }
637
+ }
638
+
639
+ // now we must have upload id
640
+ if (!serverUploadId) {
641
+ throw new Error('uploadId is required if mediaBuffer is not provided');
642
+ }
643
+
644
+ // Publish MQTT reference (no REST broadcast)
645
+ const publishResult = await this.sendMediaViaMqtt({
646
+ threadId,
647
+ uploadId: serverUploadId,
648
+ clientContext,
649
+ text,
650
+ mediaId,
540
651
  });
541
-
542
- this.enhancedDebug(`✅ Media sent via MQTT!`);
543
- return result;
652
+
653
+ this.enhancedDebug(`✅ Media reference published via MQTT for upload_id=${serverUploadId}`);
654
+ return publishResult;
655
+
544
656
  } catch (err) {
545
- this.enhancedDebug(`Media send failed: ${err.message}`);
657
+ this.enhancedDebug(`sendMedia failed: ${err.message}`);
546
658
  throw err;
547
659
  }
548
660
  }
@@ -756,18 +868,11 @@ class EnhancedDirectCommands {
756
868
  }
757
869
 
758
870
  /**
759
- * Send photo via Realtime (Upload + Broadcast)
760
- * This method uploads the photo first, then broadcasts it to the thread
761
- *
762
- * @param {Object} options - Photo sending options
763
- * @param {Buffer} options.photoBuffer - Image buffer (JPEG/PNG)
764
- * @param {string} options.threadId - Thread ID to send to
765
- * @param {string} [options.caption] - Optional caption
766
- * @param {string} [options.mimeType='image/jpeg'] - MIME type
767
- * @param {string} [options.clientContext] - Optional client context
871
+ * Send photo via Realtime (Upload + MQTT-ref)
872
+ * Uploads the photo via rupload, then sends MQTT send_item with upload_id.
768
873
  */
769
874
  async sendPhotoViaRealtime({ photoBuffer, threadId, caption = '', mimeType = 'image/jpeg', clientContext }) {
770
- this.enhancedDebug(`Sending photo to thread ${threadId} via Realtime`);
875
+ this.enhancedDebug(`Sending photo to thread ${threadId} via Realtime (rupload + mqtt-ref)`);
771
876
 
772
877
  try {
773
878
  // Validate inputs
@@ -778,87 +883,15 @@ class EnhancedDirectCommands {
778
883
  throw new Error('threadId is required');
779
884
  }
780
885
 
781
- // Get the ig client from realtime client
782
- const ig = this.realtimeClient.ig;
783
- if (!ig || !ig.request) {
784
- throw new Error('Instagram client not available. Make sure you are logged in.');
785
- }
786
-
787
- // Step 1: Upload photo using rupload endpoint
788
- this.enhancedDebug(`Step 1: Uploading photo (${photoBuffer.length} bytes)...`);
789
-
790
- const uploadId = Date.now().toString();
791
- const objectName = `${(0, uuid_1.v4)()}.${mimeType === 'image/png' ? 'png' : 'jpg'}`;
792
-
793
- const isJpeg = mimeType === 'image/jpeg' || mimeType === 'image/jpg';
794
- const compression = isJpeg
795
- ? '{"lib_name":"moz","lib_version":"3.1.m","quality":"80"}'
796
- : '{"lib_name":"png","lib_version":"1.0","quality":"100"}';
797
-
798
- const ruploadParams = {
799
- upload_id: uploadId,
800
- media_type: 1,
801
- image_compression: compression,
802
- xsharing_user_ids: JSON.stringify([]),
803
- is_clips_media: false,
804
- };
805
-
806
- const uploadHeaders = {
807
- 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
808
- 'Content-Type': mimeType,
809
- 'X_FB_PHOTO_WATERFALL_ID': (0, uuid_1.v4)(),
810
- 'X-Entity-Type': mimeType,
811
- 'X-Entity-Length': String(photoBuffer.length),
812
- 'Content-Length': String(photoBuffer.length),
813
- };
814
-
815
- const uploadUrl = `/rupload_igphoto/${objectName}`;
816
-
817
- let serverUploadId = uploadId;
818
- try {
819
- const uploadResponse = await ig.request.send({
820
- url: uploadUrl,
821
- method: 'POST',
822
- headers: uploadHeaders,
823
- body: photoBuffer,
824
- });
825
-
826
- if (uploadResponse && typeof uploadResponse === 'object' && uploadResponse.upload_id) {
827
- serverUploadId = uploadResponse.upload_id;
828
- }
829
- this.enhancedDebug(`✅ Photo uploaded! upload_id: ${serverUploadId}`);
830
- } catch (uploadErr) {
831
- this.enhancedDebug(`Upload error: ${uploadErr.message}`);
832
- throw new Error(`Photo upload failed: ${uploadErr.message}`);
833
- }
834
-
835
- // Step 2: Broadcast the uploaded photo to the thread
836
- this.enhancedDebug(`Step 2: Broadcasting photo to thread ${threadId}...`);
837
-
838
- const broadcastForm = {
839
- upload_id: serverUploadId,
840
- action: 'send_item',
841
- thread_ids: JSON.stringify([String(threadId)]),
842
- };
843
-
844
- if (caption) {
845
- broadcastForm.caption = caption;
846
- }
847
-
848
- try {
849
- const broadcastResponse = await ig.request.send({
850
- url: '/direct_v2/threads/broadcast/upload_photo/',
851
- method: 'POST',
852
- form: broadcastForm,
853
- });
854
-
855
- this.enhancedDebug(`✅ Photo sent successfully to thread ${threadId}!`);
856
- return broadcastResponse;
857
- } catch (broadcastErr) {
858
- this.enhancedDebug(`Broadcast error: ${broadcastErr.message}`);
859
- throw new Error(`Photo broadcast failed: ${broadcastErr.message}`);
860
- }
861
-
886
+ // delegate to sendMedia which handles rupload + mqtt publish
887
+ const result = await this.sendMedia({
888
+ text: caption,
889
+ threadId,
890
+ clientContext,
891
+ mediaBuffer: photoBuffer,
892
+ mimeType,
893
+ });
894
+ return result;
862
895
  } catch (err) {
863
896
  this.enhancedDebug(`sendPhotoViaRealtime failed: ${err.message}`);
864
897
  throw err;
@@ -873,19 +906,11 @@ class EnhancedDirectCommands {
873
906
  }
874
907
 
875
908
  /**
876
- * Send video via Realtime (Upload + Broadcast)
877
- *
878
- * @param {Object} options - Video sending options
879
- * @param {Buffer} options.videoBuffer - Video buffer (MP4)
880
- * @param {string} options.threadId - Thread ID to send to
881
- * @param {string} [options.caption] - Optional caption
882
- * @param {number} [options.duration] - Video duration in seconds
883
- * @param {number} [options.width] - Video width
884
- * @param {number} [options.height] - Video height
885
- * @param {string} [options.clientContext] - Optional client context
909
+ * Send video via Realtime (Upload + MQTT-ref)
910
+ * Uploads video via rupload, then sends MQTT send_item with upload_id.
886
911
  */
887
- async sendVideoViaRealtime({ videoBuffer, threadId, caption = '', duration = 0, width = 720, height = 1280, clientContext }) {
888
- this.enhancedDebug(`Sending video to thread ${threadId} via Realtime`);
912
+ async sendVideoViaRealtime({ videoBuffer, threadId, caption = '', duration = 0, width = 720, height = 1280, mimeType = 'video/mp4', clientContext }) {
913
+ this.enhancedDebug(`Sending video to thread ${threadId} via Realtime (rupload + mqtt-ref)`);
889
914
 
890
915
  try {
891
916
  // Validate inputs
@@ -896,85 +921,17 @@ class EnhancedDirectCommands {
896
921
  throw new Error('threadId is required');
897
922
  }
898
923
 
899
- // Get the ig client from realtime client
900
- const ig = this.realtimeClient.ig;
901
- if (!ig || !ig.request) {
902
- throw new Error('Instagram client not available. Make sure you are logged in.');
903
- }
904
-
905
- // Step 1: Upload video using rupload endpoint
906
- this.enhancedDebug(`Step 1: Uploading video (${videoBuffer.length} bytes)...`);
907
-
908
- const uploadId = Date.now().toString();
909
- const objectName = `${(0, uuid_1.v4)()}.mp4`;
910
-
911
- const ruploadParams = {
912
- upload_id: uploadId,
913
- media_type: 2, // 2 = video
914
- xsharing_user_ids: JSON.stringify([]),
915
- upload_media_duration_ms: Math.round(duration * 1000),
916
- upload_media_width: width,
917
- upload_media_height: height,
918
- };
919
-
920
- const uploadHeaders = {
921
- 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
922
- 'Content-Type': 'video/mp4',
923
- 'X_FB_VIDEO_WATERFALL_ID': (0, uuid_1.v4)(),
924
- 'X-Entity-Type': 'video/mp4',
925
- 'X-Entity-Length': String(videoBuffer.length),
926
- 'Content-Length': String(videoBuffer.length),
927
- 'Offset': '0',
928
- };
929
-
930
- const uploadUrl = `/rupload_igvideo/${objectName}`;
931
-
932
- let serverUploadId = uploadId;
933
- try {
934
- const uploadResponse = await ig.request.send({
935
- url: uploadUrl,
936
- method: 'POST',
937
- headers: uploadHeaders,
938
- body: videoBuffer,
939
- });
940
-
941
- if (uploadResponse && typeof uploadResponse === 'object' && uploadResponse.upload_id) {
942
- serverUploadId = uploadResponse.upload_id;
943
- }
944
- this.enhancedDebug(`✅ Video uploaded! upload_id: ${serverUploadId}`);
945
- } catch (uploadErr) {
946
- this.enhancedDebug(`Video upload error: ${uploadErr.message}`);
947
- throw new Error(`Video upload failed: ${uploadErr.message}`);
948
- }
949
-
950
- // Step 2: Broadcast the uploaded video to the thread
951
- this.enhancedDebug(`Step 2: Broadcasting video to thread ${threadId}...`);
952
-
953
- const broadcastForm = {
954
- upload_id: serverUploadId,
955
- action: 'send_item',
956
- thread_ids: JSON.stringify([String(threadId)]),
957
- video_result: '',
958
- };
959
-
960
- if (caption) {
961
- broadcastForm.caption = caption;
962
- }
963
-
964
- try {
965
- const broadcastResponse = await ig.request.send({
966
- url: '/direct_v2/threads/broadcast/upload_video/',
967
- method: 'POST',
968
- form: broadcastForm,
969
- });
970
-
971
- this.enhancedDebug(`✅ Video sent successfully to thread ${threadId}!`);
972
- return broadcastResponse;
973
- } catch (broadcastErr) {
974
- this.enhancedDebug(`Video broadcast error: ${broadcastErr.message}`);
975
- throw new Error(`Video broadcast failed: ${broadcastErr.message}`);
976
- }
977
-
924
+ const result = await this.sendMedia({
925
+ text: caption,
926
+ threadId,
927
+ clientContext,
928
+ mediaBuffer: videoBuffer,
929
+ mimeType,
930
+ duration,
931
+ width,
932
+ height,
933
+ });
934
+ return result;
978
935
  } catch (err) {
979
936
  this.enhancedDebug(`sendVideoViaRealtime failed: ${err.message}`);
980
937
  throw err;
@@ -20,12 +20,15 @@ class DirectThreadRepository extends Repository {
20
20
  } catch (error) {
21
21
  const shouldRetry =
22
22
  (error.data?.error_type === 'server_error' ||
23
- error.data?.error_type === 'rate_limited') &&
23
+ error.data?.error_type === 'rate_limited' ||
24
+ error.name === 'IgActionSpamError' ||
25
+ error.status === 503 ||
26
+ error.status === 429) &&
24
27
  retries < this.maxRetries;
25
28
 
26
29
  if (shouldRetry) {
27
30
  const delay = 1000 * (retries + 1);
28
- if (process.env.DEBUG) console.log(`[DEBUG] Retrying after ${delay}ms due to ${error.data?.error_type}`);
31
+ if (process.env.DEBUG) console.log(`[DEBUG] Retrying after ${delay}ms due to ${error.data?.error_type || error.message || error.name || error.status}`);
29
32
  await new Promise(resolve => setTimeout(resolve, delay));
30
33
  return this.requestWithRetry(requestFn, retries + 1);
31
34
  }
@@ -59,7 +62,7 @@ class DirectThreadRepository extends Repository {
59
62
  method: 'GET',
60
63
  url: `/api/v1/direct_v2/threads/${threadId}/`,
61
64
  });
62
- return response.body;
65
+ return response.body || response.data || response;
63
66
  });
64
67
  }
65
68
 
@@ -74,7 +77,7 @@ class DirectThreadRepository extends Repository {
74
77
  url: '/api/v1/direct_v2/threads/get_by_participants/',
75
78
  qs: { recipient_users: JSON.stringify(recipientUsers) },
76
79
  });
77
- return response.body;
80
+ return response.body || response.data || response;
78
81
  });
79
82
  }
80
83
 
@@ -99,13 +102,102 @@ class DirectThreadRepository extends Repository {
99
102
  };
100
103
 
101
104
  return this.requestWithRetry(async () => {
105
+ const payloadForm = options.signed && this.client.request && typeof this.client.request.sign === 'function'
106
+ ? this.client.request.sign(form)
107
+ : form;
108
+
102
109
  const response = await this.client.request.send({
103
110
  url: `/api/v1/direct_v2/threads/broadcast/${options.item}/`,
104
111
  method: 'POST',
105
- form: options.signed ? this.client.request.sign(form) : form,
112
+ form: payloadForm,
106
113
  qs: options.qs,
107
114
  });
108
- return response.body;
115
+ return response.body || response.data || response;
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Broadcast a photo to one or more threads (uses REST configure_photo)
121
+ * Options:
122
+ * - uploadId (required) : upload_id returned from rupload
123
+ * - threadIds or threadId (required) : target thread id(s)
124
+ * - caption (optional) : caption/text to attach
125
+ * - signed (optional, default true) : whether to sign the form (if client.request.sign available)
126
+ */
127
+ async broadcastPhoto(options) {
128
+ // normalize inputs
129
+ const uploadId = options.uploadId || options.upload_id || options.uploadIdStr;
130
+ const threadIds = options.threadIds || (options.threadId ? [options.threadId] : []);
131
+ const caption = options.caption || options.text || '';
132
+ const signed = (options.signed === undefined) ? true : !!options.signed; // default to true for media
133
+ const mutationToken = new Chance().guid();
134
+ const clientContext = mutationToken;
135
+
136
+ if (!uploadId) throw new Error('broadcastPhoto: uploadId is required');
137
+ if (!threadIds || !Array.isArray(threadIds) || threadIds.length === 0) throw new Error('broadcastPhoto: at least one threadId is required');
138
+
139
+ // Build the form closely matching instagram-private-api / official client expectations
140
+ const form = {
141
+ action: 'send_item', // must be send_item for DM photo
142
+ upload_id: uploadId.toString(),
143
+ thread_ids: JSON.stringify(threadIds),
144
+ client_context: clientContext,
145
+ mutation_token: mutationToken,
146
+ offline_threading_id: clientContext,
147
+ _csrftoken: this.client.state.cookieCsrfToken,
148
+ device_id: this.client.state.deviceId,
149
+ _uuid: this.client.state.uuid,
150
+ };
151
+
152
+ if (caption) {
153
+ // Instagram often expects 'text' for the message body
154
+ form.text = caption;
155
+ }
156
+
157
+ // perform request with retry wrapper
158
+ return this.requestWithRetry(async () => {
159
+ const payloadForm = (signed && this.client.request && typeof this.client.request.sign === 'function')
160
+ ? this.client.request.sign(form)
161
+ : form;
162
+
163
+ const response = await this.client.request.send({
164
+ url: `/api/v1/direct_v2/threads/broadcast/configure_photo/`,
165
+ method: 'POST',
166
+ form: payloadForm,
167
+ qs: {
168
+ use_unified_inbox: true,
169
+ },
170
+ });
171
+
172
+ // normalize: some wrappers return { body } or axios response
173
+ const body = response && (response.body || response.data || response);
174
+
175
+ if (!body) {
176
+ const err = new Error('broadcastPhoto: empty response');
177
+ throw err;
178
+ }
179
+
180
+ // parse if string
181
+ let parsed = null;
182
+ if (typeof body === 'string') {
183
+ try {
184
+ parsed = JSON.parse(body);
185
+ } catch (e) {
186
+ parsed = null;
187
+ }
188
+ } else if (typeof body === 'object') {
189
+ parsed = body;
190
+ }
191
+
192
+ // Typical success: parsed.status === 'ok' OR parsed.media/parsed.result/payload present
193
+ const ok = parsed && (parsed.status === 'ok' || parsed.media || parsed.result || parsed.payload || parsed.items || parsed.thread);
194
+ if (ok) return parsed;
195
+
196
+ // If we reach here, treat as error to trigger retry logic
197
+ const error = new Error('broadcastPhoto: Request failed');
198
+ error.response = response;
199
+ if (parsed) error.data = parsed;
200
+ throw error;
109
201
  });
110
202
  }
111
203
 
@@ -126,7 +218,7 @@ class DirectThreadRepository extends Repository {
126
218
  item_id: threadItemId,
127
219
  },
128
220
  });
129
- return response.body;
221
+ return response.body || response.data || response;
130
222
  });
131
223
  }
132
224
 
@@ -140,7 +232,7 @@ class DirectThreadRepository extends Repository {
140
232
  method: 'POST',
141
233
  form: { _csrftoken: this.client.state.cookieCsrfToken, _uuid: this.client.state.uuid },
142
234
  });
143
- return response.body;
235
+ return response.body || response.data || response;
144
236
  });
145
237
  }
146
238
 
@@ -154,7 +246,7 @@ class DirectThreadRepository extends Repository {
154
246
  method: 'POST',
155
247
  form: { _csrftoken: this.client.state.cookieCsrfToken, _uuid: this.client.state.uuid },
156
248
  });
157
- return response.body;
249
+ return response.body || response.data || response;
158
250
  });
159
251
  }
160
252
 
@@ -168,7 +260,7 @@ class DirectThreadRepository extends Repository {
168
260
  method: 'POST',
169
261
  form: { _csrftoken: this.client.state.cookieCsrfToken, _uuid: this.client.state.uuid },
170
262
  });
171
- return response.body;
263
+ return response.body || response.data || response;
172
264
  });
173
265
  }
174
266
 
@@ -182,7 +274,7 @@ class DirectThreadRepository extends Repository {
182
274
  method: 'POST',
183
275
  form: { _csrftoken: this.client.state.cookieCsrfToken, _uuid: this.client.state.uuid },
184
276
  });
185
- return response.body;
277
+ return response.body || response.data || response;
186
278
  });
187
279
  }
188
280
 
@@ -196,7 +288,7 @@ class DirectThreadRepository extends Repository {
196
288
  method: 'POST',
197
289
  form: { _csrftoken: this.client.state.cookieCsrfToken, _uuid: this.client.state.uuid },
198
290
  });
199
- return response.body;
291
+ return response.body || response.data || response;
200
292
  });
201
293
  }
202
294
 
@@ -211,7 +303,7 @@ class DirectThreadRepository extends Repository {
211
303
  method: 'POST',
212
304
  form: { _csrftoken: this.client.state.cookieCsrfToken, user_ids: JSON.stringify(userIds), _uuid: this.client.state.uuid },
213
305
  });
214
- return response.body;
306
+ return response.body || response.data || response;
215
307
  });
216
308
  }
217
309
 
@@ -225,7 +317,7 @@ class DirectThreadRepository extends Repository {
225
317
  method: 'POST',
226
318
  form: { _csrftoken: this.client.state.cookieCsrfToken, _uuid: this.client.state.uuid },
227
319
  });
228
- return response.body;
320
+ return response.body || response.data || response;
229
321
  });
230
322
  }
231
323
 
@@ -239,9 +331,10 @@ class DirectThreadRepository extends Repository {
239
331
  method: 'POST',
240
332
  form: { _csrftoken: this.client.state.cookieCsrfToken, _uuid: this.client.state.uuid, title },
241
333
  });
242
- return response.body;
334
+ return response.body || response.data || response;
243
335
  });
244
336
  }
245
337
  }
246
338
 
247
339
  module.exports = DirectThreadRepository;
340
+