nodejs-insta-private-api-mqtt 1.3.12 → 1.3.14
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 +51 -5
- package/dist/realtime/commands/enhanced.direct.commands.js +7 -3
- package/dist/repositories/upload.repository.js +5 -9
- package/dist/sendmedia/sendFile.js +17 -99
- package/dist/sendmedia/sendPhoto.js +18 -70
- package/dist/sendmedia/uploadPhoto.js +38 -106
- package/dist/sendmedia/uploadfFile.js +56 -190
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
Dear users,
|
|
2
|
-
First of all, when handling view-once images or videos, please make sure to save the received media in a folder such as /storage/emulated/0/Pictures/, or depending on your device's path. The photos and videos are saved correctly on your phone, but the library currently has some issues with uploading them back to Instagram. This will be resolved as soon as possible.
|
|
3
2
|
I post many versions of the project because Instagram changes the protocol almost daily, if you like this project leave a star on github https://github.com/Kunboruto20/nodejs-insta-private-api.git
|
|
4
3
|
|
|
5
4
|
|
|
@@ -263,7 +262,53 @@ startBot().catch(console.error);
|
|
|
263
262
|
|
|
264
263
|
---
|
|
265
264
|
|
|
266
|
-
##
|
|
265
|
+
## 🖼️ FIXED: Send Media via MQTT (v5.70.0)
|
|
266
|
+
|
|
267
|
+
The upload issue has been fully resolved. You can now send photos and videos directly through the MQTT protocol. The library now handles raw buffers correctly, matching the Instagram internal protocol for zero "Upload Failed" errors.
|
|
268
|
+
|
|
269
|
+
### Send Media via MQTT (v5.70.0)
|
|
270
|
+
|
|
271
|
+
You can now send photos, videos, voice messages, and more directly through the MQTT protocol. The library handles the upload and linking automatically.
|
|
272
|
+
|
|
273
|
+
#### 📷 Send a Photo
|
|
274
|
+
```javascript
|
|
275
|
+
const photoBuffer = fs.readFileSync('./image.jpg');
|
|
276
|
+
const upload = await ig.publish.photo({ file: photoBuffer });
|
|
277
|
+
|
|
278
|
+
await realtime.directCommands.sendMedia({
|
|
279
|
+
threadId: 'THREAD_ID',
|
|
280
|
+
mediaId: upload.upload_id,
|
|
281
|
+
uploadId: upload.upload_id
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### 🎥 Send a Video
|
|
286
|
+
```javascript
|
|
287
|
+
const videoBuffer = fs.readFileSync('./video.mp4');
|
|
288
|
+
const upload = await ig.publish.video({
|
|
289
|
+
video: videoBuffer,
|
|
290
|
+
duration: 10000, // ms
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await realtime.directCommands.sendMedia({
|
|
294
|
+
threadId: 'THREAD_ID',
|
|
295
|
+
mediaId: upload.upload_id,
|
|
296
|
+
uploadId: upload.upload_id
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### 🎤 Send Voice Message
|
|
301
|
+
```javascript
|
|
302
|
+
const audioBuffer = fs.readFileSync('./voice.mp4');
|
|
303
|
+
const upload = await ig.publish.audio({ file: audioBuffer });
|
|
304
|
+
|
|
305
|
+
await realtime.directCommands.sendMedia({
|
|
306
|
+
threadId: 'THREAD_ID',
|
|
307
|
+
mediaId: upload.upload_id,
|
|
308
|
+
uploadId: upload.upload_id
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
267
312
|
|
|
268
313
|
We've completely overhauled how the MQTT connection works. You no longer need to manually manage connection states every time your bot restarts. Just like **Baileys**, if a session is present in the default folder, the library takes care of everything for you.
|
|
269
314
|
|
|
@@ -924,7 +969,7 @@ const {
|
|
|
924
969
|
RealtimeClient,
|
|
925
970
|
downloadContentFromMessage,
|
|
926
971
|
isViewOnceMedia
|
|
927
|
-
} = require('nodejs-insta-private-api');
|
|
972
|
+
} = require('nodejs-insta-private-api-mqtt');
|
|
928
973
|
const fs = require('fs');
|
|
929
974
|
|
|
930
975
|
const ig = new IgApiClient();
|
|
@@ -976,7 +1021,8 @@ realtime.on('message', async (data) => {
|
|
|
976
1021
|
const {
|
|
977
1022
|
downloadMediaBuffer,
|
|
978
1023
|
hasMedia,
|
|
979
|
-
getMediaType
|
|
1024
|
+
getMediaType,
|
|
1025
|
+
MEDIA_TYPES
|
|
980
1026
|
} = require('nodejs-insta-private-api-mqtt');
|
|
981
1027
|
|
|
982
1028
|
realtime.on('message', async (data) => {
|
|
@@ -1165,7 +1211,7 @@ startMediaBot().catch(console.error);
|
|
|
1165
1211
|
### MEDIA_TYPES Constants
|
|
1166
1212
|
|
|
1167
1213
|
```javascript
|
|
1168
|
-
const { MEDIA_TYPES } = require('nodejs-insta-private-api');
|
|
1214
|
+
const { MEDIA_TYPES } = require('nodejs-insta-private-api-mqtt');
|
|
1169
1215
|
|
|
1170
1216
|
MEDIA_TYPES.IMAGE // Regular photo
|
|
1171
1217
|
MEDIA_TYPES.VIDEO // Regular video
|
|
@@ -504,8 +504,8 @@ class EnhancedDirectCommands {
|
|
|
504
504
|
/**
|
|
505
505
|
* Send media (image/video) via MQTT
|
|
506
506
|
*/
|
|
507
|
-
async sendMedia({ text, mediaId, threadId, clientContext }) {
|
|
508
|
-
this.enhancedDebug(`Sending media ${mediaId} to ${threadId} via MQTT`);
|
|
507
|
+
async sendMedia({ text, mediaId, threadId, clientContext, uploadId }) {
|
|
508
|
+
this.enhancedDebug(`Sending media ${mediaId} (uploadId: ${uploadId}) to ${threadId} via MQTT`);
|
|
509
509
|
|
|
510
510
|
try {
|
|
511
511
|
const mqtt = this.realtimeClient.mqtt || this.realtimeClient._mqtt;
|
|
@@ -519,16 +519,20 @@ class EnhancedDirectCommands {
|
|
|
519
519
|
thread_id: threadId,
|
|
520
520
|
item_type: 'media',
|
|
521
521
|
media_id: mediaId,
|
|
522
|
+
upload_id: uploadId || mediaId,
|
|
522
523
|
text: text || '',
|
|
523
524
|
timestamp: Date.now(),
|
|
524
525
|
client_context: ctx,
|
|
525
526
|
};
|
|
526
527
|
|
|
528
|
+
// Log payload for debugging
|
|
529
|
+
this.enhancedDebug(`Payload: ${JSON.stringify(command)}`);
|
|
530
|
+
|
|
527
531
|
const json = JSON.stringify(command);
|
|
528
532
|
const { compressDeflate } = shared_1;
|
|
529
533
|
const payload = await compressDeflate(json);
|
|
530
534
|
|
|
531
|
-
this.enhancedDebug(`Publishing media to MQTT`);
|
|
535
|
+
this.enhancedDebug(`Publishing media to MQTT topic ${constants_1.Topics.SEND_MESSAGE.id}`);
|
|
532
536
|
const result = await mqtt.publish({
|
|
533
537
|
topic: constants_1.Topics.SEND_MESSAGE.id,
|
|
534
538
|
qosLevel: 1,
|
|
@@ -16,14 +16,11 @@ class UploadRepository extends Repository {
|
|
|
16
16
|
|
|
17
17
|
const ruploadParams = this.createPhotoRuploadParams(options, uploadId);
|
|
18
18
|
|
|
19
|
-
const formData = new FormData();
|
|
20
|
-
formData.append('photo', options.file, { filename: 'photo.jpg' });
|
|
21
|
-
|
|
22
19
|
const response = await this.client.request.send({
|
|
23
20
|
url: `/rupload_igphoto/${name}`,
|
|
24
21
|
method: 'POST',
|
|
25
22
|
headers: {
|
|
26
|
-
'
|
|
23
|
+
'X_FB_PHOTO_WATERFALL_ID': waterfallId,
|
|
27
24
|
'X-Entity-Type': 'image/jpeg',
|
|
28
25
|
'Offset': '0',
|
|
29
26
|
'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
|
|
@@ -32,9 +29,8 @@ class UploadRepository extends Repository {
|
|
|
32
29
|
'Content-Type': 'application/octet-stream',
|
|
33
30
|
'Content-Length': options.file.length.toString(),
|
|
34
31
|
'Accept-Encoding': 'gzip',
|
|
35
|
-
...formData.getHeaders(),
|
|
36
32
|
},
|
|
37
|
-
|
|
33
|
+
body: options.file,
|
|
38
34
|
});
|
|
39
35
|
|
|
40
36
|
return response.body;
|
|
@@ -51,7 +47,7 @@ class UploadRepository extends Repository {
|
|
|
51
47
|
url: `/rupload_igvideo/${name}`,
|
|
52
48
|
method: 'POST',
|
|
53
49
|
headers: {
|
|
54
|
-
'
|
|
50
|
+
'X_FB_VIDEO_WATERFALL_ID': waterfallId,
|
|
55
51
|
'X-Entity-Type': 'video/mp4',
|
|
56
52
|
'Offset': options.offset || '0',
|
|
57
53
|
'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
|
|
@@ -61,7 +57,7 @@ class UploadRepository extends Repository {
|
|
|
61
57
|
'Content-Length': options.video.length.toString(),
|
|
62
58
|
'Accept-Encoding': 'gzip',
|
|
63
59
|
},
|
|
64
|
-
|
|
60
|
+
body: options.video,
|
|
65
61
|
});
|
|
66
62
|
|
|
67
63
|
return response.body;
|
|
@@ -80,7 +76,7 @@ class UploadRepository extends Repository {
|
|
|
80
76
|
image_compression: JSON.stringify({
|
|
81
77
|
lib_name: 'moz',
|
|
82
78
|
lib_version: '3.1.m',
|
|
83
|
-
quality: '
|
|
79
|
+
quality: '80',
|
|
84
80
|
}),
|
|
85
81
|
};
|
|
86
82
|
}
|
|
@@ -1,125 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sendFile.js
|
|
3
|
-
* High-level helper to send uploaded video/audio/image files to Instagram Direct (DM or existing thread).
|
|
4
|
-
*
|
|
5
|
-
* Requires:
|
|
6
|
-
* - uploadFile(session, fileBuffer, options) from ./uploadFile
|
|
7
|
-
*
|
|
8
|
-
* Supports:
|
|
9
|
-
* - Send to a single user by userId (recipient_users)
|
|
10
|
-
* - Send to an existing thread (group or DM) by threadId
|
|
11
|
-
* - Optional caption
|
|
12
|
-
* - Optional mentions (array of userIds) embedded in caption
|
|
13
|
-
* - Custom mimeType/fileName/chunkSize/isClipsMedia forwarded to uploadFile
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* const sendFile = require('./sendFile');
|
|
17
|
-
* await sendFile(session, {
|
|
18
|
-
* fileBuffer: fs.readFileSync('./clip.mp4'),
|
|
19
|
-
* mimeType: 'video/mp4',
|
|
20
|
-
* fileName: 'clip.mp4',
|
|
21
|
-
* userId: '123456789', // or threadId: '340282366841710300949128123456789'
|
|
22
|
-
* caption: 'Uite clipul!',
|
|
23
|
-
* });
|
|
24
|
-
*
|
|
25
|
-
* Notes:
|
|
26
|
-
* - Exactly one of { userId, threadId } must be provided.
|
|
27
|
-
* - For photos (JPEG/PNG), you can still use this with mimeType 'image/jpeg' or 'image/png',
|
|
28
|
-
* but uploadPhoto.js + sendPhoto.js is preferred for image-specific flows.
|
|
2
|
+
* sendFile.js - Fixed to support MQTT if realtime client provided
|
|
29
3
|
*/
|
|
30
4
|
|
|
31
5
|
const uploadFile = require('./uploadfFile');
|
|
32
6
|
|
|
33
7
|
/**
|
|
34
|
-
*
|
|
35
|
-
* @property {Buffer} fileBuffer - Required Buffer data
|
|
36
|
-
* @property {string} [mimeType='video/mp4'] - e.g., 'video/mp4', 'audio/mpeg', 'image/jpeg'
|
|
37
|
-
* @property {string} [fileName] - Optional file name; sanitized based on mime
|
|
38
|
-
* @property {number} [chunkSize] - Optional chunk size for uploadFile
|
|
39
|
-
* @property {boolean} [isClipsMedia=false] - Hint for reels-like uploads (if your flow supports it)
|
|
40
|
-
* @property {string} [caption] - Optional caption text
|
|
41
|
-
* @property {string} [userId] - Send to user (DM) — exactly one of userId or threadId
|
|
42
|
-
* @property {string} [threadId] - Send to existing thread (group or DM) — exactly one of userId or threadId
|
|
43
|
-
* @property {string[]} [mentions] - Optional array of userIds mentioned in caption
|
|
44
|
-
* @property {AbortSignal} [signal] - Optional AbortSignal to cancel
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Send a file (video/audio/image) to Instagram Direct.
|
|
49
|
-
* Internally:
|
|
50
|
-
* - Uploads via rupload to get upload_id
|
|
51
|
-
* - Broadcasts the uploaded media to either a user (DM) or an existing thread
|
|
52
|
-
*
|
|
53
|
-
* @param {object} session - Authenticated session (with request.send)
|
|
54
|
-
* @param {SendFileOptions} opts - Options
|
|
55
|
-
* @returns {Promise<object>} Instagram response object
|
|
8
|
+
* Send a file to Instagram Direct.
|
|
56
9
|
*/
|
|
57
10
|
async function sendFile(session, opts = {}) {
|
|
58
11
|
const {
|
|
59
12
|
fileBuffer,
|
|
60
13
|
mimeType = 'video/mp4',
|
|
61
14
|
fileName,
|
|
62
|
-
chunkSize,
|
|
63
|
-
isClipsMedia = false,
|
|
64
15
|
caption = '',
|
|
65
16
|
userId,
|
|
66
17
|
threadId,
|
|
67
|
-
mentions = [],
|
|
68
18
|
signal,
|
|
19
|
+
realtimeClient,
|
|
69
20
|
} = opts;
|
|
70
21
|
|
|
71
|
-
// Validate destination
|
|
72
22
|
if (!userId && !threadId) {
|
|
73
|
-
throw new Error('sendFile:
|
|
23
|
+
throw new Error('sendFile: Provide userId or threadId.');
|
|
74
24
|
}
|
|
75
|
-
if (userId && threadId) {
|
|
76
|
-
throw new Error('sendFile: Provide only one destination — userId OR threadId, not both.');
|
|
77
|
-
}
|
|
78
|
-
// Validate buffer
|
|
79
25
|
if (!fileBuffer || !Buffer.isBuffer(fileBuffer) || fileBuffer.length === 0) {
|
|
80
26
|
throw new Error('sendFile: fileBuffer must be a non-empty Buffer.');
|
|
81
27
|
}
|
|
82
|
-
if (typeof mimeType !== 'string' || mimeType.length === 0) {
|
|
83
|
-
throw new Error('sendFile: mimeType must be a non-empty string (e.g., "video/mp4").');
|
|
84
|
-
}
|
|
85
28
|
|
|
86
|
-
// 1) Upload
|
|
29
|
+
// 1) Upload
|
|
87
30
|
const upload_id = await uploadFile(session, fileBuffer, {
|
|
88
31
|
mimeType,
|
|
89
32
|
fileName,
|
|
90
|
-
chunkSize,
|
|
91
|
-
isClipsMedia,
|
|
92
33
|
signal,
|
|
93
34
|
});
|
|
94
35
|
|
|
95
|
-
// 2)
|
|
36
|
+
// 2) MQTT Check
|
|
37
|
+
if (realtimeClient && realtimeClient.direct && typeof realtimeClient.direct.sendMedia === 'function') {
|
|
38
|
+
return await realtimeClient.direct.sendMedia({
|
|
39
|
+
text: caption,
|
|
40
|
+
mediaId: upload_id,
|
|
41
|
+
threadId: threadId || userId,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3) REST Fallback
|
|
46
|
+
const url = '/direct_v2/threads/broadcast/upload_video/';
|
|
96
47
|
const form = {
|
|
97
48
|
upload_id,
|
|
98
49
|
action: 'send_item',
|
|
99
50
|
caption,
|
|
100
51
|
};
|
|
101
52
|
|
|
102
|
-
// 3) Mentions (optional)
|
|
103
|
-
if (Array.isArray(mentions) && mentions.length > 0) {
|
|
104
|
-
form.entities = JSON.stringify(
|
|
105
|
-
mentions.map((uid) => ({
|
|
106
|
-
user_id: String(uid),
|
|
107
|
-
type: 'mention',
|
|
108
|
-
}))
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 4) Destination-specific fields
|
|
113
|
-
// For video/audio/image via this flow, IG uses the "upload_video" broadcast endpoint.
|
|
114
|
-
// Images can also be sent via upload_photo (recommended via sendPhoto.js).
|
|
115
|
-
const url = '/direct_v2/threads/broadcast/upload_video/';
|
|
116
53
|
if (userId) {
|
|
117
54
|
form.recipient_users = JSON.stringify([[String(userId)]]);
|
|
118
55
|
} else {
|
|
119
56
|
form.thread_ids = JSON.stringify([String(threadId)]);
|
|
120
57
|
}
|
|
121
58
|
|
|
122
|
-
// 5) Send broadcast request
|
|
123
59
|
try {
|
|
124
60
|
const response = await session.request.send({
|
|
125
61
|
url,
|
|
@@ -127,27 +63,9 @@ async function sendFile(session, opts = {}) {
|
|
|
127
63
|
form,
|
|
128
64
|
signal,
|
|
129
65
|
});
|
|
130
|
-
|
|
131
|
-
if (!response) {
|
|
132
|
-
throw new Error('sendFile: Empty response from Instagram broadcast endpoint.');
|
|
133
|
-
}
|
|
134
66
|
return response;
|
|
135
67
|
} catch (err) {
|
|
136
|
-
throw new Error(`sendFile: Broadcast failed — ${
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Normalize error shapes to readable text.
|
|
142
|
-
*/
|
|
143
|
-
function normalizeError(err) {
|
|
144
|
-
if (!err) return 'Unknown error';
|
|
145
|
-
if (typeof err === 'string') return err;
|
|
146
|
-
if (err.message) return err.message;
|
|
147
|
-
try {
|
|
148
|
-
return JSON.stringify(err);
|
|
149
|
-
} catch {
|
|
150
|
-
return 'Unserializable error';
|
|
68
|
+
throw new Error(`sendFile: Broadcast failed — ${err.message}`);
|
|
151
69
|
}
|
|
152
70
|
}
|
|
153
71
|
|
|
@@ -1,53 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sendPhoto.js
|
|
3
|
-
* High-level helper to send a previously uploaded photo to Instagram Direct (DM or Group).
|
|
4
|
-
*
|
|
5
|
-
* Requires:
|
|
6
|
-
* - uploadPhoto(session, photoBuffer, options) from ./uploadPhoto
|
|
7
|
-
*
|
|
8
|
-
* Supports:
|
|
9
|
-
* - Send to a single user by userId (recipient_users)
|
|
10
|
-
* - Send to an existing thread (group) by threadId
|
|
11
|
-
* - Optional caption
|
|
12
|
-
* - Optional mentions (array of userIds) embedded in caption
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* const sendPhoto = require('./sendPhoto');
|
|
16
|
-
* await sendPhoto(session, {
|
|
17
|
-
* photoBuffer: fs.readFileSync('./image.jpg'),
|
|
18
|
-
* userId: '123456789', // or threadId: '340282366841710300949128123456789'
|
|
19
|
-
* caption: 'Salut! 👋',
|
|
20
|
-
* });
|
|
21
|
-
*
|
|
22
|
-
* Notes:
|
|
23
|
-
* - If you pass userId, it broadcasts to that user (DM).
|
|
24
|
-
* - If you pass threadId, it broadcasts into that existing thread (group or DM thread).
|
|
25
|
-
* - Exactly one of { userId, threadId } must be provided.
|
|
2
|
+
* sendPhoto.js - Fixed to support MQTT if realtime client provided
|
|
26
3
|
*/
|
|
27
4
|
|
|
28
5
|
const uploadPhoto = require('./uploadPhoto');
|
|
29
6
|
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {Object} SendPhotoOptions
|
|
32
|
-
* @property {Buffer} photoBuffer - Required image buffer (JPEG/PNG)
|
|
33
|
-
* @property {string} [mimeType='image/jpeg'] - 'image/jpeg' | 'image/png'
|
|
34
|
-
* @property {string} [fileName] - Optional file name (will be sanitized)
|
|
35
|
-
* @property {string} [caption] - Optional caption text
|
|
36
|
-
* @property {string} [userId] - Send to user (DM) — exactly one of userId or threadId
|
|
37
|
-
* @property {string} [threadId] - Send to existing thread (group or DM) — exactly one of userId or threadId
|
|
38
|
-
* @property {string[]} [mentions] - Optional array of userIds mentioned in caption
|
|
39
|
-
* @property {AbortSignal} [signal] - Optional AbortSignal to cancel
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
7
|
/**
|
|
43
8
|
* Send a photo to Instagram Direct.
|
|
44
|
-
* Internally:
|
|
45
|
-
* - Uploads photo via rupload to get upload_id
|
|
46
|
-
* - Broadcasts the uploaded photo to either a user (DM) or an existing thread
|
|
47
|
-
*
|
|
48
|
-
* @param {object} session - Authenticated session (with request.send)
|
|
49
|
-
* @param {SendPhotoOptions} opts - Options
|
|
50
|
-
* @returns {Promise<object>} Instagram response object
|
|
51
9
|
*/
|
|
52
10
|
async function sendPhoto(session, opts = {}) {
|
|
53
11
|
const {
|
|
@@ -59,6 +17,7 @@ async function sendPhoto(session, opts = {}) {
|
|
|
59
17
|
threadId,
|
|
60
18
|
mentions = [],
|
|
61
19
|
signal,
|
|
20
|
+
realtimeClient, // NEW: Optional realtime client for MQTT sending
|
|
62
21
|
} = opts;
|
|
63
22
|
|
|
64
23
|
// Validate destination
|
|
@@ -68,7 +27,6 @@ async function sendPhoto(session, opts = {}) {
|
|
|
68
27
|
if (userId && threadId) {
|
|
69
28
|
throw new Error('sendPhoto: Provide only one destination — userId OR threadId, not both.');
|
|
70
29
|
}
|
|
71
|
-
// Validate photo buffer
|
|
72
30
|
if (!photoBuffer || !Buffer.isBuffer(photoBuffer) || photoBuffer.length === 0) {
|
|
73
31
|
throw new Error('sendPhoto: photoBuffer must be a non-empty Buffer.');
|
|
74
32
|
}
|
|
@@ -76,41 +34,44 @@ async function sendPhoto(session, opts = {}) {
|
|
|
76
34
|
// 1) Upload photo to get upload_id
|
|
77
35
|
const upload_id = await uploadPhoto(session, photoBuffer, { mimeType, fileName, signal });
|
|
78
36
|
|
|
79
|
-
// 2)
|
|
37
|
+
// 2) If RealtimeClient is provided and connected, use MQTT
|
|
38
|
+
if (realtimeClient) {
|
|
39
|
+
const commands = realtimeClient.directCommands || realtimeClient.direct;
|
|
40
|
+
if (commands && typeof commands.sendMedia === 'function') {
|
|
41
|
+
return await commands.sendMedia({
|
|
42
|
+
text: caption,
|
|
43
|
+
mediaId: upload_id,
|
|
44
|
+
uploadId: upload_id, // Pass upload_id to MQTT
|
|
45
|
+
threadId: threadId || userId,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3) Fallback to REST Broadcast
|
|
80
51
|
const form = {
|
|
81
52
|
upload_id,
|
|
82
53
|
action: 'send_item',
|
|
83
|
-
// Optional caption field
|
|
84
54
|
caption,
|
|
85
55
|
};
|
|
86
56
|
|
|
87
|
-
// 3) Mentions (optional): IG expects entities when caption includes mentions
|
|
88
|
-
// This is a basic structure; adjust offsets if you programmatically embed @handles into caption.
|
|
89
57
|
if (Array.isArray(mentions) && mentions.length > 0) {
|
|
90
58
|
form.entities = JSON.stringify(
|
|
91
59
|
mentions.map((uid) => ({
|
|
92
|
-
// Simple entity type for user mention; offsets require matching caption positions
|
|
93
|
-
// If you don't compute offsets, IG may still accept the payload without entities.
|
|
94
|
-
// Providing user_id can help IG recognize mentions linked to caption text.
|
|
95
60
|
user_id: String(uid),
|
|
96
61
|
type: 'mention',
|
|
97
62
|
}))
|
|
98
63
|
);
|
|
99
64
|
}
|
|
100
65
|
|
|
101
|
-
// 4) Destination-specific fields
|
|
102
66
|
let url;
|
|
103
67
|
if (userId) {
|
|
104
|
-
// DM to user: recipient_users is an array-of-arrays of userIds as strings
|
|
105
68
|
url = '/direct_v2/threads/broadcast/upload_photo/';
|
|
106
69
|
form.recipient_users = JSON.stringify([[String(userId)]]);
|
|
107
70
|
} else {
|
|
108
|
-
// Existing thread: use thread id
|
|
109
71
|
url = '/direct_v2/threads/broadcast/upload_photo/';
|
|
110
72
|
form.thread_ids = JSON.stringify([String(threadId)]);
|
|
111
73
|
}
|
|
112
74
|
|
|
113
|
-
// 5) Send broadcast request
|
|
114
75
|
try {
|
|
115
76
|
const response = await session.request.send({
|
|
116
77
|
url,
|
|
@@ -124,21 +85,8 @@ async function sendPhoto(session, opts = {}) {
|
|
|
124
85
|
}
|
|
125
86
|
return response;
|
|
126
87
|
} catch (err) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Normalize error shapes to readable text.
|
|
133
|
-
*/
|
|
134
|
-
function normalizeError(err) {
|
|
135
|
-
if (!err) return 'Unknown error';
|
|
136
|
-
if (typeof err === 'string') return err;
|
|
137
|
-
if (err.message) return err.message;
|
|
138
|
-
try {
|
|
139
|
-
return JSON.stringify(err);
|
|
140
|
-
} catch {
|
|
141
|
-
return 'Unserializable error';
|
|
88
|
+
const msg = err.message || JSON.stringify(err);
|
|
89
|
+
throw new Error(`sendPhoto: Broadcast failed — ${msg}`);
|
|
142
90
|
}
|
|
143
91
|
}
|
|
144
92
|
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* uploadPhoto.js
|
|
3
|
-
* Robust image upload helper for nodejs-insta-private-api.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* const uploadPhoto = require('./uploadPhoto');
|
|
7
|
-
* const uploadId = await uploadPhoto(session, photoBuffer, { mimeType: 'image/jpeg' });
|
|
8
|
-
*
|
|
9
|
-
* Notes:
|
|
10
|
-
* - Expects a valid `session` object from nodejs-insta-private-api with `request.send`.
|
|
11
|
-
* - Handles JPEG/PNG; converts common metadata to Instagram-friendly headers.
|
|
12
|
-
* - Returns a string upload_id to be used in the subsequent direct send call.
|
|
2
|
+
* uploadPhoto.js - FIXED version based on instagram-private-api
|
|
13
3
|
*/
|
|
14
4
|
|
|
15
5
|
const { v4: uuidv4 } = require('uuid');
|
|
@@ -28,81 +18,60 @@ function validateImageInput(photoBuffer, mimeType) {
|
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
/**
|
|
31
|
-
* Build Instagram rupload params for photo
|
|
21
|
+
* Build Instagram rupload params for photo - Matching instagram-private-api
|
|
32
22
|
*/
|
|
33
23
|
function buildRuploadParams(uploadId, mimeType) {
|
|
34
|
-
// Instagram expects media_type=1 for photo.
|
|
35
|
-
// image_compression string must be JSON but sent as string.
|
|
36
24
|
const isJpeg = mimeType === 'image/jpeg' || mimeType === 'image/jpg';
|
|
37
25
|
const compression = isJpeg
|
|
38
|
-
?
|
|
39
|
-
:
|
|
26
|
+
? JSON.stringify({ lib_name: 'moz', lib_version: '3.1.m', quality: '80' })
|
|
27
|
+
: JSON.stringify({ lib_name: 'png', lib_version: '1.0', quality: '100' });
|
|
40
28
|
|
|
41
29
|
return {
|
|
42
|
-
|
|
43
|
-
media_type: 1,
|
|
44
|
-
|
|
45
|
-
// Optional hints commonly used by IG web clients
|
|
30
|
+
retry_context: JSON.stringify({ num_step_auto_retry: 0, num_reupload: 0, num_step_manual_retry: 0 }),
|
|
31
|
+
media_type: '1', // String '1' per instagram-private-api
|
|
32
|
+
upload_id: uploadId.toString(),
|
|
46
33
|
xsharing_user_ids: JSON.stringify([]),
|
|
47
|
-
|
|
48
|
-
is_clips_media: false,
|
|
34
|
+
image_compression: compression,
|
|
49
35
|
};
|
|
50
36
|
}
|
|
51
37
|
|
|
52
38
|
/**
|
|
53
39
|
* Upload a photo to Instagram's rupload endpoint.
|
|
54
|
-
* Returns upload_id string on success.
|
|
55
|
-
*
|
|
56
|
-
* @param {object} session - An authenticated session with request.send({ url, method, headers, body })
|
|
57
|
-
* @param {Buffer} photoBuffer - Image buffer (JPEG or PNG)
|
|
58
|
-
* @param {object} [options]
|
|
59
|
-
* @param {string} [options.mimeType='image/jpeg'] - 'image/jpeg' | 'image/png'
|
|
60
|
-
* @param {string} [options.fileName] - Optional file name; autogenerated if not provided
|
|
61
|
-
* @param {number} [options.quality] - 50..100 (only applies to JPEG header hint)
|
|
62
|
-
* @param {AbortSignal} [options.signal] - Optional AbortSignal to cancel
|
|
63
|
-
* @returns {Promise<string>} upload_id
|
|
64
40
|
*/
|
|
65
41
|
async function uploadPhoto(session, photoBuffer, options = {}) {
|
|
66
42
|
const {
|
|
67
43
|
mimeType = 'image/jpeg',
|
|
68
44
|
fileName,
|
|
69
|
-
quality,
|
|
70
45
|
signal,
|
|
71
46
|
} = options;
|
|
72
47
|
|
|
73
48
|
validateImageInput(photoBuffer, mimeType);
|
|
74
49
|
|
|
75
|
-
// Generate uploadId and object name used in rupload path
|
|
76
50
|
const uploadId = Date.now().toString();
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Build rupload params
|
|
51
|
+
const name = `${uploadId}_0_${Math.floor(Math.random() * (9999999999 - 1000000000) + 1000000000)}`;
|
|
52
|
+
const contentLength = photoBuffer.byteLength;
|
|
53
|
+
|
|
82
54
|
const ruploadParams = buildRuploadParams(uploadId, mimeType);
|
|
83
|
-
if (quality && Number.isFinite(quality)) {
|
|
84
|
-
const q = Math.max(50, Math.min(100, Math.round(quality)));
|
|
85
|
-
// override compression string for JPEG quality if provided
|
|
86
|
-
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') {
|
|
87
|
-
ruploadParams.image_compression = `{"lib_name":"moz","lib_version":"3.1.m","quality":"${q}"}`;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
55
|
|
|
91
|
-
// Headers expected by Instagram rupload
|
|
56
|
+
// Headers expected by Instagram rupload (matched with instagram-private-api)
|
|
92
57
|
const headers = {
|
|
58
|
+
'X_FB_PHOTO_WATERFALL_ID': uuidv4(),
|
|
59
|
+
'X-Entity-Type': 'image/jpeg',
|
|
60
|
+
'Offset': '0',
|
|
93
61
|
'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
// Some clients also send:
|
|
100
|
-
// 'Offset': '0',
|
|
62
|
+
'X-Entity-Name': name,
|
|
63
|
+
'X-Entity-Length': String(contentLength),
|
|
64
|
+
'Content-Type': 'application/octet-stream',
|
|
65
|
+
'Content-Length': String(contentLength),
|
|
66
|
+
'Accept-Encoding': 'gzip',
|
|
101
67
|
};
|
|
102
68
|
|
|
103
|
-
|
|
69
|
+
// If it's png, maybe we should adjust X-Entity-Type?
|
|
70
|
+
// instagram-private-api seems to hardcode 'image/jpeg' in the trace for 'photo' method.
|
|
71
|
+
// We'll stick to 'image/jpeg' for X-Entity-Type as per reference unless user provided PNG which might work anyway.
|
|
72
|
+
|
|
73
|
+
const url = `/rupload_igphoto/${name}`;
|
|
104
74
|
|
|
105
|
-
// Send the upload request
|
|
106
75
|
try {
|
|
107
76
|
const response = await session.request.send({
|
|
108
77
|
url,
|
|
@@ -112,63 +81,26 @@ async function uploadPhoto(session, photoBuffer, options = {}) {
|
|
|
112
81
|
signal,
|
|
113
82
|
});
|
|
114
83
|
|
|
115
|
-
// Basic success validation (IG often returns JSON with upload_id or ok status)
|
|
116
84
|
if (!response) {
|
|
117
85
|
throw new Error('uploadPhoto: Empty response from Instagram rupload endpoint.');
|
|
118
86
|
}
|
|
119
87
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
88
|
+
// Try to get upload_id from response
|
|
89
|
+
let serverUploadId = null;
|
|
90
|
+
if (typeof response === 'object' && response.body) {
|
|
91
|
+
try {
|
|
92
|
+
const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
|
|
93
|
+
serverUploadId = body.upload_id;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// ignore parse error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
124
98
|
|
|
125
99
|
return serverUploadId || uploadId;
|
|
126
100
|
} catch (err) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Ensure file name extension matches mime type; strip invalid chars.
|
|
134
|
-
*/
|
|
135
|
-
function sanitizeFileName(name, mimeType) {
|
|
136
|
-
const safe = String(name).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
137
|
-
const ext = safe.split('.').pop()?.toLowerCase();
|
|
138
|
-
const desiredExt = mimeType === 'image/png' ? 'png' : 'jpg';
|
|
139
|
-
if (ext !== desiredExt) {
|
|
140
|
-
// replace or append correct extension
|
|
141
|
-
const base = safe.replace(/\.[^.]+$/, '');
|
|
142
|
-
return `${base}.${desiredExt}`;
|
|
143
|
-
}
|
|
144
|
-
return safe;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Attempt to parse upload_id from response body if present.
|
|
149
|
-
*/
|
|
150
|
-
function parseUploadIdFromBody(body) {
|
|
151
|
-
try {
|
|
152
|
-
const text = Buffer.isBuffer(body) ? body.toString('utf8') : String(body || '');
|
|
153
|
-
const json = JSON.parse(text);
|
|
154
|
-
if (json && typeof json.upload_id === 'string') return json.upload_id;
|
|
155
|
-
return null;
|
|
156
|
-
} catch {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Normalize common error shapes to readable text.
|
|
163
|
-
*/
|
|
164
|
-
function normalizeError(err) {
|
|
165
|
-
if (!err) return 'Unknown error';
|
|
166
|
-
if (typeof err === 'string') return err;
|
|
167
|
-
if (err.message) return err.message;
|
|
168
|
-
try {
|
|
169
|
-
return JSON.stringify(err);
|
|
170
|
-
} catch {
|
|
171
|
-
return 'Unserializable error';
|
|
101
|
+
// If error, try to extract message
|
|
102
|
+
const msg = err.message || err.toString();
|
|
103
|
+
throw new Error(`uploadPhoto: Upload failed — ${msg}`);
|
|
172
104
|
}
|
|
173
105
|
}
|
|
174
106
|
|
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Robust, resumable upload helper for videos/audio/doc-like media (where supported) to Instagram rupload.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* const uploadFile = require('./uploadFile');
|
|
7
|
-
* const uploadId = await uploadFile(session, fileBuffer, {
|
|
8
|
-
* mimeType: 'video/mp4',
|
|
9
|
-
* fileName: 'clip.mp4',
|
|
10
|
-
* isClipsMedia: false, // set true for reels-like uploads if your flow supports it
|
|
11
|
-
* chunkSize: 512 * 1024, // 512KB chunks (safe default)
|
|
12
|
-
* });
|
|
13
|
-
*
|
|
14
|
-
* Notes:
|
|
15
|
-
* - Instagram Direct primarily supports photo/video/audio; arbitrary files (pdf/zip) are typically NOT accepted by IG clients.
|
|
16
|
-
* - For videos, use `video/mp4`. For audio, try `audio/mpeg` (limited support). For images, prefer uploadPhoto.js.
|
|
17
|
-
* - This helper performs chunked upload using rupload endpoints and returns an `upload_id` string.
|
|
18
|
-
* - Expects `session.request.send({ url, method, headers, body })` similar to nodejs-insta-private-api clients.
|
|
2
|
+
* uploadfFile.js - FIXED version based on instagram-private-api
|
|
19
3
|
*/
|
|
20
4
|
|
|
21
5
|
const { v4: uuidv4 } = require('uuid');
|
|
22
6
|
|
|
23
|
-
const DEFAULT_CHUNK_SIZE = 512 * 1024; // 512KB
|
|
7
|
+
const DEFAULT_CHUNK_SIZE = 512 * 1024; // 512KB
|
|
24
8
|
|
|
25
9
|
/**
|
|
26
10
|
* Validate upload input
|
|
@@ -30,234 +14,116 @@ function validateFileInput(fileBuffer, mimeType) {
|
|
|
30
14
|
throw new Error('uploadFile: fileBuffer must be a non-empty Buffer.');
|
|
31
15
|
}
|
|
32
16
|
if (typeof mimeType !== 'string' || mimeType.length === 0) {
|
|
33
|
-
throw new Error('uploadFile: mimeType must be a non-empty string
|
|
17
|
+
throw new Error('uploadFile: mimeType must be a non-empty string.');
|
|
34
18
|
}
|
|
35
19
|
}
|
|
36
20
|
|
|
37
21
|
/**
|
|
38
|
-
*
|
|
39
|
-
*/
|
|
40
|
-
function sanitizeFileName(name, mimeType) {
|
|
41
|
-
const safe = String(name || '').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
42
|
-
const ext = safe.split('.').pop()?.toLowerCase();
|
|
43
|
-
const desiredExt = guessExtFromMime(mimeType);
|
|
44
|
-
if (!safe) {
|
|
45
|
-
return `${uuidv4()}.${desiredExt}`;
|
|
46
|
-
}
|
|
47
|
-
if (!ext || ext !== desiredExt) {
|
|
48
|
-
const base = safe.replace(/\.[^.]+$/, '');
|
|
49
|
-
return `${base}.${desiredExt}`;
|
|
50
|
-
}
|
|
51
|
-
return safe;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Guess file extension based on mime type (limited map).
|
|
56
|
-
*/
|
|
57
|
-
function guessExtFromMime(mimeType) {
|
|
58
|
-
if (mimeType === 'video/mp4') return 'mp4';
|
|
59
|
-
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') return 'jpg';
|
|
60
|
-
if (mimeType === 'image/png') return 'png';
|
|
61
|
-
if (mimeType === 'audio/mpeg') return 'mp3';
|
|
62
|
-
// fallback
|
|
63
|
-
return 'bin';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Build rupload params for non-photo media.
|
|
68
|
-
* - For video: media_type=2
|
|
69
|
-
* - For audio: still treated as video-like upload in some web flows (limited)
|
|
22
|
+
* Build rupload params - Matching instagram-private-api
|
|
70
23
|
*/
|
|
71
24
|
function buildRuploadParams(uploadId, mimeType, opts) {
|
|
72
25
|
const isVideo = mimeType.startsWith('video/');
|
|
73
26
|
const isAudio = mimeType.startsWith('audio/');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Instagram expects:
|
|
77
|
-
// - video: media_type=2 with specific flags
|
|
78
|
-
// - image: use uploadPhoto.js (media_type=1). Here we allow image for completeness but recommend uploadPhoto.js
|
|
27
|
+
|
|
79
28
|
const params = {
|
|
80
|
-
|
|
81
|
-
media_type:
|
|
29
|
+
retry_context: JSON.stringify({ num_step_auto_retry: 0, num_reupload: 0, num_step_manual_retry: 0 }),
|
|
30
|
+
media_type: isVideo ? '2' : '3', // 2 for video, 3 for audio/others?
|
|
31
|
+
upload_id: uploadId.toString(),
|
|
82
32
|
xsharing_user_ids: JSON.stringify([]),
|
|
83
|
-
is_clips_media: Boolean(opts.isClipsMedia),
|
|
84
33
|
};
|
|
85
34
|
|
|
86
35
|
if (isVideo) {
|
|
87
|
-
params.
|
|
88
|
-
params.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// IG web may not fully support audio in Direct; leaving generic params
|
|
92
|
-
params.audio_format = 'mpeg';
|
|
36
|
+
params.upload_media_duration_ms = opts.duration?.toString() || '0';
|
|
37
|
+
params.upload_media_width = opts.width?.toString() || '720';
|
|
38
|
+
params.upload_media_height = opts.height?.toString() || '1280';
|
|
39
|
+
params.direct_v2 = '1';
|
|
93
40
|
}
|
|
94
41
|
|
|
95
42
|
return params;
|
|
96
43
|
}
|
|
97
44
|
|
|
98
45
|
/**
|
|
99
|
-
*
|
|
100
|
-
*/
|
|
101
|
-
function parseUploadIdFromBody(body) {
|
|
102
|
-
try {
|
|
103
|
-
const text = Buffer.isBuffer(body) ? body.toString('utf8') : String(body || '');
|
|
104
|
-
const json = JSON.parse(text);
|
|
105
|
-
if (json && typeof json.upload_id === 'string') return json.upload_id;
|
|
106
|
-
return null;
|
|
107
|
-
} catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Normalize error shapes to readable text.
|
|
114
|
-
*/
|
|
115
|
-
function normalizeError(err) {
|
|
116
|
-
if (!err) return 'Unknown error';
|
|
117
|
-
if (typeof err === 'string') return err;
|
|
118
|
-
if (err.message) return err.message;
|
|
119
|
-
try {
|
|
120
|
-
return JSON.stringify(err);
|
|
121
|
-
} catch {
|
|
122
|
-
return 'Unserializable error';
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Perform the initial "create upload" handshake for video/audio.
|
|
128
|
-
* Some IG flows accept an initial POST with headers only (no body) to initialize the rupload.
|
|
129
|
-
*/
|
|
130
|
-
async function initRupload(session, url, headers, signal) {
|
|
131
|
-
try {
|
|
132
|
-
const res = await session.request.send({
|
|
133
|
-
url,
|
|
134
|
-
method: 'POST',
|
|
135
|
-
headers,
|
|
136
|
-
// body omitted for init in some flows
|
|
137
|
-
signal,
|
|
138
|
-
});
|
|
139
|
-
return res;
|
|
140
|
-
} catch (err) {
|
|
141
|
-
// Some endpoints accept direct chunk upload without explicit init; not fatal.
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Upload a single chunk to rupload endpoint.
|
|
148
|
-
*/
|
|
149
|
-
async function uploadChunk(session, url, headers, chunk, offset, signal) {
|
|
150
|
-
const chunkHeaders = {
|
|
151
|
-
...headers,
|
|
152
|
-
'X-Entity-Length': String(headers['X-Entity-Length']), // total length
|
|
153
|
-
'X-Entity-Name': headers['X-Entity-Name'],
|
|
154
|
-
'X-Entity-Type': headers['X-Entity-Type'],
|
|
155
|
-
'Offset': String(offset),
|
|
156
|
-
'Content-Length': String(chunk.length),
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const res = await session.request.send({
|
|
160
|
-
url,
|
|
161
|
-
method: 'POST',
|
|
162
|
-
headers: chunkHeaders,
|
|
163
|
-
body: chunk,
|
|
164
|
-
signal,
|
|
165
|
-
});
|
|
166
|
-
return res;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Upload file (video/audio/image as supported) via rupload with chunking.
|
|
171
|
-
*
|
|
172
|
-
* @param {object} session - Authenticated session with request.send
|
|
173
|
-
* @param {Buffer} fileBuffer - File data buffer
|
|
174
|
-
* @param {object} [options]
|
|
175
|
-
* @param {string} [options.mimeType='video/mp4'] - e.g., 'video/mp4', 'audio/mpeg'
|
|
176
|
-
* @param {string} [options.fileName] - Optional, sanitized based on mime
|
|
177
|
-
* @param {number} [options.chunkSize=512*1024] - Chunk size in bytes
|
|
178
|
-
* @param {boolean} [options.isClipsMedia=false] - Mark as clips/reels upload hint
|
|
179
|
-
* @param {AbortSignal} [options.signal] - Optional AbortSignal
|
|
180
|
-
* @returns {Promise<string>} upload_id
|
|
46
|
+
* Upload file via rupload with chunking.
|
|
181
47
|
*/
|
|
182
48
|
async function uploadFile(session, fileBuffer, options = {}) {
|
|
183
49
|
const {
|
|
184
50
|
mimeType = 'video/mp4',
|
|
185
51
|
fileName,
|
|
186
52
|
chunkSize = DEFAULT_CHUNK_SIZE,
|
|
187
|
-
isClipsMedia = false,
|
|
188
53
|
signal,
|
|
189
54
|
} = options;
|
|
190
55
|
|
|
191
56
|
validateFileInput(fileBuffer, mimeType);
|
|
192
57
|
|
|
193
58
|
const uploadId = Date.now().toString();
|
|
194
|
-
const
|
|
59
|
+
const name = `${uploadId}_0_${Math.floor(Math.random() * 9000000000 + 1000000000)}`;
|
|
195
60
|
const totalLength = fileBuffer.length;
|
|
61
|
+
const waterfallId = uuidv4();
|
|
196
62
|
|
|
197
|
-
const ruploadParams = buildRuploadParams(uploadId, mimeType,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
'X-Entity-Name': objectName,
|
|
203
|
-
'X-Entity-Length': String(totalLength),
|
|
204
|
-
'Content-Type': mimeType,
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// Choose endpoint based on type
|
|
208
|
-
const endpoint =
|
|
209
|
-
mimeType.startsWith('image/')
|
|
210
|
-
? `/rupload_igphoto/${objectName}`
|
|
211
|
-
: `/rupload_igvideo/${objectName}`;
|
|
212
|
-
|
|
213
|
-
// Optional init step (some IG flows initialize upload session)
|
|
214
|
-
await initRupload(session, endpoint, baseHeaders, signal).catch(() => null);
|
|
63
|
+
const ruploadParams = buildRuploadParams(uploadId, mimeType, options);
|
|
64
|
+
|
|
65
|
+
const endpoint = mimeType.startsWith('image/')
|
|
66
|
+
? `/rupload_igphoto/${name}`
|
|
67
|
+
: `/rupload_igvideo/${name}`;
|
|
215
68
|
|
|
216
|
-
//
|
|
69
|
+
// Start upload
|
|
217
70
|
let offset = 0;
|
|
218
|
-
const size = Math.max(64 * 1024, Math.min(4 * 1024 * 1024, chunkSize));
|
|
71
|
+
const size = Math.max(64 * 1024, Math.min(4 * 1024 * 1024, chunkSize));
|
|
219
72
|
|
|
220
73
|
try {
|
|
221
74
|
while (offset < totalLength) {
|
|
222
75
|
const end = Math.min(offset + size, totalLength);
|
|
223
76
|
const chunk = fileBuffer.subarray(offset, end);
|
|
224
77
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
78
|
+
const headers = {
|
|
79
|
+
'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
|
|
80
|
+
'X_FB_VIDEO_WATERFALL_ID': waterfallId,
|
|
81
|
+
'X-Entity-Type': mimeType,
|
|
82
|
+
'X-Entity-Name': name,
|
|
83
|
+
'X-Entity-Length': String(totalLength),
|
|
84
|
+
'Offset': String(offset),
|
|
85
|
+
'Content-Type': 'application/octet-stream',
|
|
86
|
+
'Content-Length': String(chunk.length),
|
|
87
|
+
'Accept-Encoding': 'gzip',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const res = await session.request.send({
|
|
91
|
+
url: endpoint,
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers,
|
|
94
|
+
body: chunk,
|
|
95
|
+
signal,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!res) throw new Error(`uploadFile: Empty response at offset ${offset}`);
|
|
233
99
|
offset = end;
|
|
234
100
|
}
|
|
235
|
-
} catch (err) {
|
|
236
|
-
throw new Error(`uploadFile: Chunk upload failed at offset ${offset} — ${normalizeError(err)}`);
|
|
237
|
-
}
|
|
238
101
|
|
|
239
|
-
|
|
240
|
-
// We'll try a lightweight confirmation request and parse upload_id if present.
|
|
241
|
-
try {
|
|
102
|
+
// Finalize
|
|
242
103
|
const confirm = await session.request.send({
|
|
243
104
|
url: endpoint,
|
|
244
105
|
method: 'POST',
|
|
245
106
|
headers: {
|
|
246
|
-
|
|
107
|
+
'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
|
|
108
|
+
'X_FB_VIDEO_WATERFALL_ID': waterfallId,
|
|
109
|
+
'X-Entity-Type': mimeType,
|
|
247
110
|
'Offset': String(totalLength),
|
|
248
111
|
'Content-Length': '0',
|
|
249
112
|
},
|
|
250
113
|
signal,
|
|
251
114
|
});
|
|
252
115
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
116
|
+
let serverUploadId = null;
|
|
117
|
+
if (confirm && confirm.body) {
|
|
118
|
+
try {
|
|
119
|
+
const body = typeof confirm.body === 'string' ? JSON.parse(confirm.body) : confirm.body;
|
|
120
|
+
serverUploadId = body.upload_id;
|
|
121
|
+
} catch (e) {}
|
|
122
|
+
}
|
|
256
123
|
|
|
257
124
|
return serverUploadId || uploadId;
|
|
258
|
-
} catch {
|
|
259
|
-
|
|
260
|
-
return uploadId;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw new Error(`uploadFile: Upload failed — ${err.message}`);
|
|
261
127
|
}
|
|
262
128
|
}
|
|
263
129
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodejs-insta-private-api-mqtt",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.14",
|
|
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": {
|