mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-06 04:35:03 -05:00
feat(tiktok): add TikTok integration with Display API support
This commit is contained in:
161
apps/sim/blocks/blocks/tiktok.ts
Normal file
161
apps/sim/blocks/blocks/tiktok.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
185
apps/sim/tools/tiktok/get_user.ts
Normal file
185
apps/sim/tools/tiktok/get_user.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
7
apps/sim/tools/tiktok/index.ts
Normal file
7
apps/sim/tools/tiktok/index.ts
Normal 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 }
|
||||
131
apps/sim/tools/tiktok/list_videos.ts
Normal file
131
apps/sim/tools/tiktok/list_videos.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
117
apps/sim/tools/tiktok/query_videos.ts
Normal file
117
apps/sim/tools/tiktok/query_videos.ts
Normal 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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
84
apps/sim/tools/tiktok/types.ts
Normal file
84
apps/sim/tools/tiktok/types.ts
Normal 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
|
||||
Reference in New Issue
Block a user