@blotato/n8n-nodes-blotato
Version:
Official n8n Blotato node
967 lines • 44.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Blotato = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const SearchFunctions_1 = require("./SearchFunctions");
const THREAD_SUPPORTED_PLATFORMS = ['twitter', 'threads', 'bluesky'];
class Blotato {
constructor() {
this.description = {
displayName: 'Blotato',
name: 'blotato',
icon: 'file:blotato.svg',
group: ['input'],
version: [2],
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Use Blotato API',
defaults: {
name: 'Blotato',
},
usableAsTool: true,
inputs: ["main"],
outputs: ["main"],
credentials: [
{
name: 'blotatoApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Media',
value: 'media',
},
{
name: 'Post',
value: 'post',
},
],
default: 'post',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['media'],
},
},
options: [
{
name: 'Upload',
value: 'upload',
description: 'Upload image or video',
action: 'Upload media',
},
],
default: 'upload',
},
{
displayName: 'Use Binary Data',
name: 'useBinaryData',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['media'],
operation: ['upload'],
},
},
description: 'Whether to use binary data instead of URL',
},
{
displayName: 'Media URL',
name: 'mediaUrl',
type: 'string',
default: '',
validateType: 'url',
displayOptions: {
show: {
resource: ['media'],
operation: ['upload'],
useBinaryData: [false],
},
},
description: 'Public URL of image or video',
},
{
displayName: 'Input Binary Field',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
displayOptions: {
show: {
resource: ['media'],
operation: ['upload'],
useBinaryData: [true],
},
},
description: 'Name of the binary property which contains the media to upload',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['post'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create post',
action: 'Create post',
},
],
default: 'create',
},
{
displayName: 'Platform',
name: 'platform',
type: 'options',
options: [
{ name: 'Bluesky', value: 'bluesky' },
{ name: 'Facebook', value: 'facebook' },
{ name: 'Instagram', value: 'instagram' },
{ name: 'Linkedin', value: 'linkedin' },
{ name: 'Pinterest', value: 'pinterest' },
{ name: 'Threads', value: 'threads' },
{ name: 'Tiktok', value: 'tiktok' },
{ name: 'Twitter', value: 'twitter' },
{ name: 'Youtube', value: 'youtube' },
],
default: 'instagram',
description: 'Social media platform',
required: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
},
},
},
{
displayName: 'Account',
name: 'accountId',
type: 'resourceLocator',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select an account for the chosen platform',
typeOptions: {
searchListMethod: 'getAccounts',
searchable: false,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 1234',
validation: [
{
type: 'regex',
properties: {
regex: '^[1-9]+[0-9]*$',
errorMessage: 'Not a valid account ID',
},
},
],
},
],
default: { mode: 'list', value: '' },
required: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
},
},
description: 'Your Blotato social media account ID',
},
{
displayName: 'Text',
name: 'postContentText',
type: 'string',
typeOptions: {
rows: 3,
},
default: '',
required: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
},
},
description: 'The main text for your post',
},
{
displayName: 'Media URLs',
name: 'postContentMediaUrls',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
},
},
description: 'Comma-separated list of media URLs',
placeholder: 'https://database.blotato.com/image1.jpg, https://database.blotato.com/image2.jpg',
},
{
displayName: 'Thread (Optional)',
name: 'threadInputMethod',
type: 'options',
options: [
{
name: 'Manual',
value: 'manual',
description: 'Add each post manually',
},
{
name: 'From Data',
value: 'array',
description: 'Use array data from previous node',
},
],
default: 'manual',
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: THREAD_SUPPORTED_PLATFORMS,
},
},
description: 'Choose how to create a long-form thread',
},
{
displayName: 'Thread Posts',
name: 'postContentAdditionalPosts',
placeholder: 'Add Thread',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: THREAD_SUPPORTED_PLATFORMS,
threadInputMethod: ['manual'],
},
},
options: [
{
name: 'posts',
displayName: 'Posts',
values: [
{
displayName: 'Text',
name: 'text',
type: 'string',
typeOptions: {
rows: 3,
},
default: '',
description: 'Text content of additional thread',
required: true,
},
{
displayName: 'Media URLs (Optional)',
name: 'mediaUrls',
type: 'string',
default: '',
description: 'Comma-separated list of media URLs',
placeholder: 'https://database.blotato.com/image1.jpg, https://database.blotato.com/image2.jpg',
},
],
},
],
},
{
displayName: 'Thread Posts',
name: 'threadPostsArray',
type: 'string',
default: '[]',
description: 'Array of posts from previous node. Each item must have "text" (string) and optionally "mediaUrls" (array of strings) properties.',
placeholder: '[{"text": "Post 1", "mediaUrls": []}, {"text": "Post 2", "mediaUrls": []}]',
hint: 'Correct format: [{"text": "Post 1", "mediaUrls": []}, {"text": "Post 2", "mediaUrls": []}]',
validateType: 'array',
ignoreValidationDuringExecution: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: THREAD_SUPPORTED_PLATFORMS,
threadInputMethod: ['array'],
},
},
},
{
displayName: 'Privacy Level',
name: 'postCreateTiktokOptionPrivacyLevel',
type: 'options',
default: 'PUBLIC_TO_EVERYONE',
options: [
{
name: 'Self Only',
value: 'SELF_ONLY',
},
{
name: 'Public to Everyone',
value: 'PUBLIC_TO_EVERYONE',
},
{
name: 'Mutual Follow Friends',
value: 'MUTUAL_FOLLOW_FRIENDS',
},
{
name: 'Follower of Creator',
value: 'FOLLOWER_OF_CREATOR',
},
],
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Set the privacy level for the TikTok post',
},
{
displayName: 'Disable Comments',
name: 'postCreateTiktokOptionDisabledComments',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Whether to disable comments on this post',
},
{
displayName: 'Disable Duet',
name: 'postCreateTiktokOptionDisabledDuet',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Whether to disable duet for this post',
},
{
displayName: 'Disable Stitch',
name: 'postCreateTiktokOptionDisabledStitch',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Whether to disable stitch for this post',
},
{
displayName: 'Is Branded Content',
name: 'postCreateTiktokOptionIsBrandedContent',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Whether this post contains branded content',
},
{
displayName: 'Is Your Brand',
name: 'postCreateTiktokOptionIsYourBrand',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Whether this post is about your own brand',
},
{
displayName: 'Is AI Generated',
name: 'postCreateTiktokOptionIsAiGenerated',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Whether this content is AI generated',
},
{
displayName: 'Auto Add Music',
name: 'postCreateTiktokOptionAutoAddMusic',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['tiktok'],
},
},
description: 'Whether to automatically add music. Only works for Tiktok slideshows.',
},
{
displayName: 'Facebook Page',
name: 'facebookPageId',
type: 'resourceLocator',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a Facebook Page for this account',
typeOptions: {
searchListMethod: 'getSubaccounts',
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 123456789012345',
validation: [
{
type: 'regex',
properties: {
regex: '^[0-9]+$',
errorMessage: 'Not a valid Facebook Page ID (only numbers)',
},
},
],
},
],
default: { mode: 'list', value: '' },
required: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['facebook'],
},
},
description: 'The Facebook Page ID to post to',
},
{
displayName: 'Pinterest Board',
name: 'pinterestBoardId',
type: 'resourceLocator',
modes: [
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 123456789012345',
validation: [
{
type: 'regex',
properties: {
regex: '^[0-9]+$',
errorMessage: 'Not a valid Pinterest Board ID (only numbers)',
},
},
],
},
],
default: { mode: 'id', value: '' },
required: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['pinterest'],
},
},
description: 'The Pinterest Board ID to pin to. Pinterest requires at least one image in mediaUrls.',
},
{
displayName: 'Pin Title (Optional)',
name: 'postCreatePinterestOptionTitle',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['pinterest'],
},
},
description: 'Optional title for the Pinterest pin',
},
{
displayName: 'Video Title',
name: 'postCreateYoutubeOptionTitle',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['youtube'],
},
},
description: 'Title of the Youtube video',
},
{
displayName: 'Privacy Status',
name: 'postCreateYoutubeOptionPrivacyStatus',
type: 'options',
default: 'public',
options: [
{
name: 'Public',
value: 'public',
},
{
name: 'Private',
value: 'private',
},
{
name: 'Unlisted',
value: 'unlisted',
},
],
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['youtube'],
},
},
description: 'Privacy setting for the Youtube video',
},
{
displayName: 'Notify Subscribers',
name: 'postCreateYoutubeOptionShouldNotifySubscribers',
type: 'boolean',
default: true,
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
platform: ['youtube'],
},
},
description: 'Whether to notify subscribers about this video',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['post'],
operation: ['create'],
},
},
options: [
{
displayName: 'Linkedin Page',
name: 'linkedinPageId',
type: 'resourceLocator',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a Linkedin Page',
typeOptions: {
searchListMethod: 'getSubaccounts',
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 104410867',
validation: [
{
type: 'regex',
properties: {
regex: '^[0-9]+$',
errorMessage: 'Not a valid Linkedin Page ID (only numbers)',
},
},
],
},
],
default: '',
displayOptions: {
show: {
'/platform': ['linkedin'],
},
},
description: 'Post to a Linkedin Company Page instead of your personal profile',
},
{
displayName: 'Made for Kids',
name: 'youtubeMadeForKids',
type: 'boolean',
default: false,
displayOptions: {
show: {
'/platform': ['youtube'],
},
},
description: 'Whether this video is made for kids',
},
{
displayName: 'Media Type',
name: 'facebookMediaType',
type: 'options',
default: 'reel',
options: [
{
name: 'Video',
value: 'video',
},
{
name: 'Reel',
value: 'reel',
},
],
displayOptions: {
show: {
'/platform': ['facebook'],
},
},
description: 'Type of Facebook video post - regular video or reel. Only applies for video posts. Ignored for text and image posts.',
},
{
displayName: 'Media Type',
name: 'instagramMediaType',
type: 'options',
default: 'reel',
options: [
{
name: 'Reel',
value: 'reel',
},
{
name: 'Story',
value: 'story',
},
],
displayOptions: {
show: {
'/platform': ['instagram'],
},
},
description: 'Type of Instagram video post - reel or story. Only applies for video posts. Ignored for image-only posts.',
},
{
displayName: 'Pinterest Alt Text',
name: 'pinterestAltText',
type: 'string',
default: '',
displayOptions: {
show: {
'/platform': ['pinterest'],
},
},
description: 'Alternative text for accessibility',
},
{
displayName: 'Pinterest Link',
name: 'pinterestLink',
type: 'string',
default: '',
validateType: 'url',
displayOptions: {
show: {
'/platform': ['pinterest'],
},
},
description: 'URL the pin should link to',
},
{
displayName: 'Reply Control',
name: 'threadsReplyControl',
type: 'options',
default: 'everyone',
options: [
{
name: 'Everyone',
value: 'everyone',
},
{
name: 'Accounts You Follow',
value: 'accounts_you_follow',
},
{
name: 'Mentioned Only',
value: 'mentioned_only',
},
],
displayOptions: {
show: {
'/platform': ['threads'],
},
},
description: 'Control who can reply to your Threads post',
},
{
displayName: 'Scheduled Time',
name: 'scheduledTime',
type: 'dateTime',
default: '',
description: 'Schedule the post for a future time. For example: "2024-12-31T23:59:59Z" for UTC time.',
},
],
},
],
};
this.methods = {
listSearch: {
getAccounts: SearchFunctions_1.getAccounts,
getSubaccounts: SearchFunctions_1.getSubaccounts,
},
};
}
async execute() {
const inputItems = this.getInputData();
const returnData = [];
for (let i = 0; i < inputItems.length; i++) {
const resource = this.getNodeParameter('resource', i);
const operation = this.getNodeParameter('operation', i);
const options = {};
if (resource === 'media') {
options.json = true;
options.method = 'POST';
options.uri = '/v2/media';
if (operation === 'upload') {
const useBinaryData = this.getNodeParameter('useBinaryData', i);
if (useBinaryData) {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
const dataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
const maxSizeBytes = 15 * 1024 * 1024;
if (dataBuffer.length > maxSizeBytes) {
const sizeMB = (dataBuffer.length / (1024 * 1024)).toFixed(2);
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `File size (${sizeMB}MB) exceeds 15MB limit. Large files should be uploaded via URL instead of binary data. You can use Google Drive (up to 60MB), Dropbox, Frame.io, or similar services. S3/GCS buckets are recommended for very large files.`, { itemIndex: i });
}
const base64 = dataBuffer.toString('base64');
const mimeType = binaryData.mimeType || 'application/octet-stream';
const dataUri = `data:${mimeType};base64,${base64}`;
options.body = { url: dataUri };
}
else {
options.body = { url: this.getNodeParameter('mediaUrl', i) };
}
}
else {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operation "${operation}" is not supported for resource "media".`, { itemIndex: i });
}
}
else if (resource === 'post') {
options.json = true;
options.method = 'POST';
options.uri = '/v2/posts';
const platform = this.getNodeParameter('platform', i);
const accountId = this.getNodeParameter('accountId', i).value;
options.body = {
post: {
target: {
targetType: platform,
},
content: {
platform: platform,
text: this.getNodeParameter('postContentText', i),
mediaUrls: this.getNodeParameter('postContentMediaUrls', i)
.split(',')
.map((url) => url.trim())
.filter(Boolean),
},
accountId: accountId,
},
};
const mediaUrls = options.body.post.content.mediaUrls;
const requiresMedia = ['instagram', 'tiktok', 'pinterest', 'youtube'].includes(platform);
if (requiresMedia && mediaUrls.length === 0) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${platform.charAt(0).toUpperCase() + platform.slice(1)} requires you to post an image or video.`, { itemIndex: i });
}
const postOptions = this.getNodeParameter('options', i, {});
this.logger.debug(`PostOptions: ${JSON.stringify(postOptions)}`);
if (postOptions.scheduledTime) {
let scheduledTime = postOptions.scheduledTime;
const hasTimezone = scheduledTime.includes('Z') ||
scheduledTime.includes('+') ||
scheduledTime.match(/[+-]\d{2}:\d{2}$/);
if (!hasTimezone) {
scheduledTime += 'Z';
}
options.body.scheduledTime = scheduledTime;
}
if (THREAD_SUPPORTED_PLATFORMS.includes(platform)) {
const threadInputMethod = this.getNodeParameter('threadInputMethod', i, 'manual');
if (threadInputMethod === 'manual') {
const additionalPostsData = this.getNodeParameter('postContentAdditionalPosts', i, {
posts: [],
});
if (additionalPostsData.posts && additionalPostsData.posts.length > 0) {
options.body.post.content.additionalPosts = additionalPostsData.posts.map((post) => ({
text: post.text,
mediaUrls: post.mediaUrls
? post.mediaUrls
.split(',')
.map((url) => url.trim())
.filter(Boolean)
: [],
}));
}
}
else if (threadInputMethod === 'array') {
let threadPostsArray = this.getNodeParameter('threadPostsArray', i);
if (typeof threadPostsArray === 'string' && threadPostsArray.trim() !== '') {
try {
threadPostsArray = JSON.parse(threadPostsArray);
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Thread Posts must be a valid JSON array. Example: [{"text": "Post 1", "mediaUrls": []}, {"text": "Post 2", "mediaUrls": ["https://database.blotato.io/image.jpg"]}]', { itemIndex: i });
}
}
if (Array.isArray(threadPostsArray) && threadPostsArray.length > 0) {
options.body.post.content.additionalPosts = threadPostsArray.map((post) => ({
text: post.text || '',
mediaUrls: post.mediaUrls
? Array.isArray(post.mediaUrls)
? post.mediaUrls
: typeof post.mediaUrls === 'string'
? post.mediaUrls
.split(',')
.map((url) => url.trim())
.filter(Boolean)
: []
: [],
}));
}
}
}
switch (platform) {
case 'facebook':
options.body.post.target.pageId = this.getNodeParameter('facebookPageId', i).value;
if (postOptions.facebookMediaType) {
options.body.post.target.mediaType = postOptions.facebookMediaType;
}
break;
case 'tiktok':
options.body.post.target = {
...options.body.post.target,
privacyLevel: this.getNodeParameter('postCreateTiktokOptionPrivacyLevel', i),
disabledComments: this.getNodeParameter('postCreateTiktokOptionDisabledComments', i),
disabledDuet: this.getNodeParameter('postCreateTiktokOptionDisabledDuet', i),
disabledStitch: this.getNodeParameter('postCreateTiktokOptionDisabledStitch', i),
isBrandedContent: this.getNodeParameter('postCreateTiktokOptionIsBrandedContent', i),
isYourBrand: this.getNodeParameter('postCreateTiktokOptionIsYourBrand', i),
isAiGenerated: this.getNodeParameter('postCreateTiktokOptionIsAiGenerated', i),
autoAddMusic: this.getNodeParameter('postCreateTiktokOptionAutoAddMusic', i, undefined),
};
break;
case 'bluesky':
break;
case 'threads':
if (postOptions.threadsReplyControl) {
options.body.post.target.replyControl = postOptions.threadsReplyControl;
}
break;
case 'linkedin':
if (postOptions.linkedinPageId) {
const pageIdValue = typeof postOptions.linkedinPageId === 'object'
? postOptions.linkedinPageId.value
: postOptions.linkedinPageId;
if (pageIdValue) {
options.body.post.target.pageId = pageIdValue;
}
}
break;
case 'instagram':
if (postOptions.instagramMediaType) {
options.body.post.target.mediaType = postOptions.instagramMediaType;
}
break;
case 'pinterest':
options.body.post.target.boardId = this.getNodeParameter('pinterestBoardId', i).value;
const pinTitle = this.getNodeParameter('postCreatePinterestOptionTitle', i, '');
if (pinTitle) {
options.body.post.target.title = pinTitle;
}
if (postOptions.pinterestAltText) {
options.body.post.target.altText = postOptions.pinterestAltText;
}
if (postOptions.pinterestLink) {
options.body.post.target.link = postOptions.pinterestLink;
}
break;
case 'youtube':
options.body.post.target.title = this.getNodeParameter('postCreateYoutubeOptionTitle', i);
options.body.post.target.privacyStatus = this.getNodeParameter('postCreateYoutubeOptionPrivacyStatus', i);
options.body.post.target.shouldNotifySubscribers = this.getNodeParameter('postCreateYoutubeOptionShouldNotifySubscribers', i);
if (postOptions.youtubeMadeForKids !== undefined) {
options.body.post.target.isMadeForKids = postOptions.youtubeMadeForKids;
}
break;
case 'twitter':
break;
default:
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Platform "${platform}" is not supported for resource "post".`);
}
}
else {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Resource "${resource}" is not supported.`);
}
this.logger.debug(`Blotato API Request body:\n${JSON.stringify(options.body, null, 2)}`);
const credentials = await this.getCredentials('blotatoApi');
options.uri = credentials.server + options.uri;
const response = await this.helpers.requestWithAuthentication.call(this, 'blotatoApi', options);
returnData.push({ json: response, pairedItem: { item: i } });
}
return [returnData];
}
}
exports.Blotato = Blotato;
//# sourceMappingURL=Blotato.node.js.map