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/.github/workflows/publish.yml +67 -0
- package/README.md +288 -219
- package/index.d.ts +561 -139
- package/index.js +698 -281
- package/package.json +28 -5
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 =
|
|
20
|
+
this.baseUrl = API_BASE_URL;
|
|
105
21
|
}
|
|
106
22
|
|
|
107
23
|
/**
|
|
108
|
-
*
|
|
109
|
-
* @
|
|
110
|
-
* @param {UploadOptions} options - Upload options
|
|
111
|
-
* @returns {Promise<Object>} API response
|
|
24
|
+
* Make an API request
|
|
25
|
+
* @private
|
|
112
26
|
*/
|
|
113
|
-
async
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (options.
|
|
129
|
-
if (options.
|
|
130
|
-
if (options.
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 {
|
|
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
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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));
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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);
|
|
560
|
+
form.append('title', options.title);
|
|
561
|
+
form.append('platform[]', 'linkedin');
|
|
300
562
|
|
|
301
|
-
options.
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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;
|