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/.github/workflows/publish.yml +45 -0
- package/README.md +285 -219
- package/index.d.ts +551 -139
- package/index.js +683 -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,730 @@ 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
|
+
}
|
|
172
194
|
|
|
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);
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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') {
|
|
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));
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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);
|
|
553
|
+
form.append('title', options.title);
|
|
554
|
+
form.append('platform[]', 'linkedin');
|
|
300
555
|
|
|
301
|
-
options.
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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;
|