From 398d5a0ad6c01c131dd6e87bd65d26df453163cf Mon Sep 17 00:00:00 2001 From: BillLeoutsakosvl346 Date: Sun, 1 Feb 2026 22:25:51 +0000 Subject: [PATCH 1/3] feat(tiktok): add TikTok integration with Display API support --- apps/sim/blocks/blocks/tiktok.ts | 161 ++++++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 8 ++ apps/sim/lib/auth/auth.ts | 55 ++++++++ apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/oauth/oauth.ts | 29 ++++ apps/sim/lib/oauth/types.ts | 2 + apps/sim/tools/registry.ts | 4 + apps/sim/tools/tiktok/get_user.ts | 185 ++++++++++++++++++++++++++ apps/sim/tools/tiktok/index.ts | 7 + apps/sim/tools/tiktok/list_videos.ts | 131 ++++++++++++++++++ apps/sim/tools/tiktok/query_videos.ts | 117 ++++++++++++++++ apps/sim/tools/tiktok/types.ts | 84 ++++++++++++ 13 files changed, 787 insertions(+) create mode 100644 apps/sim/blocks/blocks/tiktok.ts create mode 100644 apps/sim/tools/tiktok/get_user.ts create mode 100644 apps/sim/tools/tiktok/index.ts create mode 100644 apps/sim/tools/tiktok/list_videos.ts create mode 100644 apps/sim/tools/tiktok/query_videos.ts create mode 100644 apps/sim/tools/tiktok/types.ts diff --git a/apps/sim/blocks/blocks/tiktok.ts b/apps/sim/blocks/blocks/tiktok.ts new file mode 100644 index 0000000000..1a6a1a4c58 --- /dev/null +++ b/apps/sim/blocks/blocks/tiktok.ts @@ -0,0 +1,161 @@ +import { TikTokIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { TikTokResponse } from '@/tools/tiktok/types' + +export const TikTokBlock: BlockConfig = { + type: 'tiktok', + name: 'TikTok', + description: 'Access TikTok user profiles and videos', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate TikTok into your workflow. Get user profile information including follower counts and video statistics. List and query videos with cover images, embed links, and metadata.', + docsLink: 'https://docs.sim.ai/tools/tiktok', + category: 'tools', + bgColor: '#000000', + icon: TikTokIcon, + subBlocks: [ + // Operation selection + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get User Info', id: 'get_user' }, + { label: 'List Videos', id: 'list_videos' }, + { label: 'Query Videos', id: 'query_videos' }, + ], + value: () => 'get_user', + }, + + // TikTok OAuth Authentication + { + id: 'credential', + title: 'TikTok Account', + type: 'oauth-input', + serviceId: 'tiktok', + placeholder: 'Select TikTok account', + required: true, + }, + + // Get User Info specific fields + { + id: 'fields', + title: 'Fields', + type: 'short-input', + placeholder: 'open_id,display_name,avatar_url,follower_count,video_count', + condition: { + field: 'operation', + value: 'get_user', + }, + }, + + // List Videos specific fields + { + id: 'maxCount', + title: 'Max Count', + type: 'short-input', + placeholder: '20', + condition: { + field: 'operation', + value: 'list_videos', + }, + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from previous response', + condition: { + field: 'operation', + value: 'list_videos', + }, + }, + + // Query Videos specific fields + { + id: 'videoIds', + title: 'Video IDs', + type: 'long-input', + placeholder: 'Comma-separated video IDs (e.g., 7077642457847994444,7080217258529732386)', + condition: { + field: 'operation', + value: 'query_videos', + }, + required: { + field: 'operation', + value: 'query_videos', + }, + }, + ], + tools: { + access: ['tiktok_get_user', 'tiktok_list_videos', 'tiktok_query_videos'], + config: { + tool: (inputs) => { + const operation = inputs.operation || 'get_user' + + switch (operation) { + case 'list_videos': + return 'tiktok_list_videos' + case 'query_videos': + return 'tiktok_query_videos' + default: + return 'tiktok_get_user' + } + }, + params: (inputs) => { + const operation = inputs.operation || 'get_user' + const { credential } = inputs + + switch (operation) { + case 'get_user': + return { + accessToken: credential, + ...(inputs.fields && { fields: inputs.fields }), + } + case 'list_videos': + return { + accessToken: credential, + ...(inputs.maxCount && { maxCount: Number(inputs.maxCount) }), + ...(inputs.cursor && { cursor: Number(inputs.cursor) }), + } + case 'query_videos': + return { + accessToken: credential, + videoIds: inputs.videoIds + ? inputs.videoIds.split(',').map((id: string) => id.trim()) + : [], + } + default: + return { + accessToken: credential, + } + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'TikTok access token' }, + fields: { type: 'string', description: 'Comma-separated list of user fields to return' }, + maxCount: { type: 'number', description: 'Maximum number of videos to return (1-20)' }, + cursor: { type: 'number', description: 'Pagination cursor from previous response' }, + videoIds: { type: 'string', description: 'Comma-separated list of video IDs to query' }, + }, + outputs: { + // Get User outputs + openId: { type: 'string', description: 'TikTok user ID' }, + displayName: { type: 'string', description: 'User display name' }, + avatarUrl: { type: 'string', description: 'Profile image URL' }, + bioDescription: { type: 'string', description: 'User bio' }, + followerCount: { type: 'number', description: 'Number of followers' }, + followingCount: { type: 'number', description: 'Number of accounts followed' }, + likesCount: { type: 'number', description: 'Total likes received' }, + videoCount: { type: 'number', description: 'Total public videos' }, + isVerified: { type: 'boolean', description: 'Whether account is verified' }, + // List/Query Videos outputs + videos: { type: 'json', description: 'Array of video objects' }, + cursor: { type: 'number', description: 'Cursor for next page' }, + hasMore: { type: 'boolean', description: 'Whether more videos are available' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 4fbaf27660..a38375031a 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -131,6 +131,7 @@ import { TavilyBlock } from '@/blocks/blocks/tavily' import { TelegramBlock } from '@/blocks/blocks/telegram' import { TextractBlock } from '@/blocks/blocks/textract' import { ThinkingBlock } from '@/blocks/blocks/thinking' +import { TikTokBlock } from '@/blocks/blocks/tiktok' import { TinybirdBlock } from '@/blocks/blocks/tinybird' import { TranslateBlock } from '@/blocks/blocks/translate' import { TrelloBlock } from '@/blocks/blocks/trello' @@ -303,6 +304,7 @@ export const registry: Record = { supabase: SupabaseBlock, tavily: TavilyBlock, telegram: TelegramBlock, + tiktok: TikTokBlock, textract: TextractBlock, thinking: ThinkingBlock, tinybird: TinybirdBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 2e1e487780..c7d30360c6 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3472,6 +3472,14 @@ export function HumanInTheLoopIcon(props: SVGProps) { ) } +export function TikTokIcon(props: SVGProps) { + return ( + + + + ) +} + export function TrelloIcon(props: SVGProps) { return ( { + try { + logger.info('Fetching TikTok user profile') + + const response = await fetch( + 'https://open.tiktokapis.com/v2/user/info/?fields=open_id,union_id,avatar_url,display_name', + { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + } + ) + + if (!response.ok) { + logger.error('Failed to fetch TikTok user info', { + status: response.status, + statusText: response.statusText, + }) + throw new Error('Failed to fetch user info') + } + + const data = await response.json() + const profile = data.data?.user + + if (!profile) { + logger.error('No user data in TikTok response') + return null + } + + return { + id: `${profile.open_id}-${crypto.randomUUID()}`, + name: profile.display_name || 'TikTok User', + email: `${profile.open_id}@tiktok.user`, + emailVerified: false, + image: profile.avatar_url || undefined, + createdAt: new Date(), + updatedAt: new Date(), + } + } catch (error) { + logger.error('Error in TikTok getUserInfo:', { error }) + return null + } + }, + }, + // WordPress.com provider { providerId: 'wordpress', diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 6bd9df299e..f663e2043a 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -244,6 +244,8 @@ export const env = createEnv({ SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret CALCOM_CLIENT_ID: z.string().optional(), // Cal.com OAuth client ID + TIKTOK_CLIENT_ID: z.string().optional(), // TikTok OAuth client ID + TIKTOK_CLIENT_SECRET: z.string().optional(), // TikTok OAuth client secret // E2B Remote Code Execution E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 7bb11ca357..113121ab8d 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -32,6 +32,7 @@ import { ShopifyIcon, SlackIcon, SpotifyIcon, + TikTokIcon, TrelloIcon, VertexIcon, WealthboxIcon, @@ -796,6 +797,21 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'spotify', }, + tiktok: { + name: 'TikTok', + icon: TikTokIcon, + services: { + tiktok: { + name: 'TikTok', + description: 'Access TikTok user profiles and videos.', + providerId: 'tiktok', + icon: TikTokIcon, + baseProviderIcon: TikTokIcon, + scopes: ['user.info.basic', 'user.info.profile', 'user.info.stats', 'video.list'], + }, + }, + defaultService: 'tiktok', + }, } interface ProviderAuthConfig { @@ -1135,6 +1151,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } + case 'tiktok': { + const { clientId, clientSecret } = getCredentials( + env.TIKTOK_CLIENT_ID, + env.TIKTOK_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://open.tiktokapis.com/v2/oauth/token/', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: true, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index 961c7a0b1c..af06ad0e46 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -42,6 +42,7 @@ export type OAuthProvider = | 'wordpress' | 'spotify' | 'calcom' + | 'tiktok' export type OAuthService = | 'google' @@ -83,6 +84,7 @@ export type OAuthService = | 'wordpress' | 'spotify' | 'calcom' + | 'tiktok' export interface OAuthProviderConfig { name: string diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6018a6f866..c34c3267e0 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1625,6 +1625,7 @@ import { } from '@/tools/telegram' import { textractParserTool } from '@/tools/textract' import { thinkingTool } from '@/tools/thinking' +import { tiktokGetUserTool, tiktokListVideosTool, tiktokQueryVideosTool } from '@/tools/tiktok' import { tinybirdEventsTool, tinybirdQueryTool } from '@/tools/tinybird' import { trelloAddCommentTool, @@ -2731,6 +2732,9 @@ export const tools: Record = { telegram_send_photo: telegramSendPhotoTool, telegram_send_video: telegramSendVideoTool, telegram_send_document: telegramSendDocumentTool, + tiktok_get_user: tiktokGetUserTool, + tiktok_list_videos: tiktokListVideosTool, + tiktok_query_videos: tiktokQueryVideosTool, clay_populate: clayPopulateTool, clerk_list_users: clerkListUsersTool, clerk_get_user: clerkGetUserTool, diff --git a/apps/sim/tools/tiktok/get_user.ts b/apps/sim/tools/tiktok/get_user.ts new file mode 100644 index 0000000000..ecc4b873b2 --- /dev/null +++ b/apps/sim/tools/tiktok/get_user.ts @@ -0,0 +1,185 @@ +import type { TikTokGetUserParams, TikTokGetUserResponse } from '@/tools/tiktok/types' +import type { ToolConfig } from '@/tools/types' + +export const tiktokGetUserTool: ToolConfig = { + id: 'tiktok_get_user', + name: 'TikTok Get User', + description: + 'Get the authenticated TikTok user profile information including display name, avatar, bio, follower count, and video statistics.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['user.info.basic'], + }, + + params: { + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + default: + 'open_id,union_id,avatar_url,avatar_url_100,avatar_large_url,display_name,bio_description,profile_deep_link,is_verified,username,follower_count,following_count,likes_count,video_count', + description: + 'Comma-separated list of fields to return. Available: open_id, union_id, avatar_url, avatar_url_100, avatar_large_url, display_name, bio_description, profile_deep_link, is_verified, username, follower_count, following_count, likes_count, video_count', + }, + }, + + request: { + url: (params: TikTokGetUserParams) => { + const fields = + params.fields || + 'open_id,union_id,avatar_url,avatar_url_100,avatar_large_url,display_name,bio_description,profile_deep_link,is_verified,username,follower_count,following_count,likes_count,video_count' + return `https://open.tiktokapis.com/v2/user/info/?fields=${encodeURIComponent(fields)}` + }, + method: 'GET', + headers: (params: TikTokGetUserParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error?.code !== 'ok' && data.error?.code) { + return { + success: false, + output: { + openId: '', + unionId: null, + displayName: '', + avatarUrl: null, + avatarUrl100: null, + avatarLargeUrl: null, + bioDescription: null, + profileDeepLink: null, + isVerified: null, + username: null, + followerCount: null, + followingCount: null, + likesCount: null, + videoCount: null, + }, + error: data.error?.message || 'Failed to fetch user info', + } + } + + const user = data.data?.user + + if (!user) { + return { + success: false, + output: { + openId: '', + unionId: null, + displayName: '', + avatarUrl: null, + avatarUrl100: null, + avatarLargeUrl: null, + bioDescription: null, + profileDeepLink: null, + isVerified: null, + username: null, + followerCount: null, + followingCount: null, + likesCount: null, + videoCount: null, + }, + error: 'No user data returned', + } + } + + return { + success: true, + output: { + openId: user.open_id ?? '', + unionId: user.union_id ?? null, + displayName: user.display_name ?? '', + avatarUrl: user.avatar_url ?? null, + avatarUrl100: user.avatar_url_100 ?? null, + avatarLargeUrl: user.avatar_large_url ?? null, + bioDescription: user.bio_description ?? null, + profileDeepLink: user.profile_deep_link ?? null, + isVerified: user.is_verified ?? null, + username: user.username ?? null, + followerCount: user.follower_count ?? null, + followingCount: user.following_count ?? null, + likesCount: user.likes_count ?? null, + videoCount: user.video_count ?? null, + }, + } + }, + + outputs: { + openId: { + type: 'string', + description: 'Unique TikTok user ID for this application', + }, + unionId: { + type: 'string', + description: 'Unique TikTok user ID across all apps from the same developer', + optional: true, + }, + displayName: { + type: 'string', + description: 'User display name', + }, + avatarUrl: { + type: 'string', + description: 'Profile image URL', + optional: true, + }, + avatarUrl100: { + type: 'string', + description: 'Profile image URL (100x100)', + optional: true, + }, + avatarLargeUrl: { + type: 'string', + description: 'Profile image URL (large)', + optional: true, + }, + bioDescription: { + type: 'string', + description: 'User bio description', + optional: true, + }, + profileDeepLink: { + type: 'string', + description: 'Deep link to user TikTok profile', + optional: true, + }, + isVerified: { + type: 'boolean', + description: 'Whether the account is verified', + optional: true, + }, + username: { + type: 'string', + description: 'TikTok username', + optional: true, + }, + followerCount: { + type: 'number', + description: 'Number of followers', + optional: true, + }, + followingCount: { + type: 'number', + description: 'Number of accounts the user follows', + optional: true, + }, + likesCount: { + type: 'number', + description: 'Total likes received across all videos', + optional: true, + }, + videoCount: { + type: 'number', + description: 'Total number of public videos', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/tiktok/index.ts b/apps/sim/tools/tiktok/index.ts new file mode 100644 index 0000000000..e471cefea8 --- /dev/null +++ b/apps/sim/tools/tiktok/index.ts @@ -0,0 +1,7 @@ +import { tiktokGetUserTool } from '@/tools/tiktok/get_user' +import { tiktokListVideosTool } from '@/tools/tiktok/list_videos' +import { tiktokQueryVideosTool } from '@/tools/tiktok/query_videos' + +export { tiktokGetUserTool } +export { tiktokListVideosTool } +export { tiktokQueryVideosTool } diff --git a/apps/sim/tools/tiktok/list_videos.ts b/apps/sim/tools/tiktok/list_videos.ts new file mode 100644 index 0000000000..4fd29d24a8 --- /dev/null +++ b/apps/sim/tools/tiktok/list_videos.ts @@ -0,0 +1,131 @@ +import type { + TikTokListVideosParams, + TikTokListVideosResponse, + TikTokVideo, +} from '@/tools/tiktok/types' +import type { ToolConfig } from '@/tools/types' + +export const tiktokListVideosTool: ToolConfig = { + id: 'tiktok_list_videos', + name: 'TikTok List Videos', + description: + "Get a list of the authenticated user's TikTok videos with cover images, titles, and metadata. Supports pagination.", + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.list'], + }, + + params: { + maxCount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + default: 20, + description: 'Maximum number of videos to return (1-20)', + }, + cursor: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination (from previous response)', + }, + }, + + request: { + url: () => + 'https://open.tiktokapis.com/v2/video/list/?fields=id,title,cover_image_url,embed_link,duration,create_time,share_url,video_description,width,height', + method: 'POST', + headers: (params: TikTokListVideosParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: TikTokListVideosParams) => ({ + max_count: params.maxCount || 20, + ...(params.cursor !== undefined && { cursor: params.cursor }), + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error?.code !== 'ok' && data.error?.code) { + return { + success: false, + output: { + videos: [], + cursor: null, + hasMore: false, + }, + error: data.error?.message || 'Failed to fetch videos', + } + } + + const videos: TikTokVideo[] = (data.data?.videos ?? []).map((video: any) => ({ + id: video.id ?? '', + title: video.title ?? null, + coverImageUrl: video.cover_image_url ?? null, + embedLink: video.embed_link ?? null, + duration: video.duration ?? null, + createTime: video.create_time ?? null, + shareUrl: video.share_url ?? null, + videoDescription: video.video_description ?? null, + width: video.width ?? null, + height: video.height ?? null, + })) + + return { + success: true, + output: { + videos, + cursor: data.data?.cursor ?? null, + hasMore: data.data?.has_more ?? false, + }, + } + }, + + outputs: { + videos: { + type: 'array', + description: 'List of TikTok videos', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Video ID' }, + title: { type: 'string', description: 'Video title', optional: true }, + coverImageUrl: { + type: 'string', + description: 'Cover image URL (may expire)', + optional: true, + }, + embedLink: { type: 'string', description: 'Embeddable video URL', optional: true }, + duration: { type: 'number', description: 'Video duration in seconds', optional: true }, + createTime: { + type: 'number', + description: 'Unix timestamp when video was created', + optional: true, + }, + shareUrl: { type: 'string', description: 'Shareable video URL', optional: true }, + videoDescription: { + type: 'string', + description: 'Video description/caption', + optional: true, + }, + width: { type: 'number', description: 'Video width in pixels', optional: true }, + height: { type: 'number', description: 'Video height in pixels', optional: true }, + }, + }, + }, + cursor: { + type: 'number', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more videos to fetch', + }, + }, +} diff --git a/apps/sim/tools/tiktok/query_videos.ts b/apps/sim/tools/tiktok/query_videos.ts new file mode 100644 index 0000000000..29c174f86e --- /dev/null +++ b/apps/sim/tools/tiktok/query_videos.ts @@ -0,0 +1,117 @@ +import type { + TikTokQueryVideosParams, + TikTokQueryVideosResponse, + TikTokVideo, +} from '@/tools/tiktok/types' +import type { ToolConfig } from '@/tools/types' + +export const tiktokQueryVideosTool: ToolConfig = + { + id: 'tiktok_query_videos', + name: 'TikTok Query Videos', + description: + 'Query specific TikTok videos by their IDs to get fresh metadata including cover images, embed links, and video details.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.list'], + }, + + params: { + videoIds: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Array of video IDs to query (maximum 20)', + items: { + type: 'string', + description: 'TikTok video ID', + }, + }, + }, + + request: { + url: () => + 'https://open.tiktokapis.com/v2/video/query/?fields=id,title,cover_image_url,embed_link,duration,create_time,share_url,video_description,width,height', + method: 'POST', + headers: (params: TikTokQueryVideosParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: TikTokQueryVideosParams) => ({ + filters: { + video_ids: params.videoIds, + }, + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error?.code !== 'ok' && data.error?.code) { + return { + success: false, + output: { + videos: [], + }, + error: data.error?.message || 'Failed to query videos', + } + } + + const videos: TikTokVideo[] = (data.data?.videos ?? []).map((video: any) => ({ + id: video.id ?? '', + title: video.title ?? null, + coverImageUrl: video.cover_image_url ?? null, + embedLink: video.embed_link ?? null, + duration: video.duration ?? null, + createTime: video.create_time ?? null, + shareUrl: video.share_url ?? null, + videoDescription: video.video_description ?? null, + width: video.width ?? null, + height: video.height ?? null, + })) + + return { + success: true, + output: { + videos, + }, + } + }, + + outputs: { + videos: { + type: 'array', + description: 'List of queried TikTok videos', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Video ID' }, + title: { type: 'string', description: 'Video title', optional: true }, + coverImageUrl: { + type: 'string', + description: 'Cover image URL (fresh URL)', + optional: true, + }, + embedLink: { type: 'string', description: 'Embeddable video URL', optional: true }, + duration: { type: 'number', description: 'Video duration in seconds', optional: true }, + createTime: { + type: 'number', + description: 'Unix timestamp when video was created', + optional: true, + }, + shareUrl: { type: 'string', description: 'Shareable video URL', optional: true }, + videoDescription: { + type: 'string', + description: 'Video description/caption', + optional: true, + }, + width: { type: 'number', description: 'Video width in pixels', optional: true }, + height: { type: 'number', description: 'Video height in pixels', optional: true }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/tiktok/types.ts b/apps/sim/tools/tiktok/types.ts new file mode 100644 index 0000000000..a5fc0b8a15 --- /dev/null +++ b/apps/sim/tools/tiktok/types.ts @@ -0,0 +1,84 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Base params that include OAuth access token + */ +export interface TikTokBaseParams { + accessToken: string +} + +/** + * Get User Info + */ +export interface TikTokGetUserParams extends TikTokBaseParams { + fields?: string +} + +export interface TikTokGetUserResponse extends ToolResponse { + output: { + openId: string + unionId: string | null + displayName: string + avatarUrl: string | null + avatarUrl100: string | null + avatarLargeUrl: string | null + bioDescription: string | null + profileDeepLink: string | null + isVerified: boolean | null + username: string | null + followerCount: number | null + followingCount: number | null + likesCount: number | null + videoCount: number | null + } +} + +/** + * List Videos + */ +export interface TikTokListVideosParams extends TikTokBaseParams { + maxCount?: number + cursor?: number +} + +export interface TikTokVideo { + id: string + title: string | null + coverImageUrl: string | null + embedLink: string | null + duration: number | null + createTime: number | null + shareUrl: string | null + videoDescription: string | null + width: number | null + height: number | null +} + +export interface TikTokListVideosResponse extends ToolResponse { + output: { + videos: TikTokVideo[] + cursor: number | null + hasMore: boolean + } +} + +/** + * Query Videos + */ +export interface TikTokQueryVideosParams extends TikTokBaseParams { + videoIds: string[] +} + +export interface TikTokQueryVideosResponse extends ToolResponse { + output: { + videos: TikTokVideo[] + } +} + +/** + * Union type of all TikTok responses + */ +export type TikTokResponse = + | TikTokGetUserResponse + | TikTokListVideosResponse + | TikTokQueryVideosResponse From 501f71142aa0b668261919821893ed7fc2d6cb82 Mon Sep 17 00:00:00 2001 From: BillLeoutsakosvl346 Date: Mon, 2 Feb 2026 22:57:36 +0000 Subject: [PATCH 2/3] fix: replace any with Record per code review Co-authored-by: Cursor --- apps/sim/tools/tiktok/list_videos.ts | 2 +- apps/sim/tools/tiktok/query_videos.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/tiktok/list_videos.ts b/apps/sim/tools/tiktok/list_videos.ts index 4fd29d24a8..736739dad0 100644 --- a/apps/sim/tools/tiktok/list_videos.ts +++ b/apps/sim/tools/tiktok/list_videos.ts @@ -63,7 +63,7 @@ export const tiktokListVideosTool: ToolConfig ({ + const videos: TikTokVideo[] = (data.data?.videos ?? []).map((video: Record) => ({ id: video.id ?? '', title: video.title ?? null, coverImageUrl: video.cover_image_url ?? null, diff --git a/apps/sim/tools/tiktok/query_videos.ts b/apps/sim/tools/tiktok/query_videos.ts index 29c174f86e..14d1725145 100644 --- a/apps/sim/tools/tiktok/query_videos.ts +++ b/apps/sim/tools/tiktok/query_videos.ts @@ -60,7 +60,7 @@ export const tiktokQueryVideosTool: ToolConfig ({ + const videos: TikTokVideo[] = (data.data?.videos ?? []).map((video: Record) => ({ id: video.id ?? '', title: video.title ?? null, coverImageUrl: video.cover_image_url ?? null, From c02d2d10ce378969def48bbf8f5d24a242b97e36 Mon Sep 17 00:00:00 2001 From: BillLeoutsakosvl346 Date: Tue, 3 Feb 2026 00:04:42 +0000 Subject: [PATCH 3/3] feat(tiktok): add Content Posting API support- Add video.publish scope to OAuth configuration- Add Query Creator Info tool to check posting permissions- Add Direct Post Video tool to publish videos from URL- Add Get Post Status tool to track post progress- Update TikTok block with new operations and UI fields- Add type definitions for all new operations --- apps/sim/blocks/blocks/tiktok.ts | 138 ++++++++++++++++- apps/sim/lib/auth/auth.ts | 8 +- apps/sim/lib/oauth/oauth.ts | 10 +- apps/sim/tools/registry.ts | 12 +- apps/sim/tools/tiktok/direct_post_video.ts | 156 ++++++++++++++++++++ apps/sim/tools/tiktok/get_post_status.ts | 97 ++++++++++++ apps/sim/tools/tiktok/index.ts | 6 + apps/sim/tools/tiktok/list_videos.ts | 26 ++-- apps/sim/tools/tiktok/query_creator_info.ts | 127 ++++++++++++++++ apps/sim/tools/tiktok/query_videos.ts | 26 ++-- apps/sim/tools/tiktok/types.ts | 56 +++++++ 11 files changed, 630 insertions(+), 32 deletions(-) create mode 100644 apps/sim/tools/tiktok/direct_post_video.ts create mode 100644 apps/sim/tools/tiktok/get_post_status.ts create mode 100644 apps/sim/tools/tiktok/query_creator_info.ts diff --git a/apps/sim/blocks/blocks/tiktok.ts b/apps/sim/blocks/blocks/tiktok.ts index 1a6a1a4c58..dba69a5db2 100644 --- a/apps/sim/blocks/blocks/tiktok.ts +++ b/apps/sim/blocks/blocks/tiktok.ts @@ -6,10 +6,10 @@ import type { TikTokResponse } from '@/tools/tiktok/types' export const TikTokBlock: BlockConfig = { type: 'tiktok', name: 'TikTok', - description: 'Access TikTok user profiles and videos', + description: 'Access TikTok user profiles, videos, and publish content', authMode: AuthMode.OAuth, longDescription: - 'Integrate TikTok into your workflow. Get user profile information including follower counts and video statistics. List and query videos with cover images, embed links, and metadata.', + 'Integrate TikTok into your workflow. Get user profile information including follower counts and video statistics. List and query videos with cover images, embed links, and metadata. Publish videos directly to TikTok from public URLs.', docsLink: 'https://docs.sim.ai/tools/tiktok', category: 'tools', bgColor: '#000000', @@ -24,6 +24,9 @@ export const TikTokBlock: BlockConfig = { { label: 'Get User Info', id: 'get_user' }, { label: 'List Videos', id: 'list_videos' }, { label: 'Query Videos', id: 'query_videos' }, + { label: 'Query Creator Info', id: 'query_creator_info' }, + { label: 'Direct Post Video', id: 'direct_post_video' }, + { label: 'Get Post Status', id: 'get_post_status' }, ], value: () => 'get_user', }, @@ -87,9 +90,88 @@ export const TikTokBlock: BlockConfig = { value: 'query_videos', }, }, + + // Direct Post Video specific fields + { + id: 'videoUrl', + title: 'Video URL', + type: 'short-input', + placeholder: 'https://example.com/video.mp4', + condition: { + field: 'operation', + value: 'direct_post_video', + }, + required: { + field: 'operation', + value: 'direct_post_video', + }, + }, + { + id: 'title', + title: 'Caption', + type: 'long-input', + placeholder: 'Video caption with #hashtags and @mentions', + condition: { + field: 'operation', + value: 'direct_post_video', + }, + }, + { + id: 'privacyLevel', + title: 'Privacy Level', + type: 'dropdown', + options: [ + { label: 'Public', id: 'PUBLIC_TO_EVERYONE' }, + { label: 'Friends', id: 'MUTUAL_FOLLOW_FRIENDS' }, + { label: 'Followers', id: 'FOLLOWER_OF_CREATOR' }, + { label: 'Only Me', id: 'SELF_ONLY' }, + ], + value: () => 'PUBLIC_TO_EVERYONE', + condition: { + field: 'operation', + value: 'direct_post_video', + }, + }, + { + id: 'disableComment', + title: 'Disable Comments', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { + field: 'operation', + value: 'direct_post_video', + }, + }, + + // Get Post Status specific fields + { + id: 'publishId', + title: 'Publish ID', + type: 'short-input', + placeholder: 'v_pub_file~v2-1.123456789', + condition: { + field: 'operation', + value: 'get_post_status', + }, + required: { + field: 'operation', + value: 'get_post_status', + }, + }, ], tools: { - access: ['tiktok_get_user', 'tiktok_list_videos', 'tiktok_query_videos'], + access: [ + 'tiktok_get_user', + 'tiktok_list_videos', + 'tiktok_query_videos', + 'tiktok_query_creator_info', + 'tiktok_direct_post_video', + 'tiktok_get_post_status', + ], config: { tool: (inputs) => { const operation = inputs.operation || 'get_user' @@ -99,6 +181,12 @@ export const TikTokBlock: BlockConfig = { return 'tiktok_list_videos' case 'query_videos': return 'tiktok_query_videos' + case 'query_creator_info': + return 'tiktok_query_creator_info' + case 'direct_post_video': + return 'tiktok_direct_post_video' + case 'get_post_status': + return 'tiktok_get_post_status' default: return 'tiktok_get_user' } @@ -126,6 +214,23 @@ export const TikTokBlock: BlockConfig = { ? inputs.videoIds.split(',').map((id: string) => id.trim()) : [], } + case 'query_creator_info': + return { + accessToken: credential, + } + case 'direct_post_video': + return { + accessToken: credential, + videoUrl: inputs.videoUrl || '', + privacyLevel: inputs.privacyLevel || 'PUBLIC_TO_EVERYONE', + ...(inputs.title && { title: inputs.title }), + ...(inputs.disableComment === 'true' && { disableComment: true }), + } + case 'get_post_status': + return { + accessToken: credential, + publishId: inputs.publishId || '', + } default: return { accessToken: credential, @@ -141,6 +246,11 @@ export const TikTokBlock: BlockConfig = { maxCount: { type: 'number', description: 'Maximum number of videos to return (1-20)' }, cursor: { type: 'number', description: 'Pagination cursor from previous response' }, videoIds: { type: 'string', description: 'Comma-separated list of video IDs to query' }, + videoUrl: { type: 'string', description: 'Public URL of the video to post' }, + title: { type: 'string', description: 'Video caption/description' }, + privacyLevel: { type: 'string', description: 'Privacy level for the video' }, + disableComment: { type: 'string', description: 'Whether to disable comments' }, + publishId: { type: 'string', description: 'Publish ID to check status for' }, }, outputs: { // Get User outputs @@ -155,7 +265,27 @@ export const TikTokBlock: BlockConfig = { isVerified: { type: 'boolean', description: 'Whether account is verified' }, // List/Query Videos outputs videos: { type: 'json', description: 'Array of video objects' }, - cursor: { type: 'number', description: 'Cursor for next page' }, hasMore: { type: 'boolean', description: 'Whether more videos are available' }, + // Query Creator Info outputs + creatorAvatarUrl: { type: 'string', description: 'Creator avatar URL' }, + creatorUsername: { type: 'string', description: 'Creator username' }, + creatorNickname: { type: 'string', description: 'Creator nickname' }, + privacyLevelOptions: { type: 'json', description: 'Available privacy levels for posting' }, + commentDisabled: { type: 'boolean', description: 'Whether comments are disabled by default' }, + duetDisabled: { type: 'boolean', description: 'Whether duets are disabled by default' }, + stitchDisabled: { type: 'boolean', description: 'Whether stitches are disabled by default' }, + maxVideoPostDurationSec: { type: 'number', description: 'Max video duration in seconds' }, + // Direct Post Video outputs + publishId: { type: 'string', description: 'Publish ID for tracking post status' }, + // Get Post Status outputs + status: { + type: 'string', + description: 'Post status (PROCESSING_DOWNLOAD, PUBLISH_COMPLETE, FAILED)', + }, + failReason: { type: 'string', description: 'Reason for failure if status is FAILED' }, + publiclyAvailablePostId: { + type: 'json', + description: 'Array of public post IDs when published', + }, }, } diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index e1a96786ce..937df10e32 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -2502,7 +2502,13 @@ export const auth = betterAuth({ clientSecret: env.TIKTOK_CLIENT_SECRET as string, authorizationUrl: 'https://www.tiktok.com/v2/auth/authorize/', tokenUrl: 'https://open.tiktokapis.com/v2/oauth/token/', - scopes: ['user.info.basic', 'user.info.profile', 'user.info.stats', 'video.list'], + scopes: [ + 'user.info.basic', + 'user.info.profile', + 'user.info.stats', + 'video.list', + 'video.publish', + ], responseType: 'code', redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/tiktok`, getUserInfo: async (tokens) => { diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 113121ab8d..f1d3419f4f 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -803,11 +803,17 @@ export const OAUTH_PROVIDERS: Record = { services: { tiktok: { name: 'TikTok', - description: 'Access TikTok user profiles and videos.', + description: 'Access TikTok user profiles, videos, and publish content.', providerId: 'tiktok', icon: TikTokIcon, baseProviderIcon: TikTokIcon, - scopes: ['user.info.basic', 'user.info.profile', 'user.info.stats', 'video.list'], + scopes: [ + 'user.info.basic', + 'user.info.profile', + 'user.info.stats', + 'video.list', + 'video.publish', + ], }, }, defaultService: 'tiktok', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index c34c3267e0..99c4087195 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1625,7 +1625,14 @@ import { } from '@/tools/telegram' import { textractParserTool } from '@/tools/textract' import { thinkingTool } from '@/tools/thinking' -import { tiktokGetUserTool, tiktokListVideosTool, tiktokQueryVideosTool } from '@/tools/tiktok' +import { + tiktokDirectPostVideoTool, + tiktokGetPostStatusTool, + tiktokGetUserTool, + tiktokListVideosTool, + tiktokQueryCreatorInfoTool, + tiktokQueryVideosTool, +} from '@/tools/tiktok' import { tinybirdEventsTool, tinybirdQueryTool } from '@/tools/tinybird' import { trelloAddCommentTool, @@ -2735,6 +2742,9 @@ export const tools: Record = { tiktok_get_user: tiktokGetUserTool, tiktok_list_videos: tiktokListVideosTool, tiktok_query_videos: tiktokQueryVideosTool, + tiktok_query_creator_info: tiktokQueryCreatorInfoTool, + tiktok_direct_post_video: tiktokDirectPostVideoTool, + tiktok_get_post_status: tiktokGetPostStatusTool, clay_populate: clayPopulateTool, clerk_list_users: clerkListUsersTool, clerk_get_user: clerkGetUserTool, diff --git a/apps/sim/tools/tiktok/direct_post_video.ts b/apps/sim/tools/tiktok/direct_post_video.ts new file mode 100644 index 0000000000..0acbde86fa --- /dev/null +++ b/apps/sim/tools/tiktok/direct_post_video.ts @@ -0,0 +1,156 @@ +import type { + TikTokDirectPostVideoParams, + TikTokDirectPostVideoResponse, +} from '@/tools/tiktok/types' +import type { ToolConfig } from '@/tools/types' + +export const tiktokDirectPostVideoTool: ToolConfig< + TikTokDirectPostVideoParams, + TikTokDirectPostVideoResponse +> = { + id: 'tiktok_direct_post_video', + name: 'TikTok Direct Post Video', + description: + 'Publish a video to TikTok from a public URL. TikTok will fetch the video from the provided URL and post it to the authenticated user account. Rate limit: 6 requests per minute per user.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.publish'], + }, + + params: { + videoUrl: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Public URL of the video to post. Must be accessible by TikTok servers.', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Video caption/description. Maximum 2200 characters.', + }, + privacyLevel: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Privacy level for the video. Options: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Note: Unaudited apps may be restricted to SELF_ONLY.', + }, + disableDuet: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Disable duet for this video. Defaults to false.', + }, + disableStitch: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Disable stitch for this video. Defaults to false.', + }, + disableComment: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Disable comments for this video. Defaults to false.', + }, + videoCoverTimestampMs: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Timestamp in milliseconds to use as the video cover image.', + }, + isAigc: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Set to true if the video is AI-generated content (AIGC).', + }, + }, + + request: { + url: () => 'https://open.tiktokapis.com/v2/post/publish/video/init/', + method: 'POST', + headers: (params: TikTokDirectPostVideoParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json; charset=UTF-8', + }), + body: (params: TikTokDirectPostVideoParams) => { + const postInfo: Record = { + privacy_level: params.privacyLevel, + } + + if (params.title) { + postInfo.title = params.title + } + if (params.disableDuet !== undefined) { + postInfo.disable_duet = params.disableDuet + } + if (params.disableStitch !== undefined) { + postInfo.disable_stitch = params.disableStitch + } + if (params.disableComment !== undefined) { + postInfo.disable_comment = params.disableComment + } + if (params.videoCoverTimestampMs !== undefined) { + postInfo.video_cover_timestamp_ms = params.videoCoverTimestampMs + } + if (params.isAigc !== undefined) { + postInfo.is_aigc = params.isAigc + } + + return { + post_info: postInfo, + source_info: { + source: 'PULL_FROM_URL', + video_url: params.videoUrl, + }, + } + }, + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error?.code !== 'ok' && data.error?.code) { + return { + success: false, + output: { + publishId: '', + }, + error: data.error?.message || 'Failed to initiate video post', + } + } + + const publishId = data.data?.publish_id + + if (!publishId) { + return { + success: false, + output: { + publishId: '', + }, + error: 'No publish ID returned', + } + } + + return { + success: true, + output: { + publishId: publishId, + }, + } + }, + + outputs: { + publishId: { + type: 'string', + description: + 'Unique identifier for tracking the post status. Use this with the Get Post Status tool to check if the video was successfully published.', + }, + }, +} diff --git a/apps/sim/tools/tiktok/get_post_status.ts b/apps/sim/tools/tiktok/get_post_status.ts new file mode 100644 index 0000000000..a524d35baf --- /dev/null +++ b/apps/sim/tools/tiktok/get_post_status.ts @@ -0,0 +1,97 @@ +import type { TikTokGetPostStatusParams, TikTokGetPostStatusResponse } from '@/tools/tiktok/types' +import type { ToolConfig } from '@/tools/types' + +export const tiktokGetPostStatusTool: ToolConfig< + TikTokGetPostStatusParams, + TikTokGetPostStatusResponse +> = { + id: 'tiktok_get_post_status', + name: 'TikTok Get Post Status', + description: + 'Check the status of a video post initiated with Direct Post Video. Use the publishId returned from the post request to track progress.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.publish'], + }, + + params: { + publishId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The publish ID returned from the Direct Post Video tool.', + }, + }, + + request: { + url: () => 'https://open.tiktokapis.com/v2/post/publish/status/fetch/', + method: 'POST', + headers: (params: TikTokGetPostStatusParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json; charset=UTF-8', + }), + body: (params: TikTokGetPostStatusParams) => ({ + publish_id: params.publishId, + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error?.code !== 'ok' && data.error?.code) { + return { + success: false, + output: { + status: '', + failReason: null, + publiclyAvailablePostId: [], + }, + error: data.error?.message || 'Failed to fetch post status', + } + } + + const statusData = data.data + + if (!statusData) { + return { + success: false, + output: { + status: '', + failReason: null, + publiclyAvailablePostId: [], + }, + error: 'No status data returned', + } + } + + return { + success: true, + output: { + status: statusData.status ?? '', + failReason: statusData.fail_reason ?? null, + publiclyAvailablePostId: statusData.publicaly_available_post_id ?? [], + }, + } + }, + + outputs: { + status: { + type: 'string', + description: + 'Current status of the post. Values: PROCESSING_DOWNLOAD (TikTok is downloading the video), PUBLISH_COMPLETE (successfully posted), FAILED (check failReason).', + }, + failReason: { + type: 'string', + description: 'Reason for failure if status is FAILED. Null otherwise.', + optional: true, + }, + publiclyAvailablePostId: { + type: 'array', + description: + 'Array of public post IDs once the video is published. Can be used to construct the TikTok video URL.', + }, + }, +} diff --git a/apps/sim/tools/tiktok/index.ts b/apps/sim/tools/tiktok/index.ts index e471cefea8..ca1d5b1530 100644 --- a/apps/sim/tools/tiktok/index.ts +++ b/apps/sim/tools/tiktok/index.ts @@ -1,7 +1,13 @@ +import { tiktokDirectPostVideoTool } from '@/tools/tiktok/direct_post_video' +import { tiktokGetPostStatusTool } from '@/tools/tiktok/get_post_status' import { tiktokGetUserTool } from '@/tools/tiktok/get_user' import { tiktokListVideosTool } from '@/tools/tiktok/list_videos' +import { tiktokQueryCreatorInfoTool } from '@/tools/tiktok/query_creator_info' import { tiktokQueryVideosTool } from '@/tools/tiktok/query_videos' export { tiktokGetUserTool } export { tiktokListVideosTool } export { tiktokQueryVideosTool } +export { tiktokQueryCreatorInfoTool } +export { tiktokDirectPostVideoTool } +export { tiktokGetPostStatusTool } diff --git a/apps/sim/tools/tiktok/list_videos.ts b/apps/sim/tools/tiktok/list_videos.ts index 736739dad0..76e2a58b83 100644 --- a/apps/sim/tools/tiktok/list_videos.ts +++ b/apps/sim/tools/tiktok/list_videos.ts @@ -63,18 +63,20 @@ export const tiktokListVideosTool: ToolConfig) => ({ - id: video.id ?? '', - title: video.title ?? null, - coverImageUrl: video.cover_image_url ?? null, - embedLink: video.embed_link ?? null, - duration: video.duration ?? null, - createTime: video.create_time ?? null, - shareUrl: video.share_url ?? null, - videoDescription: video.video_description ?? null, - width: video.width ?? null, - height: video.height ?? null, - })) + const videos: TikTokVideo[] = (data.data?.videos ?? []).map( + (video: Record) => ({ + id: video.id ?? '', + title: video.title ?? null, + coverImageUrl: video.cover_image_url ?? null, + embedLink: video.embed_link ?? null, + duration: video.duration ?? null, + createTime: video.create_time ?? null, + shareUrl: video.share_url ?? null, + videoDescription: video.video_description ?? null, + width: video.width ?? null, + height: video.height ?? null, + }) + ) return { success: true, diff --git a/apps/sim/tools/tiktok/query_creator_info.ts b/apps/sim/tools/tiktok/query_creator_info.ts new file mode 100644 index 0000000000..d67ee7ce65 --- /dev/null +++ b/apps/sim/tools/tiktok/query_creator_info.ts @@ -0,0 +1,127 @@ +import type { + TikTokQueryCreatorInfoParams, + TikTokQueryCreatorInfoResponse, +} from '@/tools/tiktok/types' +import type { ToolConfig } from '@/tools/types' + +export const tiktokQueryCreatorInfoTool: ToolConfig< + TikTokQueryCreatorInfoParams, + TikTokQueryCreatorInfoResponse +> = { + id: 'tiktok_query_creator_info', + name: 'TikTok Query Creator Info', + description: + 'Check if the authenticated TikTok user can post content and retrieve their available privacy options, interaction settings, and maximum video duration.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'tiktok', + requiredScopes: ['video.publish'], + }, + + params: {}, + + request: { + url: () => 'https://open.tiktokapis.com/v2/post/publish/creator_info/query/', + method: 'POST', + headers: (params: TikTokQueryCreatorInfoParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error?.code !== 'ok' && data.error?.code) { + return { + success: false, + output: { + creatorAvatarUrl: null, + creatorUsername: null, + creatorNickname: null, + privacyLevelOptions: [], + commentDisabled: false, + duetDisabled: false, + stitchDisabled: false, + maxVideoPostDurationSec: null, + }, + error: data.error?.message || 'Failed to query creator info', + } + } + + const creatorInfo = data.data + + if (!creatorInfo) { + return { + success: false, + output: { + creatorAvatarUrl: null, + creatorUsername: null, + creatorNickname: null, + privacyLevelOptions: [], + commentDisabled: false, + duetDisabled: false, + stitchDisabled: false, + maxVideoPostDurationSec: null, + }, + error: 'No creator info returned', + } + } + + return { + success: true, + output: { + creatorAvatarUrl: creatorInfo.creator_avatar_url ?? null, + creatorUsername: creatorInfo.creator_username ?? null, + creatorNickname: creatorInfo.creator_nickname ?? null, + privacyLevelOptions: creatorInfo.privacy_level_options ?? [], + commentDisabled: creatorInfo.comment_disabled ?? false, + duetDisabled: creatorInfo.duet_disabled ?? false, + stitchDisabled: creatorInfo.stitch_disabled ?? false, + maxVideoPostDurationSec: creatorInfo.max_video_post_duration_sec ?? null, + }, + } + }, + + outputs: { + creatorAvatarUrl: { + type: 'string', + description: 'URL of the creator avatar', + optional: true, + }, + creatorUsername: { + type: 'string', + description: 'TikTok username of the creator', + optional: true, + }, + creatorNickname: { + type: 'string', + description: 'Display name/nickname of the creator', + optional: true, + }, + privacyLevelOptions: { + type: 'array', + description: + 'Available privacy levels for posting (e.g., PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY)', + }, + commentDisabled: { + type: 'boolean', + description: 'Whether the creator has disabled comments by default', + }, + duetDisabled: { + type: 'boolean', + description: 'Whether the creator has disabled duets by default', + }, + stitchDisabled: { + type: 'boolean', + description: 'Whether the creator has disabled stitches by default', + }, + maxVideoPostDurationSec: { + type: 'number', + description: 'Maximum allowed video duration in seconds', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/tiktok/query_videos.ts b/apps/sim/tools/tiktok/query_videos.ts index 14d1725145..9769b76c42 100644 --- a/apps/sim/tools/tiktok/query_videos.ts +++ b/apps/sim/tools/tiktok/query_videos.ts @@ -60,18 +60,20 @@ export const tiktokQueryVideosTool: ToolConfig) => ({ - id: video.id ?? '', - title: video.title ?? null, - coverImageUrl: video.cover_image_url ?? null, - embedLink: video.embed_link ?? null, - duration: video.duration ?? null, - createTime: video.create_time ?? null, - shareUrl: video.share_url ?? null, - videoDescription: video.video_description ?? null, - width: video.width ?? null, - height: video.height ?? null, - })) + const videos: TikTokVideo[] = (data.data?.videos ?? []).map( + (video: Record) => ({ + id: video.id ?? '', + title: video.title ?? null, + coverImageUrl: video.cover_image_url ?? null, + embedLink: video.embed_link ?? null, + duration: video.duration ?? null, + createTime: video.create_time ?? null, + shareUrl: video.share_url ?? null, + videoDescription: video.video_description ?? null, + width: video.width ?? null, + height: video.height ?? null, + }) + ) return { success: true, diff --git a/apps/sim/tools/tiktok/types.ts b/apps/sim/tools/tiktok/types.ts index a5fc0b8a15..f0264bb7f5 100644 --- a/apps/sim/tools/tiktok/types.ts +++ b/apps/sim/tools/tiktok/types.ts @@ -75,6 +75,59 @@ export interface TikTokQueryVideosResponse extends ToolResponse { } } +/** + * Query Creator Info - Check posting permissions and get privacy options + */ +export interface TikTokQueryCreatorInfoParams extends TikTokBaseParams {} + +export interface TikTokQueryCreatorInfoResponse extends ToolResponse { + output: { + creatorAvatarUrl: string | null + creatorUsername: string | null + creatorNickname: string | null + privacyLevelOptions: string[] + commentDisabled: boolean + duetDisabled: boolean + stitchDisabled: boolean + maxVideoPostDurationSec: number | null + } +} + +/** + * Direct Post Video - Publish video from URL to TikTok + */ +export interface TikTokDirectPostVideoParams extends TikTokBaseParams { + videoUrl: string + title?: string + privacyLevel: string + disableDuet?: boolean + disableStitch?: boolean + disableComment?: boolean + videoCoverTimestampMs?: number + isAigc?: boolean +} + +export interface TikTokDirectPostVideoResponse extends ToolResponse { + output: { + publishId: string + } +} + +/** + * Get Post Status - Check status of a published post + */ +export interface TikTokGetPostStatusParams extends TikTokBaseParams { + publishId: string +} + +export interface TikTokGetPostStatusResponse extends ToolResponse { + output: { + status: string + failReason: string | null + publiclyAvailablePostId: string[] + } +} + /** * Union type of all TikTok responses */ @@ -82,3 +135,6 @@ export type TikTokResponse = | TikTokGetUserResponse | TikTokListVideosResponse | TikTokQueryVideosResponse + | TikTokQueryCreatorInfoResponse + | TikTokDirectPostVideoResponse + | TikTokGetPostStatusResponse