Skip to content

Commit 398d5a0

Browse files
feat(tiktok): add TikTok integration with Display API support
1 parent c6357f7 commit 398d5a0

File tree

13 files changed

+787
-0
lines changed

13 files changed

+787
-0
lines changed

apps/sim/blocks/blocks/tiktok.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { TikTokIcon } from '@/components/icons'
2+
import type { BlockConfig } from '@/blocks/types'
3+
import { AuthMode } from '@/blocks/types'
4+
import type { TikTokResponse } from '@/tools/tiktok/types'
5+
6+
export const TikTokBlock: BlockConfig<TikTokResponse> = {
7+
type: 'tiktok',
8+
name: 'TikTok',
9+
description: 'Access TikTok user profiles and videos',
10+
authMode: AuthMode.OAuth,
11+
longDescription:
12+
'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.',
13+
docsLink: 'https://docs.sim.ai/tools/tiktok',
14+
category: 'tools',
15+
bgColor: '#000000',
16+
icon: TikTokIcon,
17+
subBlocks: [
18+
// Operation selection
19+
{
20+
id: 'operation',
21+
title: 'Operation',
22+
type: 'dropdown',
23+
options: [
24+
{ label: 'Get User Info', id: 'get_user' },
25+
{ label: 'List Videos', id: 'list_videos' },
26+
{ label: 'Query Videos', id: 'query_videos' },
27+
],
28+
value: () => 'get_user',
29+
},
30+
31+
// TikTok OAuth Authentication
32+
{
33+
id: 'credential',
34+
title: 'TikTok Account',
35+
type: 'oauth-input',
36+
serviceId: 'tiktok',
37+
placeholder: 'Select TikTok account',
38+
required: true,
39+
},
40+
41+
// Get User Info specific fields
42+
{
43+
id: 'fields',
44+
title: 'Fields',
45+
type: 'short-input',
46+
placeholder: 'open_id,display_name,avatar_url,follower_count,video_count',
47+
condition: {
48+
field: 'operation',
49+
value: 'get_user',
50+
},
51+
},
52+
53+
// List Videos specific fields
54+
{
55+
id: 'maxCount',
56+
title: 'Max Count',
57+
type: 'short-input',
58+
placeholder: '20',
59+
condition: {
60+
field: 'operation',
61+
value: 'list_videos',
62+
},
63+
},
64+
{
65+
id: 'cursor',
66+
title: 'Cursor',
67+
type: 'short-input',
68+
placeholder: 'Pagination cursor from previous response',
69+
condition: {
70+
field: 'operation',
71+
value: 'list_videos',
72+
},
73+
},
74+
75+
// Query Videos specific fields
76+
{
77+
id: 'videoIds',
78+
title: 'Video IDs',
79+
type: 'long-input',
80+
placeholder: 'Comma-separated video IDs (e.g., 7077642457847994444,7080217258529732386)',
81+
condition: {
82+
field: 'operation',
83+
value: 'query_videos',
84+
},
85+
required: {
86+
field: 'operation',
87+
value: 'query_videos',
88+
},
89+
},
90+
],
91+
tools: {
92+
access: ['tiktok_get_user', 'tiktok_list_videos', 'tiktok_query_videos'],
93+
config: {
94+
tool: (inputs) => {
95+
const operation = inputs.operation || 'get_user'
96+
97+
switch (operation) {
98+
case 'list_videos':
99+
return 'tiktok_list_videos'
100+
case 'query_videos':
101+
return 'tiktok_query_videos'
102+
default:
103+
return 'tiktok_get_user'
104+
}
105+
},
106+
params: (inputs) => {
107+
const operation = inputs.operation || 'get_user'
108+
const { credential } = inputs
109+
110+
switch (operation) {
111+
case 'get_user':
112+
return {
113+
accessToken: credential,
114+
...(inputs.fields && { fields: inputs.fields }),
115+
}
116+
case 'list_videos':
117+
return {
118+
accessToken: credential,
119+
...(inputs.maxCount && { maxCount: Number(inputs.maxCount) }),
120+
...(inputs.cursor && { cursor: Number(inputs.cursor) }),
121+
}
122+
case 'query_videos':
123+
return {
124+
accessToken: credential,
125+
videoIds: inputs.videoIds
126+
? inputs.videoIds.split(',').map((id: string) => id.trim())
127+
: [],
128+
}
129+
default:
130+
return {
131+
accessToken: credential,
132+
}
133+
}
134+
},
135+
},
136+
},
137+
inputs: {
138+
operation: { type: 'string', description: 'Operation to perform' },
139+
credential: { type: 'string', description: 'TikTok access token' },
140+
fields: { type: 'string', description: 'Comma-separated list of user fields to return' },
141+
maxCount: { type: 'number', description: 'Maximum number of videos to return (1-20)' },
142+
cursor: { type: 'number', description: 'Pagination cursor from previous response' },
143+
videoIds: { type: 'string', description: 'Comma-separated list of video IDs to query' },
144+
},
145+
outputs: {
146+
// Get User outputs
147+
openId: { type: 'string', description: 'TikTok user ID' },
148+
displayName: { type: 'string', description: 'User display name' },
149+
avatarUrl: { type: 'string', description: 'Profile image URL' },
150+
bioDescription: { type: 'string', description: 'User bio' },
151+
followerCount: { type: 'number', description: 'Number of followers' },
152+
followingCount: { type: 'number', description: 'Number of accounts followed' },
153+
likesCount: { type: 'number', description: 'Total likes received' },
154+
videoCount: { type: 'number', description: 'Total public videos' },
155+
isVerified: { type: 'boolean', description: 'Whether account is verified' },
156+
// List/Query Videos outputs
157+
videos: { type: 'json', description: 'Array of video objects' },
158+
cursor: { type: 'number', description: 'Cursor for next page' },
159+
hasMore: { type: 'boolean', description: 'Whether more videos are available' },
160+
},
161+
}

