upload-post 2.2.0 → 2.5.0
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 +19 -8
- package/README.md +66 -4
- package/index.d.ts +95 -8
- package/index.js +99 -18
- package/package.json +1 -1
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
permissions:
|
|
21
21
|
contents: read
|
|
22
|
-
id-token: write
|
|
22
|
+
id-token: write # mints the OIDC token npm trades for publish rights
|
|
23
23
|
steps:
|
|
24
24
|
- name: Checkout
|
|
25
25
|
uses: actions/checkout@v4
|
|
@@ -27,9 +27,22 @@ jobs:
|
|
|
27
27
|
- name: Setup Node.js
|
|
28
28
|
uses: actions/setup-node@v4
|
|
29
29
|
with:
|
|
30
|
-
node-version: '
|
|
30
|
+
node-version: '24'
|
|
31
31
|
registry-url: 'https://registry.npmjs.org'
|
|
32
32
|
|
|
33
|
+
# Trusted publishing needs npm >= 11.5.1, which Node 24 already bundles.
|
|
34
|
+
# Do NOT `npm install -g npm@latest`: it leaves npm's own tree short of
|
|
35
|
+
# `sigstore`, and `--provenance` then dies on it.
|
|
36
|
+
- name: Check npm supports trusted publishing
|
|
37
|
+
run: |
|
|
38
|
+
NPM_VERSION=$(npm --version)
|
|
39
|
+
echo "npm $NPM_VERSION"
|
|
40
|
+
MIN=11.5.1
|
|
41
|
+
if [ "$(printf '%s\n%s\n' "$MIN" "$NPM_VERSION" | sort -V | head -1)" != "$MIN" ]; then
|
|
42
|
+
echo "::error::npm $NPM_VERSION is older than $MIN, which trusted publishing requires"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
33
46
|
- name: Check if version is new
|
|
34
47
|
id: version_check
|
|
35
48
|
run: |
|
|
@@ -54,14 +67,12 @@ jobs:
|
|
|
54
67
|
if: steps.version_check.outputs.new == 'true' || github.event_name == 'workflow_dispatch'
|
|
55
68
|
run: npm run build --if-present
|
|
56
69
|
|
|
70
|
+
# No NODE_AUTH_TOKEN: npm authenticates via the OIDC token, and the
|
|
71
|
+
# trusted publisher is configured on npmjs.com against this repo+workflow.
|
|
57
72
|
- name: Publish to npm (dry-run)
|
|
58
73
|
if: ${{ inputs.dry_run == true }}
|
|
59
|
-
run: npm publish --access public --dry-run
|
|
60
|
-
env:
|
|
61
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
74
|
+
run: npm publish --provenance --access public --dry-run
|
|
62
75
|
|
|
63
76
|
- name: Publish to npm
|
|
64
77
|
if: (steps.version_check.outputs.new == 'true' && inputs.dry_run != true) || github.event_name == 'release'
|
|
65
|
-
run: npm publish --access public
|
|
66
|
-
env:
|
|
67
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
78
|
+
run: npm publish --provenance --access public
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official Node.js client for the [Upload-Post API](https://www.upload-post.com) - Cross-platform social media upload.
|
|
4
4
|
|
|
5
|
-
Upload videos, photos, text posts, and documents to **TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky,
|
|
5
|
+
Upload videos, photos, text posts, and documents to **TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X (Twitter), Discord, and Telegram** with a single API.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -29,9 +29,9 @@ console.log(response);
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
- ✅ **Video Upload** - TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Bluesky, X
|
|
33
|
-
- ✅ **Photo Upload** - TikTok, Instagram, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X
|
|
34
|
-
- ✅ **Text Posts** - X, LinkedIn, Facebook, Threads, Reddit, Bluesky
|
|
32
|
+
- ✅ **Video Upload** - TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Bluesky, X, Discord, Telegram
|
|
33
|
+
- ✅ **Photo Upload** - TikTok, Instagram, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, X, Discord, Telegram
|
|
34
|
+
- ✅ **Text Posts** - X, LinkedIn, Facebook, Threads, Reddit, Bluesky, Discord, Telegram
|
|
35
35
|
- ✅ **Document Upload** - LinkedIn (PDF, PPT, PPTX, DOC, DOCX)
|
|
36
36
|
- ✅ **Scheduling** - Schedule posts for later
|
|
37
37
|
- ✅ **Posting Queue** - Add posts to your configured queue
|
|
@@ -169,6 +169,10 @@ await client.deleteUser('old-profile');
|
|
|
169
169
|
const jwt = await client.generateJwt('my-profile', {
|
|
170
170
|
redirectUrl: 'https://yourapp.com/callback',
|
|
171
171
|
platforms: ['tiktok', 'instagram'],
|
|
172
|
+
// Optional: force the connection page language for this profile.
|
|
173
|
+
// Supported: 'en' | 'es' | 'de' | 'fr' | 'pt'. When omitted, the page
|
|
174
|
+
// auto-detects the visitor's browser language and falls back to English.
|
|
175
|
+
language: 'es',
|
|
172
176
|
});
|
|
173
177
|
```
|
|
174
178
|
|
|
@@ -312,6 +316,64 @@ These options work across all upload methods:
|
|
|
312
316
|
| `addToQueue` | Add to posting queue |
|
|
313
317
|
| `maxPostsPerSlot` | Max posts per queue slot (overrides profile setting) |
|
|
314
318
|
| `asyncUpload` | Process asynchronously (default: true) |
|
|
319
|
+
| `autogenerate` | If true, AI generates native per-platform title/description from the media and fills any field left empty |
|
|
320
|
+
| `autogenerateTitle` / `autogenerateDescription` | Generate only the title or only the description with AI |
|
|
321
|
+
| `autogenerateLanguage` | Force the AI output language (ISO code); omit to auto-detect from the media |
|
|
322
|
+
| `idempotencyKey` | Collapses duplicate uploads within 24h. Reuse the same value when retrying. Alias: `requestId` |
|
|
323
|
+
|
|
324
|
+
## Google Business Profile
|
|
325
|
+
|
|
326
|
+
Pass the target location on the upload itself. There is no separate "select a location" call — the API resolves the location per post.
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
const { locations } = await client.getGoogleBusinessLocations('myprofile');
|
|
330
|
+
|
|
331
|
+
await client.upload('video.mp4', {
|
|
332
|
+
user: 'myprofile',
|
|
333
|
+
platforms: ['google_business'],
|
|
334
|
+
title: 'Now open on Sundays',
|
|
335
|
+
gbpLocationId: locations[0].name, // "accounts/123/locations/456"
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`gbpLocationId` is **required when the account has more than one location** — the API only auto-selects when exactly one exists. Beyond a standard post you can publish an event or an offer:
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
await client.uploadText({
|
|
343
|
+
user: 'myprofile',
|
|
344
|
+
platforms: ['google_business'],
|
|
345
|
+
title: 'Summer sale',
|
|
346
|
+
gbpLocationId: locations[0].name,
|
|
347
|
+
gbpTopicType: 'OFFER',
|
|
348
|
+
gbpOfferCoupon: 'SUMMER25',
|
|
349
|
+
gbpOfferRedeemUrl: 'https://example.com/redeem',
|
|
350
|
+
gbpOfferTerms: 'One per customer',
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Also available: `gbpTopicType: 'EVENT'` with `gbpEventTitle` / `gbpEventStartDate` / `gbpEventStartTime` / `gbpEventEndDate` / `gbpEventEndTime`, a call-to-action via `gbpCtaType` + `gbpCtaUrl`, and `gbpMediaUrl` / `gbpMediaFormat`.
|
|
355
|
+
|
|
356
|
+
## Retrying an upload safely
|
|
357
|
+
|
|
358
|
+
An upload that times out may still have been accepted by the API. Retrying it without an idempotency key publishes the post a second time.
|
|
359
|
+
|
|
360
|
+
Pass the same `idempotencyKey` on every attempt and the API returns the original job instead of creating a new one:
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
import { randomUUID } from 'crypto';
|
|
364
|
+
|
|
365
|
+
const idempotencyKey = randomUUID(); // generate ONCE, outside the retry loop
|
|
366
|
+
|
|
367
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
368
|
+
try {
|
|
369
|
+
return await client.upload('video.mp4', { user, platforms: ['tiktok'], title, idempotencyKey });
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (attempt === 2) throw err;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Generating the key inside the loop defeats the mechanism: each attempt would look like a new upload.
|
|
315
377
|
|
|
316
378
|
## TypeScript Support
|
|
317
379
|
|
package/index.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ declare module 'upload-post' {
|
|
|
2
2
|
// ==================== Common Types ====================
|
|
3
3
|
|
|
4
4
|
/** Supported platforms for video upload */
|
|
5
|
-
export type VideoPlatform = 'tiktok' | 'instagram' | 'youtube' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'bluesky' | 'x' | 'google_business';
|
|
5
|
+
export type VideoPlatform = 'tiktok' | 'instagram' | 'youtube' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x' | 'google_business' | 'discord' | 'telegram';
|
|
6
6
|
|
|
7
7
|
/** Supported platforms for photo upload */
|
|
8
|
-
export type PhotoPlatform = 'tiktok' | 'instagram' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x' | 'google_business';
|
|
8
|
+
export type PhotoPlatform = 'tiktok' | 'instagram' | 'linkedin' | 'facebook' | 'pinterest' | 'threads' | 'reddit' | 'bluesky' | 'x' | 'google_business' | 'discord' | 'telegram';
|
|
9
9
|
|
|
10
10
|
/** Supported platforms for text upload */
|
|
11
|
-
export type TextPlatform = 'x' | 'linkedin' | 'facebook' | 'threads' | 'reddit' | 'bluesky' | 'google_business';
|
|
11
|
+
export type TextPlatform = 'x' | 'linkedin' | 'facebook' | 'threads' | 'reddit' | 'bluesky' | 'google_business' | 'discord' | 'telegram';
|
|
12
12
|
|
|
13
13
|
/** TikTok privacy levels */
|
|
14
14
|
export type TikTokPrivacyLevel = 'PUBLIC_TO_EVERYONE' | 'MUTUAL_FOLLOW_FRIENDS' | 'FOLLOWER_OF_CREATOR' | 'SELF_ONLY';
|
|
@@ -45,6 +45,13 @@ declare module 'upload-post' {
|
|
|
45
45
|
export interface CommonUploadOptions {
|
|
46
46
|
/** Post title/caption. Required for YouTube, Reddit, and text posts. Optional for TikTok, Instagram, Facebook, LinkedIn, X, Threads, Bluesky, Pinterest. */
|
|
47
47
|
title?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Key the API uses to collapse duplicate uploads within a 24-hour window.
|
|
50
|
+
* Pass the same value when retrying a failed upload, otherwise the retry publishes a second post.
|
|
51
|
+
*/
|
|
52
|
+
idempotencyKey?: string;
|
|
53
|
+
/** Alias for {@link CommonUploadOptions.idempotencyKey}. */
|
|
54
|
+
requestId?: string;
|
|
48
55
|
/** User identifier (profile name) */
|
|
49
56
|
user: string;
|
|
50
57
|
/** First comment to post after publishing */
|
|
@@ -61,6 +68,14 @@ declare module 'upload-post' {
|
|
|
61
68
|
maxPostsPerSlot?: number;
|
|
62
69
|
/** Process upload asynchronously (default: true) */
|
|
63
70
|
asyncUpload?: boolean;
|
|
71
|
+
/** If true, AI generates native per-platform title/description from the media and fills any field left empty */
|
|
72
|
+
autogenerate?: boolean;
|
|
73
|
+
/** Generate only the title with AI */
|
|
74
|
+
autogenerateTitle?: boolean;
|
|
75
|
+
/** Generate only the description with AI */
|
|
76
|
+
autogenerateDescription?: boolean;
|
|
77
|
+
/** Force the AI output language (ISO code); omit to auto-detect from the media */
|
|
78
|
+
autogenerateLanguage?: string;
|
|
64
79
|
|
|
65
80
|
// Platform-specific title overrides
|
|
66
81
|
blueskyTitle?: string;
|
|
@@ -192,6 +207,8 @@ declare module 'upload-post' {
|
|
|
192
207
|
youtubeHasPaidProductPlacement?: boolean;
|
|
193
208
|
/** Recording date (ISO 8601) */
|
|
194
209
|
youtubeRecordingDate?: string;
|
|
210
|
+
/** Playlist ID(s) to add the uploaded video to — one ID, an array, or a comma-separated list */
|
|
211
|
+
youtubePlaylistId?: string | string[];
|
|
195
212
|
/** Subtitle/caption files to upload. Each entry needs a language code and either a file path or URL. */
|
|
196
213
|
youtubeSubtitles?: Array<{
|
|
197
214
|
/** BCP-47 language code (e.g. "en", "es") */
|
|
@@ -317,6 +334,8 @@ declare module 'upload-post' {
|
|
|
317
334
|
threadsLongTextAsPost?: boolean;
|
|
318
335
|
/** Comma-separated list of how many media items per Threads post (e.g. "5,5"). Each value 1-10, total must equal file count. */
|
|
319
336
|
threadsThreadMediaLayout?: string;
|
|
337
|
+
/** Topic tag for the Threads post */
|
|
338
|
+
threadsTopicTag?: string;
|
|
320
339
|
}
|
|
321
340
|
|
|
322
341
|
// ==================== Reddit Options ====================
|
|
@@ -330,6 +349,37 @@ declare module 'upload-post' {
|
|
|
330
349
|
|
|
331
350
|
// ==================== Combined Upload Options ====================
|
|
332
351
|
|
|
352
|
+
export interface GoogleBusinessOptions {
|
|
353
|
+
/** Google Business location id, e.g. "accounts/123/locations/456". Required when the account has more than one location; the API auto-selects only for single-location accounts. List them with getGoogleBusinessLocations(). */
|
|
354
|
+
gbpLocationId?: string;
|
|
355
|
+
/** Post type. Defaults to STANDARD. */
|
|
356
|
+
gbpTopicType?: 'STANDARD' | 'EVENT' | 'OFFER';
|
|
357
|
+
/** Media URL attached to the post */
|
|
358
|
+
gbpMediaUrl?: string;
|
|
359
|
+
/** Media format */
|
|
360
|
+
gbpMediaFormat?: 'PHOTO' | 'VIDEO';
|
|
361
|
+
/** Call-to-action button */
|
|
362
|
+
gbpCtaType?: 'BOOK' | 'ORDER' | 'SHOP' | 'LEARN_MORE' | 'SIGN_UP' | 'CALL';
|
|
363
|
+
/** URL the call-to-action button opens */
|
|
364
|
+
gbpCtaUrl?: string;
|
|
365
|
+
/** Event title. Used when gbpTopicType is EVENT. */
|
|
366
|
+
gbpEventTitle?: string;
|
|
367
|
+
/** Event start date, YYYY-MM-DD */
|
|
368
|
+
gbpEventStartDate?: string;
|
|
369
|
+
/** Event start time, HH:MM */
|
|
370
|
+
gbpEventStartTime?: string;
|
|
371
|
+
/** Event end date, YYYY-MM-DD */
|
|
372
|
+
gbpEventEndDate?: string;
|
|
373
|
+
/** Event end time, HH:MM */
|
|
374
|
+
gbpEventEndTime?: string;
|
|
375
|
+
/** Offer coupon code. Used when gbpTopicType is OFFER. */
|
|
376
|
+
gbpOfferCoupon?: string;
|
|
377
|
+
/** URL to redeem the offer */
|
|
378
|
+
gbpOfferRedeemUrl?: string;
|
|
379
|
+
/** Offer terms and conditions */
|
|
380
|
+
gbpOfferTerms?: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
333
383
|
export interface UploadVideoOptions extends CommonUploadOptions,
|
|
334
384
|
TikTokVideoOptions,
|
|
335
385
|
InstagramVideoOptions,
|
|
@@ -338,7 +388,9 @@ declare module 'upload-post' {
|
|
|
338
388
|
FacebookVideoOptions,
|
|
339
389
|
PinterestVideoOptions,
|
|
340
390
|
XMediaOptions,
|
|
341
|
-
ThreadsOptions
|
|
391
|
+
ThreadsOptions,
|
|
392
|
+
RedditOptions,
|
|
393
|
+
GoogleBusinessOptions {
|
|
342
394
|
/** Target platforms */
|
|
343
395
|
platforms: VideoPlatform[];
|
|
344
396
|
}
|
|
@@ -351,7 +403,8 @@ declare module 'upload-post' {
|
|
|
351
403
|
PinterestPhotoOptions,
|
|
352
404
|
XMediaOptions,
|
|
353
405
|
ThreadsOptions,
|
|
354
|
-
RedditOptions
|
|
406
|
+
RedditOptions,
|
|
407
|
+
GoogleBusinessOptions {
|
|
355
408
|
/** Target platforms */
|
|
356
409
|
platforms: PhotoPlatform[];
|
|
357
410
|
}
|
|
@@ -371,7 +424,8 @@ declare module 'upload-post' {
|
|
|
371
424
|
XTextOptions,
|
|
372
425
|
ThreadsOptions,
|
|
373
426
|
RedditOptions,
|
|
374
|
-
LinkPreviewOptions
|
|
427
|
+
LinkPreviewOptions,
|
|
428
|
+
GoogleBusinessOptions {
|
|
375
429
|
/** Target platforms */
|
|
376
430
|
platforms: TextPlatform[];
|
|
377
431
|
}
|
|
@@ -482,7 +536,7 @@ declare module 'upload-post' {
|
|
|
482
536
|
* Upload-Post API client
|
|
483
537
|
*
|
|
484
538
|
* Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
|
|
485
|
-
* Pinterest, Threads, Reddit, Bluesky, X (Twitter)
|
|
539
|
+
* Pinterest, Threads, Reddit, Bluesky, X (Twitter), Discord, Telegram
|
|
486
540
|
*/
|
|
487
541
|
export class UploadPost {
|
|
488
542
|
/**
|
|
@@ -673,6 +727,7 @@ declare module 'upload-post' {
|
|
|
673
727
|
readonlyCalendar?: boolean;
|
|
674
728
|
connectTitle?: string;
|
|
675
729
|
connectDescription?: string;
|
|
730
|
+
language?: 'en' | 'es' | 'de' | 'fr' | 'pt';
|
|
676
731
|
}): Promise<JwtResponse>;
|
|
677
732
|
|
|
678
733
|
/**
|
|
@@ -701,7 +756,19 @@ declare module 'upload-post' {
|
|
|
701
756
|
* Update notification configuration (including webhook settings)
|
|
702
757
|
* @param options - Notification config options
|
|
703
758
|
*/
|
|
704
|
-
updateNotificationConfig(options?: {
|
|
759
|
+
updateNotificationConfig(options?: {
|
|
760
|
+
webhookEvents?: string[];
|
|
761
|
+
webhookUrl?: string;
|
|
762
|
+
channels?: string[];
|
|
763
|
+
telegramChatId?: string;
|
|
764
|
+
slackWebhookUrl?: string;
|
|
765
|
+
whatsappToPhone?: string;
|
|
766
|
+
}): Promise<{ success: boolean; config?: { webhook_events?: string[]; webhook_url?: string; [key: string]: any } }>;
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Send a test notification through the configured channels
|
|
770
|
+
*/
|
|
771
|
+
testNotifications(): Promise<{ success: boolean; [key: string]: any }>;
|
|
705
772
|
|
|
706
773
|
// Instagram Comments
|
|
707
774
|
|
|
@@ -761,6 +828,26 @@ declare module 'upload-post' {
|
|
|
761
828
|
* @param profile - Profile username
|
|
762
829
|
*/
|
|
763
830
|
getPinterestBoards(profile?: string): Promise<BoardsResponse>;
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Get analytics for a post published outside Upload-Post, by its native platform ID
|
|
834
|
+
* @param platformPostId - The post ID assigned by the platform
|
|
835
|
+
* @param platform - Platform the post lives on
|
|
836
|
+
* @param user - Profile username that owns the connected account
|
|
837
|
+
*/
|
|
838
|
+
getPostAnalyticsByPlatformId(platformPostId: string, platform: string, user: string): Promise<Record<string, unknown>>;
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Get detailed Reddit posts with full media information
|
|
842
|
+
* @param profileUsername - Profile username
|
|
843
|
+
*/
|
|
844
|
+
getRedditDetailedPosts(profileUsername: string): Promise<Record<string, unknown>>;
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Get available Google Business locations for a profile
|
|
848
|
+
* @param profile - Profile username
|
|
849
|
+
*/
|
|
850
|
+
getGoogleBusinessLocations(profile?: string): Promise<Record<string, unknown>>;
|
|
764
851
|
}
|
|
765
852
|
|
|
766
853
|
export default UploadPost;
|
package/index.js
CHANGED
|
@@ -8,8 +8,8 @@ const API_BASE_URL = 'https://api.upload-post.com/api';
|
|
|
8
8
|
/**
|
|
9
9
|
* Upload-Post API client
|
|
10
10
|
*
|
|
11
|
-
* Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
|
|
12
|
-
* Pinterest, Threads, Reddit, Bluesky, X (Twitter)
|
|
11
|
+
* Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
|
|
12
|
+
* Pinterest, Threads, Reddit, Bluesky, X (Twitter), Discord, Telegram
|
|
13
13
|
*/
|
|
14
14
|
export class UploadPost {
|
|
15
15
|
/**
|
|
@@ -24,13 +24,14 @@ export class UploadPost {
|
|
|
24
24
|
* Make an API request
|
|
25
25
|
* @private
|
|
26
26
|
*/
|
|
27
|
-
async _request(endpoint, method = 'GET', data = null, isFormData = false) {
|
|
27
|
+
async _request(endpoint, method = 'GET', data = null, isFormData = false, extraHeaders = {}) {
|
|
28
28
|
const config = {
|
|
29
29
|
method,
|
|
30
30
|
url: `${this.baseUrl}${endpoint}`,
|
|
31
31
|
headers: {
|
|
32
32
|
'Authorization': `Apikey ${this.apiKey}`,
|
|
33
33
|
'X-Upload-Post-Source': 'npm',
|
|
34
|
+
...extraHeaders,
|
|
34
35
|
},
|
|
35
36
|
maxContentLength: Infinity,
|
|
36
37
|
maxBodyLength: Infinity
|
|
@@ -57,6 +58,16 @@ export class UploadPost {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The API collapses two uploads carrying the same key within 24 hours into a
|
|
63
|
+
* single post. Retrying an upload without one publishes it twice.
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
_idempotencyHeaders(options) {
|
|
67
|
+
const key = options.idempotencyKey || options.requestId;
|
|
68
|
+
return key ? { 'Idempotency-Key': String(key) } : {};
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/**
|
|
61
72
|
* Add common upload parameters to form
|
|
62
73
|
* @private
|
|
@@ -77,6 +88,11 @@ export class UploadPost {
|
|
|
77
88
|
if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
|
|
78
89
|
if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
|
|
79
90
|
if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
|
|
91
|
+
// AI auto-generation of native per-platform copy from the media (fills blank fields)
|
|
92
|
+
if (options.autogenerate !== undefined) form.append('autogenerate', String(options.autogenerate));
|
|
93
|
+
if (options.autogenerateTitle !== undefined) form.append('autogenerate_title', String(options.autogenerateTitle));
|
|
94
|
+
if (options.autogenerateDescription !== undefined) form.append('autogenerate_description', String(options.autogenerateDescription));
|
|
95
|
+
if (options.autogenerateLanguage) form.append('autogenerate_language', String(options.autogenerateLanguage));
|
|
80
96
|
|
|
81
97
|
// Platform-specific title overrides
|
|
82
98
|
if (options.blueskyTitle) form.append('bluesky_title', options.blueskyTitle);
|
|
@@ -198,6 +214,12 @@ export class UploadPost {
|
|
|
198
214
|
if (options.youtubeBlockedCountries) form.append('blockedCountries', options.youtubeBlockedCountries);
|
|
199
215
|
if (options.youtubeHasPaidProductPlacement !== undefined) form.append('hasPaidProductPlacement', String(options.youtubeHasPaidProductPlacement));
|
|
200
216
|
if (options.youtubeRecordingDate) form.append('recordingDate', options.youtubeRecordingDate);
|
|
217
|
+
if (options.youtubePlaylistId) {
|
|
218
|
+
const playlistIds = Array.isArray(options.youtubePlaylistId)
|
|
219
|
+
? options.youtubePlaylistId.join(',')
|
|
220
|
+
: options.youtubePlaylistId;
|
|
221
|
+
form.append('youtube_playlist_id', playlistIds);
|
|
222
|
+
}
|
|
201
223
|
if (options.youtubeSubtitles && Array.isArray(options.youtubeSubtitles)) {
|
|
202
224
|
options.youtubeSubtitles.forEach((sub, idx) => {
|
|
203
225
|
if (sub.language) {
|
|
@@ -311,6 +333,30 @@ export class UploadPost {
|
|
|
311
333
|
if (options.threadsTopicTag) form.append('threads_topic_tag', options.threadsTopicTag);
|
|
312
334
|
}
|
|
313
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Add Google Business Profile parameters
|
|
338
|
+
*
|
|
339
|
+
* gbpLocationId is required for accounts with more than one location; the API
|
|
340
|
+
* only auto-selects when exactly one location exists.
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
_addGoogleBusinessParams(form, options) {
|
|
344
|
+
if (options.gbpLocationId) form.append('gbp_location_id', options.gbpLocationId);
|
|
345
|
+
if (options.gbpTopicType) form.append('gbp_topic_type', String(options.gbpTopicType).toUpperCase());
|
|
346
|
+
if (options.gbpMediaUrl) form.append('gbp_media_url', options.gbpMediaUrl);
|
|
347
|
+
if (options.gbpMediaFormat) form.append('gbp_media_format', String(options.gbpMediaFormat).toUpperCase());
|
|
348
|
+
if (options.gbpCtaType) form.append('gbp_cta_type', String(options.gbpCtaType).toUpperCase());
|
|
349
|
+
if (options.gbpCtaUrl) form.append('gbp_cta_url', options.gbpCtaUrl);
|
|
350
|
+
if (options.gbpEventTitle) form.append('gbp_event_title', options.gbpEventTitle);
|
|
351
|
+
if (options.gbpEventStartDate) form.append('gbp_event_start_date', options.gbpEventStartDate);
|
|
352
|
+
if (options.gbpEventStartTime) form.append('gbp_event_start_time', options.gbpEventStartTime);
|
|
353
|
+
if (options.gbpEventEndDate) form.append('gbp_event_end_date', options.gbpEventEndDate);
|
|
354
|
+
if (options.gbpEventEndTime) form.append('gbp_event_end_time', options.gbpEventEndTime);
|
|
355
|
+
if (options.gbpOfferCoupon) form.append('gbp_offer_coupon', options.gbpOfferCoupon);
|
|
356
|
+
if (options.gbpOfferRedeemUrl) form.append('gbp_offer_redeem_url', options.gbpOfferRedeemUrl);
|
|
357
|
+
if (options.gbpOfferTerms) form.append('gbp_offer_terms', options.gbpOfferTerms);
|
|
358
|
+
}
|
|
359
|
+
|
|
314
360
|
/**
|
|
315
361
|
* Add Reddit-specific parameters
|
|
316
362
|
* @private
|
|
@@ -330,7 +376,7 @@ export class UploadPost {
|
|
|
330
376
|
* @param {Object} options - Upload options
|
|
331
377
|
* @param {string} options.title - Video title/caption
|
|
332
378
|
* @param {string} options.user - User identifier (profile name)
|
|
333
|
-
* @param {string[]} options.platforms - Target platforms (tiktok, instagram, youtube, linkedin, facebook, pinterest, threads, bluesky, x)
|
|
379
|
+
* @param {string[]} options.platforms - Target platforms (tiktok, instagram, youtube, linkedin, facebook, pinterest, threads, bluesky, x, discord, telegram)
|
|
334
380
|
* @param {string} [options.description] - Video description
|
|
335
381
|
* @param {string} [options.firstComment] - First comment to post
|
|
336
382
|
* @param {string} [options.scheduledDate] - ISO date for scheduling (e.g., "2024-12-25T10:00:00Z")
|
|
@@ -434,8 +480,10 @@ export class UploadPost {
|
|
|
434
480
|
if (platforms.includes('pinterest')) this._addPinterestParams(form, options, true);
|
|
435
481
|
if (platforms.includes('x')) this._addXParams(form, options, false);
|
|
436
482
|
if (platforms.includes('threads')) this._addThreadsParams(form, options);
|
|
483
|
+
if (platforms.includes('reddit')) this._addRedditParams(form, options);
|
|
484
|
+
if (platforms.includes('google_business')) this._addGoogleBusinessParams(form, options);
|
|
437
485
|
|
|
438
|
-
return this._request('/upload', 'POST', form, true);
|
|
486
|
+
return this._request('/upload', 'POST', form, true, this._idempotencyHeaders(options));
|
|
439
487
|
}
|
|
440
488
|
|
|
441
489
|
/**
|
|
@@ -445,7 +493,7 @@ export class UploadPost {
|
|
|
445
493
|
* @param {Object} options - Upload options
|
|
446
494
|
* @param {string} options.title - Post title/caption
|
|
447
495
|
* @param {string} options.user - User identifier (profile name)
|
|
448
|
-
* @param {string[]} options.platforms - Target platforms (tiktok, instagram, linkedin, facebook, x, threads, pinterest, reddit, bluesky)
|
|
496
|
+
* @param {string[]} options.platforms - Target platforms (tiktok, instagram, linkedin, facebook, x, threads, pinterest, reddit, bluesky, discord, telegram)
|
|
449
497
|
* @param {string} [options.description] - Photo description
|
|
450
498
|
* @param {string} [options.firstComment] - First comment to post
|
|
451
499
|
* @param {string} [options.altText] - Alt text for accessibility
|
|
@@ -522,8 +570,9 @@ export class UploadPost {
|
|
|
522
570
|
if (platforms.includes('x')) this._addXParams(form, options, false);
|
|
523
571
|
if (platforms.includes('threads')) this._addThreadsParams(form, options);
|
|
524
572
|
if (platforms.includes('reddit')) this._addRedditParams(form, options);
|
|
573
|
+
if (platforms.includes('google_business')) this._addGoogleBusinessParams(form, options);
|
|
525
574
|
|
|
526
|
-
return this._request('/upload_photos', 'POST', form, true);
|
|
575
|
+
return this._request('/upload_photos', 'POST', form, true, this._idempotencyHeaders(options));
|
|
527
576
|
}
|
|
528
577
|
|
|
529
578
|
/**
|
|
@@ -532,7 +581,7 @@ export class UploadPost {
|
|
|
532
581
|
* @param {Object} options - Upload options
|
|
533
582
|
* @param {string} options.title - Text content for the post
|
|
534
583
|
* @param {string} options.user - User identifier (profile name)
|
|
535
|
-
* @param {string[]} options.platforms - Target platforms (x, linkedin, facebook, threads, reddit, bluesky)
|
|
584
|
+
* @param {string[]} options.platforms - Target platforms (x, linkedin, facebook, threads, reddit, bluesky, discord, telegram)
|
|
536
585
|
* @param {string} [options.firstComment] - First comment to post
|
|
537
586
|
* @param {string} [options.scheduledDate] - ISO date for scheduling
|
|
538
587
|
* @param {string} [options.timezone] - Timezone for scheduled date
|
|
@@ -589,8 +638,9 @@ export class UploadPost {
|
|
|
589
638
|
if (platforms.includes('bluesky') && (options.blueskyLinkUrl || options.linkUrl)) {
|
|
590
639
|
form.append('bluesky_link_url', options.blueskyLinkUrl || options.linkUrl);
|
|
591
640
|
}
|
|
641
|
+
if (platforms.includes('google_business')) this._addGoogleBusinessParams(form, options);
|
|
592
642
|
|
|
593
|
-
return this._request('/upload_text', 'POST', form, true);
|
|
643
|
+
return this._request('/upload_text', 'POST', form, true, this._idempotencyHeaders(options));
|
|
594
644
|
}
|
|
595
645
|
|
|
596
646
|
/**
|
|
@@ -634,10 +684,15 @@ export class UploadPost {
|
|
|
634
684
|
if (options.addToQueue !== undefined) form.append('add_to_queue', String(options.addToQueue));
|
|
635
685
|
if (options.maxPostsPerSlot !== undefined) form.append('max_posts_per_slot', String(options.maxPostsPerSlot));
|
|
636
686
|
if (options.asyncUpload !== undefined) form.append('async_upload', String(options.asyncUpload));
|
|
687
|
+
// AI auto-generation of native per-platform copy from the media (fills blank fields)
|
|
688
|
+
if (options.autogenerate !== undefined) form.append('autogenerate', String(options.autogenerate));
|
|
689
|
+
if (options.autogenerateTitle !== undefined) form.append('autogenerate_title', String(options.autogenerateTitle));
|
|
690
|
+
if (options.autogenerateDescription !== undefined) form.append('autogenerate_description', String(options.autogenerateDescription));
|
|
691
|
+
if (options.autogenerateLanguage) form.append('autogenerate_language', String(options.autogenerateLanguage));
|
|
637
692
|
|
|
638
693
|
this._addLinkedinParams(form, options);
|
|
639
694
|
|
|
640
|
-
return this._request('/upload_document', 'POST', form, true);
|
|
695
|
+
return this._request('/upload_document', 'POST', form, true, this._idempotencyHeaders(options));
|
|
641
696
|
}
|
|
642
697
|
|
|
643
698
|
// ==================== Status & History ====================
|
|
@@ -762,6 +817,16 @@ export class UploadPost {
|
|
|
762
817
|
return this._request('/uploadposts/platform-metrics', 'GET');
|
|
763
818
|
}
|
|
764
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Get detailed Reddit posts with full media information
|
|
822
|
+
*
|
|
823
|
+
* @param {string} profileUsername - Profile username
|
|
824
|
+
* @returns {Promise<Object>} Detailed Reddit posts
|
|
825
|
+
*/
|
|
826
|
+
async getRedditDetailedPosts(profileUsername) {
|
|
827
|
+
return this._request('/uploadposts/reddit/detailed-posts/', 'GET', { profile_username: profileUsername });
|
|
828
|
+
}
|
|
829
|
+
|
|
765
830
|
/**
|
|
766
831
|
* Get recent media from a connected social account.
|
|
767
832
|
*
|
|
@@ -811,7 +876,7 @@ export class UploadPost {
|
|
|
811
876
|
const body = {};
|
|
812
877
|
if (options.scheduledDate) body.scheduled_date = options.scheduledDate;
|
|
813
878
|
if (options.timezone) body.timezone = options.timezone;
|
|
814
|
-
return this._request(`/uploadposts/schedule/${jobId}`, '
|
|
879
|
+
return this._request(`/uploadposts/schedule/${jobId}`, 'PATCH', body);
|
|
815
880
|
}
|
|
816
881
|
|
|
817
882
|
// ==================== User Management ====================
|
|
@@ -859,6 +924,7 @@ export class UploadPost {
|
|
|
859
924
|
* @param {boolean} [options.readonlyCalendar] - Show only a read-only calendar (no editing, no account connection)
|
|
860
925
|
* @param {string} [options.connectTitle] - Custom title for the connection page
|
|
861
926
|
* @param {string} [options.connectDescription] - Custom description for the connection page
|
|
927
|
+
* @param {('en'|'es'|'de'|'fr'|'pt')} [options.language] - Force the connection page language for this profile. When omitted, the page auto-detects the visitor's browser language and falls back to English.
|
|
862
928
|
* @returns {Promise<Object>} JWT and connection URL
|
|
863
929
|
*/
|
|
864
930
|
async generateJwt(username, options = {}) {
|
|
@@ -871,6 +937,7 @@ export class UploadPost {
|
|
|
871
937
|
if (options.readonlyCalendar !== undefined) body.readonly_calendar = options.readonlyCalendar;
|
|
872
938
|
if (options.connectTitle) body.connect_title = options.connectTitle;
|
|
873
939
|
if (options.connectDescription) body.connect_description = options.connectDescription;
|
|
940
|
+
if (options.language) body.language = options.language;
|
|
874
941
|
return this._request('/uploadposts/users/generate-jwt', 'POST', body);
|
|
875
942
|
}
|
|
876
943
|
|
|
@@ -881,7 +948,7 @@ export class UploadPost {
|
|
|
881
948
|
* @returns {Promise<Object>} Validation result
|
|
882
949
|
*/
|
|
883
950
|
async validateJwt(jwt) {
|
|
884
|
-
return this._request('/uploadposts/users/validate-jwt', '
|
|
951
|
+
return this._request('/uploadposts/users/validate-jwt', 'GET', null, false, { Authorization: `Bearer ${jwt}` });
|
|
885
952
|
}
|
|
886
953
|
|
|
887
954
|
/**
|
|
@@ -912,7 +979,7 @@ export class UploadPost {
|
|
|
912
979
|
* @returns {Promise<Object>} Notification config
|
|
913
980
|
*/
|
|
914
981
|
async getNotificationConfig() {
|
|
915
|
-
return this._request('/uploadposts/
|
|
982
|
+
return this._request('/uploadposts/users/notifications', 'GET');
|
|
916
983
|
}
|
|
917
984
|
|
|
918
985
|
/**
|
|
@@ -927,7 +994,20 @@ export class UploadPost {
|
|
|
927
994
|
const body = {};
|
|
928
995
|
if (options.webhookEvents) body.webhook_events = options.webhookEvents;
|
|
929
996
|
if (options.webhookUrl) body.webhook_url = options.webhookUrl;
|
|
930
|
-
|
|
997
|
+
if (options.channels) body.channels = options.channels;
|
|
998
|
+
if (options.telegramChatId) body.telegram_chat_id = options.telegramChatId;
|
|
999
|
+
if (options.slackWebhookUrl) body.slack_webhook_url = options.slackWebhookUrl;
|
|
1000
|
+
if (options.whatsappToPhone) body.whatsapp_to_phone = options.whatsappToPhone;
|
|
1001
|
+
return this._request('/uploadposts/users/notifications', 'POST', body);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Send a test notification through the configured channels
|
|
1006
|
+
*
|
|
1007
|
+
* @returns {Promise<Object>} Test result
|
|
1008
|
+
*/
|
|
1009
|
+
async testNotifications() {
|
|
1010
|
+
return this._request('/uploadposts/users/notifications/test', 'POST', {});
|
|
931
1011
|
}
|
|
932
1012
|
|
|
933
1013
|
// ==================== Instagram Comments ====================
|
|
@@ -1037,10 +1117,11 @@ export class UploadPost {
|
|
|
1037
1117
|
* @param {string} [profile] - Profile username
|
|
1038
1118
|
* @returns {Promise<Object>} Selection result with google_business_id and display_name
|
|
1039
1119
|
*/
|
|
1040
|
-
async selectGoogleBusinessLocation(
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1120
|
+
async selectGoogleBusinessLocation() {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
'selectGoogleBusinessLocation is not supported: the API has no persistent location selection. ' +
|
|
1123
|
+
'Pass gbpLocationId on the upload instead (list ids with getGoogleBusinessLocations).'
|
|
1124
|
+
);
|
|
1044
1125
|
}
|
|
1045
1126
|
}
|
|
1046
1127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "upload-post",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Official client library for Upload-Post API - Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn, Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|