feat(tiktok): add TikTok integration with Display API support

This commit is contained in:
BillLeoutsakosvl346
2026-02-01 22:25:51 +00:00
parent c6357f7438
commit 398d5a0ad6
13 changed files with 787 additions and 0 deletions

View File

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

View File

@@ -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<string, BlockConfig> = {
supabase: SupabaseBlock,
tavily: TavilyBlock,
telegram: TelegramBlock,
tiktok: TikTokBlock,
textract: TextractBlock,
thinking: ThinkingBlock,
tinybird: TinybirdBlock,

View File

@@ -3472,6 +3472,14 @@ export function HumanInTheLoopIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function TikTokIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'>
<path d='M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z' />
</svg>
)
}
export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -2495,6 +2495,61 @@ export const auth = betterAuth({
},
},
// TikTok provider
{
providerId: 'tiktok',
clientId: env.TIKTOK_CLIENT_ID as string,
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'],
responseType: 'code',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/tiktok`,
getUserInfo: async (tokens) => {
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',

View File

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

View File

@@ -32,6 +32,7 @@ import {
ShopifyIcon,
SlackIcon,
SpotifyIcon,
TikTokIcon,
TrelloIcon,
VertexIcon,
WealthboxIcon,
@@ -796,6 +797,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
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}`)
}

View File

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

View File

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

View File

@@ -0,0 +1,185 @@
import type { TikTokGetUserParams, TikTokGetUserResponse } from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokGetUserTool: ToolConfig<TikTokGetUserParams, TikTokGetUserResponse> = {
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<TikTokGetUserResponse> => {
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,
},
},
}

View File

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

View File

@@ -0,0 +1,131 @@
import type {
TikTokListVideosParams,
TikTokListVideosResponse,
TikTokVideo,
} from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokListVideosTool: ToolConfig<TikTokListVideosParams, TikTokListVideosResponse> = {
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<TikTokListVideosResponse> => {
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',
},
},
}

View File

@@ -0,0 +1,117 @@
import type {
TikTokQueryVideosParams,
TikTokQueryVideosResponse,
TikTokVideo,
} from '@/tools/tiktok/types'
import type { ToolConfig } from '@/tools/types'
export const tiktokQueryVideosTool: ToolConfig<TikTokQueryVideosParams, TikTokQueryVideosResponse> =
{
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<TikTokQueryVideosResponse> => {
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 },
},
},
},
},
}

View File

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