diff --git a/src/clients/tiktok.ts b/src/clients/tiktok.ts index a61a29f..20e7b21 100644 --- a/src/clients/tiktok.ts +++ b/src/clients/tiktok.ts @@ -82,43 +82,34 @@ export async function getUserProfile( }; } -export async function getUserVideos( - args: { max_count?: number; account?: string }, +export async function getCreatorInfo( + args: { account?: string }, customer?: Customer -): Promise> { +): Promise<{ + creator_username: string; + creator_nickname: string; + creator_avatar_url?: string; + privacy_level_options: string[]; + max_video_post_duration_sec: number; + comment_disabled: boolean; + duet_disabled: boolean; + stitch_disabled: boolean; +}> { const accessToken = await resolveToken(args, customer); - const fields = 'id,title,video_description,duration,cover_image_url,share_url,view_count,like_count,comment_count,share_count,create_time'; - const data = await tiktokRequest('/video/list/', accessToken, 'POST', { - max_count: Math.min(args.max_count ?? 10, 20), - fields: fields.split(','), - }); + const data = await tiktokRequest('/post/publish/creator_info/query/', accessToken, 'POST', {}); + const c = data; - return (data.videos ?? []).map((v: Record) => ({ - id: String(v.id ?? ''), - title: v.title as string | undefined, - video_description: v.video_description as string | undefined, - duration: Number(v.duration ?? 0), - cover_image_url: v.cover_image_url as string | undefined, - share_url: v.share_url as string | undefined, - view_count: Number(v.view_count ?? 0), - like_count: Number(v.like_count ?? 0), - comment_count: Number(v.comment_count ?? 0), - share_count: Number(v.share_count ?? 0), - create_time: Number(v.create_time ?? 0), - })); + return { + creator_username: c.creator_username ?? '', + creator_nickname: c.creator_nickname ?? '', + creator_avatar_url: c.creator_avatar_url, + privacy_level_options: c.privacy_level_options ?? [], + max_video_post_duration_sec: c.max_video_post_duration_sec ?? 0, + comment_disabled: c.comment_disabled ?? false, + duet_disabled: c.duet_disabled ?? false, + stitch_disabled: c.stitch_disabled ?? false, + }; } export async function createVideo( @@ -129,15 +120,21 @@ export async function createVideo( const auditArgs = { title: args.title }; const accessToken = await resolveToken(args, customer); - // Step 1: initialise upload - const init = await tiktokRequest('/post/video/init/', accessToken, 'POST', { + // Step 1: query creator info to get valid privacy levels (sandbox may not support PUBLIC_TO_EVERYONE) + const creatorInfo = await getCreatorInfo(args, customer); + const privacyLevel = creatorInfo.privacy_level_options.includes('PUBLIC_TO_EVERYONE') + ? 'PUBLIC_TO_EVERYONE' + : creatorInfo.privacy_level_options[0] ?? 'SELF_ONLY'; + + // Step 2: initialise upload + const init = await tiktokRequest('/post/publish/video/init/', accessToken, 'POST', { post_info: { title: args.title ?? '', description: args.description ?? '', disable_duet: false, disable_comment: false, disable_stitch: false, - privacy_level: 'PUBLIC_TO_EVERYONE', + privacy_level: privacyLevel, }, source_info: { source: 'PULL_FROM_URL', diff --git a/src/index.ts b/src/index.ts index 344b1fb..11e2234 100644 --- a/src/index.ts +++ b/src/index.ts @@ -594,8 +594,8 @@ app.get('/auth/tiktok/callback', async (req, res) => { `Open ID: ${openId || 'not returned'}`, `Scopes: ${scope || 'not returned'}`, `Expires in: ${expiresIn || 'not returned'} seconds`, - `Access token captured: ${accessToken ? `${accessToken.slice(0, 8)}...` : 'no'}`, - 'Next step: use this token with /api/connect/tiktok or set TIKTOK_DEFAULT_ACCESS_TOKEN for server-side publishing.', + `Access token: ${accessToken}`, + 'Copy this token and paste it to your AI assistant to store it for future TikTok API calls.', ...(state ? [`State: ${state}`] : []), ], }) @@ -1350,11 +1350,10 @@ app.get('/api/tiktok/profile', requireAuth, async (req, res) => { } }); -app.get('/api/tiktok/videos', requireAuth, async (req, res) => { - const max_count = req.query.max_count ? parseInt(req.query.max_count as string, 10) : undefined; +app.get('/api/tiktok/creator-info', requireAuth, async (req, res) => { const account = req.query.account as string | undefined; try { - const result = await handleToolCall('tiktok_get_videos', { max_count, account }); + const result = await handleToolCall('tiktok_get_creator_info', { account }); res.json(parseToolResult(result)); } catch (err) { res.status(500).json({ error: (err as Error).message }); diff --git a/src/manifest.ts b/src/manifest.ts index f0cdb3b..617bb07 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -681,15 +681,14 @@ export function getOpenApiSpec(serverUrl: string) { responses: { '200': { description: 'Profile info' } }, }, }, - '/api/tiktok/videos': { + '/api/tiktok/creator-info': { get: { - operationId: 'tiktok_get_videos', - summary: 'Get TikTok videos', + operationId: 'tiktok_get_creator_info', + summary: 'Get TikTok creator info', parameters: [ - { name: 'max_count', in: 'query', schema: { type: 'integer' } }, { name: 'account', in: 'query', schema: { type: 'string' } }, ], - responses: { '200': { description: 'Video list' } }, + responses: { '200': { description: 'Creator publishing info' } }, }, }, '/api/tiktok/video': { @@ -1822,31 +1821,30 @@ export function getManifest(serverUrl: string, authEnabled: boolean) { examples: [{ account: 'default' }], }, { - name: 'tiktok_get_videos', + name: 'tiktok_get_creator_info', category: 'tiktok', - description: 'List recent videos from the authenticated TikTok account', - when_to_use: 'User wants to see their recent TikTok videos and performance stats.', + description: 'Get TikTok creator publishing info including privacy levels and max video duration', + when_to_use: 'User wants to check their TikTok publishing capabilities before posting a video.', input_schema: { type: 'object', properties: { - max_count: { type: 'number', description: 'Max videos to return (default: 10, max: 20)' }, account: { type: 'string', description: 'Which TikTok account to use (default: "default")' }, }, }, returns: { - type: 'array', - items: { - type: 'object', - properties: { - id: { type: 'string' }, - title: { type: 'string' }, - view_count: { type: 'number' }, - like_count: { type: 'number' }, - share_url: { type: 'string' }, - }, + type: 'object', + properties: { + creator_username: { type: 'string' }, + creator_nickname: { type: 'string' }, + creator_avatar_url: { type: 'string' }, + privacy_level_options: { type: 'array', items: { type: 'string' } }, + max_video_post_duration_sec: { type: 'number' }, + comment_disabled: { type: 'boolean' }, + duet_disabled: { type: 'boolean' }, + stitch_disabled: { type: 'boolean' }, }, }, - examples: [{ max_count: 10, account: 'default' }], + examples: [{ account: 'default' }], }, { name: 'tiktok_create_video', diff --git a/src/tools.ts b/src/tools.ts index 61c05b4..35a0602 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -9,7 +9,7 @@ import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto a import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js'; import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createImagePost as createInstagramPost, createReel as createInstagramReel } from './clients/instagram.js'; import { searchTweets, getUserProfile, getUserTweets, createTweet, uploadVideoAndTweet } from './clients/twitter.js'; -import { getUserProfile as getTikTokProfile, getUserVideos, createVideo, getVideoStatus } from './clients/tiktok.js'; +import { getUserProfile as getTikTokProfile, getCreatorInfo, createVideo, getVideoStatus } from './clients/tiktok.js'; import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js'; import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.js'; @@ -577,13 +577,12 @@ export const tools: Tool[] = [ }, }, { - name: 'tiktok_get_videos', + name: 'tiktok_get_creator_info', description: - 'List recent videos from the authenticated TikTok account with view, like, comment, and share counts.', + 'Get TikTok creator publishing info: username, avatar, available privacy levels, and max video duration. Required before publishing.', inputSchema: { type: 'object', properties: { - max_count: { type: 'number', description: 'Max videos to return (default: 10, max: 20)' }, account: { type: 'string', description: 'Which TikTok account to use (default: "default")' }, }, }, @@ -1018,9 +1017,8 @@ export async function handleToolCall( }, customer); break; - case 'tiktok_get_videos': - result = await getUserVideos({ - max_count: (args.max_count as number) ?? 10, + case 'tiktok_get_creator_info': + result = await getCreatorInfo({ account: args.account as string | undefined, }, customer); break;