upload-post 1.0.3 → 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,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,730 @@ 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
+ }
172
194
 
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);
195
+ if (isText && options.facebookLinkUrl) {
196
+ form.append('facebook_link_url', options.facebookLinkUrl);
184
197
  }
198
+ }
185
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
+ }
186
216
 
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}`);
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);
199
248
  }
249
+ }
250
+ }
200
251
 
201
- return response.data;
202
- } catch (error) {
203
- throw new Error(`Upload failed: ${error.message}`);
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));
204
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);
205
379
  }
206
380
 
207
381
  /**
208
382
  * Upload photos to social media platforms
383
+ *
209
384
  * @param {string[]} photosPathsOrUrls - Array of photo file paths or URLs
210
- * @param {UploadPhotosOptions} options - Upload photos options
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
+ *
211
435
  * @returns {Promise<Object>} API response
212
436
  */
213
437
  async uploadPhotos(photosPathsOrUrls, options) {
214
438
  const form = new FormData();
215
439
 
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') {
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 {
230
445
  if (!fs.existsSync(photoItem)) {
231
446
  throw new Error(`Photo file not found: ${photoItem}`);
232
447
  }
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.`);
448
+ form.append('photos[]', createReadStream(photoItem));
236
449
  }
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
450
  }
451
+
452
+ this._addCommonParams(form, options);
453
+
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);
463
+
464
+ return this._request('/upload_photos', 'POST', form, true);
287
465
  }
288
466
 
289
467
  /**
290
468
  * Upload text posts to social media platforms
291
- * @param {UploadTextOptions} options - Upload text options
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
+ *
292
504
  * @returns {Promise<Object>} API response
293
505
  */
294
506
  async uploadText(options) {
295
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();
296
540
 
297
- // Common parameters
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}`);
547
+ }
548
+ form.append('document', createReadStream(documentPathOrUrl));
549
+ }
550
+
551
+ // Force linkedin platform
298
552
  form.append('user', options.user);
299
- form.append('title', options.title); // 'title' is used as the text content field for all supported platforms
553
+ form.append('title', options.title);
554
+ form.append('platform[]', 'linkedin');
300
555
 
301
- options.platforms.forEach(platform => {
302
- form.append('platform[]', platform);
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
303
591
  });
592
+ }
304
593
 
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);
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(',');
309
606
  }
607
+ return this._request(`/analytics/${encodeURIComponent(profileUsername)}`, 'GET', params);
608
+ }
310
609
 
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.
610
+ // ==================== Scheduled Posts ====================
316
611
 
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
- }
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
+ }
328
620
 
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
- }
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);
343
742
  }
344
743
  }
744
+
745
+ // Default export for CommonJS compatibility
746
+ export default UploadPost;