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 CHANGED
@@ -262,53 +262,182 @@ startBot().catch(console.error);
262
262
 
263
263
  ---
264
264
 
265
- ## 🖼️ FIXED: Send Media via MQTT (v5.70.0)
266
265
 
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.
266
+ ## 🖼️ IMPORTANT: How media actually works (photos & videos)
268
267
 
269
- ### Send Media via MQTT (v5.70.0)
268
+ ## 🖼️ IMPORTANT: How media actually works (photos & videos) — UPDATED (rupload + MQTT reference)
270
269
 
271
- You can now send photos, videos, voice messages, and more directly through the MQTT protocol. The library handles the upload and linking automatically.
270
+ > **Clarification (APK + client behaviour):**
271
+ > The mobile app uploads raw media bytes via **HTTP rupload** (rupload_igphoto / rupload_igvideo). After the rupload completes and the app receives an `upload_id`, the app **notifies Instagram's realtime layer (MQTT)** that "there is media with `upload_id` — attach it to thread X".
272
+ >
273
+ > **Key points**
274
+ > - **Media bytes are uploaded over HTTP (rupload).** MQTT never carries the raw image/video bytes.
275
+ > - **MQTT is used as a realtime *reference/notification* channel**: the client publishes a `send_item` command containing `item_type: "media"` and `upload_id: "<id>"`. That tells the server: "the media exists; attach it to this thread".
276
+ > - In some cases the client also calls HTTP broadcast/configure endpoints. The exact flow can vary by platform/version; the APK shows the pattern `RuploadClient.upload() -> upload_id` then `SendMessage.sendMedia(upload_id)` (MQTT). The hybrid pattern below matches that behavior.
272
277
 
273
- #### 📷 Send a Photo
274
- ```javascript
275
- const photoBuffer = fs.readFileSync('./image.jpg');
276
- const upload = await ig.publish.photo({ file: photoBuffer });
278
+ ---
277
279
 
278
- await realtime.directCommands.sendMedia({
279
- threadId: 'THREAD_ID',
280
- mediaId: upload.upload_id,
281
- uploadId: upload.upload_id
282
- });
283
- ```
280
+ ### Correct / supported patterns
281
+
282
+ There are two practical, compatible flows you can use depending on your client helpers and what the server expects:
283
+
284
+ **A) Hybrid (rupload HTTP -> MQTT reference broadcast)** — matches what the APK does in many versions
285
+ 1. Upload bytes to rupload_igphoto / rupload_igvideo (HTTP) -> receive `upload_id`.
286
+ 2. Publish an MQTT `send_item` with `item_type: "media"` and `upload_id`: this tells the realtime server to attach the previously uploaded media to the thread.
287
+ - MQTT payload is **only the metadata** (JSON), typically compressed/deflated by the realtime client before publish.
288
+ - This is the recommended **APK-compatible** approach.
289
+
290
+ **B) Full HTTP (rupload HTTP -> configure/broadcast HTTP)** — always works, server-side creation via REST
291
+ 1. Upload bytes to rupload endpoint (HTTP) -> receive `upload_id`.
292
+ 2. Call the direct broadcast / configure_photo or configure_video HTTP endpoint to create the message server-side. MQTT will then deliver the server-created message to connected clients.
293
+ - Use this if you prefer to avoid relying on the realtime command to create messages.
294
+
295
+ > Summary: **Always rupload via HTTP.** Then either broadcast via HTTP (configure endpoints) or publish an MQTT `send_item` referencing `upload_id`. MQTT does not carry the media bytes, only the reference.
296
+
297
+ ---
298
+
299
+ ### Example: Hybrid flow — Photo (rupload HTTP then MQTT reference)
300
+
301
+ This example shows the two steps required. It assumes you have `ig.request.send` for HTTP and a `realtime.mqtt.publish()` helper for MQTT. It also shows the `compressDeflate` helper pattern used in this project.
284
302
 
