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

This commit is contained in:
BillLeoutsakosvl346
2026-02-03 00:04:42 +00:00
parent 501f71142a
commit c02d2d10ce
11 changed files with 630 additions and 32 deletions

View File

@@ -6,10 +6,10 @@ import type { TikTokResponse } from '@/tools/tiktok/types'
export const TikTokBlock: BlockConfig<TikTokResponse> = {
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<TikTokResponse> = {
{ 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<TikTokResponse> = {
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<TikTokResponse> = {
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<TikTokResponse> = {
? 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<TikTokResponse> = {
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<TikTokResponse> = {
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',
},
},
}

View File

@@ -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) => {

View File

@@ -803,11 +803,17 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
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',

View File

@@ -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<string, ToolConfig> = {
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,

View File

@@ -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<string, unknown> = {
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<TikTokDirectPostVideoResponse> => {
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.',
},
},
}

View File

@@ -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<TikTokGetPostStatusResponse> => {
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.',
},
},
}

View File

@@ -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 }

View File

@@ -63,18 +63,20 @@ export const tiktokListVideosTool: ToolConfig<TikTokListVideosParams, TikTokList
}
}
const videos: TikTokVideo[] = (data.data?.videos ?? []).map((video: Record<string, unknown>) => ({
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<string, unknown>) => ({
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,

View File

@@ -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<TikTokQueryCreatorInfoResponse> => {
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,
},
},
}

View File

@@ -60,18 +60,20 @@ export const tiktokQueryVideosTool: ToolConfig<TikTokQueryVideosParams, TikTokQu
}
}
const videos: TikTokVideo[] = (data.data?.videos ?? []).map((video: Record<string, unknown>) => ({
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<string, unknown>) => ({
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,

View File

@@ -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