nodejs-insta-private-api-mqtt 1.0.0

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.
Files changed (210) hide show
  1. package/README.md +1650 -0
  2. package/dist/constants/constants.js +280 -0
  3. package/dist/constants/index.js +41 -0
  4. package/dist/core/client.js +243 -0
  5. package/dist/core/repository.js +7 -0
  6. package/dist/core/request.js +212 -0
  7. package/dist/core/state.js +1456 -0
  8. package/dist/core/utils.js +786 -0
  9. package/dist/downloadMedia.js +381 -0
  10. package/dist/errors/index.d.ts +16 -0
  11. package/dist/errors/index.js +30 -0
  12. package/dist/errors/index.js.map +1 -0
  13. package/dist/fbns/fbns.client.d.ts +32 -0
  14. package/dist/fbns/fbns.client.events.d.ts +41 -0
  15. package/dist/fbns/fbns.client.events.js +3 -0
  16. package/dist/fbns/fbns.client.events.js.map +1 -0
  17. package/dist/fbns/fbns.client.js +179 -0
  18. package/dist/fbns/fbns.client.js.map +1 -0
  19. package/dist/fbns/fbns.device-auth.d.ts +17 -0
  20. package/dist/fbns/fbns.device-auth.js +54 -0
  21. package/dist/fbns/fbns.device-auth.js.map +1 -0
  22. package/dist/fbns/fbns.types.d.ts +83 -0
  23. package/dist/fbns/fbns.types.js +3 -0
  24. package/dist/fbns/fbns.types.js.map +1 -0
  25. package/dist/fbns/fbns.utilities.d.ts +2 -0
  26. package/dist/fbns/fbns.utilities.js +79 -0
  27. package/dist/fbns/fbns.utilities.js.map +1 -0
  28. package/dist/fbns/index.d.ts +4 -0
  29. package/dist/fbns/index.js +21 -0
  30. package/dist/fbns/index.js.map +1 -0
  31. package/dist/index.js +39 -0
  32. package/dist/mqttot/index.d.ts +4 -0
  33. package/dist/mqttot/index.js +21 -0
  34. package/dist/mqttot/index.js.map +1 -0
  35. package/dist/mqttot/mqttot.client.d.ts +39 -0
  36. package/dist/mqttot/mqttot.client.js +120 -0
  37. package/dist/mqttot/mqttot.client.js.map +1 -0
  38. package/dist/mqttot/mqttot.connect.request.packet.d.ts +7 -0
  39. package/dist/mqttot/mqttot.connect.request.packet.js +9 -0
  40. package/dist/mqttot/mqttot.connect.request.packet.js.map +1 -0
  41. package/dist/mqttot/mqttot.connect.response.packet.d.ts +7 -0
  42. package/dist/mqttot/mqttot.connect.response.packet.js +24 -0
  43. package/dist/mqttot/mqttot.connect.response.packet.js.map +1 -0
  44. package/dist/mqttot/mqttot.connection.d.ts +57 -0
  45. package/dist/mqttot/mqttot.connection.js +56 -0
  46. package/dist/mqttot/mqttot.connection.js.map +1 -0
  47. package/dist/package.json +59 -0
  48. package/dist/realtime/commands/commands.d.ts +15 -0
  49. package/dist/realtime/commands/commands.js +21 -0
  50. package/dist/realtime/commands/commands.js.map +1 -0
  51. package/dist/realtime/commands/direct.commands.d.ts +75 -0
  52. package/dist/realtime/commands/direct.commands.js +186 -0
  53. package/dist/realtime/commands/direct.commands.js.map +1 -0
  54. package/dist/realtime/commands/enhanced.direct.commands.js +987 -0
  55. package/dist/realtime/commands/index.d.ts +2 -0
  56. package/dist/realtime/commands/index.js +19 -0
  57. package/dist/realtime/commands/index.js.map +1 -0
  58. package/dist/realtime/delta-sync.manager.js +293 -0
  59. package/dist/realtime/features/dm-sender.js +88 -0
  60. package/dist/realtime/features/error-handler.js +73 -0
  61. package/dist/realtime/features/gap-handler.js +61 -0
  62. package/dist/realtime/features/presence.manager.js +66 -0
  63. package/dist/realtime/index.js +30 -0
  64. package/dist/realtime/messages/app-presence.event.d.ts +9 -0
  65. package/dist/realtime/messages/app-presence.event.js +3 -0
  66. package/dist/realtime/messages/app-presence.event.js.map +1 -0
  67. package/dist/realtime/messages/index.d.ts +3 -0
  68. package/dist/realtime/messages/index.js +20 -0
  69. package/dist/realtime/messages/index.js.map +1 -0
  70. package/dist/realtime/messages/message-sync.message.d.ts +222 -0
  71. package/dist/realtime/messages/message-sync.message.js +43 -0
  72. package/dist/realtime/messages/message-sync.message.js.map +1 -0
  73. package/dist/realtime/messages/realtime-sub.direct.data.d.ts +11 -0
  74. package/dist/realtime/messages/realtime-sub.direct.data.js +3 -0
  75. package/dist/realtime/messages/realtime-sub.direct.data.js.map +1 -0
  76. package/dist/realtime/messages/thread-update.message.d.ts +68 -0
  77. package/dist/realtime/messages/thread-update.message.js +3 -0
  78. package/dist/realtime/messages/thread-update.message.js.map +1 -0
  79. package/dist/realtime/mixins/index.d.ts +3 -0
  80. package/dist/realtime/mixins/index.js +20 -0
  81. package/dist/realtime/mixins/index.js.map +1 -0
  82. package/dist/realtime/mixins/message-sync.mixin.d.ts +8 -0
  83. package/dist/realtime/mixins/message-sync.mixin.js +381 -0
  84. package/dist/realtime/mixins/message-sync.mixin.js.map +1 -0
  85. package/dist/realtime/mixins/mixin.d.ts +19 -0
  86. package/dist/realtime/mixins/mixin.js +41 -0
  87. package/dist/realtime/mixins/mixin.js.map +1 -0
  88. package/dist/realtime/mixins/presence-typing.mixin.js +33 -0
  89. package/dist/realtime/mixins/realtime-sub.mixin.d.ts +8 -0
  90. package/dist/realtime/mixins/realtime-sub.mixin.js +55 -0
  91. package/dist/realtime/mixins/realtime-sub.mixin.js.map +1 -0
  92. package/dist/realtime/parsers/graphql-parser.js +43 -0
  93. package/dist/realtime/parsers/graphql.parser.d.ts +15 -0
  94. package/dist/realtime/parsers/graphql.parser.js +22 -0
  95. package/dist/realtime/parsers/graphql.parser.js.map +1 -0
  96. package/dist/realtime/parsers/index.d.ts +6 -0
  97. package/dist/realtime/parsers/index.js +23 -0
  98. package/dist/realtime/parsers/index.js.map +1 -0
  99. package/dist/realtime/parsers/iris-parser.js +43 -0
  100. package/dist/realtime/parsers/iris.parser.d.ts +17 -0
  101. package/dist/realtime/parsers/iris.parser.js +10 -0
  102. package/dist/realtime/parsers/iris.parser.js.map +1 -0
  103. package/dist/realtime/parsers/json-parser.js +43 -0
  104. package/dist/realtime/parsers/json.parser.d.ts +6 -0
  105. package/dist/realtime/parsers/json.parser.js +10 -0
  106. package/dist/realtime/parsers/json.parser.js.map +1 -0
  107. package/dist/realtime/parsers/parser.d.ts +9 -0
  108. package/dist/realtime/parsers/parser.js +3 -0
  109. package/dist/realtime/parsers/parser.js.map +1 -0
  110. package/dist/realtime/parsers/region-hint-parser.js +43 -0
  111. package/dist/realtime/parsers/region-hint.parser.d.ts +12 -0
  112. package/dist/realtime/parsers/region-hint.parser.js +15 -0
  113. package/dist/realtime/parsers/region-hint.parser.js.map +1 -0
  114. package/dist/realtime/parsers/skywalker-parser.js +43 -0
  115. package/dist/realtime/parsers/skywalker.parser.d.ts +12 -0
  116. package/dist/realtime/parsers/skywalker.parser.js +15 -0
  117. package/dist/realtime/parsers/skywalker.parser.js.map +1 -0
  118. package/dist/realtime/parsers-advanced.js +158 -0
  119. package/dist/realtime/proto/common.proto +38 -0
  120. package/dist/realtime/proto/direct.proto +65 -0
  121. package/dist/realtime/proto/ig-messages.proto +83 -0
  122. package/dist/realtime/proto/iris.proto +188 -0
  123. package/dist/realtime/proto-parser.js +195 -0
  124. package/dist/realtime/protocols/iris.handshake.js +74 -0
  125. package/dist/realtime/protocols/proto-definitions.js +80 -0
  126. package/dist/realtime/protocols/skywalker.protocol.js +91 -0
  127. package/dist/realtime/realtime.client.events.js +3 -0
  128. package/dist/realtime/realtime.client.js +449 -0
  129. package/dist/realtime/realtime.service.js +462 -0
  130. package/dist/realtime/reconnect.manager.js +94 -0
  131. package/dist/realtime/session.manager.js +121 -0
  132. package/dist/realtime/subscriptions/graphql.subscription.d.ts +47 -0
  133. package/dist/realtime/subscriptions/graphql.subscription.js +99 -0
  134. package/dist/realtime/subscriptions/graphql.subscription.js.map +1 -0
  135. package/dist/realtime/subscriptions/index.d.ts +2 -0
  136. package/dist/realtime/subscriptions/index.js +19 -0
  137. package/dist/realtime/subscriptions/index.js.map +1 -0
  138. package/dist/realtime/subscriptions/skywalker.subscription.d.ts +4 -0
  139. package/dist/realtime/subscriptions/skywalker.subscription.js +13 -0
  140. package/dist/realtime/subscriptions/skywalker.subscription.js.map +1 -0
  141. package/dist/realtime/topic-map.js +71 -0
  142. package/dist/realtime/topic.js +80 -0
  143. package/dist/repositories/account.repository.js +261 -0
  144. package/dist/repositories/direct-thread.repository.js +247 -0
  145. package/dist/repositories/direct.repository.js +153 -0
  146. package/dist/repositories/feed.repository.js +233 -0
  147. package/dist/repositories/friendship.repository.js +190 -0
  148. package/dist/repositories/hashtag.repository.js +101 -0
  149. package/dist/repositories/highlights.repository.js +127 -0
  150. package/dist/repositories/location.repository.js +84 -0
  151. package/dist/repositories/media.repository.js +165 -0
  152. package/dist/repositories/story.repository.js +156 -0
  153. package/dist/repositories/upload.repository.js +167 -0
  154. package/dist/repositories/user.repository.js +94 -0
  155. package/dist/sendmedia/index.js +11 -0
  156. package/dist/sendmedia/sendFile.js +154 -0
  157. package/dist/sendmedia/sendPhoto.js +145 -0
  158. package/dist/sendmedia/uploadPhoto.js +175 -0
  159. package/dist/sendmedia/uploadfFile.js +264 -0
  160. package/dist/services/live.service.js +147 -0
  161. package/dist/services/search.service.js +116 -0
  162. package/dist/shared/index.js +35 -0
  163. package/dist/shared/shared.js +86 -0
  164. package/dist/thrift/index.d.ts +3 -0
  165. package/dist/thrift/index.js +20 -0
  166. package/dist/thrift/index.js.map +1 -0
  167. package/dist/thrift/thrift.d.ts +59 -0
  168. package/dist/thrift/thrift.js +101 -0
  169. package/dist/thrift/thrift.js.map +1 -0
  170. package/dist/thrift/thrift.reading.d.ts +41 -0
  171. package/dist/thrift/thrift.reading.js +327 -0
  172. package/dist/thrift/thrift.reading.js.map +1 -0
  173. package/dist/thrift/thrift.writing.d.ts +44 -0
  174. package/dist/thrift/thrift.writing.js +342 -0
  175. package/dist/thrift/thrift.writing.js.map +1 -0
  176. package/dist/types/index.js +285 -0
  177. package/dist/useMultiFileAuthState.js +437 -0
  178. package/dist/utils/helper-1.js +1 -0
  179. package/dist/utils/helper-10.js +1 -0
  180. package/dist/utils/helper-11.js +1 -0
  181. package/dist/utils/helper-12.js +1 -0
  182. package/dist/utils/helper-13.js +1 -0
  183. package/dist/utils/helper-14.js +1 -0
  184. package/dist/utils/helper-15.js +1 -0
  185. package/dist/utils/helper-16.js +1 -0
  186. package/dist/utils/helper-17.js +1 -0
  187. package/dist/utils/helper-18.js +1 -0
  188. package/dist/utils/helper-19.js +1 -0
  189. package/dist/utils/helper-2.js +1 -0
  190. package/dist/utils/helper-20.js +1 -0
  191. package/dist/utils/helper-21.js +1 -0
  192. package/dist/utils/helper-22.js +1 -0
  193. package/dist/utils/helper-23.js +1 -0
  194. package/dist/utils/helper-24.js +1 -0
  195. package/dist/utils/helper-25.js +1 -0
  196. package/dist/utils/helper-26.js +1 -0
  197. package/dist/utils/helper-27.js +1 -0
  198. package/dist/utils/helper-28.js +1 -0
  199. package/dist/utils/helper-29.js +1 -0
  200. package/dist/utils/helper-3.js +1 -0
  201. package/dist/utils/helper-30.js +1 -0
  202. package/dist/utils/helper-4.js +1 -0
  203. package/dist/utils/helper-5.js +1 -0
  204. package/dist/utils/helper-6.js +1 -0
  205. package/dist/utils/helper-7.js +1 -0
  206. package/dist/utils/helper-8.js +1 -0
  207. package/dist/utils/helper-9.js +1 -0
  208. package/dist/utils/index.js +280 -0
  209. package/examples/listen-to-messages.js +86 -0
  210. package/package.json +79 -0
