Skip to content

Commit 53adaf1

Browse files
Merging pull request #18866
* blotato new components * added wrapper for upload media api call
1 parent 7030774 commit 53adaf1

File tree

8 files changed

+658
-10
lines changed

8 files changed

+658
-10
lines changed
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
import blotato from "../../blotato.app.mjs";
2+
3+
export default {
4+
key: "blotato-create-post",
5+
name: "Create Post",
6+
description: "Posts to a social media platform. [See documentation](https://help.blotato.com/api/api-reference/publish-post)",
7+
version: "0.0.1",
8+
annotations: {
9+
destructiveHint: false,
10+
openWorldHint: true,
11+
readOnlyHint: false,
12+
},
13+
type: "action",
14+
props: {
15+
blotato,
16+
accountId: {
17+
type: "string",
18+
label: "Account ID",
19+
description: "The ID of the connected account for publishing the post",
20+
},
21+
text: {
22+
type: "string",
23+
label: "Text",
24+
description: "The main textual content of the post",
25+
},
26+
mediaUrls: {
27+
type: "string[]",
28+
label: "Media URLs",
29+
description: "An array of media URLs attached to the post. The URLs must originate from the blotato.com domain. See the Upload Media section for more info.",
30+
},
31+
targetType: {
32+
type: "string",
33+
label: "Target Type",
34+
description: "The target platform type",
35+
options: [
36+
"webhook",
37+
"twitter",
38+
"linkedin",
39+
"facebook",
40+
"instagram",
41+
"pinterest",
42+
"tiktok",
43+
"threads",
44+
"bluesky",
45+
"youtube",
46+
],
47+
reloadProps: true,
48+
},
49+
additionalPosts: {
50+
type: "string",
51+
label: "Additional Posts",
52+
description: "A JSON array of additional posts for thread-like posts (e.g., Twitter, Bluesky, Threads). Each post should have `text` and `mediaUrls` properties. Example: `[{\"text\": \"Second post\", \"mediaUrls\": []}]`",
53+
optional: true,
54+
},
55+
scheduledTime: {
56+
type: "string",
57+
label: "Scheduled Time",
58+
description: "The timestamp (ISO 8601 format: `YYYY-MM-DDTHH:mm:ssZ`) when the post should be published. If not provided, the post will be published immediately.",
59+
optional: true,
60+
},
61+
},
62+
async additionalProps() {
63+
const props = {};
64+
65+
switch (this.targetType) {
66+
case "webhook":
67+
props.webhookUrl = {
68+
type: "string",
69+
label: "Webhook URL",
70+
description: "The webhook URL to send the post data",
71+
};
72+
break;
73+
case "linkedin":
74+
props.linkedinPageId = {
75+
type: "string",
76+
label: "LinkedIn Page ID",
77+
description: "Optional LinkedIn Page ID",
78+
optional: true,
79+
};
80+
break;
81+
case "facebook":
82+
props.facebookPageId = {
83+
type: "string",
84+
label: "Facebook Page ID",
85+
description: "Facebook Page ID",
86+
};
87+
props.facebookMediaType = {
88+
type: "string",
89+
label: "Media Type",
90+
description: "Determines whether the video will be uploaded as a regular video or a reel. Only applicable if one of the media URLs is a video.",
91+
options: [
92+
"video",
93+
"reel",
94+
],
95+
optional: true,
96+
};
97+
break;
98+
case "instagram":
99+
props.instagramMediaType = {
100+
type: "string",
101+
label: "Media Type",
102+
description: "Is it a story or a reel? Reels are video only and cannot appear in carousel items. The default value is `reel`.",
103+
options: [
104+
"reel",
105+
"story",
106+
],
107+
optional: true,
108+
default: "reel",
109+
};
110+
props.instagramAltText = {
111+
type: "string",
112+
label: "Alt Text",
113+
description: "Alternative text, up to 1000 characters, for an image. Only supported on a single image or image media in a carousel.",
114+
optional: true,
115+
};
116+
break;
117+
case "tiktok":
118+
props.tiktokPrivacyLevel = {
119+
type: "string",
120+
label: "Privacy Level",
121+
description: "Privacy level of the post",
122+
options: [
123+
"SELF_ONLY",
124+
"PUBLIC_TO_EVERYONE",
125+
"MUTUAL_FOLLOW_FRIENDS",
126+
"FOLLOWER_OF_CREATOR",
127+
],
128+
};
129+
props.tiktokDisabledComments = {
130+
type: "boolean",
131+
label: "Disabled Comments",
132+
description: "If true, comments will be disabled",
133+
};
134+
props.tiktokDisabledDuet = {
135+
type: "boolean",
136+
label: "Disabled Duet",
137+
description: "If true, duet will be disabled",
138+
};
139+
props.tiktokDisabledStitch = {
140+
type: "boolean",
141+
label: "Disabled Stitch",
142+
description: "If true, stitch will be disabled",
143+
};
144+
props.tiktokIsBrandedContent = {
145+
type: "boolean",
146+
label: "Is Branded Content",
147+
description: "If true, the post is branded content",
148+
};
149+
props.tiktokIsYourBrand = {
150+
type: "boolean",
151+
label: "Is Your Brand",
152+
description: "If true, the content belongs to your brand",
153+
};
154+
props.tiktokIsAiGenerated = {
155+
type: "boolean",
156+
label: "Is AI Generated",
157+
description: "If true, the content is AI-generated",
158+
};
159+
props.tiktokTitle = {
160+
type: "string",
161+
label: "Title",
162+
description: "Title for image posts. Must be less than 90 characters. Has no effect on video posts. Defaults to the first 90 characters of the post text.",
163+
optional: true,
164+
};
165+
props.tiktokAutoAddMusic = {
166+
type: "boolean",
167+
label: "Auto Add Music",
168+
description: "If true, automatically add recommended music to photo posts. Has no effect on video posts.",
169+
optional: true,
170+
default: false,
171+
};
172+
props.tiktokIsDraft = {
173+
type: "boolean",
174+
label: "Is Draft",
175+
description: "If true, post as a draft",
176+
optional: true,
177+
};
178+
props.tiktokImageCoverIndex = {
179+
type: "string",
180+
label: "Image Cover Index",
181+
description: "Index of the image (starts from 0) to use as the cover for carousel posts. Only applicable for TikTok slideshows.",
182+
optional: true,
183+
};
184+
props.tiktokVideoCoverTimestamp = {
185+
type: "string",
186+
label: "Video Cover Timestamp",
187+
description: "Location in milliseconds of the video to use as the cover image. Only applicable for videos. If not provided, the frame at 0 milliseconds will be used.",
188+
optional: true,
189+
};
190+
break;
191+
case "pinterest":
192+
props.pinterestBoardId = {
193+
type: "string",
194+
label: "Board ID",
195+
description: "Pinterest board ID. To get your board ID, go to the Remix screen, create a draft Pinterest post, and click 'Publish'.",
196+
};
197+
props.pinterestTitle = {
198+
type: "string",
199+
label: "Pin Title",
200+
description: "Pin title",
201+
optional: true,
202+
};
203+
props.pinterestAltText = {
204+
type: "string",
205+
label: "Pin Alt Text",
206+
description: "Pin alternative text",
207+
optional: true,
208+
};
209+
props.pinterestLink = {
210+
type: "string",
211+
label: "Pin Link",
212+
description: "Pin URL link",
213+
optional: true,
214+
};
215+
break;
216+
case "threads":
217+
props.threadsReplyControl = {
218+
type: "string",
219+
label: "Reply Control",
220+
description: "Who can reply",
221+
options: [
222+
"everyone",
223+
"accounts_you_follow",
224+
"mentioned_only",
225+
],
226+
optional: true,
227+
};
228+
break;
229+
case "youtube":
230+
props.youtubeTitle = {
231+
type: "string",
232+
label: "Video Title",
233+
description: "Video title",
234+
};
235+
props.youtubePrivacyStatus = {
236+
type: "string",
237+
label: "Privacy Status",
238+
description: "Video privacy status",
239+
options: [
240+
"private",
241+
"public",
242+
"unlisted",
243+
],
244+
};
245+
props.youtubeShouldNotifySubscribers = {
246+
type: "boolean",
247+
label: "Notify Subscribers",
248+
description: "If true, subscribers will be notified",
249+
};
250+
props.youtubeIsMadeForKids = {
251+
type: "boolean",
252+
label: "Is Made For Kids",
253+
description: "If true, marks the video as made for kids",
254+
optional: true,
255+
default: false,
256+
};
257+
props.youtubeContainsSyntheticMedia = {
258+
type: "boolean",
259+
label: "Contains Synthetic Media",
260+
description: "If true, the media contains synthetic content, such as AI images, AI videos, or AI avatars",
261+
optional: true,
262+
};
263+
break;
264+
}
265+
266+
return props;
267+
},
268+
async run({ $ }) {
269+
const {
270+
accountId,
271+
text,
272+
mediaUrls,
273+
targetType,
274+
additionalPosts,
275+
scheduledTime,
276+
} = this;
277+
278+
// Set platform based on targetType - "webhook" becomes "other", all others use targetType value
279+
const platform = targetType === "webhook"
280+
? "other"
281+
: targetType;
282+
283+
// Build content object
284+
const content = {
285+
text,
286+
mediaUrls,
287+
platform,
288+
};
289+
290+
// Parse and add additional posts if provided
291+
if (additionalPosts) {
292+
try {
293+
content.additionalPosts = typeof additionalPosts === "string"
294+
? JSON.parse(additionalPosts)
295+
: additionalPosts;
296+
} catch (error) {
297+
throw new Error("Invalid JSON format in Additional Posts");
298+
}
299+
}
300+
301+
// Build target object based on targetType - axios will automatically exclude undefined values
302+
const target = {
303+
targetType,
304+
};
305+
306+
switch (targetType) {
307+
case "webhook":
308+
target.url = this.webhookUrl;
309+
break;
310+
case "linkedin":
311+
target.pageId = this.linkedinPageId;
312+
break;
313+
case "facebook":
314+
target.pageId = this.facebookPageId;
315+
target.mediaType = this.facebookMediaType;
316+
break;
317+
case "instagram":
318+
target.mediaType = this.instagramMediaType;
319+
target.altText = this.instagramAltText;
320+
break;
321+
case "tiktok":
322+
target.privacyLevel = this.tiktokPrivacyLevel;
323+
target.disabledComments = this.tiktokDisabledComments;
324+
target.disabledDuet = this.tiktokDisabledDuet;
325+
target.disabledStitch = this.tiktokDisabledStitch;
326+
target.isBrandedContent = this.tiktokIsBrandedContent;
327+
target.isYourBrand = this.tiktokIsYourBrand;
328+
target.isAiGenerated = this.tiktokIsAiGenerated;
329+
target.title = this.tiktokTitle;
330+
target.autoAddMusic = this.tiktokAutoAddMusic;
331+
target.isDraft = this.tiktokIsDraft;
332+
target.imageCoverIndex = this.tiktokImageCoverIndex
333+
? parseInt(this.tiktokImageCoverIndex)
334+
: undefined;
335+
target.videoCoverTimestamp = this.tiktokVideoCoverTimestamp
336+
? parseInt(this.tiktokVideoCoverTimestamp)
337+
: undefined;
338+
break;
339+
case "pinterest":
340+
target.boardId = this.pinterestBoardId;
341+
target.title = this.pinterestTitle;
342+
target.altText = this.pinterestAltText;
343+
target.link = this.pinterestLink;
344+
break;
345+
case "threads":
346+
target.replyControl = this.threadsReplyControl;
347+
break;
348+
case "youtube":
349+
target.title = this.youtubeTitle;
350+
target.privacyStatus = this.youtubePrivacyStatus;
351+
target.shouldNotifySubscribers = this.youtubeShouldNotifySubscribers;
352+
target.isMadeForKids = this.youtubeIsMadeForKids;
353+
target.containsSyntheticMedia = this.youtubeContainsSyntheticMedia;
354+
break;
355+
}
356+
357+
// Build the request body - axios will automatically exclude undefined values
358+
const data = {
359+
post: {
360+
accountId,
361+
content,
362+
target,
363+
},
364+
scheduledTime,
365+
};
366+
367+
const response = await this.blotato._makeRequest({
368+
$,
369+
method: "POST",
370+
path: "/v2/posts",
371+
data,
372+
});
373+
374+
$.export("$summary", `Successfully submitted post. Post Submission ID: ${response.postSubmissionId}`);
375+
376+
return response;
377+
},
378+
};

0 commit comments

Comments
 (0)