From f1e3e313abbd54d87e24baed4991eaf5922faa76 Mon Sep 17 00:00:00 2001 From: Sergio Wong Date: Sat, 25 Oct 2025 22:11:03 -0700 Subject: [PATCH 1/2] blotato new components --- .../actions/create-post/create-post.mjs | 378 ++++++++++++++++++ .../actions/create-video/create-video.mjs | 70 ++++ .../actions/delete-video/delete-video.mjs | 34 ++ .../blotato/actions/get-video/get-video.mjs | 34 ++ .../actions/upload-media/upload-media.mjs | 43 ++ components/blotato/blotato.app.mjs | 94 ++++- components/blotato/package.json | 7 +- pnpm-lock.yaml | 12 +- 8 files changed, 662 insertions(+), 10 deletions(-) create mode 100644 components/blotato/actions/create-post/create-post.mjs create mode 100644 components/blotato/actions/create-video/create-video.mjs create mode 100644 components/blotato/actions/delete-video/delete-video.mjs create mode 100644 components/blotato/actions/get-video/get-video.mjs create mode 100644 components/blotato/actions/upload-media/upload-media.mjs diff --git a/components/blotato/actions/create-post/create-post.mjs b/components/blotato/actions/create-post/create-post.mjs new file mode 100644 index 0000000000000..1dc5460438f8c --- /dev/null +++ b/components/blotato/actions/create-post/create-post.mjs @@ -0,0 +1,378 @@ +import blotato from "../../blotato.app.mjs"; + +export default { + key: "blotato-create-post", + name: "Create Post", + description: "Posts to a social media platform. [See documentation](https://help.blotato.com/api/api-reference/publish-post)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + blotato, + accountId: { + type: "string", + label: "Account ID", + description: "The ID of the connected account for publishing the post", + }, + text: { + type: "string", + label: "Text", + description: "The main textual content of the post", + }, + mediaUrls: { + type: "string[]", + label: "Media URLs", + 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.", + }, + targetType: { + type: "string", + label: "Target Type", + description: "The target platform type", + options: [ + "webhook", + "twitter", + "linkedin", + "facebook", + "instagram", + "pinterest", + "tiktok", + "threads", + "bluesky", + "youtube", + ], + reloadProps: true, + }, + additionalPosts: { + type: "string", + label: "Additional Posts", + 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\": []}]`", + optional: true, + }, + scheduledTime: { + type: "string", + label: "Scheduled Time", + 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.", + optional: true, + }, + }, + async additionalProps() { + const props = {}; + + switch (this.targetType) { + case "webhook": + props.webhookUrl = { + type: "string", + label: "Webhook URL", + description: "The webhook URL to send the post data", + }; + break; + case "linkedin": + props.linkedinPageId = { + type: "string", + label: "LinkedIn Page ID", + description: "Optional LinkedIn Page ID", + optional: true, + }; + break; + case "facebook": + props.facebookPageId = { + type: "string", + label: "Facebook Page ID", + description: "Facebook Page ID", + }; + props.facebookMediaType = { + type: "string", + label: "Media Type", + 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.", + options: [ + "video", + "reel", + ], + optional: true, + }; + break; + case "instagram": + props.instagramMediaType = { + type: "string", + label: "Media Type", + description: "Is it a story or a reel? Reels are video only and cannot appear in carousel items. The default value is `reel`.", + options: [ + "reel", + "story", + ], + optional: true, + default: "reel", + }; + props.instagramAltText = { + type: "string", + label: "Alt Text", + description: "Alternative text, up to 1000 characters, for an image. Only supported on a single image or image media in a carousel.", + optional: true, + }; + break; + case "tiktok": + props.tiktokPrivacyLevel = { + type: "string", + label: "Privacy Level", + description: "Privacy level of the post", + options: [ + "SELF_ONLY", + "PUBLIC_TO_EVERYONE", + "MUTUAL_FOLLOW_FRIENDS", + "FOLLOWER_OF_CREATOR", + ], + }; + props.tiktokDisabledComments = { + type: "boolean", + label: "Disabled Comments", + description: "If true, comments will be disabled", + }; + props.tiktokDisabledDuet = { + type: "boolean", + label: "Disabled Duet", + description: "If true, duet will be disabled", + }; + props.tiktokDisabledStitch = { + type: "boolean", + label: "Disabled Stitch", + description: "If true, stitch will be disabled", + }; + props.tiktokIsBrandedContent = { + type: "boolean", + label: "Is Branded Content", + description: "If true, the post is branded content", + }; + props.tiktokIsYourBrand = { + type: "boolean", + label: "Is Your Brand", + description: "If true, the content belongs to your brand", + }; + props.tiktokIsAiGenerated = { + type: "boolean", + label: "Is AI Generated", + description: "If true, the content is AI-generated", + }; + props.tiktokTitle = { + type: "string", + label: "Title", + 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.", + optional: true, + }; + props.tiktokAutoAddMusic = { + type: "boolean", + label: "Auto Add Music", + description: "If true, automatically add recommended music to photo posts. Has no effect on video posts.", + optional: true, + default: false, + }; + props.tiktokIsDraft = { + type: "boolean", + label: "Is Draft", + description: "If true, post as a draft", + optional: true, + }; + props.tiktokImageCoverIndex = { + type: "string", + label: "Image Cover Index", + description: "Index of the image (starts from 0) to use as the cover for carousel posts. Only applicable for TikTok slideshows.", + optional: true, + }; + props.tiktokVideoCoverTimestamp = { + type: "string", + label: "Video Cover Timestamp", + 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.", + optional: true, + }; + break; + case "pinterest": + props.pinterestBoardId = { + type: "string", + label: "Board ID", + description: "Pinterest board ID. To get your board ID, go to the Remix screen, create a draft Pinterest post, and click 'Publish'.", + }; + props.pinterestTitle = { + type: "string", + label: "Pin Title", + description: "Pin title", + optional: true, + }; + props.pinterestAltText = { + type: "string", + label: "Pin Alt Text", + description: "Pin alternative text", + optional: true, + }; + props.pinterestLink = { + type: "string", + label: "Pin Link", + description: "Pin URL link", + optional: true, + }; + break; + case "threads": + props.threadsReplyControl = { + type: "string", + label: "Reply Control", + description: "Who can reply", + options: [ + "everyone", + "accounts_you_follow", + "mentioned_only", + ], + optional: true, + }; + break; + case "youtube": + props.youtubeTitle = { + type: "string", + label: "Video Title", + description: "Video title", + }; + props.youtubePrivacyStatus = { + type: "string", + label: "Privacy Status", + description: "Video privacy status", + options: [ + "private", + "public", + "unlisted", + ], + }; + props.youtubeShouldNotifySubscribers = { + type: "boolean", + label: "Notify Subscribers", + description: "If true, subscribers will be notified", + }; + props.youtubeIsMadeForKids = { + type: "boolean", + label: "Is Made For Kids", + description: "If true, marks the video as made for kids", + optional: true, + default: false, + }; + props.youtubeContainsSyntheticMedia = { + type: "boolean", + label: "Contains Synthetic Media", + description: "If true, the media contains synthetic content, such as AI images, AI videos, or AI avatars", + optional: true, + }; + break; + } + + return props; + }, + async run({ $ }) { + const { + accountId, + text, + mediaUrls, + targetType, + additionalPosts, + scheduledTime, + } = this; + + // Set platform based on targetType - "webhook" becomes "other", all others use targetType value + const platform = targetType === "webhook" + ? "other" + : targetType; + + // Build content object + const content = { + text, + mediaUrls, + platform, + }; + + // Parse and add additional posts if provided + if (additionalPosts) { + try { + content.additionalPosts = typeof additionalPosts === "string" + ? JSON.parse(additionalPosts) + : additionalPosts; + } catch (error) { + throw new Error("Invalid JSON format in Additional Posts"); + } + } + + // Build target object based on targetType - axios will automatically exclude undefined values + const target = { + targetType, + }; + + switch (targetType) { + case "webhook": + target.url = this.webhookUrl; + break; + case "linkedin": + target.pageId = this.linkedinPageId; + break; + case "facebook": + target.pageId = this.facebookPageId; + target.mediaType = this.facebookMediaType; + break; + case "instagram": + target.mediaType = this.instagramMediaType; + target.altText = this.instagramAltText; + break; + case "tiktok": + target.privacyLevel = this.tiktokPrivacyLevel; + target.disabledComments = this.tiktokDisabledComments; + target.disabledDuet = this.tiktokDisabledDuet; + target.disabledStitch = this.tiktokDisabledStitch; + target.isBrandedContent = this.tiktokIsBrandedContent; + target.isYourBrand = this.tiktokIsYourBrand; + target.isAiGenerated = this.tiktokIsAiGenerated; + target.title = this.tiktokTitle; + target.autoAddMusic = this.tiktokAutoAddMusic; + target.isDraft = this.tiktokIsDraft; + target.imageCoverIndex = this.tiktokImageCoverIndex + ? parseInt(this.tiktokImageCoverIndex) + : undefined; + target.videoCoverTimestamp = this.tiktokVideoCoverTimestamp + ? parseInt(this.tiktokVideoCoverTimestamp) + : undefined; + break; + case "pinterest": + target.boardId = this.pinterestBoardId; + target.title = this.pinterestTitle; + target.altText = this.pinterestAltText; + target.link = this.pinterestLink; + break; + case "threads": + target.replyControl = this.threadsReplyControl; + break; + case "youtube": + target.title = this.youtubeTitle; + target.privacyStatus = this.youtubePrivacyStatus; + target.shouldNotifySubscribers = this.youtubeShouldNotifySubscribers; + target.isMadeForKids = this.youtubeIsMadeForKids; + target.containsSyntheticMedia = this.youtubeContainsSyntheticMedia; + break; + } + + // Build the request body - axios will automatically exclude undefined values + const data = { + post: { + accountId, + content, + target, + }, + scheduledTime, + }; + + const response = await this.blotato._makeRequest({ + $, + method: "POST", + path: "/v2/posts", + data, + }); + + $.export("$summary", `Successfully submitted post. Post Submission ID: ${response.postSubmissionId}`); + + return response; + }, +}; diff --git a/components/blotato/actions/create-video/create-video.mjs b/components/blotato/actions/create-video/create-video.mjs new file mode 100644 index 0000000000000..9a2f406e97412 --- /dev/null +++ b/components/blotato/actions/create-video/create-video.mjs @@ -0,0 +1,70 @@ +import blotato from "../../blotato.app.mjs"; + +export default { + key: "blotato-create-video", + name: "Create Video", + description: "Creates a new video using a template. The template takes an ID and input parameters. [See documentation](https://help.blotato.com/api/api-reference/create-video)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + blotato, + templateId: { + type: "string", + label: "Template ID", + description: "The ID of the template to use", + async options() { + const { items } = await this.blotato.listTemplates({ + fields: "id,title,description", + }); + return items.map((template) => ({ + label: template.title || template.id, + value: template.id, + })); + }, + }, + inputs: { + type: "string", + label: "Inputs", + description: "Template-specific input parameters as a string that parses to a JSON object. The structure depends on the selected template.", + }, + isDraft: { + type: "boolean", + label: "Is Draft", + description: "If true, the video will be created as a draft", + optional: true, + default: false, + }, + render: { + type: "boolean", + label: "Render", + description: "If true, the video will be rendered immediately", + default: true, + }, + }, + async run({ $ }) { + const { + templateId, + isDraft, + render, + } = this; + + const inputs = JSON.parse(this.inputs); + + const response = await this.blotato.createVideoFromTemplate({ + $, + templateId, + inputs, + isDraft, + render, + }); + + $.export("$summary", `Successfully created video with ID: ${response.item.id}. Status: ${response.item.status}. To view progress, visit https://my.blotato.com/videos/${response.item.id}`); + + return response; + }, +}; diff --git a/components/blotato/actions/delete-video/delete-video.mjs b/components/blotato/actions/delete-video/delete-video.mjs new file mode 100644 index 0000000000000..508fd0e5a0510 --- /dev/null +++ b/components/blotato/actions/delete-video/delete-video.mjs @@ -0,0 +1,34 @@ +import blotato from "../../blotato.app.mjs"; + +export default { + key: "blotato-delete-video", + name: "Delete Video", + description: "Delete a video, carousel, or graphic. [See documentation](https://help.blotato.com/api/api-reference/delete-video)", + version: "0.0.1", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + blotato, + videoId: { + type: "string", + label: "Video ID", + description: "The ID of the video to delete", + }, + }, + async run({ $ }) { + const { videoId } = this; + + const response = await this.blotato.deleteVideo({ + $, + videoId, + }); + + $.export("$summary", `Successfully deleted video with ID: ${videoId}`); + + return response; + }, +}; diff --git a/components/blotato/actions/get-video/get-video.mjs b/components/blotato/actions/get-video/get-video.mjs new file mode 100644 index 0000000000000..16334cae4ae25 --- /dev/null +++ b/components/blotato/actions/get-video/get-video.mjs @@ -0,0 +1,34 @@ +import blotato from "../../blotato.app.mjs"; + +export default { + key: "blotato-get-video", + name: "Get Video", + description: "Get a video, carousel, or graphic. [See documentation](https://help.blotato.com/api/api-reference/find-video)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: true, + }, + type: "action", + props: { + blotato, + videoId: { + type: "string", + label: "Video ID", + description: "The ID of the video to retrieve", + }, + }, + async run({ $ }) { + const { videoId } = this; + + const response = await this.blotato.getVideo({ + $, + videoId, + }); + + $.export("$summary", `Successfully retrieved video with ID: ${videoId}`); + + return response; + }, +}; diff --git a/components/blotato/actions/upload-media/upload-media.mjs b/components/blotato/actions/upload-media/upload-media.mjs new file mode 100644 index 0000000000000..7dd888851b759 --- /dev/null +++ b/components/blotato/actions/upload-media/upload-media.mjs @@ -0,0 +1,43 @@ +import blotato from "../../blotato.app.mjs"; + +export default { + key: "blotato-upload-media", + name: "Upload Media", + description: "Uploads a media file by providing a URL. The uploaded media will be processed and stored, returning a new media URL that can be used to publish a post. [See documentation](https://help.blotato.com/api/api-reference/upload-media-v2-media)", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + blotato, + alert: { + type: "alert", + alertType: "info", + content: "Media uploads are limited to 200MB file size or smaller.", + }, + url: { + type: "string", + label: "URL", + description: "The publicly accessible URL of the media to upload. For Google Drive files, use the format: `https://drive.google.com/uc?export=download&id=YOUR_FILE_ID`", + }, + }, + async run({ $ }) { + const { url } = this; + + const response = await this.blotato._makeRequest({ + $, + method: "POST", + path: "/v2/media", + data: { + url, + }, + }); + + $.export("$summary", `Successfully uploaded media. New URL: ${response.url}`); + + return response; + }, +}; diff --git a/components/blotato/blotato.app.mjs b/components/blotato/blotato.app.mjs index 6a2fc1c48f757..53d7c9995aa65 100644 --- a/components/blotato/blotato.app.mjs +++ b/components/blotato/blotato.app.mjs @@ -1,11 +1,99 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "blotato", propDefinitions: {}, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _makeRequest({ + $ = this, path, method = "GET", data, headers = {}, ...opts + }) { + return axios($, { + url: `https://backend.blotato.com${path}`, + method, + headers: { + "blotato-api-key": `${this.$auth.api_key}`, + "accept": "*/*", + ...headers, + }, + data, + ...opts, + }); + }, + createPost({ + title, content, media, ...args + } = {}) { + return this._makeRequest({ + method: "POST", + path: "/v2/posts", + data: { + title, + content, + media, + ...args, + }, + }); + }, + createVideoFromTemplate({ + $, templateId, inputs, isDraft, render, ...args + } = {}) { + return this._makeRequest({ + $, + method: "POST", + path: "/v2/videos/from-templates", + data: { + templateId, + inputs, + isDraft, + render, + }, + ...args, + }); + }, + deleteVideo({ + $, videoId, ...args + } = {}) { + return this._makeRequest({ + $, + method: "DELETE", + path: `/v2/videos/${videoId}`, + ...args, + }); + }, + getVideo({ + $, videoId, ...args + } = {}) { + return this._makeRequest({ + $, + path: `/v2/videos/creations/${videoId}`, + ...args, + }); + }, + uploadMedia({ + file, mediaType, ...args + } = {}) { + return this._makeRequest({ + method: "POST", + path: "/v2/media", + data: { + file, + mediaType, + ...args, + }, + }); + }, + listTemplates({ + fields, search, id, ...args + } = {}) { + return this._makeRequest({ + path: "/v2/videos/templates", + params: { + fields, + search, + id, + }, + ...args, + }); }, }, }; diff --git a/components/blotato/package.json b/components/blotato/package.json index 4c097a4f5ad87..f8f9e935fb29c 100644 --- a/components/blotato/package.json +++ b/components/blotato/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/blotato", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Blotato Components", "main": "blotato.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71092e10a27f2..a617b98622616 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -728,8 +728,7 @@ importers: components/alibaba_cloud: {} - components/alienvault: - specifiers: {} + components/alienvault: {} components/all_images_ai: dependencies: @@ -1618,8 +1617,7 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/beyond_presence: - specifiers: {} + components/beyond_presence: {} components/bidsketch: dependencies: @@ -1824,7 +1822,11 @@ importers: specifier: ^3.0.3 version: 3.0.3 - components/blotato: {} + components/blotato: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 components/blue: {} From 6ad8c4551524921b325c744fadfe1f7840e7912d Mon Sep 17 00:00:00 2001 From: Sergio Wong Date: Sun, 26 Oct 2025 09:18:46 -0700 Subject: [PATCH 2/2] added wrapper for upload media api call --- components/blotato/actions/upload-media/upload-media.mjs | 8 ++------ components/blotato/blotato.app.mjs | 8 ++++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/components/blotato/actions/upload-media/upload-media.mjs b/components/blotato/actions/upload-media/upload-media.mjs index 7dd888851b759..9223c6cb8a873 100644 --- a/components/blotato/actions/upload-media/upload-media.mjs +++ b/components/blotato/actions/upload-media/upload-media.mjs @@ -27,13 +27,9 @@ export default { async run({ $ }) { const { url } = this; - const response = await this.blotato._makeRequest({ + const response = await this.blotato.uploadMedia({ $, - method: "POST", - path: "/v2/media", - data: { - url, - }, + url, }); $.export("$summary", `Successfully uploaded media. New URL: ${response.url}`); diff --git a/components/blotato/blotato.app.mjs b/components/blotato/blotato.app.mjs index 53d7c9995aa65..83da2dcd64841 100644 --- a/components/blotato/blotato.app.mjs +++ b/components/blotato/blotato.app.mjs @@ -70,16 +70,16 @@ export default { }); }, uploadMedia({ - file, mediaType, ...args + $, url, ...args } = {}) { return this._makeRequest({ + $, method: "POST", path: "/v2/media", data: { - file, - mediaType, - ...args, + url, }, + ...args, }); }, listTemplates({