285
- #### 🎥 Send a Video
286
303
  ```javascript
287
- const videoBuffer = fs.readFileSync('./video.mp4');
288
- const upload = await ig.publish.video({
289
- video: videoBuffer,
290
- duration: 10000, // ms
304
+ // 1) RUUPLOAD (HTTP) - upload bytes and get upload_id
305
+ const uploadId = Date.now().toString(); // or use library helper that returns upload_id
306
+ const objectName = `${uploadId}_${Math.floor(Math.random()*1e9)}.jpg`;
307
+
308
+ const ruploadParams = {
309
+ upload_id: uploadId,
310
+ media_type: 1, // photo
311
+ image_compression: '{"lib_name":"moz","lib_version":"3.1.m","quality":"80"}',
312
+ xsharing_user_ids: JSON.stringify([]),
313
+ is_clips_media: false,
314
+ };
315
+
316
+ const headers = {
317
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
318
+ 'Content-Type': 'image/jpeg',
319
+ 'X-Entity-Type': 'image/jpeg',
320
+ 'X-Entity-Length': String(photoBuffer.length),
321
+ 'Content-Length': String(photoBuffer.length),
322
+ };
323
+
324
+ // send bytes to /rupload_igphoto/<objectName>
325
+ await ig.request.send({
326
+ url: `/rupload_igphoto/${objectName}`,
327
+ method: 'POST',
328
+ headers,
329
+ body: photoBuffer,
330
+ timeout: 120000,
291
331
  });
292
332
 
293
- await realtime.directCommands.sendMedia({
294
- threadId: 'THREAD_ID',
295
- mediaId: upload.upload_id,
296
- uploadId: upload.upload_id
333
+ // server should now provide or accept the upload_id (uploadId)
334
+
335
+ // 2) MQTT publish - tell realtime: "attach upload_id X to thread Y"
336
+ const clientContext = require('uuid').v4();
337
+ const command = {
338
+ action: 'send_item',
339
+ thread_id: String(threadId),
340
+ item_type: 'media',
341
+ upload_id: String(uploadId),
342
+ text: caption || '',
343
+ timestamp: Date.now(),
344
+ client_context: clientContext
345
+ };
346
+
347
+ // compress the JSON (match client's compress method)
348
+ const json = JSON.stringify(command);
349
+ const payload = await compressDeflate(json); // project helper
350
+
351
+ // publish to MQTT send_message topic
352
+ await mqtt.publish({
353
+ topic: Topics.SEND_MESSAGE.id,
354
+ qosLevel: 1,
355
+ payload: payload
297
356
  });
298
357
  ```
299
358
 
300
- #### 🎤 Send Voice Message
359
+ **Notes**
360
+ - This sends only a *reference* to the server. The bytes were already sent via rupload.
361
+ - The APK calls something equivalent to `RuploadClient.upload()` then `SendMessage.sendMedia(upload_id)` — the second call is an MQTT notification that references `upload_id`.
362
+
363
+ ---
364
+
365
+ ### Example: Hybrid flow — Video (rupload HTTP then MQTT reference)
366
+
301
367
  ```javascript
302
- const audioBuffer = fs.readFileSync('./voice.mp4');
303
- const upload = await ig.publish.audio({ file: audioBuffer });
368
+ // 1) RUUPLOAD (video)
369
+ const uploadId = Date.now().toString();
370
+ const objectName = `${uploadId}_${Math.floor(Math.random()*1e9)}.mp4`;
371
+
372
+ const ruploadParams = {
373
+ upload_id: uploadId,
374
+ media_type: 2, // video
375
+ xsharing_user_ids: JSON.stringify([]),
376
+ upload_media_duration_ms: Math.round(duration * 1000),
377
+ upload_media_width: width,
378
+ upload_media_height: height
379
+ };
380
+
381
+ const headers = {
382
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
383
+ 'Content-Type': 'video/mp4',
384
+ 'X-Entity-Type': 'video/mp4',
385
+ 'X-Entity-Length': String(videoBuffer.length),
386
+ 'Content-Length': String(videoBuffer.length),
387
+ 'Offset': '0'
388
+ };
389
+
390
+ await ig.request.send({
391
+ url: `/rupload_igvideo/${objectName}`,
392
+ method: 'POST',
393
+ headers,
394
+ body: videoBuffer,
395
+ timeout: 300000
396
+ });
304
397
 
305
- await realtime.directCommands.sendMedia({
306
- threadId: 'THREAD_ID',
307
- mediaId: upload.upload_id,
308
- uploadId: upload.upload_id
398
+ // 2) MQTT publish - notify realtime to attach upload_id to thread
399
+ const command = {
400
+ action: 'send_item',
401
+ thread_id: String(threadId),
402
+ item_type: 'media',
403
+ upload_id: String(uploadId),
404
+ text: caption || '',
405
+ timestamp: Date.now(),
406
+ client_context: require('uuid').v4()
407
+ };
408
+
409
+ const payload = await compressDeflate(JSON.stringify(command));
410
+ await mqtt.publish({
411
+ topic: Topics.SEND_MESSAGE.id,
412
+ qosLevel: 1,
413
+ payload: payload
309
414
  });
310
415
  ```
