upload-post 1.0.1 → 2.0.3

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/index.js CHANGED
@@ -3,15 +3,13 @@ import FormData from 'form-data';
3
3
  import fs from 'fs';
4
4
  import { createReadStream } from 'fs';
5
5
 
6
- /**
7
- * @typedef {Object} UploadOptions
8
- * @property {string} title - Video title
9
- * @property {string} user - User identifier
10
- * @property {string[]} platforms - Array of platforms (e.g. ['tiktok'])
11
- */
6
+ const API_BASE_URL = 'https://api.upload-post.com/api';
12
7
 
13
8
  /**
14
9
  * Upload-Post API client
10
+ *
11
+ * Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
12
+ * Pinterest, Threads, Reddit, Bluesky, X (Twitter)
15
13
  */
16
14
  export class UploadPost {
17
15
  /**
@@ -19,42 +17,730 @@ export class UploadPost {
19
17
  */
20
18
  constructor(apiKey) {
21
19
  this.apiKey = apiKey;
22
- this.baseUrl = 'https://api.upload-post.com/api';
20
+ this.baseUrl = API_BASE_URL;
21
+ }
22
+
23
+ /**
24
+ * Make an API request
25
+ * @private
26
+ */
27
+ async _request(endpoint, method = 'GET', data = null, isFormData = false) {
28
+ const config = {
29
+ method,
30
+ url: `${this.baseUrl}${endpoint}`,
31
+ headers: {
32
+ 'Authorization': `Apikey ${this.apiKey}`
33
+ },
34
+ maxContentLength: Infinity,
35
+ maxBodyLength: Infinity
36
+ };
37
+
38
+ if (data) {
39
+ if (isFormData) {
40
+ config.headers = { ...config.headers, ...data.getHeaders() };
41
+ config.data = data;
42
+ } else if (method === 'GET') {
43
+ config.params = data;
44
+ } else {
45
+ config.headers['Content-Type'] = 'application/json';
46
+ config.data = data;
47
+ }
48
+ }
49
+
50
+ try {
51
+ const response = await axios(config);
52
+ return response.data;
53
+ } catch (error) {
54
+ const message = error.response?.data?.message || error.response?.data?.detail || error.message;
55
+ throw new Error(`Upload-Post API error: ${message}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Add common upload parameters to form
61
+ * @private
62
+ */
63
+ _addCommonParams(form, options) {
64
+ form.append('user', options.user);
65
+ form.append('title', options.title);
66
+
67
+ // Platforms
68
+ const platforms = Array.isArray(options.platforms) ? options.platforms : [options.platforms];
69
+ platforms.forEach(p => form.append('platform[]', p));
70
+
71
+ // Optional common parameters
72
+ if (options.firstComment) form.append('first_comment', options.firstComment);
73
+ if (options.altText) form.append('alt_text', options.altText);
74
+ if (options.scheduledDate) form.append('scheduled_date', options.scheduledDate);
75
+ if (options.timezone) form.append('timezone', options.timezone);
76
+ if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
77
+ if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
78
+
79
+ // Platform-specific title overrides
80
+ if (options.blueskyTitle) form.append('bluesky_title', options.blueskyTitle);
81
+ if (options.instagramTitle) form.append('instagram_title', options.instagramTitle);
82
+ if (options.facebookTitle) form.append('facebook_title', options.facebookTitle);
83
+ if (options.tiktokTitle) form.append('tiktok_title', options.tiktokTitle);
84
+ if (options.linkedinTitle) form.append('linkedin_title', options.linkedinTitle);
85
+ if (options.xTitle) form.append('x_title', options.xTitle);
86
+ if (options.youtubeTitle) form.append('youtube_title', options.youtubeTitle);
87
+ if (options.pinterestTitle) form.append('pinterest_title', options.pinterestTitle);
88
+ if (options.threadsTitle) form.append('threads_title', options.threadsTitle);
89
+
90
+ // Platform-specific description overrides
91
+ if (options.description) form.append('description', options.description);
92
+ if (options.linkedinDescription) form.append('linkedin_description', options.linkedinDescription);
93
+ if (options.youtubeDescription) form.append('youtube_description', options.youtubeDescription);
94
+ if (options.facebookDescription) form.append('facebook_description', options.facebookDescription);
95
+ if (options.tiktokDescription) form.append('tiktok_description', options.tiktokDescription);
96
+ if (options.pinterestDescription) form.append('pinterest_description', options.pinterestDescription);
97
+
98
+ // Platform-specific first comment overrides
99
+ if (options.instagramFirstComment) form.append('instagram_first_comment', options.instagramFirstComment);
100
+ if (options.facebookFirstComment) form.append('facebook_first_comment', options.facebookFirstComment);
101
+ if (options.xFirstComment) form.append('x_first_comment', options.xFirstComment);
102
+ if (options.threadsFirstComment) form.append('threads_first_comment', options.threadsFirstComment);
103
+ if (options.youtubeFirstComment) form.append('youtube_first_comment', options.youtubeFirstComment);
104
+ if (options.redditFirstComment) form.append('reddit_first_comment', options.redditFirstComment);
105
+ if (options.blueskyFirstComment) form.append('bluesky_first_comment', options.blueskyFirstComment);
106
+ }
107
+
108
+ /**
109
+ * Add TikTok-specific parameters
110
+ * @private
111
+ */
112
+ _addTiktokParams(form, options, isVideo = true) {
113
+ if (options.tiktokDisableComment !== undefined) form.append('disable_comment', String(options.tiktokDisableComment));
114
+ if (options.brandContentToggle !== undefined) form.append('brand_content_toggle', String(options.brandContentToggle));
115
+ if (options.brandOrganicToggle !== undefined) form.append('brand_organic_toggle', String(options.brandOrganicToggle));
116
+
117
+ if (isVideo) {
118
+ if (options.tiktokPrivacyLevel) form.append('privacy_level', options.tiktokPrivacyLevel);
119
+ if (options.tiktokDisableDuet !== undefined) form.append('disable_duet', String(options.tiktokDisableDuet));
120
+ if (options.tiktokDisableStitch !== undefined) form.append('disable_stitch', String(options.tiktokDisableStitch));
121
+ if (options.tiktokCoverTimestamp !== undefined) form.append('cover_timestamp', options.tiktokCoverTimestamp);
122
+ if (options.tiktokIsAigc !== undefined) form.append('is_aigc', String(options.tiktokIsAigc));
123
+ if (options.tiktokPostMode) form.append('post_mode', options.tiktokPostMode);
124
+ } else {
125
+ // Photo-specific
126
+ if (options.tiktokAutoAddMusic !== undefined) form.append('auto_add_music', String(options.tiktokAutoAddMusic));
127
+ if (options.tiktokPhotoCoverIndex !== undefined) form.append('photo_cover_index', options.tiktokPhotoCoverIndex);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Add Instagram-specific parameters
133
+ * @private
134
+ */
135
+ _addInstagramParams(form, options, isVideo = true) {
136
+ if (options.instagramMediaType) form.append('media_type', options.instagramMediaType);
137
+ if (options.instagramCollaborators) form.append('collaborators', options.instagramCollaborators);
138
+ if (options.instagramUserTags) form.append('user_tags', options.instagramUserTags);
139
+ if (options.instagramLocationId) form.append('location_id', options.instagramLocationId);
140
+
141
+ if (isVideo) {
142
+ if (options.instagramShareToFeed !== undefined) form.append('share_to_feed', String(options.instagramShareToFeed));
143
+ if (options.instagramCoverUrl) form.append('cover_url', options.instagramCoverUrl);
144
+ if (options.instagramAudioName) form.append('audio_name', options.instagramAudioName);
145
+ if (options.instagramThumbOffset) form.append('thumb_offset', options.instagramThumbOffset);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Add YouTube-specific parameters
151
+ * @private
152
+ */
153
+ _addYoutubeParams(form, options) {
154
+ if (options.youtubeTags) {
155
+ const tags = Array.isArray(options.youtubeTags) ? options.youtubeTags : options.youtubeTags.split(',').map(t => t.trim());
156
+ tags.forEach(tag => form.append('tags[]', tag));
157
+ }
158
+ if (options.youtubeCategoryId) form.append('categoryId', options.youtubeCategoryId);
159
+ if (options.youtubePrivacyStatus) form.append('privacyStatus', options.youtubePrivacyStatus);
160
+ if (options.youtubeEmbeddable !== undefined) form.append('embeddable', String(options.youtubeEmbeddable));
161
+ if (options.youtubeLicense) form.append('license', options.youtubeLicense);
162
+ if (options.youtubePublicStatsViewable !== undefined) form.append('publicStatsViewable', String(options.youtubePublicStatsViewable));
163
+ if (options.youtubeThumbnailUrl) form.append('thumbnail_url', options.youtubeThumbnailUrl);
164
+ if (options.youtubeSelfDeclaredMadeForKids !== undefined) form.append('selfDeclaredMadeForKids', String(options.youtubeSelfDeclaredMadeForKids));
165
+ if (options.youtubeContainsSyntheticMedia !== undefined) form.append('containsSyntheticMedia', String(options.youtubeContainsSyntheticMedia));
166
+ if (options.youtubeDefaultLanguage) form.append('defaultLanguage', options.youtubeDefaultLanguage);
167
+ if (options.youtubeDefaultAudioLanguage) form.append('defaultAudioLanguage', options.youtubeDefaultAudioLanguage);
168
+ if (options.youtubeAllowedCountries) form.append('allowedCountries', options.youtubeAllowedCountries);
169
+ if (options.youtubeBlockedCountries) form.append('blockedCountries', options.youtubeBlockedCountries);
170
+ if (options.youtubeHasPaidProductPlacement !== undefined) form.append('hasPaidProductPlacement', String(options.youtubeHasPaidProductPlacement));
171
+ if (options.youtubeRecordingDate) form.append('recordingDate', options.youtubeRecordingDate);
172
+ }
173
+
174
+ /**
175
+ * Add LinkedIn-specific parameters
176
+ * @private
177
+ */
178
+ _addLinkedinParams(form, options) {
179
+ if (options.linkedinVisibility) form.append('visibility', options.linkedinVisibility);
180
+ if (options.targetLinkedinPageId) form.append('target_linkedin_page_id', options.targetLinkedinPageId);
23
181
  }
24
182
 
25
183
  /**
26
- * Upload video to social media platforms
27
- * @param {string} videoPath - Path to video file
28
- * @param {UploadOptions} options - Upload options
184
+ * Add Facebook-specific parameters
185
+ * @private
186
+ */
187
+ _addFacebookParams(form, options, isVideo = false, isText = false) {
188
+ if (options.facebookPageId) form.append('facebook_page_id', options.facebookPageId);
189
+
190
+ if (isVideo) {
191
+ if (options.facebookVideoState) form.append('video_state', options.facebookVideoState);
192
+ if (options.facebookMediaType) form.append('facebook_media_type', options.facebookMediaType);
193
+ }
194
+
195
+ if (isText && options.facebookLinkUrl) {
196
+ form.append('facebook_link_url', options.facebookLinkUrl);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Add Pinterest-specific parameters
202
+ * @private
203
+ */
204
+ _addPinterestParams(form, options, isVideo = false) {
205
+ if (options.pinterestBoardId) form.append('pinterest_board_id', options.pinterestBoardId);
206
+ if (options.pinterestAltText) form.append('pinterest_alt_text', options.pinterestAltText);
207
+ if (options.pinterestLink) form.append('pinterest_link', options.pinterestLink);
208
+
209
+ if (isVideo) {
210
+ if (options.pinterestCoverImageUrl) form.append('pinterest_cover_image_url', options.pinterestCoverImageUrl);
211
+ if (options.pinterestCoverImageContentType) form.append('pinterest_cover_image_content_type', options.pinterestCoverImageContentType);
212
+ if (options.pinterestCoverImageData) form.append('pinterest_cover_image_data', options.pinterestCoverImageData);
213
+ if (options.pinterestCoverImageKeyFrameTime !== undefined) form.append('pinterest_cover_image_key_frame_time', options.pinterestCoverImageKeyFrameTime);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Add X (Twitter) specific parameters
219
+ * @private
220
+ */
221
+ _addXParams(form, options, isText = false) {
222
+ if (options.xReplySettings && options.xReplySettings !== 'everyone') form.append('reply_settings', options.xReplySettings);
223
+ if (options.xNullcast !== undefined) form.append('nullcast', String(options.xNullcast));
224
+ if (options.xQuoteTweetId) form.append('quote_tweet_id', options.xQuoteTweetId);
225
+ if (options.xGeoPlaceId) form.append('geo_place_id', options.xGeoPlaceId);
226
+ if (options.xForSuperFollowersOnly !== undefined) form.append('for_super_followers_only', String(options.xForSuperFollowersOnly));
227
+ if (options.xCommunityId) form.append('community_id', options.xCommunityId);
228
+ if (options.xShareWithFollowers !== undefined) form.append('share_with_followers', String(options.xShareWithFollowers));
229
+ if (options.xDirectMessageDeepLink) form.append('direct_message_deep_link', options.xDirectMessageDeepLink);
230
+ if (options.xLongTextAsPost !== undefined) form.append('x_long_text_as_post', String(options.xLongTextAsPost));
231
+
232
+ if (!isText) {
233
+ if (options.xTaggedUserIds) {
234
+ const ids = Array.isArray(options.xTaggedUserIds) ? options.xTaggedUserIds : options.xTaggedUserIds.split(',').map(t => t.trim());
235
+ ids.forEach(id => form.append('tagged_user_ids[]', id));
236
+ }
237
+ if (options.xPlaceId) form.append('place_id', options.xPlaceId);
238
+ } else {
239
+ if (options.xPostUrl) form.append('post_url', options.xPostUrl);
240
+ if (options.xCardUri) form.append('card_uri', options.xCardUri);
241
+
242
+ // Poll options
243
+ if (options.xPollOptions) {
244
+ const pollOpts = Array.isArray(options.xPollOptions) ? options.xPollOptions : options.xPollOptions.split(',').map(t => t.trim());
245
+ pollOpts.forEach(opt => form.append('poll_options[]', opt));
246
+ if (options.xPollDuration) form.append('poll_duration', options.xPollDuration);
247
+ if (options.xPollReplySettings) form.append('poll_reply_settings', options.xPollReplySettings);
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Add Threads-specific parameters
254
+ * @private
255
+ */
256
+ _addThreadsParams(form, options) {
257
+ if (options.threadsLongTextAsPost !== undefined) form.append('threads_long_text_as_post', String(options.threadsLongTextAsPost));
258
+ }
259
+
260
+ /**
261
+ * Add Reddit-specific parameters
262
+ * @private
263
+ */
264
+ _addRedditParams(form, options) {
265
+ if (options.redditSubreddit) form.append('subreddit', options.redditSubreddit);
266
+ if (options.redditFlairId) form.append('flair_id', options.redditFlairId);
267
+ }
268
+
269
+ /**
270
+ * Upload a video to social media platforms
271
+ *
272
+ * @param {string} videoPathOrUrl - Path to video file or video URL
273
+ * @param {Object} options - Upload options
274
+ * @param {string} options.title - Video title/caption
275
+ * @param {string} options.user - User identifier (profile name)
276
+ * @param {string[]} options.platforms - Target platforms (tiktok, instagram, youtube, linkedin, facebook, pinterest, threads, bluesky, x)
277
+ * @param {string} [options.description] - Video description
278
+ * @param {string} [options.firstComment] - First comment to post
279
+ * @param {string} [options.scheduledDate] - ISO date for scheduling (e.g., "2024-12-25T10:00:00Z")
280
+ * @param {string} [options.timezone] - Timezone for scheduled date (e.g., "Europe/Madrid")
281
+ * @param {boolean} [options.addToQueue] - Add to posting queue instead of immediate post
282
+ * @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
283
+ *
284
+ * TikTok options:
285
+ * @param {string} [options.tiktokPrivacyLevel] - PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY
286
+ * @param {boolean} [options.tiktokDisableDuet] - Disable duet
287
+ * @param {boolean} [options.tiktokDisableComment] - Disable comments
288
+ * @param {boolean} [options.tiktokDisableStitch] - Disable stitch
289
+ * @param {number} [options.tiktokCoverTimestamp] - Timestamp in ms for video cover
290
+ * @param {boolean} [options.tiktokIsAigc] - AI-generated content flag
291
+ * @param {string} [options.tiktokPostMode] - DIRECT_POST or MEDIA_UPLOAD
292
+ * @param {boolean} [options.brandContentToggle] - Branded content toggle
293
+ * @param {boolean} [options.brandOrganicToggle] - Brand organic toggle
294
+ *
295
+ * Instagram options:
296
+ * @param {string} [options.instagramMediaType] - REELS or STORIES
297
+ * @param {boolean} [options.instagramShareToFeed] - Share to feed
298
+ * @param {string} [options.instagramCollaborators] - Comma-separated collaborator usernames
299
+ * @param {string} [options.instagramCoverUrl] - Custom cover URL
300
+ * @param {string} [options.instagramAudioName] - Audio track name
301
+ * @param {string} [options.instagramUserTags] - Comma-separated user tags
302
+ * @param {string} [options.instagramLocationId] - Location ID
303
+ * @param {string} [options.instagramThumbOffset] - Thumbnail offset
304
+ *
305
+ * YouTube options:
306
+ * @param {string|string[]} [options.youtubeTags] - Video tags
307
+ * @param {string} [options.youtubeCategoryId] - Category ID (e.g., "22" for People & Blogs)
308
+ * @param {string} [options.youtubePrivacyStatus] - public, unlisted, or private
309
+ * @param {boolean} [options.youtubeEmbeddable] - Allow embedding
310
+ * @param {string} [options.youtubeLicense] - youtube or creativeCommon
311
+ * @param {boolean} [options.youtubePublicStatsViewable] - Show public stats
312
+ * @param {string} [options.youtubeThumbnailUrl] - Custom thumbnail URL
313
+ * @param {boolean} [options.youtubeSelfDeclaredMadeForKids] - Made for kids flag
314
+ * @param {boolean} [options.youtubeContainsSyntheticMedia] - AI/synthetic content flag
315
+ * @param {string} [options.youtubeDefaultLanguage] - Title/description language (BCP-47)
316
+ * @param {string} [options.youtubeDefaultAudioLanguage] - Audio language (BCP-47)
317
+ * @param {string} [options.youtubeAllowedCountries] - Comma-separated country codes
318
+ * @param {string} [options.youtubeBlockedCountries] - Comma-separated country codes
319
+ * @param {boolean} [options.youtubeHasPaidProductPlacement] - Paid placement flag
320
+ * @param {string} [options.youtubeRecordingDate] - Recording date (ISO 8601)
321
+ *
322
+ * LinkedIn options:
323
+ * @param {string} [options.linkedinVisibility] - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
324
+ * @param {string} [options.targetLinkedinPageId] - Page ID for organization posts
325
+ *
326
+ * Facebook options:
327
+ * @param {string} [options.facebookPageId] - Facebook Page ID
328
+ * @param {string} [options.facebookVideoState] - PUBLISHED or DRAFT
329
+ * @param {string} [options.facebookMediaType] - REELS or STORIES
330
+ *
331
+ * Pinterest options:
332
+ * @param {string} [options.pinterestBoardId] - Board ID
333
+ * @param {string} [options.pinterestLink] - Destination link
334
+ * @param {string} [options.pinterestCoverImageUrl] - Cover image URL
335
+ * @param {number} [options.pinterestCoverImageKeyFrameTime] - Key frame time in ms
336
+ *
337
+ * X (Twitter) options:
338
+ * @param {string} [options.xReplySettings] - everyone, following, mentionedUsers, subscribers, verified
339
+ * @param {boolean} [options.xNullcast] - Promoted-only post
340
+ * @param {string|string[]} [options.xTaggedUserIds] - User IDs to tag
341
+ * @param {string} [options.xPlaceId] - Location place ID
342
+ * @param {string} [options.xGeoPlaceId] - Geographic place ID
343
+ * @param {boolean} [options.xForSuperFollowersOnly] - Exclusive for super followers
344
+ * @param {string} [options.xCommunityId] - Community ID
345
+ * @param {boolean} [options.xShareWithFollowers] - Share community post with followers
346
+ * @param {boolean} [options.xLongTextAsPost] - Post long text as single post
347
+ *
348
+ * Threads options:
349
+ * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post (vs thread)
350
+ *
351
+ * @returns {Promise<Object>} API response with request_id for async uploads
352
+ */
353
+ async upload(videoPathOrUrl, options) {
354
+ const form = new FormData();
355
+
356
+ // Handle video (file path or URL)
357
+ if (videoPathOrUrl.toLowerCase().startsWith('http://') || videoPathOrUrl.toLowerCase().startsWith('https://')) {
358
+ form.append('video', videoPathOrUrl);
359
+ } else {
360
+ if (!fs.existsSync(videoPathOrUrl)) {
361
+ throw new Error(`Video file not found: ${videoPathOrUrl}`);
362
+ }
363
+ form.append('video', createReadStream(videoPathOrUrl));
364
+ }
365
+
366
+ this._addCommonParams(form, options);
367
+
368
+ const platforms = Array.isArray(options.platforms) ? options.platforms : [options.platforms];
369
+ if (platforms.includes('tiktok')) this._addTiktokParams(form, options, true);
370
+ if (platforms.includes('instagram')) this._addInstagramParams(form, options, true);
371
+ if (platforms.includes('youtube')) this._addYoutubeParams(form, options);
372
+ if (platforms.includes('linkedin')) this._addLinkedinParams(form, options);
373
+ if (platforms.includes('facebook')) this._addFacebookParams(form, options, true);
374
+ if (platforms.includes('pinterest')) this._addPinterestParams(form, options, true);
375
+ if (platforms.includes('x')) this._addXParams(form, options, false);
376
+ if (platforms.includes('threads')) this._addThreadsParams(form, options);
377
+
378
+ return this._request('/upload', 'POST', form, true);
379
+ }
380
+
381
+ /**
382
+ * Upload photos to social media platforms
383
+ *
384
+ * @param {string[]} photosPathsOrUrls - Array of photo file paths or URLs
385
+ * @param {Object} options - Upload options
386
+ * @param {string} options.title - Post title/caption
387
+ * @param {string} options.user - User identifier (profile name)
388
+ * @param {string[]} options.platforms - Target platforms (tiktok, instagram, linkedin, facebook, x, threads, pinterest, reddit, bluesky)
389
+ * @param {string} [options.description] - Photo description
390
+ * @param {string} [options.firstComment] - First comment to post
391
+ * @param {string} [options.altText] - Alt text for accessibility
392
+ * @param {string} [options.scheduledDate] - ISO date for scheduling
393
+ * @param {string} [options.timezone] - Timezone for scheduled date
394
+ * @param {boolean} [options.addToQueue] - Add to posting queue
395
+ * @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
396
+ *
397
+ * TikTok options:
398
+ * @param {boolean} [options.tiktokAutoAddMusic] - Auto add music
399
+ * @param {boolean} [options.tiktokDisableComment] - Disable comments
400
+ * @param {number} [options.tiktokPhotoCoverIndex] - Index of photo for cover (0-based)
401
+ * @param {boolean} [options.brandContentToggle] - Branded content toggle
402
+ * @param {boolean} [options.brandOrganicToggle] - Brand organic toggle
403
+ *
404
+ * Instagram options:
405
+ * @param {string} [options.instagramMediaType] - IMAGE or STORIES
406
+ * @param {string} [options.instagramCollaborators] - Comma-separated collaborator usernames
407
+ * @param {string} [options.instagramUserTags] - Comma-separated user tags
408
+ * @param {string} [options.instagramLocationId] - Location ID
409
+ *
410
+ * LinkedIn options:
411
+ * @param {string} [options.linkedinVisibility] - PUBLIC (only PUBLIC supported for photos)
412
+ * @param {string} [options.targetLinkedinPageId] - Page ID for organization posts
413
+ *
414
+ * Facebook options:
415
+ * @param {string} [options.facebookPageId] - Facebook Page ID
416
+ *
417
+ * Pinterest options:
418
+ * @param {string} [options.pinterestBoardId] - Board ID
419
+ * @param {string} [options.pinterestAltText] - Alt text
420
+ * @param {string} [options.pinterestLink] - Destination link
421
+ *
422
+ * X (Twitter) options:
423
+ * @param {string} [options.xReplySettings] - Who can reply
424
+ * @param {boolean} [options.xNullcast] - Promoted-only post
425
+ * @param {string|string[]} [options.xTaggedUserIds] - User IDs to tag
426
+ * @param {boolean} [options.xLongTextAsPost] - Post long text as single post
427
+ *
428
+ * Threads options:
429
+ * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post
430
+ *
431
+ * Reddit options:
432
+ * @param {string} [options.redditSubreddit] - Subreddit name (without r/)
433
+ * @param {string} [options.redditFlairId] - Flair template ID
434
+ *
29
435
  * @returns {Promise<Object>} API response
30
436
  */
31
- async upload(videoPath, options) {
437
+ async uploadPhotos(photosPathsOrUrls, options) {
32
438
  const form = new FormData();
439
+
440
+ // Handle photos (file paths or URLs)
441
+ for (const photoItem of photosPathsOrUrls) {
442
+ if (typeof photoItem === 'string' && (photoItem.toLowerCase().startsWith('http://') || photoItem.toLowerCase().startsWith('https://'))) {
443
+ form.append('photos[]', photoItem);
444
+ } else {
445
+ if (!fs.existsSync(photoItem)) {
446
+ throw new Error(`Photo file not found: ${photoItem}`);
447
+ }
448
+ form.append('photos[]', createReadStream(photoItem));
449
+ }
450
+ }
451
+
452
+ this._addCommonParams(form, options);
33
453
 
34
- form.append('video', createReadStream(videoPath));
35
- form.append('title', options.title);
36
- form.append('user', options.user);
37
- options.platforms.forEach(platform => {
38
- form.append('platform[]', platform);
39
- });
454
+ const platforms = Array.isArray(options.platforms) ? options.platforms : [options.platforms];
455
+ if (platforms.includes('tiktok')) this._addTiktokParams(form, options, false);
456
+ if (platforms.includes('instagram')) this._addInstagramParams(form, options, false);
457
+ if (platforms.includes('linkedin')) this._addLinkedinParams(form, options);
458
+ if (platforms.includes('facebook')) this._addFacebookParams(form, options, false);
459
+ if (platforms.includes('pinterest')) this._addPinterestParams(form, options, false);
460
+ if (platforms.includes('x')) this._addXParams(form, options, false);
461
+ if (platforms.includes('threads')) this._addThreadsParams(form, options);
462
+ if (platforms.includes('reddit')) this._addRedditParams(form, options);
40
463
 
41
- try {
42
- const response = await axios.post(`${this.baseUrl}/upload`, form, {
43
- headers: {
44
- ...form.getHeaders(),
45
- 'Authorization': `Apikey ${this.apiKey}`
46
- },
47
- maxContentLength: Infinity,
48
- maxBodyLength: Infinity
49
- });
50
-
51
- if (response.status !== 200) {
52
- throw new Error(`API request failed with status ${response.status}`);
464
+ return this._request('/upload_photos', 'POST', form, true);
465
+ }
466
+
467
+ /**
468
+ * Upload text posts to social media platforms
469
+ *
470
+ * @param {Object} options - Upload options
471
+ * @param {string} options.title - Text content for the post
472
+ * @param {string} options.user - User identifier (profile name)
473
+ * @param {string[]} options.platforms - Target platforms (x, linkedin, facebook, threads, reddit, bluesky)
474
+ * @param {string} [options.firstComment] - First comment to post
475
+ * @param {string} [options.scheduledDate] - ISO date for scheduling
476
+ * @param {string} [options.timezone] - Timezone for scheduled date
477
+ * @param {boolean} [options.addToQueue] - Add to posting queue
478
+ * @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
479
+ *
480
+ * LinkedIn options:
481
+ * @param {string} [options.targetLinkedinPageId] - Page ID for organization posts
482
+ *
483
+ * Facebook options:
484
+ * @param {string} [options.facebookPageId] - Facebook Page ID
485
+ * @param {string} [options.facebookLinkUrl] - URL to attach as link preview
486
+ *
487
+ * X (Twitter) options:
488
+ * @param {string} [options.xReplySettings] - Who can reply
489
+ * @param {string} [options.xPostUrl] - URL to attach
490
+ * @param {string} [options.xQuoteTweetId] - Tweet ID to quote
491
+ * @param {string|string[]} [options.xPollOptions] - Poll options (2-4 options)
492
+ * @param {number} [options.xPollDuration] - Poll duration in minutes (5-10080)
493
+ * @param {string} [options.xPollReplySettings] - Who can reply to poll
494
+ * @param {string} [options.xCardUri] - Card URI for Twitter Cards
495
+ * @param {boolean} [options.xLongTextAsPost] - Post long text as single post
496
+ *
497
+ * Threads options:
498
+ * @param {boolean} [options.threadsLongTextAsPost] - Post long text as single post
499
+ *
500
+ * Reddit options:
501
+ * @param {string} [options.redditSubreddit] - Subreddit name (without r/)
502
+ * @param {string} [options.redditFlairId] - Flair template ID
503
+ *
504
+ * @returns {Promise<Object>} API response
505
+ */
506
+ async uploadText(options) {
507
+ const form = new FormData();
508
+
509
+ this._addCommonParams(form, options);
510
+
511
+ const platforms = Array.isArray(options.platforms) ? options.platforms : [options.platforms];
512
+ if (platforms.includes('linkedin')) this._addLinkedinParams(form, options);
513
+ if (platforms.includes('facebook')) this._addFacebookParams(form, options, false, true);
514
+ if (platforms.includes('x')) this._addXParams(form, options, true);
515
+ if (platforms.includes('threads')) this._addThreadsParams(form, options);
516
+ if (platforms.includes('reddit')) this._addRedditParams(form, options);
517
+
518
+ return this._request('/upload_text', 'POST', form, true);
519
+ }
520
+
521
+ /**
522
+ * Upload a document to LinkedIn (PDF, PPT, PPTX, DOC, DOCX)
523
+ *
524
+ * @param {string} documentPathOrUrl - Path to document file or document URL
525
+ * @param {Object} options - Upload options
526
+ * @param {string} options.title - Post title/caption
527
+ * @param {string} options.user - User identifier (profile name)
528
+ * @param {string} [options.description] - Document description/commentary
529
+ * @param {string} [options.linkedinVisibility] - PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
530
+ * @param {string} [options.targetLinkedinPageId] - Page ID for organization posts
531
+ * @param {string} [options.scheduledDate] - ISO date for scheduling
532
+ * @param {string} [options.timezone] - Timezone for scheduled date
533
+ * @param {boolean} [options.addToQueue] - Add to posting queue
534
+ * @param {boolean} [options.asyncUpload=true] - Process upload asynchronously
535
+ *
536
+ * @returns {Promise<Object>} API response
537
+ */
538
+ async uploadDocument(documentPathOrUrl, options) {
539
+ const form = new FormData();
540
+
541
+ // Handle document (file path or URL)
542
+ if (documentPathOrUrl.toLowerCase().startsWith('http://') || documentPathOrUrl.toLowerCase().startsWith('https://')) {
543
+ form.append('document', documentPathOrUrl);
544
+ } else {
545
+ if (!fs.existsSync(documentPathOrUrl)) {
546
+ throw new Error(`Document file not found: ${documentPathOrUrl}`);
53
547
  }
548
+ form.append('document', createReadStream(documentPathOrUrl));
549
+ }
54
550
 
55
- return response.data;
56
- } catch (error) {
57
- throw new Error(`Upload failed: ${error.message}`);
551
+ // Force linkedin platform
552
+ form.append('user', options.user);
553
+ form.append('title', options.title);
554
+ form.append('platform[]', 'linkedin');
555
+
556
+ if (options.description) form.append('description', options.description);
557
+ if (options.scheduledDate) form.append('scheduled_date', options.scheduledDate);
558
+ if (options.timezone) form.append('timezone', options.timezone);
559
+ if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
560
+ if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
561
+
562
+ this._addLinkedinParams(form, options);
563
+
564
+ return this._request('/upload_document', 'POST', form, true);
565
+ }
566
+
567
+ // ==================== Status & History ====================
568
+
569
+ /**
570
+ * Get the status of an async upload
571
+ *
572
+ * @param {string} requestId - The request_id from an async upload
573
+ * @returns {Promise<Object>} Upload status
574
+ */
575
+ async getStatus(requestId) {
576
+ return this._request('/uploadposts/status', 'GET', { request_id: requestId });
577
+ }
578
+
579
+ /**
580
+ * Get upload history
581
+ *
582
+ * @param {Object} [options] - Query options
583
+ * @param {number} [options.page=1] - Page number
584
+ * @param {number} [options.limit=20] - Items per page (20, 50, or 100)
585
+ * @returns {Promise<Object>} Upload history
586
+ */
587
+ async getHistory(options = {}) {
588
+ return this._request('/uploadposts/history', 'GET', {
589
+ page: options.page || 1,
590
+ limit: options.limit || 20
591
+ });
592
+ }
593
+
594
+ /**
595
+ * Get analytics for a profile
596
+ *
597
+ * @param {string} profileUsername - Profile username
598
+ * @param {Object} [options] - Query options
599
+ * @param {string[]} [options.platforms] - Filter by platforms (instagram, linkedin, facebook, x)
600
+ * @returns {Promise<Object>} Analytics data
601
+ */
602
+ async getAnalytics(profileUsername, options = {}) {
603
+ const params = {};
604
+ if (options.platforms && options.platforms.length > 0) {
605
+ params.platforms = options.platforms.join(',');
58
606
  }
607
+ return this._request(`/analytics/${encodeURIComponent(profileUsername)}`, 'GET', params);
608
+ }
609
+
610
+ // ==================== Scheduled Posts ====================
611
+
612
+ /**
613
+ * List scheduled posts
614
+ *
615
+ * @returns {Promise<Object>} List of scheduled posts
616
+ */
617
+ async listScheduled() {
618
+ return this._request('/uploadposts/schedule', 'GET');
619
+ }
620
+
621
+ /**
622
+ * Cancel a scheduled post
623
+ *
624
+ * @param {string} jobId - Scheduled job ID
625
+ * @returns {Promise<Object>} Cancellation result
626
+ */
627
+ async cancelScheduled(jobId) {
628
+ return this._request(`/uploadposts/schedule/${jobId}`, 'DELETE');
629
+ }
630
+
631
+ /**
632
+ * Edit a scheduled post
633
+ *
634
+ * @param {string} jobId - Scheduled job ID
635
+ * @param {Object} options - Edit options
636
+ * @param {string} [options.scheduledDate] - New scheduled date (ISO 8601)
637
+ * @param {string} [options.timezone] - New timezone
638
+ * @returns {Promise<Object>} Edit result
639
+ */
640
+ async editScheduled(jobId, options) {
641
+ const body = {};
642
+ if (options.scheduledDate) body.scheduled_date = options.scheduledDate;
643
+ if (options.timezone) body.timezone = options.timezone;
644
+ return this._request(`/uploadposts/schedule/${jobId}`, 'POST', body);
645
+ }
646
+
647
+ // ==================== User Management ====================
648
+
649
+ /**
650
+ * List all users/profiles
651
+ *
652
+ * @returns {Promise<Object>} List of users
653
+ */
654
+ async listUsers() {
655
+ return this._request('/uploadposts/users', 'GET');
656
+ }
657
+
658
+ /**
659
+ * Create a new user/profile
660
+ *
661
+ * @param {string} username - Profile name to create
662
+ * @returns {Promise<Object>} Created user
663
+ */
664
+ async createUser(username) {
665
+ return this._request('/uploadposts/users', 'POST', { username });
666
+ }
667
+
668
+ /**
669
+ * Delete a user/profile
670
+ *
671
+ * @param {string} username - Profile name to delete
672
+ * @returns {Promise<Object>} Deletion result
673
+ */
674
+ async deleteUser(username) {
675
+ return this._request('/uploadposts/users', 'DELETE', { username });
676
+ }
677
+
678
+ /**
679
+ * Generate a JWT for platform integration
680
+ * Used when integrating Upload-Post into your own platform
681
+ *
682
+ * @param {string} username - Profile username
683
+ * @param {Object} [options] - JWT options
684
+ * @param {string} [options.redirectUrl] - URL to redirect after linking
685
+ * @param {string} [options.logoImage] - Logo image URL for the linking page
686
+ * @param {string} [options.redirectButtonText] - Text for redirect button
687
+ * @param {string[]} [options.platforms] - Platforms to show for connection
688
+ * @returns {Promise<Object>} JWT and connection URL
689
+ */
690
+ async generateJwt(username, options = {}) {
691
+ const body = { username };
692
+ if (options.redirectUrl) body.redirect_url = options.redirectUrl;
693
+ if (options.logoImage) body.logo_image = options.logoImage;
694
+ if (options.redirectButtonText) body.redirect_button_text = options.redirectButtonText;
695
+ if (options.platforms && options.platforms.length > 0) body.platforms = options.platforms;
696
+ return this._request('/uploadposts/users/generate-jwt', 'POST', body);
697
+ }
698
+
699
+ /**
700
+ * Validate a JWT token
701
+ *
702
+ * @param {string} jwt - JWT token to validate
703
+ * @returns {Promise<Object>} Validation result
704
+ */
705
+ async validateJwt(jwt) {
706
+ return this._request('/uploadposts/users/validate-jwt', 'POST', { jwt });
707
+ }
708
+
709
+ // ==================== Helper Endpoints ====================
710
+
711
+ /**
712
+ * Get Facebook pages for a profile
713
+ *
714
+ * @param {string} [profile] - Profile username
715
+ * @returns {Promise<Object>} List of Facebook pages
716
+ */
717
+ async getFacebookPages(profile) {
718
+ const params = profile ? { profile } : {};
719
+ return this._request('/uploadposts/facebook/pages', 'GET', params);
720
+ }
721
+
722
+ /**
723
+ * Get LinkedIn pages for a profile
724
+ *
725
+ * @param {string} [profile] - Profile username
726
+ * @returns {Promise<Object>} List of LinkedIn pages
727
+ */
728
+ async getLinkedinPages(profile) {
729
+ const params = profile ? { profile } : {};
730
+ return this._request('/uploadposts/linkedin/pages', 'GET', params);
731
+ }
732
+
733
+ /**
734
+ * Get Pinterest boards for a profile
735
+ *
736
+ * @param {string} [profile] - Profile username
737
+ * @returns {Promise<Object>} List of Pinterest boards
738
+ */
739
+ async getPinterestBoards(profile) {
740
+ const params = profile ? { profile } : {};
741
+ return this._request('/uploadposts/pinterest/boards', 'GET', params);
59
742
  }
60
743
  }
744
+
745
+ // Default export for CommonJS compatibility
746
+ export default UploadPost;