@@ -0,0 +1,145 @@
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.
26
+ */
27
+
28
+ const uploadPhoto = require('./uploadPhoto');
29
+
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
+ /**
43
+ * 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
+ */
52
+ async function sendPhoto(session, opts = {}) {
53
+ const {
54
+ photoBuffer,
55
+ mimeType = 'image/jpeg',
56
+ fileName,
57
+ caption = '',
58
+ userId,
59
+ threadId,
60
+ mentions = [],
61
+ signal,
62
+ } = opts;
63
+
64
+ // Validate destination
65
+ if (!userId && !threadId) {
66
+ throw new Error('sendPhoto: You must provide either userId (DM) or threadId (existing thread).');
67
+ }
68
+ if (userId && threadId) {
69
+ throw new Error('sendPhoto: Provide only one destination — userId OR threadId, not both.');
70
+ }
71
+ // Validate photo buffer
72
+ if (!photoBuffer || !Buffer.isBuffer(photoBuffer) || photoBuffer.length === 0) {
73
+ throw new Error('sendPhoto: photoBuffer must be a non-empty Buffer.');
74
+ }
75
+
76
+ // 1) Upload photo to get upload_id
77
+ const upload_id = await uploadPhoto(session, photoBuffer, { mimeType, fileName, signal });
78
+
79
+ // 2) Build base form payload
80
+ const form = {
81
+ upload_id,
82
+ action: 'send_item',
83
+ // Optional caption field
84
+ caption,
85
+ };
86
+
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
+ if (Array.isArray(mentions) && mentions.length > 0) {
90
+ form.entities = JSON.stringify(
91
+ 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
+ user_id: String(uid),
96
+ type: 'mention',
97
+ }))
98
+ );
99
+ }
100
+
101
+ // 4) Destination-specific fields
102
+ let url;
103
+ if (userId) {
104
+ // DM to user: recipient_users is an array-of-arrays of userIds as strings
105
+ url = '/direct_v2/threads/broadcast/upload_photo/';
106
+ form.recipient_users = JSON.stringify([[String(userId)]]);
107
+ } else {
108
+ // Existing thread: use thread id
109
+ url = '/direct_v2/threads/broadcast/upload_photo/';
110
+ form.thread_ids = JSON.stringify([String(threadId)]);
111
+ }
112
+
113
+ // 5) Send broadcast request
114
+ try {
115
+ const response = await session.request.send({
116
+ url,
117
+ method: 'POST',
118
+ form,
119
+ signal,
120
+ });
121
+
122
+ if (!response) {
123
+ throw new Error('sendPhoto: Empty response from Instagram broadcast endpoint.');
124
+ }
125
+ return response;
126
+ } catch (err) {
127
+ throw new Error(`sendPhoto: Broadcast failed — ${normalizeError(err)}`);
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';
142
+ }
143
+ }
144
+
145
+ module.exports = sendPhoto;
@@ -0,0 +1,175 @@
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.
13
+ */
14
+
15
+ const { v4: uuidv4 } = require('uuid');
16
+
17
+ /**
18
+ * Validate buffer and mime type
19
+ */
20
+ function validateImageInput(photoBuffer, mimeType) {
21
+ if (!photoBuffer || !Buffer.isBuffer(photoBuffer) || photoBuffer.length === 0) {
22
+ throw new Error('uploadPhoto: photoBuffer must be a non-empty Buffer.');
23
+ }
24
+ const allowed = ['image/jpeg', 'image/jpg', 'image/png'];
25
+ if (!allowed.includes(mimeType)) {
26
+ throw new Error(`uploadPhoto: mimeType must be one of ${allowed.join(', ')}.`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Build Instagram rupload params for photo
32
+ */
33
+ function buildRuploadParams(uploadId, mimeType) {
34
+ // Instagram expects media_type=1 for photo.
35
+ // image_compression string must be JSON but sent as string.
36
+ const isJpeg = mimeType === 'image/jpeg' || mimeType === 'image/jpg';
37
+ const compression = isJpeg
38
+ ? '{"lib_name":"moz","lib_version":"3.1.m","quality":"80"}'
39
+ : '{"lib_name":"png","lib_version":"1.0","quality":"100"}';
40
+
41
+ return {
42
+ upload_id: uploadId,
43
+ media_type: 1,
44
+ image_compression: compression,
45
+ // Optional hints commonly used by IG web clients
46
+ xsharing_user_ids: JSON.stringify([]),
47
+ // mark this upload as photo (not reels/clips)
48
+ is_clips_media: false,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * 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
+ */
65
+ async function uploadPhoto(session, photoBuffer, options = {}) {
66
+ const {
67
+ mimeType = 'image/jpeg',
68
+ fileName,
69
+ quality,
70
+ signal,
71
+ } = options;
72
+
73
+ validateImageInput(photoBuffer, mimeType);
74
+
75
+ // Generate uploadId and object name used in rupload path
76
+ const uploadId = Date.now().toString();
77
+ const objectName = fileName
78
+ ? sanitizeFileName(fileName, mimeType)
79
+ : `${uuidv4()}.${mimeType === 'image/png' ? 'png' : 'jpg'}`;
80
+
81
+ // Build rupload params
82
+ 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
+
91
+ // Headers expected by Instagram rupload
92
+ const headers = {
93
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
94
+ 'Content-Type': mimeType,
95
+ 'X_FB_PHOTO_WATERFALL_ID': uuidv4(), // optional tracing header used by IG
96
+ 'X-Entity-Type': mimeType,
97
+ 'X-Entity-Length': String(photoBuffer.length),
98
+ 'Content-Length': String(photoBuffer.length),
99
+ // Some clients also send:
100
+ // 'Offset': '0',
101
+ };
102
+
103
+ const url = `/rupload_igphoto/${objectName}`;
104
+
105
+ // Send the upload request
106
+ try {
107
+ const response = await session.request.send({
108
+ url,
109
+ method: 'POST',
110
+ headers,
111
+ body: photoBuffer,
112
+ signal,
113
+ });
114
+
115
+ // Basic success validation (IG often returns JSON with upload_id or ok status)
116
+ if (!response) {
117
+ throw new Error('uploadPhoto: Empty response from Instagram rupload endpoint.');
118
+ }
119
+
120
+ // If response contains upload_id, prefer it; otherwise use our generated one
121
+ const serverUploadId =
122
+ (typeof response === 'object' && response.upload_id) ||
123
+ (response?.body && parseUploadIdFromBody(response.body));
124
+
125
+ return serverUploadId || uploadId;
126
+ } catch (err) {
127
+ const message = normalizeError(err);
128
+ throw new Error(`uploadPhoto: Upload failed — ${message}`);
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';
172
+ }
173
+ }
174
+
175
+ module.exports = uploadPhoto;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * uploadFile.js
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.
19
+ */
20
+
21
+ const { v4: uuidv4 } = require('uuid');
22
+
23
+ const DEFAULT_CHUNK_SIZE = 512 * 1024; // 512KB default chunks
24
+
25
+ /**
26
+ * Validate upload input
27
+ */
28
+ function validateFileInput(fileBuffer, mimeType) {
29
+ if (!fileBuffer || !Buffer.isBuffer(fileBuffer) || fileBuffer.length === 0) {
30
+ throw new Error('uploadFile: fileBuffer must be a non-empty Buffer.');
31
+ }
32
+ if (typeof mimeType !== 'string' || mimeType.length === 0) {
33
+ throw new Error('uploadFile: mimeType must be a non-empty string (e.g., "video/mp4").');
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Ensure file name extension matches mime type; strip invalid chars.
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)
70
+ */
71
+ function buildRuploadParams(uploadId, mimeType, opts) {
72
+ const isVideo = mimeType.startsWith('video/');
73
+ const isAudio = mimeType.startsWith('audio/');
74
+ const isImage = mimeType.startsWith('image/');
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
79
+ const params = {
80
+ upload_id: uploadId,
81
+ media_type: isImage ? 1 : 2,
82
+ xsharing_user_ids: JSON.stringify([]),
83
+ is_clips_media: Boolean(opts.isClipsMedia),
84
+ };
85
+
86
+ if (isVideo) {
87
+ params.video_format = 'mp4';
88
+ params.for_direct_story = false;
89
+ }
90
+ if (isAudio) {
91
+ // IG web may not fully support audio in Direct; leaving generic params
92
+ params.audio_format = 'mpeg';
93
+ }
94
+
95
+ return params;
96
+ }
97
+
98
+ /**
99
+ * Parse `upload_id` from response body if present.
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
181
+ */
182
+ async function uploadFile(session, fileBuffer, options = {}) {
183
+ const {
184
+ mimeType = 'video/mp4',
185
+ fileName,
186
+ chunkSize = DEFAULT_CHUNK_SIZE,
187
+ isClipsMedia = false,
188
+ signal,
189
+ } = options;
190
+
191
+ validateFileInput(fileBuffer, mimeType);
192
+
193
+ const uploadId = Date.now().toString();
194
+ const objectName = sanitizeFileName(fileName, mimeType);
195
+ const totalLength = fileBuffer.length;
196
+
197
+ const ruploadParams = buildRuploadParams(uploadId, mimeType, { isClipsMedia });
198
+ const baseHeaders = {
199
+ 'X-Instagram-Rupload-Params': JSON.stringify(ruploadParams),
200
+ 'X_FB_VIDEO_WATERFALL_ID': uuidv4(), // tracing header for video-like uploads
201
+ 'X-Entity-Type': mimeType,
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);
215
+
216
+ // Chunked upload loop
217
+ let offset = 0;
218
+ const size = Math.max(64 * 1024, Math.min(4 * 1024 * 1024, chunkSize)); // clamp to 64KB..4MB
219
+
220
+ try {
221
+ while (offset < totalLength) {
222
+ const end = Math.min(offset + size, totalLength);
223
+ const chunk = fileBuffer.subarray(offset, end);
224
+
225
+ const res = await uploadChunk(session, endpoint, baseHeaders, chunk, offset, signal);
226
+
227
+ // Server may respond with intermediate states; we proceed unless explicit failure
228
+ if (!res) {
229
+ throw new Error(`uploadFile: Empty response at offset ${offset}.`);
230
+ }
231
+
232
+ // Advance offset
233
+ offset = end;
234
+ }
235
+ } catch (err) {
236
+ throw new Error(`uploadFile: Chunk upload failed at offset ${offset} — ${normalizeError(err)}`);
237
+ }
238
+
239
+ // Final confirmation: attempt a "finish" ping (some clients re-POST zero-length or rely on server auto-finish)
240
+ // We'll try a lightweight confirmation request and parse upload_id if present.
241
+ try {
242
+ const confirm = await session.request.send({
243
+ url: endpoint,
244
+ method: 'POST',
245
+ headers: {
246
+ ...baseHeaders,
247
+ 'Offset': String(totalLength),
248
+ 'Content-Length': '0',
249
+ },
250
+ signal,
251
+ });
252
+
253
+ const serverUploadId =
254
+ (typeof confirm === 'object' && confirm.upload_id) ||
255
+ (confirm?.body && parseUploadIdFromBody(confirm.body));
256
+
257
+ return serverUploadId || uploadId;
258
+ } catch {
259
+ // If confirmation fails but chunks completed, many flows still accept the generated uploadId.
260
+ return uploadId;
261
+ }
262
+ }
263
+
264
+ module.exports = uploadFile;