311
416
 
417
+ ---
418
+
419
+ ### Short FAQ & Troubleshooting
420
+
421
+ - **Q:** *Can I send the raw image through MQTT?*
422
+ **A:** No — MQTT is not used for large raw bytes. The app uploads bytes via rupload HTTP.
423
+
424
+ - **Q:** *If I publish the MQTT `send_item` with `upload_id`, will the message appear in the thread?*
425
+ **A:** Yes — the realtime server accepts an MQTT `send_item` referencing a valid `upload_id`. The server will attempt to create/attach the media message and then propagate the message via MESSAGE_SYNC to other clients. The exact behavior can differ between server versions, so keep fallback to HTTP broadcast if reliability is essential.
426
+
427
+ - **Q:** *Do I need to call `/direct_v2/threads/broadcast/...` if I published MQTT?*
428
+ **A:** Often not — the APK pattern uses MQTT for the attach request. However, for absolute compatibility or for older server behavior, the HTTP configure/broadcast endpoints are a safe fallback.
429
+
430
+ ---
431
+
432
+ ### Security & Best Practices
433
+
434
+ - Ensure `upload_id` you publish via MQTT is the one returned by rupload; otherwise servers will reject or ignore it.
435
+ - Keep rupload headers accurate (`X-Entity-Length`, `X-Instagram-Rupload-Params`, etc.).
436
+ - Use `client_context` (UUID) per message to track mutations.
437
+ - Implement retries and exponential backoff for both rupload and MQTT publish.
438
+ - Log both the rupload response and the MQTT publish result for debugging.
439
+
440
+ ---
312
441
 
313
442
  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.
314
443
 
@@ -1359,6 +1488,205 @@ const fs = require('fs');
1359
1488
 
1360
1489
  ---
1361
1490
 
