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.
- package/README.md +596 -30
- package/dist/core/request.js +127 -53
- package/dist/realtime/commands/enhanced.direct.commands.js +175 -218
- package/dist/repositories/direct-thread.repository.js +108 -15
- package/dist/sendmedia/sendPhoto.js +115 -66
- package/package.json +1 -1
|
@@ -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)
|
|
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(`
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
throw new Error('MQTT client not available');
|
|
565
|
+
if (!threadId) {
|
|
566
|
+
throw new Error('threadId is required');
|
|
514
567
|
}
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
543
|
-
return
|
|
652
|
+
|
|
653
|
+
this.enhancedDebug(`✅ Media reference published via MQTT for upload_id=${serverUploadId}`);
|
|
654
|
+
return publishResult;
|
|
655
|
+
|
|
544
656
|
} catch (err) {
|
|
545
|
-
this.enhancedDebug(`
|
|
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 +
|
|
760
|
-
*
|
|
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
|
-
//
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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 +
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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:
|
|
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
|
+
|