upload-post 1.0.3 → 2.0.4

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