nodejs-insta-private-api-mqtt 1.3.14 → 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 CHANGED
@@ -262,52 +262,104 @@ 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
+ > **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:
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
+ 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.
272
+
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).
275
+
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.
278
+
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)
272
291
 
273
- #### 📷 Send a Photo
274
292
  ```javascript
293
+ const fs = require('fs');
294
+ const { v4: uuidv4 } = require('uuid');
295
+
275
296
  const photoBuffer = fs.readFileSync('./image.jpg');
276
- const upload = await ig.publish.photo({ file: photoBuffer });
277
297
 
278
- await realtime.directCommands.sendMedia({
279
- threadId: 'THREAD_ID',
280
- mediaId: upload.upload_id,
281
- uploadId: upload.upload_id
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
282
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.
283
324
  ```
284
325
 
285
- #### 🎥 Send a Video
326
+ #### Upload and broadcast a video
327
+
286
328
  ```javascript
287
329
  const videoBuffer = fs.readFileSync('./video.mp4');
288
- const upload = await ig.publish.video({
289
- video: videoBuffer,
290
- duration: 10000, // ms
291
- });
292
330
 
293
- await realtime.directCommands.sendMedia({
294
- threadId: 'THREAD_ID',
295
- mediaId: upload.upload_id,
296
- uploadId: upload.upload_id
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
297
352
  });
298
353
  ```
299
354
 
300
- #### 🎤 Send Voice Message
301
- ```javascript
302
- const audioBuffer = fs.readFileSync('./voice.mp4');
303
- const upload = await ig.publish.audio({ file: audioBuffer });
355
+ ---
304
356
 
305
- await realtime.directCommands.sendMedia({
306
- threadId: 'THREAD_ID',
307
- mediaId: upload.upload_id,
308
- uploadId: upload.upload_id
309
- });
310
- ```
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
+ ---
311
363
 
312
364
 
313
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.
@@ -1359,6 +1411,205 @@ const fs = require('fs');
1359
1411
 
1360
1412
  ---
1361
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
+
1362
1613
  ## API Reference
1363
1614
 
1364
1615
  ### IgApiClient
@@ -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
- // Create axios instance with default config
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: 30000,
15
- headers: {
16
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
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
- async send(options) {
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
- ...options,
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
- // Handle form data
59
- if (options.form) {
60
- if (options.method === 'POST' || options.method === 'PUT') {
61
- const formData = new URLSearchParams();
62
- Object.keys(options.form).forEach(key => {
63
- formData.append(key, options.form[key]);
64
- });
65
- config.data = formData.toString();
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
- // Handle query parameters
70
- if (options.qs) {
71
- config.params = options.qs;
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
- const response = await this.httpClient(config);
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
- if (response.data.status === 'ok' || response.status === 200) {
79
- return { body: response.data, headers: response.headers };
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
- if (error.response) {
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
- this.client.state.cookieStore.setCookieSync(cookieString, this.client.state.constants.HOST);
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
- // Ignore cookie parsing errors
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
- if (data.spam) {
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 (response.status === 404) {
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 || 'Request failed');
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 = response.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
+