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.
- package/README.md +1650 -0
- package/dist/constants/constants.js +280 -0
- package/dist/constants/index.js +41 -0
- package/dist/core/client.js +243 -0
- package/dist/core/repository.js +7 -0
- package/dist/core/request.js +212 -0
- package/dist/core/state.js +1456 -0
- package/dist/core/utils.js +786 -0
- package/dist/downloadMedia.js +381 -0
- package/dist/errors/index.d.ts +16 -0
- package/dist/errors/index.js +30 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/fbns/fbns.client.d.ts +32 -0
- package/dist/fbns/fbns.client.events.d.ts +41 -0
- package/dist/fbns/fbns.client.events.js +3 -0
- package/dist/fbns/fbns.client.events.js.map +1 -0
- package/dist/fbns/fbns.client.js +179 -0
- package/dist/fbns/fbns.client.js.map +1 -0
- package/dist/fbns/fbns.device-auth.d.ts +17 -0
- package/dist/fbns/fbns.device-auth.js +54 -0
- package/dist/fbns/fbns.device-auth.js.map +1 -0
- package/dist/fbns/fbns.types.d.ts +83 -0
- package/dist/fbns/fbns.types.js +3 -0
- package/dist/fbns/fbns.types.js.map +1 -0
- package/dist/fbns/fbns.utilities.d.ts +2 -0
- package/dist/fbns/fbns.utilities.js +79 -0
- package/dist/fbns/fbns.utilities.js.map +1 -0
- package/dist/fbns/index.d.ts +4 -0
- package/dist/fbns/index.js +21 -0
- package/dist/fbns/index.js.map +1 -0
- package/dist/index.js +39 -0
- package/dist/mqttot/index.d.ts +4 -0
- package/dist/mqttot/index.js +21 -0
- package/dist/mqttot/index.js.map +1 -0
- package/dist/mqttot/mqttot.client.d.ts +39 -0
- package/dist/mqttot/mqttot.client.js +120 -0
- package/dist/mqttot/mqttot.client.js.map +1 -0
- package/dist/mqttot/mqttot.connect.request.packet.d.ts +7 -0
- package/dist/mqttot/mqttot.connect.request.packet.js +9 -0
- package/dist/mqttot/mqttot.connect.request.packet.js.map +1 -0
- package/dist/mqttot/mqttot.connect.response.packet.d.ts +7 -0
- package/dist/mqttot/mqttot.connect.response.packet.js +24 -0
- package/dist/mqttot/mqttot.connect.response.packet.js.map +1 -0
- package/dist/mqttot/mqttot.connection.d.ts +57 -0
- package/dist/mqttot/mqttot.connection.js +56 -0
- package/dist/mqttot/mqttot.connection.js.map +1 -0
- package/dist/package.json +59 -0
- package/dist/realtime/commands/commands.d.ts +15 -0
- package/dist/realtime/commands/commands.js +21 -0
- package/dist/realtime/commands/commands.js.map +1 -0
- package/dist/realtime/commands/direct.commands.d.ts +75 -0
- package/dist/realtime/commands/direct.commands.js +186 -0
- package/dist/realtime/commands/direct.commands.js.map +1 -0
- package/dist/realtime/commands/enhanced.direct.commands.js +987 -0
- package/dist/realtime/commands/index.d.ts +2 -0
- package/dist/realtime/commands/index.js +19 -0
- package/dist/realtime/commands/index.js.map +1 -0
- package/dist/realtime/delta-sync.manager.js +293 -0
- package/dist/realtime/features/dm-sender.js +88 -0
- package/dist/realtime/features/error-handler.js +73 -0
- package/dist/realtime/features/gap-handler.js +61 -0
- package/dist/realtime/features/presence.manager.js +66 -0
- package/dist/realtime/index.js +30 -0
- package/dist/realtime/messages/app-presence.event.d.ts +9 -0
- package/dist/realtime/messages/app-presence.event.js +3 -0
- package/dist/realtime/messages/app-presence.event.js.map +1 -0
- package/dist/realtime/messages/index.d.ts +3 -0
- package/dist/realtime/messages/index.js +20 -0
- package/dist/realtime/messages/index.js.map +1 -0
- package/dist/realtime/messages/message-sync.message.d.ts +222 -0
- package/dist/realtime/messages/message-sync.message.js +43 -0
- package/dist/realtime/messages/message-sync.message.js.map +1 -0
- package/dist/realtime/messages/realtime-sub.direct.data.d.ts +11 -0
- package/dist/realtime/messages/realtime-sub.direct.data.js +3 -0
- package/dist/realtime/messages/realtime-sub.direct.data.js.map +1 -0
- package/dist/realtime/messages/thread-update.message.d.ts +68 -0
- package/dist/realtime/messages/thread-update.message.js +3 -0
- package/dist/realtime/messages/thread-update.message.js.map +1 -0
- package/dist/realtime/mixins/index.d.ts +3 -0
- package/dist/realtime/mixins/index.js +20 -0
- package/dist/realtime/mixins/index.js.map +1 -0
- package/dist/realtime/mixins/message-sync.mixin.d.ts +8 -0
- package/dist/realtime/mixins/message-sync.mixin.js +381 -0
- package/dist/realtime/mixins/message-sync.mixin.js.map +1 -0
- package/dist/realtime/mixins/mixin.d.ts +19 -0
- package/dist/realtime/mixins/mixin.js +41 -0
- package/dist/realtime/mixins/mixin.js.map +1 -0
- package/dist/realtime/mixins/presence-typing.mixin.js +33 -0
- package/dist/realtime/mixins/realtime-sub.mixin.d.ts +8 -0
- package/dist/realtime/mixins/realtime-sub.mixin.js +55 -0
- package/dist/realtime/mixins/realtime-sub.mixin.js.map +1 -0
- package/dist/realtime/parsers/graphql-parser.js +43 -0
- package/dist/realtime/parsers/graphql.parser.d.ts +15 -0
- package/dist/realtime/parsers/graphql.parser.js +22 -0
- package/dist/realtime/parsers/graphql.parser.js.map +1 -0
- package/dist/realtime/parsers/index.d.ts +6 -0
- package/dist/realtime/parsers/index.js +23 -0
- package/dist/realtime/parsers/index.js.map +1 -0
- package/dist/realtime/parsers/iris-parser.js +43 -0
- package/dist/realtime/parsers/iris.parser.d.ts +17 -0
- package/dist/realtime/parsers/iris.parser.js +10 -0
- package/dist/realtime/parsers/iris.parser.js.map +1 -0
- package/dist/realtime/parsers/json-parser.js +43 -0
- package/dist/realtime/parsers/json.parser.d.ts +6 -0
- package/dist/realtime/parsers/json.parser.js +10 -0
- package/dist/realtime/parsers/json.parser.js.map +1 -0
- package/dist/realtime/parsers/parser.d.ts +9 -0
- package/dist/realtime/parsers/parser.js +3 -0
- package/dist/realtime/parsers/parser.js.map +1 -0
- package/dist/realtime/parsers/region-hint-parser.js +43 -0
- package/dist/realtime/parsers/region-hint.parser.d.ts +12 -0
- package/dist/realtime/parsers/region-hint.parser.js +15 -0
- package/dist/realtime/parsers/region-hint.parser.js.map +1 -0
- package/dist/realtime/parsers/skywalker-parser.js +43 -0
- package/dist/realtime/parsers/skywalker.parser.d.ts +12 -0
- package/dist/realtime/parsers/skywalker.parser.js +15 -0
- package/dist/realtime/parsers/skywalker.parser.js.map +1 -0
- package/dist/realtime/parsers-advanced.js +158 -0
- package/dist/realtime/proto/common.proto +38 -0
- package/dist/realtime/proto/direct.proto +65 -0
- package/dist/realtime/proto/ig-messages.proto +83 -0
- package/dist/realtime/proto/iris.proto +188 -0
- package/dist/realtime/proto-parser.js +195 -0
- package/dist/realtime/protocols/iris.handshake.js +74 -0
- package/dist/realtime/protocols/proto-definitions.js +80 -0
- package/dist/realtime/protocols/skywalker.protocol.js +91 -0
- package/dist/realtime/realtime.client.events.js +3 -0
- package/dist/realtime/realtime.client.js +449 -0
- package/dist/realtime/realtime.service.js +462 -0
- package/dist/realtime/reconnect.manager.js +94 -0
- package/dist/realtime/session.manager.js +121 -0
- package/dist/realtime/subscriptions/graphql.subscription.d.ts +47 -0
- package/dist/realtime/subscriptions/graphql.subscription.js +99 -0
- package/dist/realtime/subscriptions/graphql.subscription.js.map +1 -0
- package/dist/realtime/subscriptions/index.d.ts +2 -0
- package/dist/realtime/subscriptions/index.js +19 -0
- package/dist/realtime/subscriptions/index.js.map +1 -0
- package/dist/realtime/subscriptions/skywalker.subscription.d.ts +4 -0
- package/dist/realtime/subscriptions/skywalker.subscription.js +13 -0
- package/dist/realtime/subscriptions/skywalker.subscription.js.map +1 -0
- package/dist/realtime/topic-map.js +71 -0
- package/dist/realtime/topic.js +80 -0
- package/dist/repositories/account.repository.js +261 -0
- package/dist/repositories/direct-thread.repository.js +247 -0
- package/dist/repositories/direct.repository.js +153 -0
- package/dist/repositories/feed.repository.js +233 -0
- package/dist/repositories/friendship.repository.js +190 -0
- package/dist/repositories/hashtag.repository.js +101 -0
- package/dist/repositories/highlights.repository.js +127 -0
- package/dist/repositories/location.repository.js +84 -0
- package/dist/repositories/media.repository.js +165 -0
- package/dist/repositories/story.repository.js +156 -0
- package/dist/repositories/upload.repository.js +167 -0
- package/dist/repositories/user.repository.js +94 -0
- package/dist/sendmedia/index.js +11 -0
- package/dist/sendmedia/sendFile.js +154 -0
- package/dist/sendmedia/sendPhoto.js +145 -0
- package/dist/sendmedia/uploadPhoto.js +175 -0
- package/dist/sendmedia/uploadfFile.js +264 -0
- package/dist/services/live.service.js +147 -0
- package/dist/services/search.service.js +116 -0
- package/dist/shared/index.js +35 -0
- package/dist/shared/shared.js +86 -0
- package/dist/thrift/index.d.ts +3 -0
- package/dist/thrift/index.js +20 -0
- package/dist/thrift/index.js.map +1 -0
- package/dist/thrift/thrift.d.ts +59 -0
- package/dist/thrift/thrift.js +101 -0
- package/dist/thrift/thrift.js.map +1 -0
- package/dist/thrift/thrift.reading.d.ts +41 -0
- package/dist/thrift/thrift.reading.js +327 -0
- package/dist/thrift/thrift.reading.js.map +1 -0
- package/dist/thrift/thrift.writing.d.ts +44 -0
- package/dist/thrift/thrift.writing.js +342 -0
- package/dist/thrift/thrift.writing.js.map +1 -0
- package/dist/types/index.js +285 -0
- package/dist/useMultiFileAuthState.js +437 -0
- package/dist/utils/helper-1.js +1 -0
- package/dist/utils/helper-10.js +1 -0
- package/dist/utils/helper-11.js +1 -0
- package/dist/utils/helper-12.js +1 -0
- package/dist/utils/helper-13.js +1 -0
- package/dist/utils/helper-14.js +1 -0
- package/dist/utils/helper-15.js +1 -0
- package/dist/utils/helper-16.js +1 -0
- package/dist/utils/helper-17.js +1 -0
- package/dist/utils/helper-18.js +1 -0
- package/dist/utils/helper-19.js +1 -0
- package/dist/utils/helper-2.js +1 -0
- package/dist/utils/helper-20.js +1 -0
- package/dist/utils/helper-21.js +1 -0
- package/dist/utils/helper-22.js +1 -0
- package/dist/utils/helper-23.js +1 -0
- package/dist/utils/helper-24.js +1 -0
- package/dist/utils/helper-25.js +1 -0
- package/dist/utils/helper-26.js +1 -0
- package/dist/utils/helper-27.js +1 -0
- package/dist/utils/helper-28.js +1 -0
- package/dist/utils/helper-29.js +1 -0
- package/dist/utils/helper-3.js +1 -0
- package/dist/utils/helper-30.js +1 -0
- package/dist/utils/helper-4.js +1 -0
- package/dist/utils/helper-5.js +1 -0
- package/dist/utils/helper-6.js +1 -0
- package/dist/utils/helper-7.js +1 -0
- package/dist/utils/helper-8.js +1 -0
- package/dist/utils/helper-9.js +1 -0
- package/dist/utils/index.js +280 -0
- package/examples/listen-to-messages.js +86 -0
- 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;
|