1491
+
1492
+ ---
1493
+
1494
+ ### Example 3: CLI Photo Sender (upload HTTP + broadcast HTTP, with MQTT sync)
1495
+
1496
+ This example is a ready-to-run CLI script that demonstrates the correct flow to **send a photo** to one or more threads:
1497
+ 1. upload the photo to Instagram's rupload endpoint (HTTP),
1498
+ 2. broadcast (configure) the photo to the target thread(s) using the Direct API (HTTP),
1499
+ 3. use MQTT (RealtimeClient) only for realtime sync and confirmation.
1500
+
1501
+ Save this as `examples/send-photo-cli.js`.
1502
+
1503
+ ```javascript
1504
+ #!/usr/bin/env node
1505
+ // examples/send-photo-cli.js
1506
+ // Node 18+, install nodejs-insta-private-api-mqtt in the project
1507
+
1508
+ const fs = require('fs');
1509
+ const path = require('path');
1510
+ const readline = require('readline/promises');
1511
+ const { stdin: input, stdout: output } = require('process');
1512
+ const { v4: uuidv4 } = require('uuid');
1513
+
1514
+ (async () => {
1515
+ const rl = readline.createInterface({ input, output });
1516
+ try {
1517
+ console.log('\\n--- Instagram Photo Sender (HTTP + broadcast) ---\\n');
1518
+ const username = (await rl.question('Enter your Instagram username: ')).trim();
1519
+ const password = (await rl.question('Enter your Instagram password: ')).trim();
1520
+ if (!username || !password) {
1521
+ console.error('username & password required'); process.exit(1);
1522
+ }
1523
+
1524
+ const { IgApiClient, RealtimeClient, useMultiFileAuthState } = require('nodejs-insta-private-api-mqtt');
1525
+ const AUTH_FOLDER = path.resolve(process.cwd(), './auth_info_instagram');
1526
+
1527
+ const authState = await useMultiFileAuthState(AUTH_FOLDER);
1528
+ const ig = new IgApiClient();
1529
+ ig.state.generateDevice?.(username);
1530
+
1531
+ // load saved credentials if available
1532
+ if (authState.hasSession && authState.hasSession()) {
1533
+ try { await authState.loadCreds?.(ig); console.log('[*] Loaded saved creds'); } catch(e){}
1534
+ }
1535
+
1536
+ // login if needed
1537
+ if (!authState.hasSession || (typeof authState.hasSession === 'function' && !authState.hasSession())) {
1538
+ await ig.login({ username, password });
1539
+ await authState.saveCreds?.(ig);
1540
+ console.log('[+] Logged in and saved creds');
1541
+ }
1542
+
1543
+ // start realtime client (optional, helps sync)
1544
+ const realtime = new RealtimeClient(ig);
1545
+ realtime.on?.('connected', () => console.log('✅ MQTT connected (realtime)'));
1546
+
1547
+ // fetch inbox and list threads
1548
+ const inbox = await ig.direct.getInbox();
1549
+ const threads = inbox.threads || inbox.items || inbox.threads_response?.threads || [];
1550
+ if (!threads || threads.length === 0) {
1551
+ console.error('No threads found in inbox. Exiting.'); process.exit(1);
1552
+ }
1553
+
1554
+ console.log('\\nAvailable threads:');
1555
+ const indexToThread = [];
1556
+ threads.slice(0, 50).forEach((t, i) => {
1557
+ const id = String(t.thread_id || t.threadId || t.id || t.thread_id_str || (t.thread && t.thread.thread_id) || '');
1558
+ 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';
1559
+ indexToThread.push({ id, title });
1560
+ console.log(`${i+1}. ${title} (thread_id: ${id})`);
1561
+ });
1562
+
1563
+ const pick = (await rl.question('\\nSelect thread numbers (e.g. "1 2 3"): ')).trim();
1564
+ const picks = pick.split(/[\s,]+/).map(x => parseInt(x,10)).filter(n => Number.isFinite(n) && n >= 1 && n <= indexToThread.length);
1565
+ const selected = picks.map(p => indexToThread[p-1].id);
1566
+
1567
+ const photoPath = (await rl.question('\\nEnter your Photo path here (absolute or relative): ')).trim();
1568
+ const resolved = path.isAbsolute(photoPath) ? photoPath : path.resolve(process.cwd(), photoPath);
1569
+ if (!fs.existsSync(resolved)) { console.error('File not found', resolved); process.exit(1); }
1570
+ const buffer = fs.readFileSync(resolved);
1571
+ console.log('Photo size (MB):', (buffer.length/(1024*1024)).toFixed(2));
1572
+
1573
+ // RUUPLOAD (HTTP) - try to use ig.publish.photo helper if available
1574
+ let uploadId;
1575
+ try {
1576
+ if (typeof ig.publish?.photo === 'function') {
1577
+ const up = await ig.publish.photo({ file: buffer });
1578
+ uploadId = up.upload_id || up.uploadId || up;
1579
+ } else {
1580
+ // manual rupload via request
1581
+ uploadId = Date.now().toString();
1582
+ const objectName = `${uploadId}_0_${Math.floor(Math.random() * 1e10)}`;
1583
+ const ruploadParams = {
1584
+ retry_context: JSON.stringify({ num_step_auto_retry: 0, num_reupload: 0, num_step_manual_retry: 0 }),
1585
+ media_type: '1',
1586
+ upload_id: uploadId,
1587
+ xsharing_user_ids: JSON.stringify([]),
1588
+ image_compression: JSON.stringify({ lib_name: 'moz', lib_version: '3.1.m', quality: '80' })
1589
+ };
1590
+ const headers = {
1591
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
1592
+ 'Content-Type': 'application/octet-stream',
1593
+ 'X-Entity-Type': 'image/jpeg',
1594
+ 'Offset': '0',
1595
+ 'X-Entity-Name': objectName,
1596
+ 'X-Entity-Length': String(buffer.length),
1597
+ 'Content-Length': String(buffer.length),
1598
+ };
1599
+ await ig.request.send({
1600
+ url: `/rupload_igphoto/${objectName}`,
1601
+ method: 'POST',
1602
+ headers,
1603
+ body: buffer,
1604
+ timeout: 120000,
1605
+ });
1606
+ }
1607
+ console.log('[+] Upload completed. upload_id =', uploadId);
1608
+ } catch (err) {
1609
+ console.error('Upload failed:', err?.message || err); process.exit(1);
1610
+ }
1611
+
1612
+ // BROADCAST (HTTP) - create message on server
1613
+ try {
1614
+ const mutationToken = uuidv4();
1615
+ const payload = {
1616
+ action: 'configure_photo',
1617
+ upload_id: String(uploadId),
1618
+ thread_ids: JSON.stringify(selected),
1619
+ client_context: mutationToken,
1620
+ mutation_token: mutationToken,
1621
+ _csrftoken: ig.state.cookieCsrfToken || '',
1622
+ _uuid: ig.state.uuid || '',
1623
+ device_id: ig.state.deviceId || ''
1624
+ };
1625
+ const resp = await ig.request.send({
1626
+ url: '/api/v1/direct_v2/threads/broadcast/configure_photo/',
1627
+ method: 'POST',
1628
+ form: payload,
1629
+ timeout: 60000,
1630
+ });
1631
+ console.log('✅ Broadcast response received:', resp && (resp.status || resp.body || resp.data) ? 'OK' : JSON.stringify(resp).slice(0,120));
1632
+ } catch (err) {
1633
+ console.error('Broadcast failed:', err?.message || err);
1634
+ process.exit(1);
1635
+ }
1636
+
1637
+ console.log('\\nDone. MQTT will sync and you should see the message in the app.');
1638
+ rl.close();
1639
+ process.exit(0);
1640
+ } catch (e) {
1641
+ console.error('Fatal:', e);
1642
+ process.exit(1);
1643
+ }
1644
+ })();
1645
+ ```
1646
+
1647
+ ---
1648
+
1649
+ ### Example 4: Video Sender (upload + broadcast)
1650
+
1651
+ 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/`.
1652
+
1653
+ ```javascript
1654
+ // pseudo-code outline
1655
+ const videoBuffer = fs.readFileSync('./video.mp4');
1656
+ const upload = await ig.publish.video({ video: videoBuffer, duration: 12000 });
1657
+ // upload.upload_id -> then broadcast with configure_video payload similar to the photo flow
1658
+ ```
1659
+
1660
+ **Notes:** video uploads often require multipart/segmented upload and longer timeouts; prefer library helper `ig.publish.video()`.
1661
+
1662
+ ---
1663
+
1664
+ ### Example 5: Share a file / document (workaround)
1665
+
1666
+ Instagram DM does not provide a simple "attach arbitrary file" endpoint. Common workarounds:
1667
+
1668
+ 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).
1669
+ 2. If the file is an image or video, use the rupload + broadcast flow above.
1670
+
1671
+ Example (send link message):
1672
+
1673
+ ```javascript
1674
+ // send link as text via realtime (MQTT)
1675
+ await realtime.directCommands.sendTextViaRealtime(threadId, 'Download the file here: https://myhost.example.com/myfile.pdf');
1676
+ ```
1677
+
1678
+ ---
1679
+
1680
+ ### Tips for bots sending media
1681
+
1682
+ - Always **upload first**, then **broadcast**. Do not rely on MQTT-only calls for media.
1683
+ - Use retries and exponential backoff for both rupload and broadcast steps (Instagram can return transient 503).
1684
+ - Save `upload_id` and broadcast responses for debugging.
1685
+ - If you need to send the same file to many threads, upload once, reuse the same `upload_id` in multiple broadcast calls.
1686
+
1687
+ ---
1688
+
1689
+
1362
1690
  ## API Reference
1363
1691
 
1364
1692
  ### IgApiClient
@@ -1710,3 +2038,241 @@ For issues, bugs, or feature requests: https://github.com/Kunboruto20/nodejs-ins
1710
2038
  Documentation: https://github.com/Kunboruto20/nodejs-insta-private-api
1711
2039
 
1712
2040
  Examples: See repository examples/ directory for working implementations
2041
+
2042
+
2043
+ ---
2044
+
2045
+ ## Examples using `ig.request.send` wrapper (photo & video, MQTT *reference* + REST broadcast)
2046
+
2047
+ Below are practical code snippets that use the `ig.request.send` wrapper (the same request helper used inside the Instagram client).
2048
+ Each example shows **two flows** so you can pick the behavior you want:
2049
+
2050
+ 1. **MQTT reference flow** — upload via `rupload` (HTTP) to obtain `upload_id` then **publish an MQTT `send_item` that contains only the `upload_id`** (Instagram attaches the already-uploaded file to the thread). This matches the APK behavior: MQTT only carries a reference, not the binary.
2051
+ 2. **REST broadcast flow** — upload via `rupload` (HTTP) and then call the `direct_v2` broadcast REST endpoint to attach the uploaded media to a thread (alternate, server-side broadcast).
2052
+
2053
+ > Note: these examples assume your runtime has access to:
2054
+ > - `ig` (Instagram client wrapper) and `ig.request.send` method,
2055
+ > - `mqtt` connected client exposing `.publish({ topic, qosLevel, payload })`,
2056
+ > - `compressDeflate` helper (same as in project `shared`),
2057
+ > - `uuid` (for `client_context` generation), and
2058
+ > - `constants.Topics.SEND_MESSAGE.id` for the MQTT send topic.
2059
+
2060
+ ### Photo — MQTT reference example (rupload via `ig.request.send` then MQTT reference)
2061
+ ```javascript
2062
+ // Photo upload -> get upload_id (rupload) -> publish MQTT 'send_item' with upload_id
2063
+ const { v4: uuidv4 } = require('uuid');
2064
+
2065
+ async function sendPhotoAsMqttReference({ ig, mqtt, photoBuffer, threadId, caption = '' }) {
2066
+ if (!ig || !ig.request) throw new Error('ig.request not available');
2067
+ if (!mqtt || typeof mqtt.publish !== 'function') throw new Error('mqtt client not available');
2068
+ if (!Buffer.isBuffer(photoBuffer)) throw new Error('photoBuffer must be a Buffer');
2069
+
2070
+ // 1) perform rupload via ig.request.send
2071
+ const uploadId = Date.now().toString();
2072
+ const objectName = `${uuidv4()}.jpg`;
2073
+ const ruploadParams = {
2074
+ upload_id: uploadId,
2075
+ media_type: 1,
2076
+ image_compression: '{"lib_name":"moz","lib_version":"3.1.m","quality":"80"}',
2077
+ xsharing_user_ids: JSON.stringify([]),
2078
+ };
2079
+
2080
+ const uploadHeaders = {
2081
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
2082
+ 'Content-Type': 'image/jpeg',
2083
+ 'X-Entity-Type': 'image/jpeg',
2084
+ 'X-Entity-Length': String(photoBuffer.length),
2085
+ 'Content-Length': String(photoBuffer.length),
2086
+ };
2087
+
2088
+ const uploadUrl = `/rupload_igphoto/${objectName}`;
2089
+
2090
+ const uploadResponse = await ig.request.send({
2091
+ url: uploadUrl,
2092
+ method: 'POST',
2093
+ headers: uploadHeaders,
2094
+ body: photoBuffer,
2095
+ });
2096
+
2097
+ const serverUploadId = (uploadResponse && uploadResponse.upload_id) ? uploadResponse.upload_id : uploadId;
2098
+
2099
+ // 2) publish MQTT reference telling server "attach upload_id X to thread Y"
2100
+ const clientContext = uuidv4();
2101
+ const command = {
2102
+ action: 'send_item',
2103
+ thread_id: String(threadId),
2104
+ item_type: 'media',
2105
+ upload_id: serverUploadId,
2106
+ text: caption || '',
2107
+ client_context: clientContext,
2108
+ timestamp: Date.now(),
2109
+ };
2110
+
2111
+ const json = JSON.stringify(command);
2112
+ const payload = await compressDeflate(json); // reuse shared helper
2113
+ await mqtt.publish({
2114
+ topic: constants.Topics.SEND_MESSAGE.id,
2115
+ qosLevel: 1,
2116
+ payload,
2117
+ });
2118
+
2119
+ return { upload_id: serverUploadId };
2120
+ }
2121
+ ```
2122
+
2123
+ ### Photo — REST broadcast example (rupload via `ig.request.send` then direct_v2 broadcast)
2124
+ ```javascript
2125
+ // Photo upload -> rupload via ig.request.send -> REST broadcast to the thread
2126
+ async function sendPhotoViaRestBroadcast({ ig, photoBuffer, threadId, caption = '' }) {
2127
+ const uploadId = Date.now().toString();
2128
+ const objectName = `${uuidv4()}.jpg`;
2129
+
2130
+ const ruploadParams = {
2131
+ upload_id: uploadId,
2132
+ media_type: 1,
2133
+ image_compression: '{"lib_name":"moz","lib_version":"3.1.m","quality":"80"}',
2134
+ xsharing_user_ids: JSON.stringify([]),
2135
+ };
2136
+
2137
+ const uploadResponse = await ig.request.send({
2138
+ url: `/rupload_igphoto/${objectName}`,
2139
+ method: 'POST',
2140
+ headers: {
2141
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
2142
+ 'Content-Type': 'image/jpeg',
2143
+ 'X-Entity-Type': 'image/jpeg',
2144
+ 'X-Entity-Length': String(photoBuffer.length),
2145
+ 'Content-Length': String(photoBuffer.length),
2146
+ },
2147
+ body: photoBuffer,
2148
+ });
2149
+
2150
+ const serverUploadId = (uploadResponse && uploadResponse.upload_id) ? uploadResponse.upload_id : uploadId;
2151
+
2152
+ // Broadcast attach via REST endpoint
2153
+ const broadcastResponse = await ig.request.send({
2154
+ url: '/direct_v2/threads/broadcast/upload_photo/',
2155
+ method: 'POST',
2156
+ form: {
2157
+ upload_id: serverUploadId,
2158
+ action: 'send_item',
2159
+ thread_ids: JSON.stringify([String(threadId)]),
2160
+ caption: caption || '',
2161
+ },
2162
+ });
2163
+
2164
+ return { upload_id: serverUploadId, broadcastResponse };
2165
+ }
2166
+ ```
2167
+
2168
+ ### Video — MQTT reference example (rupload video + MQTT reference)
2169
+ ```javascript
2170
+ // Video upload -> rupload video via ig.request.send -> MQTT reference
2171
+ async function sendVideoAsMqttReference({ ig, mqtt, videoBuffer, threadId, caption = '', duration = 0, width = 720, height = 1280 }) {
2172
+ const uploadId = Date.now().toString();
2173
+ const objectName = `${uuidv4()}.mp4`;
2174
+
2175
+ const ruploadParams = {
2176
+ upload_id: uploadId,
2177
+ media_type: 2, // video
2178
+ xsharing_user_ids: JSON.stringify([]),
2179
+ upload_media_duration_ms: Math.round(duration * 1000),
2180
+ upload_media_width: width,
2181
+ upload_media_height: height,
2182
+ };
2183
+
2184
+ const uploadResponse = await ig.request.send({
2185
+ url: `/rupload_igvideo/${objectName}`,
2186
+ method: 'POST',
2187
+ headers: {
2188
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
2189
+ 'Content-Type': 'video/mp4',
2190
+ 'X-Entity-Type': 'video/mp4',
2191
+ 'X-Entity-Length': String(videoBuffer.length),
2192
+ 'Content-Length': String(videoBuffer.length),
2193
+ 'Offset': '0',
2194
+ },
2195
+ body: videoBuffer,
2196
+ });
2197
+
2198
+ const serverUploadId = (uploadResponse && uploadResponse.upload_id) ? uploadResponse.upload_id : uploadId;
2199
+
2200
+ // Publish MQTT reference
2201
+ const clientContext = uuidv4();
2202
+ const command = {
2203
+ action: 'send_item',
2204
+ thread_id: String(threadId),
2205
+ item_type: 'media',
2206
+ upload_id: serverUploadId,
2207
+ text: caption || '',
2208
+ client_context: clientContext,
2209
+ timestamp: Date.now(),
2210
+ };
2211
+
2212
+ const json = JSON.stringify(command);
2213
+ const payload = await compressDeflate(json);
2214
+ await mqtt.publish({
2215
+ topic: constants.Topics.SEND_MESSAGE.id,
2216
+ qosLevel: 1,
2217
+ payload,
2218
+ });
2219
+
2220
+ return { upload_id: serverUploadId };
2221
+ }
2222
+ ```
2223
+
2224
+ ### Video — REST broadcast example (rupload video + direct_v2 broadcast)
2225
+ ```javascript
2226
+ async function sendVideoViaRestBroadcast({ ig, videoBuffer, threadId, caption = '', duration = 0, width = 720, height = 1280 }) {
2227
+ const uploadId = Date.now().toString();
2228
+ const objectName = `${uuidv4()}.mp4`;
2229
+
2230
+ const ruploadParams = {
2231
+ upload_id: uploadId,
2232
+ media_type: 2,
2233
+ xsharing_user_ids: JSON.stringify([]),
2234
+ upload_media_duration_ms: Math.round(duration * 1000),
2235
+ upload_media_width: width,
2236
+ upload_media_height: height,
2237
+ };
2238
+
2239
+ const uploadResponse = await ig.request.send({
2240
+ url: `/rupload_igvideo/${objectName}`,
2241
+ method: 'POST',
2242
+ headers: {
2243
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
2244
+ 'Content-Type': 'video/mp4',
2245
+ 'X-Entity-Type': 'video/mp4',
2246
+ 'X-Entity-Length': String(videoBuffer.length),
2247
+ 'Content-Length': String(videoBuffer.length),
2248
+ 'Offset': '0',
2249
+ },
2250
+ body: videoBuffer,
2251
+ });
2252
+
2253
+ const serverUploadId = (uploadResponse && uploadResponse.upload_id) ? uploadResponse.upload_id : uploadId;
2254
+
2255
+ const broadcastResponse = await ig.request.send({
2256
+ url: '/direct_v2/threads/broadcast/upload_video/',
2257
+ method: 'POST',
2258
+ form: {
2259
+ upload_id: serverUploadId,
2260
+ action: 'send_item',
2261
+ thread_ids: JSON.stringify([String(threadId)]),
2262
+ video_result: '',
2263
+ caption: caption || '',
2264
+ },
2265
+ });
2266
+
2267
+ return { upload_id: serverUploadId, broadcastResponse };
2268
+ }
2269
+ ```
2270
+
2271
+ ### FAQ notes
2272
+ - **Does MQTT send the file?** No — MQTT only transports the JSON reference (`upload_id`) telling the server to attach that already-uploaded file into the thread.
2273
+ - **Why use MQTT for reference instead of REST broadcast?** MQTT gives near-realtime messaging semantics; the server applies the `upload_id` to the thread (same result), but the heavy binary upload must be done via HTTP `rupload`.
2274
+ - **Can you skip rupload and only send MQTT?** No — server needs the binary available under the `upload_id`. If you send only MQTT without previously uploading, the server won't have the file to attach.
2275
+
2276
+ ---
2277
+
2278
+ (Section added programmatically: `Examples using ig.request.send` — appended to the existing README content.)