Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions apps/sim/blocks/blocks/tiktok.ts
Original file line number Diff line number Diff line change
@@ -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' },
},
}
2 changes: 2 additions & 0 deletions apps/sim/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -303,6 +304,7 @@ export const registry: Record<string, BlockConfig> = {
supabase: SupabaseBlock,
tavily: TavilyBlock,
telegram: TelegramBlock,
tiktok: TikTokBlock,
textract: TextractBlock,
thinking: ThinkingBlock,
tinybird: TinybirdBlock,
Expand Down
8 changes: 8 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions apps/sim/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions apps/sim/lib/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ShopifyIcon,
SlackIcon,
SpotifyIcon,
TikTokIcon,
TrelloIcon,
VertexIcon,
WealthboxIcon,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}`)
}
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/oauth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type OAuthProvider =
| 'wordpress'
| 'spotify'
| 'calcom'
| 'tiktok'

export type OAuthService =
| 'google'
Expand Down Expand Up @@ -83,6 +84,7 @@ export type OAuthService =
| 'wordpress'
| 'spotify'
| 'calcom'
| 'tiktok'

export interface OAuthProviderConfig {
name: string
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading