nodejs-insta-private-api-mqtt 1.3.13 → 1.3.15
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 +290 -31
- package/dist/core/request.js +127 -53
- package/dist/realtime/commands/enhanced.direct.commands.js +93 -74
- package/dist/repositories/direct-thread.repository.js +108 -15
- package/dist/repositories/upload.repository.js +5 -9
- package/dist/sendmedia/sendPhoto.js +115 -74
- package/dist/sendmedia/uploadPhoto.js +2 -2
- 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,45 +262,105 @@ startBot().catch(console.error);
|
|
|
263
262
|
|
|
264
263
|
---
|
|
265
264
|
|
|
266
|
-
## 🖼️ NEW: Send Media via MQTT (v5.70.0)
|
|
267
265
|
|
|
268
|
-
|
|
266
|
+
## 🖼️ IMPORTANT: How media actually works (photos & videos)
|
|
269
267
|
|
|
270
|
-
|
|
271
|
-
```javascript
|
|
272
|
-
const { IgApiClient, RealtimeClient, sendmedia } = require('nodejs-insta-private-api-mqtt');
|
|
273
|
-
const fs = require('fs');
|
|
268
|
+
> **Critical clarification:** The previous README wording that claimed *"Send Media via MQTT"* was misleading. Instagram's real-time (MQTT) layer is a *sync/notification* channel — **it does not replace the HTTP upload + broadcast flow required to create new photo/video DM messages server-side**. The correct and reliable sequence to send media is:
|
|
274
269
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// ... login logic ...
|
|
270
|
+
1. **Upload the media (rupload) — HTTP**
|
|
271
|
+
Upload the raw bytes to Instagram's rupload endpoint (this returns an `upload_id`). This step stores the media on Instagram's storage/CDN.
|
|
278
272
|
|
|
279
|
-
|
|
280
|
-
|
|
273
|
+
2. **Create / broadcast the media message — HTTP**
|
|
274
|
+
Use the Direct API `configure_photo` (or video equivalent) endpoint to create the DM message that references the `upload_id`. This action makes the message exist on Instagram's servers and associates it with the target thread(s).
|
|
281
275
|
|
|
282
|
-
|
|
276
|
+
3. **MQTT = realtime sync / notification**
|
|
277
|
+
Once the server-side message exists, MQTT will propagate a `MESSAGE_SYNC` (or similar) event so clients receive the message in realtime. MQTT does **not** perform the storage, processing or permanent creation of media on the server.
|
|
283
278
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### Why this change matters
|
|
282
|
+
|
|
283
|
+
- MQTT is excellent for **text**, **reactions**, **typing**, **presence** and **read receipts** — these can be (and are) sent directly over realtime topics.
|
|
284
|
+
- Media (images, videos, view-once content) require storage, processing and metadata which are handled by HTTP endpoints. Without the HTTP broadcast step, other clients will not see the uploaded bytes even if an MQTT "send" payload is transmitted.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### Correct examples (upload + broadcast)
|
|
289
|
+
|
|
290
|
+
#### Upload a photo and broadcast (recommended pattern)
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
const fs = require('fs');
|
|
294
|
+
const { v4: uuidv4 } = require('uuid');
|
|
295
|
+
|
|
296
|
+
const photoBuffer = fs.readFileSync('./image.jpg');
|
|
297
|
+
|
|
298
|
+
// 1) rupload (helper may exist as ig.publish.photo)
|
|
299
|
+
const upload = await ig.publish.photo({ file: photoBuffer });
|
|
300
|
+
// upload.upload_id should be available (or upload.uploadId)
|
|
301
|
+
|
|
302
|
+
// 2) broadcast the uploaded photo via HTTP to the thread(s)
|
|
303
|
+
const mutationToken = uuidv4();
|
|
304
|
+
const payload = {
|
|
305
|
+
action: 'configure_photo',
|
|
306
|
+
upload_id: String(upload.upload_id || upload.uploadId || upload),
|
|
307
|
+
thread_ids: JSON.stringify([threadId]), // or JSON array of thread ids
|
|
308
|
+
client_context: mutationToken,
|
|
309
|
+
mutation_token: mutationToken,
|
|
310
|
+
_csrftoken: ig.state.cookieCsrfToken || '',
|
|
311
|
+
_uuid: ig.state.uuid || '',
|
|
312
|
+
device_id: ig.state.deviceId || ''
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const response = await ig.request.send({
|
|
316
|
+
url: '/api/v1/direct_v2/threads/broadcast/configure_photo/',
|
|
317
|
+
method: 'POST',
|
|
318
|
+
form: payload,
|
|
319
|
+
timeout: 60000
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// If response indicates success (status: 'ok' or similar), the message is now created server-side.
|
|
323
|
+
// MQTT will usually emit a MESSAGE_SYNC event shortly after with the CDN URL for the image.
|
|
292
324
|
```
|
|
293
325
|
|
|
294
|
-
|
|
326
|
+
#### Upload and broadcast a video
|
|
327
|
+
|
|
295
328
|
```javascript
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
329
|
+
const videoBuffer = fs.readFileSync('./video.mp4');
|
|
330
|
+
|
|
331
|
+
// 1) rupload video (may be ig.publish.video)
|
|
332
|
+
const upload = await ig.publish.video({ video: videoBuffer, duration: 15000 });
|
|
333
|
+
|
|
334
|
+
// 2) broadcast via HTTP similar to photo but to the upload_video endpoint
|
|
335
|
+
const mutationToken = uuidv4();
|
|
336
|
+
const payload = {
|
|
337
|
+
action: 'configure_video',
|
|
338
|
+
upload_id: String(upload.upload_id || upload.uploadId || upload),
|
|
339
|
+
thread_ids: JSON.stringify([threadId]),
|
|
340
|
+
client_context: mutationToken,
|
|
341
|
+
mutation_token: mutationToken,
|
|
342
|
+
_csrftoken: ig.state.cookieCsrfToken || '',
|
|
343
|
+
_uuid: ig.state.uuid || '',
|
|
344
|
+
device_id: ig.state.deviceId || ''
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const response = await ig.request.send({
|
|
348
|
+
url: '/api/v1/direct_v2/threads/broadcast/configure_video/',
|
|
349
|
+
method: 'POST',
|
|
350
|
+
form: payload,
|
|
351
|
+
timeout: 90000
|
|
302
352
|
});
|
|
303
353
|
```
|
|
304
354
|
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
### What was removed/changed
|
|
358
|
+
- Any claim that photos/videos can be **created** solely through MQTT was corrected.
|
|
359
|
+
- Examples and helper text were updated to show the required HTTP upload + broadcast flow.
|
|
360
|
+
- MQTT examples for text and realtime events remain unchanged; MQTT should still be used for fast realtime behavior.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
305
364
|
|
|
306
365
|
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.
|
|
307
366
|
|
|
@@ -962,7 +1021,7 @@ const {
|
|
|
962
1021
|
RealtimeClient,
|
|
963
1022
|
downloadContentFromMessage,
|
|
964
1023
|
isViewOnceMedia
|
|
965
|
-
} = require('nodejs-insta-private-api');
|
|
1024
|
+
} = require('nodejs-insta-private-api-mqtt');
|
|
966
1025
|
const fs = require('fs');
|
|
967
1026
|
|
|
968
1027
|
const ig = new IgApiClient();
|
|
@@ -1014,7 +1073,8 @@ realtime.on('message', async (data) => {
|
|
|
1014
1073
|
const {
|
|
1015
1074
|
downloadMediaBuffer,
|
|
1016
1075
|
hasMedia,
|
|
1017
|
-
getMediaType
|
|
1076
|
+
getMediaType,
|
|
1077
|
+
MEDIA_TYPES
|
|
1018
1078
|
} = require('nodejs-insta-private-api-mqtt');
|
|
1019
1079
|
|
|
1020
1080
|
realtime.on('message', async (data) => {
|
|
@@ -1203,7 +1263,7 @@ startMediaBot().catch(console.error);
|
|
|
1203
1263
|
### MEDIA_TYPES Constants
|
|
1204
1264
|
|
|
1205
1265
|
```javascript
|
|
1206
|
-
const { MEDIA_TYPES } = require('nodejs-insta-private-api');
|
|
1266
|
+
const { MEDIA_TYPES } = require('nodejs-insta-private-api-mqtt');
|
|
1207
1267
|
|
|
1208
1268
|
MEDIA_TYPES.IMAGE // Regular photo
|
|
1209
1269
|
MEDIA_TYPES.VIDEO // Regular video
|
|
@@ -1351,6 +1411,205 @@ const fs = require('fs');
|
|
|
1351
1411
|
|
|
1352
1412
|
---
|
|
1353
1413
|
|
|
1414
|
+
|
|
1415
|
+
---
|
|
1416
|
+
|
|
1417
|
+
### Example 3: CLI Photo Sender (upload HTTP + broadcast HTTP, with MQTT sync)
|
|
1418
|
+
|
|
1419
|
+
This example is a ready-to-run CLI script that demonstrates the correct flow to **send a photo** to one or more threads:
|
|
1420
|
+
1. upload the photo to Instagram's rupload endpoint (HTTP),
|
|
1421
|
+
2. broadcast (configure) the photo to the target thread(s) using the Direct API (HTTP),
|
|
1422
|
+
3. use MQTT (RealtimeClient) only for realtime sync and confirmation.
|
|
1423
|
+
|
|
1424
|
+
Save this as `examples/send-photo-cli.js`.
|
|
1425
|
+
|
|
1426
|
+
```javascript
|
|
1427
|
+
#!/usr/bin/env node
|
|
1428
|
+
// examples/send-photo-cli.js
|
|
1429
|
+
// Node 18+, install nodejs-insta-private-api-mqtt in the project
|
|
1430
|
+
|
|
1431
|
+
const fs = require('fs');
|
|
1432
|
+
const path = require('path');
|
|
1433
|
+
const readline = require('readline/promises');
|
|
1434
|
+
const { stdin: input, stdout: output } = require('process');
|
|
1435
|
+
const { v4: uuidv4 } = require('uuid');
|
|
1436
|
+
|
|
1437
|
+
(async () => {
|
|
1438
|
+
const rl = readline.createInterface({ input, output });
|
|
1439
|
+
try {
|
|
1440
|
+
console.log('\\n--- Instagram Photo Sender (HTTP + broadcast) ---\\n');
|
|
1441
|
+
const username = (await rl.question('Enter your Instagram username: ')).trim();
|
|
1442
|
+
const password = (await rl.question('Enter your Instagram password: ')).trim();
|
|
1443
|
+
if (!username || !password) {
|
|
1444
|
+
console.error('username & password required'); process.exit(1);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const { IgApiClient, RealtimeClient, useMultiFileAuthState } = require('nodejs-insta-private-api-mqtt');
|
|
1448
|
+
const AUTH_FOLDER = path.resolve(process.cwd(), './auth_info_instagram');
|
|
1449
|
+
|
|
1450
|
+
const authState = await useMultiFileAuthState(AUTH_FOLDER);
|
|
1451
|
+
const ig = new IgApiClient();
|
|
1452
|
+
ig.state.generateDevice?.(username);
|
|
1453
|
+
|
|
1454
|
+
// load saved credentials if available
|
|
1455
|
+
if (authState.hasSession && authState.hasSession()) {
|
|
1456
|
+
try { await authState.loadCreds?.(ig); console.log('[*] Loaded saved creds'); } catch(e){}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// login if needed
|
|
1460
|
+
if (!authState.hasSession || (typeof authState.hasSession === 'function' && !authState.hasSession())) {
|
|
1461
|
+
await ig.login({ username, password });
|
|
1462
|
+
await authState.saveCreds?.(ig);
|
|
1463
|
+
console.log('[+] Logged in and saved creds');
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// start realtime client (optional, helps sync)
|
|
1467
|
+
const realtime = new RealtimeClient(ig);
|
|
1468
|
+
realtime.on?.('connected', () => console.log('✅ MQTT connected (realtime)'));
|
|
1469
|
+
|
|
1470
|
+
// fetch inbox and list threads
|
|
1471
|
+
const inbox = await ig.direct.getInbox();
|
|
1472
|
+
const threads = inbox.threads || inbox.items || inbox.threads_response?.threads || [];
|
|
1473
|
+
if (!threads || threads.length === 0) {
|
|
1474
|
+
console.error('No threads found in inbox. Exiting.'); process.exit(1);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
console.log('\\nAvailable threads:');
|
|
1478
|
+
const indexToThread = [];
|
|
1479
|
+
threads.slice(0, 50).forEach((t, i) => {
|
|
1480
|
+
const id = String(t.thread_id || t.threadId || t.id || t.thread_id_str || (t.thread && t.thread.thread_id) || '');
|
|
1481
|
+
const title = t.thread_title || t.title || t.item_title || (t.users || t.participants || []).map(u => u.username || u.full_name).slice(0,3).join(', ') || 'Unnamed';
|
|
1482
|
+
indexToThread.push({ id, title });
|
|
1483
|
+
console.log(`${i+1}. ${title} (thread_id: ${id})`);
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
const pick = (await rl.question('\\nSelect thread numbers (e.g. "1 2 3"): ')).trim();
|
|
1487
|
+
const picks = pick.split(/[\s,]+/).map(x => parseInt(x,10)).filter(n => Number.isFinite(n) && n >= 1 && n <= indexToThread.length);
|
|
1488
|
+
const selected = picks.map(p => indexToThread[p-1].id);
|
|
1489
|
+
|
|
1490
|
+
const photoPath = (await rl.question('\\nEnter your Photo path here (absolute or relative): ')).trim();
|
|
1491
|
+
const resolved = path.isAbsolute(photoPath) ? photoPath : path.resolve(process.cwd(), photoPath);
|
|
1492
|
+
if (!fs.existsSync(resolved)) { console.error('File not found', resolved); process.exit(1); }
|
|
1493
|
+
const buffer = fs.readFileSync(resolved);
|
|
1494
|
+
console.log('Photo size (MB):', (buffer.length/(1024*1024)).toFixed(2));
|
|
1495
|
+
|
|
1496
|
+
// RUUPLOAD (HTTP) - try to use ig.publish.photo helper if available
|
|
1497
|
+
let uploadId;
|
|
1498
|
+
try {
|
|
1499
|
+
if (typeof ig.publish?.photo === 'function') {
|
|
1500
|
+
const up = await ig.publish.photo({ file: buffer });
|
|
1501
|
+
uploadId = up.upload_id || up.uploadId || up;
|
|
1502
|
+
} else {
|
|
1503
|
+
// manual rupload via request
|
|
1504
|
+
uploadId = Date.now().toString();
|
|
1505
|
+
const objectName = `${uploadId}_0_${Math.floor(Math.random() * 1e10)}`;
|
|
1506
|
+
const ruploadParams = {
|
|
1507
|
+
retry_context: JSON.stringify({ num_step_auto_retry: 0, num_reupload: 0, num_step_manual_retry: 0 }),
|
|
1508
|
+
media_type: '1',
|
|
1509
|
+
upload_id: uploadId,
|
|
1510
|
+
xsharing_user_ids: JSON.stringify([]),
|
|
1511
|
+
image_compression: JSON.stringify({ lib_name: 'moz', lib_version: '3.1.m', quality: '80' })
|
|
1512
|
+
};
|
|
1513
|
+
const headers = {
|
|
1514
|
+
'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
|
|
1515
|
+
'Content-Type': 'application/octet-stream',
|
|
1516
|
+
'X-Entity-Type': 'image/jpeg',
|
|
1517
|
+
'Offset': '0',
|
|
1518
|
+
'X-Entity-Name': objectName,
|
|
1519
|
+
'X-Entity-Length': String(buffer.length),
|
|
1520
|
+
'Content-Length': String(buffer.length),
|
|
1521
|
+
};
|
|
1522
|
+
await ig.request.send({
|
|
1523
|
+
url: `/rupload_igphoto/${objectName}`,
|
|
1524
|
+
method: 'POST',
|
|
1525
|
+
headers,
|
|
1526
|
+
body: buffer,
|
|
1527
|
+
timeout: 120000,
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
console.log('[+] Upload completed. upload_id =', uploadId);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
console.error('Upload failed:', err?.message || err); process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// BROADCAST (HTTP) - create message on server
|
|
1536
|
+
try {
|
|
1537
|
+
const mutationToken = uuidv4();
|
|
1538
|
+
const payload = {
|
|
1539
|
+
action: 'configure_photo',
|
|
1540
|
+
upload_id: String(uploadId),
|
|
1541
|
+
thread_ids: JSON.stringify(selected),
|
|
1542
|
+
client_context: mutationToken,
|
|
1543
|
+
mutation_token: mutationToken,
|
|
1544
|
+
_csrftoken: ig.state.cookieCsrfToken || '',
|
|
1545
|
+
_uuid: ig.state.uuid || '',
|
|
1546
|
+
device_id: ig.state.deviceId || ''
|
|
1547
|
+
};
|
|
1548
|
+
const resp = await ig.request.send({
|
|
1549
|
+
url: '/api/v1/direct_v2/threads/broadcast/configure_photo/',
|
|
1550
|
+
method: 'POST',
|
|
1551
|
+
form: payload,
|
|
1552
|
+
timeout: 60000,
|
|
1553
|
+
});
|
|
1554
|
+
console.log('✅ Broadcast response received:', resp && (resp.status || resp.body || resp.data) ? 'OK' : JSON.stringify(resp).slice(0,120));
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
console.error('Broadcast failed:', err?.message || err);
|
|
1557
|
+
process.exit(1);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
console.log('\\nDone. MQTT will sync and you should see the message in the app.');
|
|
1561
|
+
rl.close();
|
|
1562
|
+
process.exit(0);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
console.error('Fatal:', e);
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
})();
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
---
|
|
1571
|
+
|
|
1572
|
+
### Example 4: Video Sender (upload + broadcast)
|
|
1573
|
+
|
|
1574
|
+
This is similar to the photo sender but uses the video upload flow and longer timeouts. Use `ig.publish.video()` if available; otherwise rupload manually and call `/api/v1/direct_v2/threads/broadcast/configure_video/`.
|
|
1575
|
+
|
|
1576
|
+
```javascript
|
|
1577
|
+
// pseudo-code outline
|
|
1578
|
+
const videoBuffer = fs.readFileSync('./video.mp4');
|
|
1579
|
+
const upload = await ig.publish.video({ video: videoBuffer, duration: 12000 });
|
|
1580
|
+
// upload.upload_id -> then broadcast with configure_video payload similar to the photo flow
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
**Notes:** video uploads often require multipart/segmented upload and longer timeouts; prefer library helper `ig.publish.video()`.
|
|
1584
|
+
|
|
1585
|
+
---
|
|
1586
|
+
|
|
1587
|
+
### Example 5: Share a file / document (workaround)
|
|
1588
|
+
|
|
1589
|
+
Instagram DM does not provide a simple "attach arbitrary file" endpoint. Common workarounds:
|
|
1590
|
+
|
|
1591
|
+
1. Upload the file to a hosting service (S3, Dropbox, your own server), then send the link as a text message via MQTT (or HTTP direct send).
|
|
1592
|
+
2. If the file is an image or video, use the rupload + broadcast flow above.
|
|
1593
|
+
|
|
1594
|
+
Example (send link message):
|
|
1595
|
+
|
|
1596
|
+
```javascript
|
|
1597
|
+
// send link as text via realtime (MQTT)
|
|
1598
|
+
await realtime.directCommands.sendTextViaRealtime(threadId, 'Download the file here: https://myhost.example.com/myfile.pdf');
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
---
|
|
1602
|
+
|
|
1603
|
+
### Tips for bots sending media
|
|
1604
|
+
|
|
1605
|
+
- Always **upload first**, then **broadcast**. Do not rely on MQTT-only calls for media.
|
|
1606
|
+
- Use retries and exponential backoff for both rupload and broadcast steps (Instagram can return transient 503).
|
|
1607
|
+
- Save `upload_id` and broadcast responses for debugging.
|
|
1608
|
+
- If you need to send the same file to many threads, upload once, reuse the same `upload_id` in multiple broadcast calls.
|
|
1609
|
+
|
|
1610
|
+
---
|
|
1611
|
+
|
|
1612
|
+
|
|
1354
1613
|
## API Reference
|
|
1355
1614
|
|
|
1356
1615
|
### IgApiClient
|
package/dist/core/request.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request.fixed.js
|
|
3
|
+
*
|
|
4
|
+
* Repaired Request wrapper for nodejs-insta-private-api(-mqtt).
|
|
5
|
+
* Changes / fixes applied:
|
|
6
|
+
* - Removed global Content-Type header (was forcing urlencoded for all requests).
|
|
7
|
+
* - Made axios timeout configurable via client.state.requestTimeout (fallback 120s).
|
|
8
|
+
* - Set maxContentLength / maxBodyLength = Infinity to allow binary uploads.
|
|
9
|
+
* - Accept both `data` and `body` when callers pass payload; ensures axios receives `data`.
|
|
10
|
+
* - Preserve ability to pass signal (AbortController) through axios config.
|
|
11
|
+
* - Keep updateState / cookie handling intact.
|
|
12
|
+
*
|
|
13
|
+
* Replace the original request.js with this file (or apply same changes).
|
|
14
|
+
*/
|
|
15
|
+
|
|
1
16
|
const axios = require('axios');
|
|
2
17
|
const crypto = require('crypto');
|
|
3
18
|
const { random } = require('lodash');
|
|
@@ -7,15 +22,25 @@ class Request {
|
|
|
7
22
|
this.client = client;
|
|
8
23
|
this.end$ = { complete: () => {} };
|
|
9
24
|
this.error$ = { complete: () => {} };
|
|
10
|
-
|
|
11
|
-
//
|
|
25
|
+
|
|
26
|
+
// Determine timeout: prefer client.state.requestTimeout if provided, otherwise 120s
|
|
27
|
+
const timeoutMs = (this.client && this.client.state && this.client.state.requestTimeout)
|
|
28
|
+
? this.client.state.requestTimeout
|
|
29
|
+
: 120000;
|
|
30
|
+
|
|
31
|
+
// Create axios instance with sensible defaults for uploads
|
|
12
32
|
this.httpClient = axios.create({
|
|
13
33
|
baseURL: 'https://i.instagram.com/',
|
|
14
|
-
timeout:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
timeout: timeoutMs,
|
|
35
|
+
// Allow large uploads
|
|
36
|
+
maxContentLength: Infinity,
|
|
37
|
+
maxBodyLength: Infinity,
|
|
38
|
+
// Do not set a global Content-Type here -- requests will set their own appropriate Content-Type
|
|
39
|
+
// headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
|
|
18
40
|
});
|
|
41
|
+
|
|
42
|
+
// Optional: you can add interceptors for debugging if needed
|
|
43
|
+
// this.httpClient.interceptors.response.use(resp => resp, err => Promise.reject(err));
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
signature(data) {
|
|
@@ -46,51 +71,96 @@ class Request {
|
|
|
46
71
|
return `${signature}\n${body}\n`;
|
|
47
72
|
}
|
|
48
73
|
|
|
49
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Send a request.
|
|
76
|
+
* options should follow axios request config shape but this wrapper supports:
|
|
77
|
+
* - options.form -> object (will be turned into application/x-www-form-urlencoded)
|
|
78
|
+
* - options.qs -> query params
|
|
79
|
+
* - options.data or options.body -> request payload (we prefer data)
|
|
80
|
+
*/
|
|
81
|
+
async send(options = {}) {
|
|
82
|
+
// base axios config
|
|
50
83
|
const config = {
|
|
51
|
-
|
|
84
|
+
url: options.url || options.path || options.uri || '',
|
|
85
|
+
method: (options.method || 'GET').toUpperCase(),
|
|
52
86
|
headers: {
|
|
53
87
|
...this.getDefaultHeaders(),
|
|
54
88
|
...(options.headers || {})
|
|
55
|
-
}
|
|
89
|
+
},
|
|
90
|
+
// allow override of responseType if needed
|
|
91
|
+
responseType: options.responseType || undefined,
|
|
92
|
+
// allow axios to handle decompress etc.
|
|
93
|
+
decompress: options.decompress !== undefined ? options.decompress : true,
|
|
56
94
|
};
|
|
57
95
|
|
|
58
|
-
//
|
|
59
|
-
if (options.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
// Query string / params
|
|
97
|
+
if (options.qs) {
|
|
98
|
+
config.params = options.qs;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Abort signal support (axios v0.22+ supports signal)
|
|
102
|
+
if (options.signal) {
|
|
103
|
+
config.signal = options.signal;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle form data (application/x-www-form-urlencoded)
|
|
107
|
+
if (options.form && (config.method === 'POST' || config.method === 'PUT' || config.method === 'PATCH')) {
|
|
108
|
+
// Build a urlencoded string
|
|
109
|
+
const formData = new URLSearchParams();
|
|
110
|
+
Object.keys(options.form).forEach(key => {
|
|
111
|
+
const val = options.form[key];
|
|
112
|
+
// For arrays/objects convert to JSON string to be safe
|
|
113
|
+
if (typeof val === 'object') {
|
|
114
|
+
formData.append(key, JSON.stringify(val));
|
|
115
|
+
} else {
|
|
116
|
+
formData.append(key, String(val));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
config.data = formData.toString();
|
|
120
|
+
config.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
121
|
+
} else {
|
|
122
|
+
// If caller supplied data or body, prefer data
|
|
123
|
+
if (options.data !== undefined) {
|
|
124
|
+
config.data = options.data;
|
|
125
|
+
} else if (options.body !== undefined) {
|
|
126
|
+
// Accept legacy 'body' name used in some wrappers: ensure binary stays as-is
|
|
127
|
+
config.data = options.body;
|
|
66
128
|
}
|
|
67
129
|
}
|
|
68
130
|
|
|
69
|
-
//
|
|
70
|
-
if (options.
|
|
71
|
-
config.
|
|
131
|
+
// If caller explicitly passed paramsSerializer (rare), keep it
|
|
132
|
+
if (options.paramsSerializer) {
|
|
133
|
+
config.paramsSerializer = options.paramsSerializer;
|
|
72
134
|
}
|
|
73
135
|
|
|
74
136
|
try {
|
|
75
|
-
|
|
137
|
+
// Use axios instance
|
|
138
|
+
const response = await this.httpClient.request(config);
|
|
139
|
+
// Update internal client state (cookies, headers, auth, etc.)
|
|
76
140
|
this.updateState(response);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
141
|
+
|
|
142
|
+
// Normalize success check: either HTTP 200 or response.data.status === 'ok'
|
|
143
|
+
const data = response.data;
|
|
144
|
+
if ((data && data.status && data.status === 'ok') || response.status === 200 || response.status === 201) {
|
|
145
|
+
return { body: data, headers: response.headers, status: response.status };
|
|
80
146
|
}
|
|
81
|
-
|
|
147
|
+
|
|
148
|
+
// If not explicitly ok, throw a processed error
|
|
82
149
|
throw this.handleResponseError(response);
|
|
83
150
|
} catch (error) {
|
|
84
|
-
|
|
151
|
+
// If axios error with response, map to IG-specific errors
|
|
152
|
+
if (error && error.response) {
|
|
85
153
|
throw this.handleResponseError(error.response);
|
|
86
154
|
}
|
|
155
|
+
|
|
156
|
+
// Re-throw axios error (timeout, network, abort, etc.)
|
|
87
157
|
throw error;
|
|
88
158
|
}
|
|
89
159
|
}
|
|
90
160
|
|
|
91
161
|
updateState(response) {
|
|
92
|
-
const headers = response.headers;
|
|
93
|
-
|
|
162
|
+
const headers = response.headers || {};
|
|
163
|
+
|
|
94
164
|
if (headers['x-ig-set-www-claim']) {
|
|
95
165
|
this.client.state.igWWWClaim = headers['x-ig-set-www-claim'];
|
|
96
166
|
}
|
|
@@ -104,14 +174,16 @@ class Request {
|
|
|
104
174
|
this.client.state.passwordEncryptionPubKey = headers['ig-set-password-encryption-pub-key'];
|
|
105
175
|
}
|
|
106
176
|
|
|
107
|
-
// Update cookies from Set-Cookie headers
|
|
108
|
-
const setCookieHeaders = headers['set-cookie'];
|
|
109
|
-
if (setCookieHeaders) {
|
|
177
|
+
// Update cookies from Set-Cookie headers (if cookieJar is available)
|
|
178
|
+
const setCookieHeaders = headers['set-cookie'] || headers['Set-Cookie'];
|
|
179
|
+
if (setCookieHeaders && Array.isArray(setCookieHeaders) && this.client.state && this.client.state.cookieStore && typeof this.client.state.cookieStore.setCookieSync === 'function') {
|
|
110
180
|
setCookieHeaders.forEach(cookieString => {
|
|
111
181
|
try {
|
|
112
|
-
|
|
182
|
+
// host constant fallback if available
|
|
183
|
+
const host = (this.client.state.constants && this.client.state.constants.HOST) ? this.client.state.constants.HOST : 'https://i.instagram.com';
|
|
184
|
+
this.client.state.cookieStore.setCookieSync(cookieString, host);
|
|
113
185
|
} catch (e) {
|
|
114
|
-
//
|
|
186
|
+
// ignore cookie parsing errors
|
|
115
187
|
}
|
|
116
188
|
});
|
|
117
189
|
}
|
|
@@ -119,61 +191,62 @@ class Request {
|
|
|
119
191
|
|
|
120
192
|
handleResponseError(response) {
|
|
121
193
|
const data = response.data || {};
|
|
122
|
-
|
|
123
|
-
|
|
194
|
+
const status = response.status;
|
|
195
|
+
|
|
196
|
+
if (data && data.spam) {
|
|
124
197
|
const error = new Error('Action blocked as spam');
|
|
125
198
|
error.name = 'IgActionSpamError';
|
|
126
199
|
error.response = response;
|
|
127
200
|
return error;
|
|
128
201
|
}
|
|
129
|
-
|
|
130
|
-
if (
|
|
202
|
+
|
|
203
|
+
if (status === 404) {
|
|
131
204
|
const error = new Error('Not found');
|
|
132
205
|
error.name = 'IgNotFoundError';
|
|
133
206
|
error.response = response;
|
|
134
207
|
return error;
|
|
135
208
|
}
|
|
136
|
-
|
|
137
|
-
if (data.message === 'challenge_required') {
|
|
209
|
+
|
|
210
|
+
if (data && data.message === 'challenge_required') {
|
|
138
211
|
this.client.state.checkpoint = data;
|
|
139
212
|
const error = new Error('Challenge required');
|
|
140
213
|
error.name = 'IgCheckpointError';
|
|
141
214
|
error.response = response;
|
|
142
215
|
return error;
|
|
143
216
|
}
|
|
144
|
-
|
|
145
|
-
if (data.message === 'user_has_logged_out') {
|
|
217
|
+
|
|
218
|
+
if (data && data.message === 'user_has_logged_out') {
|
|
146
219
|
const error = new Error('User has logged out');
|
|
147
220
|
error.name = 'IgUserHasLoggedOutError';
|
|
148
221
|
error.response = response;
|
|
149
222
|
return error;
|
|
150
223
|
}
|
|
151
|
-
|
|
152
|
-
if (data.message === 'login_required') {
|
|
224
|
+
|
|
225
|
+
if (data && data.message === 'login_required') {
|
|
153
226
|
const error = new Error('Login required');
|
|
154
227
|
error.name = 'IgLoginRequiredError';
|
|
155
228
|
error.response = response;
|
|
156
229
|
return error;
|
|
157
230
|
}
|
|
158
|
-
|
|
159
|
-
if (data.error_type === 'sentry_block') {
|
|
231
|
+
|
|
232
|
+
if (data && data.error_type === 'sentry_block') {
|
|
160
233
|
const error = new Error('Sentry block');
|
|
161
234
|
error.name = 'IgSentryBlockError';
|
|
162
235
|
error.response = response;
|
|
163
236
|
return error;
|
|
164
237
|
}
|
|
165
|
-
|
|
166
|
-
if (data.error_type === 'inactive user') {
|
|
238
|
+
|
|
239
|
+
if (data && data.error_type === 'inactive user') {
|
|
167
240
|
const error = new Error('Inactive user');
|
|
168
241
|
error.name = 'IgInactiveUserError';
|
|
169
242
|
error.response = response;
|
|
170
243
|
return error;
|
|
171
244
|
}
|
|
172
245
|
|
|
173
|
-
const error = new Error(data.message
|
|
246
|
+
const error = new Error((data && data.message) ? data.message : 'Request failed');
|
|
174
247
|
error.name = 'IgResponseError';
|
|
175
248
|
error.response = response;
|
|
176
|
-
error.status =
|
|
249
|
+
error.status = status;
|
|
177
250
|
error.data = data;
|
|
178
251
|
return error;
|
|
179
252
|
}
|
|
@@ -190,16 +263,16 @@ class Request {
|
|
|
190
263
|
'X-IG-Bandwidth-Speed-KBPS': '-1.000',
|
|
191
264
|
'X-IG-Bandwidth-TotalBytes-B': '0',
|
|
192
265
|
'X-IG-Bandwidth-TotalTime-MS': '0',
|
|
193
|
-
'X-IG-Extended-CDN-Thumbnail-Cache-Busting-Value': this.client.state.thumbnailCacheBustingValue.toString(),
|
|
266
|
+
'X-IG-Extended-CDN-Thumbnail-Cache-Busting-Value': (this.client.state.thumbnailCacheBustingValue || 0).toString(),
|
|
194
267
|
'X-Bloks-Version-Id': this.client.state.bloksVersionId,
|
|
195
268
|
'X-IG-WWW-Claim': this.client.state.igWWWClaim || '0',
|
|
196
|
-
'X-Bloks-Is-Layout-RTL': this.client.state.isLayoutRTL.toString(),
|
|
269
|
+
'X-Bloks-Is-Layout-RTL': (this.client.state.isLayoutRTL !== undefined) ? this.client.state.isLayoutRTL.toString() : 'false',
|
|
197
270
|
'X-IG-Connection-Type': this.client.state.connectionTypeHeader,
|
|
198
271
|
'X-IG-Capabilities': this.client.state.capabilitiesHeader,
|
|
199
272
|
'X-IG-App-ID': this.client.state.fbAnalyticsApplicationId,
|
|
200
273
|
'X-IG-Device-ID': this.client.state.uuid,
|
|
201
274
|
'X-IG-Android-ID': this.client.state.deviceId,
|
|
202
|
-
'Accept-Language': this.client.state.language.replace('_', '-'),
|
|
275
|
+
'Accept-Language': (this.client.state.language || 'en_US').replace('_', '-'),
|
|
203
276
|
'X-FB-HTTP-Engine': 'Liger',
|
|
204
277
|
'Authorization': this.client.state.authorization,
|
|
205
278
|
'Host': 'i.instagram.com',
|
|
@@ -209,4 +282,5 @@ class Request {
|
|
|
209
282
|
}
|
|
210
283
|
}
|
|
211
284
|
|
|
212
|
-
module.exports = Request;
|
|
285
|
+
module.exports = Request;
|
|
286
|
+
|