apps/sim/blocks/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import { TavilyBlock } from '@/blocks/blocks/tavily'
131131
import { TelegramBlock } from '@/blocks/blocks/telegram'
132132
import { TextractBlock } from '@/blocks/blocks/textract'
133133
import { ThinkingBlock } from '@/blocks/blocks/thinking'
134+
import { TikTokBlock } from '@/blocks/blocks/tiktok'
134135
import { TinybirdBlock } from '@/blocks/blocks/tinybird'
135136
import { TranslateBlock } from '@/blocks/blocks/translate'
136137
import { TrelloBlock } from '@/blocks/blocks/trello'
@@ -303,6 +304,7 @@ export const registry: Record<string, BlockConfig> = {
303304
supabase: SupabaseBlock,
304305
tavily: TavilyBlock,
305306
telegram: TelegramBlock,
307+
tiktok: TikTokBlock,
306308
textract: TextractBlock,
307309
thinking: ThinkingBlock,
308310
tinybird: TinybirdBlock,

apps/sim/components/icons.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3472,6 +3472,14 @@ export function HumanInTheLoopIcon(props: SVGProps<SVGSVGElement>) {
34723472
)
34733473
}
34743474

3475+
export function TikTokIcon(props: SVGProps<SVGSVGElement>) {
3476+
return (
3477+
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'>
3478+
<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' />
3479+
</svg>
3480+
)
3481+
}
3482+
34753483
export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
34763484
return (
34773485
<svg

apps/sim/lib/auth/auth.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,6 +2495,61 @@ export const auth = betterAuth({
24952495
},
24962496
},
24972497

2498+
// TikTok provider
2499+
{
2500+
providerId: 'tiktok',
2501+
clientId: env.TIKTOK_CLIENT_ID as string,
2502+
clientSecret: env.TIKTOK_CLIENT_SECRET as string,
2503+
authorizationUrl: 'https://www.tiktok.com/v2/auth/authorize/',
2504+
tokenUrl: 'https://open.tiktokapis.com/v2/oauth/token/',
2505+
scopes: ['user.info.basic', 'user.info.profile', 'user.info.stats', 'video.list'],
2506+
responseType: 'code',
2507+
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/tiktok`,
2508+
getUserInfo: async (tokens) => {
2509+
try {
2510+
logger.info('Fetching TikTok user profile')
2511+
2512+
const response = await fetch(
2513+
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,union_id,avatar_url,display_name',
2514+
{
2515+
headers: {
2516+
Authorization: `Bearer ${tokens.accessToken}`,
2517+
},
2518+
}
2519+
)
2520+
2521+
if (!response.ok) {
2522+
logger.error('Failed to fetch TikTok user info', {
2523+
status: response.status,
2524+
statusText: response.statusText,
2525+
})
2526+
throw new Error('Failed to fetch user info')
2527+
}
2528+
2529+
const data = await response.json()
2530+
const profile = data.data?.user
2531+
2532+
if (!profile) {
2533+
logger.error('No user data in TikTok response')
2534+
return null
2535+
}
2536+
2537+
return {
2538+
id: `${profile.open_id}-${crypto.randomUUID()}`,
2539+
name: profile.display_name || 'TikTok User',
2540+
email: `${profile.open_id}@tiktok.user`,
2541+
emailVerified: false,
2542+
image: profile.avatar_url || undefined,
2543+
createdAt: new Date(),
2544+
updatedAt: new Date(),
2545+
}
2546+
} catch (error) {
2547+
logger.error('Error in TikTok getUserInfo:', { error })
2548+
return null
2549+
}
2550+
},
2551+
},
2552+
24982553
// WordPress.com provider
24992554
{
25002555
providerId: 'wordpress',

apps/sim/lib/core/config/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ export const env = createEnv({
244244
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
245245
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
246246
CALCOM_CLIENT_ID: z.string().optional(), // Cal.com OAuth client ID
247+
TIKTOK_CLIENT_ID: z.string().optional(), // TikTok OAuth client ID
248+
TIKTOK_CLIENT_SECRET: z.string().optional(), // TikTok OAuth client secret
247249

248250
// E2B Remote Code Execution
249251
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution

apps/sim/lib/oauth/oauth.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ShopifyIcon,
3333
SlackIcon,
3434
SpotifyIcon,
35+
TikTokIcon,
3536
TrelloIcon,
3637
VertexIcon,
3738
WealthboxIcon,
@@ -796,6 +797,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
796797
},
797798
defaultService: 'spotify',
798799
},
800+
tiktok: {
801+
name: 'TikTok',
802+
icon: TikTokIcon,
803+
services: {
804+
tiktok: {
805+
name: 'TikTok',
806+
description: 'Access TikTok user profiles and videos.',
807+
providerId: 'tiktok',
808+
icon: TikTokIcon,
809+
baseProviderIcon: TikTokIcon,
810+
scopes: ['user.info.basic', 'user.info.profile', 'user.info.stats', 'video.list'],
811+
},
812+
},
813+
defaultService: 'tiktok',
814+
},
799815
}
800816

801817
interface ProviderAuthConfig {
@@ -1135,6 +1151,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
11351151
supportsRefreshTokenRotation: false,
11361152
}
11371153
}
1154+
case 'tiktok': {
1155+
const { clientId, clientSecret } = getCredentials(
1156+
env.TIKTOK_CLIENT_ID,
1157+
env.TIKTOK_CLIENT_SECRET
1158+
)
1159+
return {
1160+
tokenEndpoint: 'https://open.tiktokapis.com/v2/oauth/token/',
1161+
clientId,
1162+
clientSecret,
1163+
useBasicAuth: false,
1164+
supportsRefreshTokenRotation: true,
1165+
}
1166+
}
11381167
default:
11391168
throw new Error(`Unsupported provider: ${provider}`)
11401169
}

apps/sim/lib/oauth/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type OAuthProvider =
4242
| 'wordpress'
4343
| 'spotify'
4444
| 'calcom'
45+
| 'tiktok'
4546

4647
export type OAuthService =
4748
| 'google'
@@ -83,6 +84,7 @@ export type OAuthService =
8384
| 'wordpress'
8485
| 'spotify'
8586
| 'calcom'
87+
| 'tiktok'
8688

8789
export interface OAuthProviderConfig {
8890
name: string

apps/sim/tools/registry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,6 +1625,7 @@ import {
16251625
} from '@/tools/telegram'
16261626
import { textractParserTool } from '@/tools/textract'
16271627
import { thinkingTool } from '@/tools/thinking'
1628+
import { tiktokGetUserTool, tiktokListVideosTool, tiktokQueryVideosTool } from '@/tools/tiktok'
16281629
import { tinybirdEventsTool, tinybirdQueryTool } from '@/tools/tinybird'
16291630
import {
16301631
trelloAddCommentTool,
@@ -2731,6 +2732,9 @@ export const tools: Record<string, ToolConfig> = {
27312732
telegram_send_photo: telegramSendPhotoTool,
27322733
telegram_send_video: telegramSendVideoTool,
27332734
telegram_send_document: telegramSendDocumentTool,
2735+
tiktok_get_user: tiktokGetUserTool,
2736+
tiktok_list_videos: tiktokListVideosTool,
2737+
tiktok_query_videos: tiktokQueryVideosTool,
27342738
clay_populate: clayPopulateTool,
27352739
clerk_list_users: clerkListUsersTool,
27362740
clerk_get_user: clerkGetUserTool,

0 commit comments

Comments
 (0)