From 8afe7f2e1a0ae85ad83ecefab30ce62b91e37c1b Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Wed, 15 Jul 2026 21:59:57 -0700 Subject: [PATCH 1/3] Simplify TikTok to draft-only Content Posting Remove Direct Post stubs and legacy Share Kit webhook triggers that Sim does not ship, and fix draft upload response handling so real TikTok errors are not misreported as size-limit failures. Co-authored-by: Cursor --- .../tools/tiktok/publish-video/route.test.ts | 31 ----- .../api/tools/tiktok/publish-video/route.ts | 19 +-- apps/sim/blocks/blocks/tiktok.test.ts | 9 +- apps/sim/blocks/blocks/tiktok.ts | 16 +-- .../lib/api/contracts/tiktok-tools.test.ts | 56 +------- apps/sim/lib/api/contracts/tiktok-tools.ts | 30 +--- apps/sim/lib/core/utils/stream-limits.ts | 1 + apps/sim/lib/oauth/utils.ts | 2 +- .../sim/lib/webhooks/providers/tiktok.test.ts | 11 +- apps/sim/lib/webhooks/providers/tiktok.ts | 12 +- apps/sim/tools/index.ts | 5 +- apps/sim/tools/registry.ts | 2 - .../tools/tiktok/direct_post_video.test.ts | 32 ----- apps/sim/tools/tiktok/direct_post_video.ts | 128 ------------------ apps/sim/tools/tiktok/get_post_status.ts | 2 +- apps/sim/tools/tiktok/index.ts | 2 - apps/sim/tools/tiktok/query_creator_info.ts | 2 +- apps/sim/tools/tiktok/types.ts | 22 +-- apps/sim/tools/tiktok/upload_video_draft.ts | 1 - apps/sim/tools/tiktok/utils.test.ts | 15 +- apps/sim/tools/tiktok/utils.ts | 18 ++- apps/sim/triggers/registry.ts | 4 - apps/sim/triggers/tiktok/index.ts | 2 - .../triggers/tiktok/post_publish_complete.ts | 2 +- apps/sim/triggers/tiktok/utils.ts | 11 -- .../tiktok/video_publish_completed.ts | 28 ---- .../triggers/tiktok/video_upload_failed.ts | 25 ---- 27 files changed, 54 insertions(+), 434 deletions(-) delete mode 100644 apps/sim/tools/tiktok/direct_post_video.test.ts delete mode 100644 apps/sim/tools/tiktok/direct_post_video.ts delete mode 100644 apps/sim/triggers/tiktok/video_publish_completed.ts delete mode 100644 apps/sim/triggers/tiktok/video_upload_failed.ts diff --git a/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts b/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts index 4bd583a2a92..cb18a42b0a0 100644 --- a/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts +++ b/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts @@ -67,7 +67,6 @@ describe('POST /api/tools/tiktok/publish-video', () => { vi.stubGlobal('fetch', fetchMock) const request = createMockRequest('POST', { accessToken: 'access-token', - mode: 'draft', file, }) @@ -104,35 +103,6 @@ describe('POST /api/tools/tiktok/publish-video', () => { }) }) - it('keeps Direct Post unavailable until per-post approval exists', async () => { - const fetchMock = vi.fn() - vi.stubGlobal('fetch', fetchMock) - - const response = await POST( - createMockRequest('POST', { - accessToken: 'access-token', - mode: 'direct', - file, - musicUsageConsent: 'accepted', - postInfo: { - privacy_level: 'SELF_ONLY', - disable_duet: true, - disable_stitch: true, - disable_comment: true, - brand_content_toggle: false, - }, - }) - ) - - expect(response.status).toBe(400) - await expect(response.json()).resolves.toMatchObject({ - success: false, - error: expect.stringMatching(/Direct Post is not available/), - }) - expect(mockGetStoredVideoSize).not.toHaveBeenCalled() - expect(fetchMock).not.toHaveBeenCalled() - }) - it('returns 413 when the storage object exceeds the relay limit', async () => { mockGetStoredVideoSize.mockRejectedValue( new PayloadSizeLimitError({ @@ -145,7 +115,6 @@ describe('POST /api/tools/tiktok/publish-video', () => { const response = await POST( createMockRequest('POST', { accessToken: 'access-token', - mode: 'draft', file, }) ) diff --git a/apps/sim/app/api/tools/tiktok/publish-video/route.ts b/apps/sim/app/api/tools/tiktok/publish-video/route.ts index ec1cf21c55b..8e2239782be 100644 --- a/apps/sim/app/api/tools/tiktok/publish-video/route.ts +++ b/apps/sim/app/api/tools/tiktok/publish-video/route.ts @@ -54,17 +54,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const data = parsed.data.body - if (data.mode === 'direct') { - return NextResponse.json( - { - success: false, - error: - 'TikTok Direct Post is not available until per-post human approval is implemented. Use Upload Video Draft instead.', - }, - { status: 400 } - ) - } - let userFile: UserFile try { userFile = processSingleFileToUserFile(data.file, requestId, logger) @@ -146,7 +135,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (initError) { logger.error(`[${requestId}] TikTok init failed`, { error: initError }) return NextResponse.json( - { success: false, error: initError.message || 'Failed to initialize TikTok upload' }, + { + success: false, + error: + initError.message || + initError.code || + 'Failed to initialize TikTok upload', + }, { status: initResponse.status >= 400 ? initResponse.status : 502 } ) } diff --git a/apps/sim/blocks/blocks/tiktok.test.ts b/apps/sim/blocks/blocks/tiktok.test.ts index a9aec3ca9eb..81a81c40e09 100644 --- a/apps/sim/blocks/blocks/tiktok.test.ts +++ b/apps/sim/blocks/blocks/tiktok.test.ts @@ -8,9 +8,8 @@ describe('TikTokBlock', () => { const buildParams = TikTokBlock.tools.config.params! const selectTool = TikTokBlock.tools.config.tool! - it('keeps direct posting unavailable until one-use human approval is supported', () => { - expect(TikTokBlock.tools.access).not.toContain('tiktok_direct_post_video') - expect(() => selectTool({ operation: 'tiktok_direct_post_video' })).toThrow( + it('rejects unsupported operations', () => { + expect(() => selectTool({ operation: 'tiktok_unknown_operation' })).toThrow( 'Unsupported TikTok operation' ) }) @@ -37,15 +36,11 @@ describe('TikTokBlock', () => { const inputs = { operation: 'tiktok_upload_video_draft', file, - privacyLevel: 'PUBLIC_TO_EVERYONE', - musicUsageConsent: 'accepted', videoIds: 'stale-video-id', } const finalInputs = { ...inputs, ...buildParams(inputs) } expect(finalInputs.file).toEqual(file) - expect(finalInputs.privacyLevel).toBeUndefined() - expect(finalInputs.musicUsageConsent).toBeUndefined() expect(finalInputs.videoIds).toBeUndefined() }) diff --git a/apps/sim/blocks/blocks/tiktok.ts b/apps/sim/blocks/blocks/tiktok.ts index 0cddbde45d6..15be9e5f13c 100644 --- a/apps/sim/blocks/blocks/tiktok.ts +++ b/apps/sim/blocks/blocks/tiktok.ts @@ -22,16 +22,6 @@ const TIKTOK_OPERATION_INPUT_KEYS = [ 'videoIds', 'file', 'publishId', - 'title', - 'privacyLevel', - 'disableDuet', - 'disableStitch', - 'disableComment', - 'videoCoverTimestampMs', - 'isAigc', - 'brandContentToggle', - 'brandOrganicToggle', - 'musicUsageConsent', ] as const export const TikTokBlock: BlockConfig = { @@ -47,7 +37,7 @@ export const TikTokBlock: BlockConfig = { bgColor: '#000000', icon: TikTokIcon, triggerAllowed: true, - hideFromToolbar: true, + hideFromToolbar: false, subBlocks: [ { @@ -168,8 +158,6 @@ export const TikTokBlock: BlockConfig = { ...getTrigger('tiktok_post_inbox_delivered').subBlocks, ...getTrigger('tiktok_post_publicly_available').subBlocks, ...getTrigger('tiktok_post_no_longer_public').subBlocks, - ...getTrigger('tiktok_video_publish_completed').subBlocks, - ...getTrigger('tiktok_video_upload_failed').subBlocks, ...getTrigger('tiktok_authorization_removed').subBlocks, ], @@ -181,8 +169,6 @@ export const TikTokBlock: BlockConfig = { 'tiktok_post_inbox_delivered', 'tiktok_post_publicly_available', 'tiktok_post_no_longer_public', - 'tiktok_video_publish_completed', - 'tiktok_video_upload_failed', 'tiktok_authorization_removed', ], }, diff --git a/apps/sim/lib/api/contracts/tiktok-tools.test.ts b/apps/sim/lib/api/contracts/tiktok-tools.test.ts index ee24ba93bd3..eed99eb4cd9 100644 --- a/apps/sim/lib/api/contracts/tiktok-tools.test.ts +++ b/apps/sim/lib/api/contracts/tiktok-tools.test.ts @@ -9,77 +9,27 @@ const file = { } describe('tiktokPublishVideoBodySchema', () => { - it('accepts a draft without direct-post metadata', () => { + it('accepts a draft upload body', () => { expect( tiktokPublishVideoBodySchema.safeParse({ accessToken: 'token', - mode: 'draft', file, }).success ).toBe(true) }) - it('requires direct-post privacy and commercial-content disclosure', () => { + it('rejects a body without an access token', () => { expect( tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - mode: 'direct', - file, - postInfo: { privacy_level: 'SELF_ONLY' }, - }).success - ).toBe(false) - }) - - it('accepts documented direct-post metadata', () => { - expect( - tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - mode: 'direct', file, - musicUsageConsent: 'accepted', - postInfo: { - title: 'A test video', - privacy_level: 'SELF_ONLY', - disable_duet: true, - disable_stitch: true, - disable_comment: true, - brand_content_toggle: false, - }, - }).success - ).toBe(true) - }) - - it('rejects direct posting without explicit music usage consent', () => { - expect( - tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - mode: 'direct', - file, - postInfo: { - privacy_level: 'SELF_ONLY', - disable_duet: true, - disable_stitch: true, - disable_comment: true, - brand_content_toggle: false, - }, }).success ).toBe(false) }) - it('rejects an undocumented privacy value', () => { + it('rejects a body without a file', () => { expect( tiktokPublishVideoBodySchema.safeParse({ accessToken: 'token', - mode: 'direct', - file, - musicUsageConsent: 'accepted', - postInfo: { - privacy_level: 'PRIVATE', - disable_duet: true, - disable_stitch: true, - disable_comment: true, - brand_content_toggle: false, - }, }).success ).toBe(false) }) diff --git a/apps/sim/lib/api/contracts/tiktok-tools.ts b/apps/sim/lib/api/contracts/tiktok-tools.ts index 2023b29de6b..1a3f2e5ac66 100644 --- a/apps/sim/lib/api/contracts/tiktok-tools.ts +++ b/apps/sim/lib/api/contracts/tiktok-tools.ts @@ -6,39 +6,11 @@ import { } from '@/lib/api/contracts/types' import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' -const tiktokPublishVideoPostInfoSchema = z.object({ - title: z.string().max(2200).optional(), - privacy_level: z.enum([ - 'PUBLIC_TO_EVERYONE', - 'MUTUAL_FOLLOW_FRIENDS', - 'FOLLOWER_OF_CREATOR', - 'SELF_ONLY', - ]), - disable_duet: z.boolean(), - disable_stitch: z.boolean(), - disable_comment: z.boolean(), - video_cover_timestamp_ms: z.number().int().nonnegative().optional(), - is_aigc: z.boolean().optional(), - brand_content_toggle: z.boolean(), - brand_organic_toggle: z.boolean().optional(), -}) - -const tiktokPublishVideoBaseSchema = z.object({ +export const tiktokPublishVideoBodySchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), file: RawFileInputSchema, }) -export const tiktokPublishVideoBodySchema = z.discriminatedUnion('mode', [ - tiktokPublishVideoBaseSchema.extend({ - mode: z.literal('direct'), - postInfo: tiktokPublishVideoPostInfoSchema, - musicUsageConsent: z.literal('accepted'), - }), - tiktokPublishVideoBaseSchema.extend({ - mode: z.literal('draft'), - }), -]) - export const tiktokPublishVideoResponseSchema = z.object({ success: z.boolean(), output: z.object({ publishId: z.string() }).optional(), diff --git a/apps/sim/lib/core/utils/stream-limits.ts b/apps/sim/lib/core/utils/stream-limits.ts index 24d5182a9a8..ebbfe4463f8 100644 --- a/apps/sim/lib/core/utils/stream-limits.ts +++ b/apps/sim/lib/core/utils/stream-limits.ts @@ -255,6 +255,7 @@ export async function readResponseToBufferWithLimit( } if ( !options.allowNoBodyFallback && + !options.preferTextFallback && !response.body && contentLength === null && (response.arrayBuffer || response.text) diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 7960aad288e..45035d4603b 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -126,7 +126,7 @@ export const SCOPE_DESCRIPTIONS: Record = { 'user.info.basic': "Read a user's profile info (open id, avatar, display name)", 'user.info.profile': "Read a user's profile info (bio, verification status, username)", 'user.info.stats': "Read a user's stats (follower, following, likes, and video counts)", - 'video.publish': "Directly post content to a user's TikTok profile", + 'video.publish': "Query creator posting capabilities for a user's TikTok account", 'video.upload': "Share content to a creator's account as a draft for further edit and post", 'video.list': "Read a user's public TikTok videos", diff --git a/apps/sim/lib/webhooks/providers/tiktok.test.ts b/apps/sim/lib/webhooks/providers/tiktok.test.ts index 3d40d8060e1..f0b82bd79ec 100644 --- a/apps/sim/lib/webhooks/providers/tiktok.test.ts +++ b/apps/sim/lib/webhooks/providers/tiktok.test.ts @@ -276,16 +276,7 @@ describe('tiktokHandler', () => { ).toBe('post.publish.publicly_available:act.user:pub-1:post-1') }) - it('extractIdempotencyId falls back to share_id then create_time', () => { - expect( - tiktokHandler.extractIdempotencyId!({ - event: 'video.publish.completed', - user_openid: 'act.user', - create_time: 99, - content: '{"share_id":"share-1"}', - }) - ).toBe('video.publish.completed:act.user:share-1') - + it('extractIdempotencyId falls back to create_time', () => { expect( tiktokHandler.extractIdempotencyId!({ event: 'authorization.removed', diff --git a/apps/sim/lib/webhooks/providers/tiktok.ts b/apps/sim/lib/webhooks/providers/tiktok.ts index 0abedaa7e03..4196a11a2fd 100644 --- a/apps/sim/lib/webhooks/providers/tiktok.ts +++ b/apps/sim/lib/webhooks/providers/tiktok.ts @@ -201,15 +201,6 @@ export const tiktokHandler: WebhookProviderHandler = { } } - if (event === 'video.publish.completed' || event === 'video.upload.failed') { - return { - input: { - ...commonInput, - shareId: stringField(content, 'share_id') ?? null, - }, - } - } - return { input: commonInput } }, @@ -224,7 +215,6 @@ export const tiktokHandler: WebhookProviderHandler = { const content = parseTikTokContent(envelope.content) const publishId = stringField(content, 'publish_id') const postId = stringField(content, 'post_id') - const shareId = stringField(content, 'share_id') const createTime = typeof envelope.create_time === 'number' || typeof envelope.create_time === 'string' ? String(envelope.create_time) @@ -236,7 +226,7 @@ export const tiktokHandler: WebhookProviderHandler = { } else if (event === 'post.publish.complete' && publishId && createTime) { unique = `${publishId}:${createTime}` } else { - unique = publishId ?? shareId ?? createTime + unique = publishId ?? createTime } if (!unique) return null diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 5e597c53422..470b788afff 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -1946,13 +1946,16 @@ async function executeToolRequest( // Success case: use transformResponse if available if (tool.transformResponse) { try { - // Create a mock response object that provides the methods transformResponse needs + // Forward the real body stream. Some transformResponse helpers (e.g. TikTok) + // read via readResponseTextWithLimit, which requires `.body` (or Content-Length) + // and otherwise mis-reports a false "response exceeded maximum size" error. const mockResponse = { ok: response.ok, status: response.status, statusText: response.statusText, headers: response.headers, url: fullUrl, + body: response.body, json: () => response.json(), text: () => response.text(), arrayBuffer: () => response.arrayBuffer(), diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 28318b68ef0..f7eecc8741a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -4130,7 +4130,6 @@ import { thriveUpdateUserTool, } from '@/tools/thrive' import { - tiktokDirectPostVideoTool, tiktokGetPostStatusTool, tiktokGetUserTool, tiktokListVideosTool, @@ -7294,7 +7293,6 @@ export const tools: Record = { thrive_remove_user_tags: thriveRemoveUserTagsTool, thrive_update_user_skills: thriveUpdateUserSkillsTool, thrive_get_skill_levels: thriveGetSkillLevelsTool, - tiktok_direct_post_video: tiktokDirectPostVideoTool, tiktok_get_post_status: tiktokGetPostStatusTool, tiktok_get_user: tiktokGetUserTool, tiktok_list_videos: tiktokListVideosTool, diff --git a/apps/sim/tools/tiktok/direct_post_video.test.ts b/apps/sim/tools/tiktok/direct_post_video.test.ts deleted file mode 100644 index 38130270ba7..00000000000 --- a/apps/sim/tools/tiktok/direct_post_video.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @vitest-environment node - */ -import { describe, expect, it } from 'vitest' -import { tiktokDirectPostVideoTool } from '@/tools/tiktok/direct_post_video' -import type { TikTokDirectPostVideoParams } from '@/tools/tiktok/types' - -describe('tiktokDirectPostVideoTool', () => { - it('fails closed until one-use human approval is supported', () => { - const params = { - accessToken: 'token', - brandContentToggle: false, - disableComment: false, - disableDuet: false, - disableStitch: false, - file: { - id: 'file-1', - key: 'workspace/workspace-1/file-1', - name: 'video.mp4', - size: 1024, - type: 'video/mp4', - url: '/api/files/serve?key=workspace%2Fworkspace-1%2Ffile-1', - }, - musicUsageConsent: 'accepted', - privacyLevel: 'SELF_ONLY', - } satisfies TikTokDirectPostVideoParams - - expect(() => tiktokDirectPostVideoTool.request.body?.(params)).toThrow( - 'TikTok Direct Post is unavailable' - ) - }) -}) diff --git a/apps/sim/tools/tiktok/direct_post_video.ts b/apps/sim/tools/tiktok/direct_post_video.ts deleted file mode 100644 index 7522f0662d1..00000000000 --- a/apps/sim/tools/tiktok/direct_post_video.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { - TikTokDirectPostVideoParams, - TikTokDirectPostVideoResponse, -} from '@/tools/tiktok/types' -import { readTikTokPublishInitResponse, toTikTokPublishToolResponse } from '@/tools/tiktok/utils' -import type { ToolConfig } from '@/tools/types' - -const DIRECT_POST_UNAVAILABLE_MESSAGE = - 'TikTok Direct Post is unavailable until Sim can bind TikTok-required human review and consent to one exact upload. Use Upload Video Draft for review and publishing in TikTok.' - -export const tiktokDirectPostVideoTool: ToolConfig< - TikTokDirectPostVideoParams, - TikTokDirectPostVideoResponse -> = { - id: 'tiktok_direct_post_video', - name: 'TikTok Direct Post Video', - description: - 'Unavailable compatibility tool. Use Upload Video Draft so the account owner can review and publish in TikTok.', - version: '1.0.0', - - oauth: { - required: true, - provider: 'tiktok', - requiredScopes: ['video.publish'], - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'TikTok OAuth access token', - }, - file: { - type: 'file', - required: true, - visibility: 'user-only', - description: 'Video file to upload from the workflow. Maximum size: 250 MB.', - }, - title: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Video caption/description. Maximum 2200 characters.', - }, - privacyLevel: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: - 'Privacy level for the video. Options: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY. Must match one of the privacyLevelOptions returned by Query Creator Info. Note: unaudited apps (including sandbox apps) are restricted to SELF_ONLY.', - }, - disableDuet: { - type: 'boolean', - required: true, - visibility: 'user-or-llm', - description: 'Whether to disable duet for this video. The user must choose explicitly.', - }, - disableStitch: { - type: 'boolean', - required: true, - visibility: 'user-or-llm', - description: 'Whether to disable stitch for this video. The user must choose explicitly.', - }, - disableComment: { - type: 'boolean', - required: true, - visibility: 'user-or-llm', - description: 'Whether to disable comments for this video. The user must choose explicitly.', - }, - videoCoverTimestampMs: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Timestamp in milliseconds to use as the video cover image.', - }, - isAigc: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Set to true if the video is AI-generated content (AIGC).', - }, - brandContentToggle: { - type: 'boolean', - required: true, - visibility: 'user-or-llm', - description: - 'Whether the video is a paid partnership promoting a third-party business. The user must choose explicitly. Branded content cannot be posted with Only Me privacy.', - }, - brandOrganicToggle: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: "Set to true if the video is promoting the creator's own business.", - }, - musicUsageConsent: { - type: 'string', - required: true, - visibility: 'user-only', - description: - "Must be 'accepted' after the user explicitly agrees to TikTok's Music Usage Confirmation.", - }, - }, - - request: { - url: () => '/api/tools/tiktok/publish-video', - method: 'POST', - headers: () => ({ - 'Content-Type': 'application/json', - }), - body: () => { - throw new Error(DIRECT_POST_UNAVAILABLE_MESSAGE) - }, - }, - - transformResponse: async (response: Response): Promise => { - const result = await readTikTokPublishInitResponse(response) - return toTikTokPublishToolResponse(result) - }, - - outputs: { - publishId: { - type: 'string', - description: - 'Unique identifier for tracking the post status. Use this with the Get Post Status tool to check if the video was successfully published.', - }, - }, -} diff --git a/apps/sim/tools/tiktok/get_post_status.ts b/apps/sim/tools/tiktok/get_post_status.ts index 99ff8ee84c8..a883e97f423 100644 --- a/apps/sim/tools/tiktok/get_post_status.ts +++ b/apps/sim/tools/tiktok/get_post_status.ts @@ -20,7 +20,7 @@ export const tiktokGetPostStatusTool: ToolConfig< id: 'tiktok_get_post_status', name: 'TikTok Get Post Status', description: - 'Check the status of a post initiated with Direct Post Video or Upload Video Draft. Use the publishId returned from the post request to track progress.', + 'Check the status of a video draft upload. Use the publishId returned from Upload Video Draft to track progress.', version: '1.0.0', oauth: { diff --git a/apps/sim/tools/tiktok/index.ts b/apps/sim/tools/tiktok/index.ts index d20db2ae69a..92fc56d1835 100644 --- a/apps/sim/tools/tiktok/index.ts +++ b/apps/sim/tools/tiktok/index.ts @@ -1,4 +1,3 @@ -import { tiktokDirectPostVideoTool } from '@/tools/tiktok/direct_post_video' import { tiktokGetPostStatusTool } from '@/tools/tiktok/get_post_status' import { tiktokGetUserTool } from '@/tools/tiktok/get_user' import { tiktokListVideosTool } from '@/tools/tiktok/list_videos' @@ -12,6 +11,5 @@ export { tiktokGetUserTool } export { tiktokListVideosTool } export { tiktokQueryVideosTool } export { tiktokQueryCreatorInfoTool } -export { tiktokDirectPostVideoTool } export { tiktokUploadVideoDraftTool } export { tiktokGetPostStatusTool } diff --git a/apps/sim/tools/tiktok/query_creator_info.ts b/apps/sim/tools/tiktok/query_creator_info.ts index 2b969b9aa1e..8395319bd8c 100644 --- a/apps/sim/tools/tiktok/query_creator_info.ts +++ b/apps/sim/tools/tiktok/query_creator_info.ts @@ -26,7 +26,7 @@ export const tiktokQueryCreatorInfoTool: ToolConfig< id: 'tiktok_query_creator_info', name: 'TikTok Query Creator Info', description: - "Inspect the authenticated creator's Direct Post capabilities, privacy options, interaction settings, and maximum video duration. Direct Post is currently unavailable in Sim; Upload Video Draft does not require this step.", + "Inspect the authenticated creator's privacy options, interaction settings, and maximum video duration.", version: '1.0.0', oauth: { diff --git a/apps/sim/tools/tiktok/types.ts b/apps/sim/tools/tiktok/types.ts index b9320ab1622..9a2b2a7059b 100644 --- a/apps/sim/tools/tiktok/types.ts +++ b/apps/sim/tools/tiktok/types.ts @@ -140,32 +140,13 @@ export interface TikTokQueryCreatorInfoResponse extends ToolResponse { } } -/** - * Direct Post Video - Publish an uploaded video to TikTok - */ -export interface TikTokDirectPostVideoParams extends TikTokBaseParams { - file: UserFile - title?: string - privacyLevel: string - disableDuet: boolean - disableStitch: boolean - disableComment: boolean - videoCoverTimestampMs?: number - isAigc?: boolean - brandContentToggle: boolean - brandOrganicToggle?: boolean - musicUsageConsent: 'accepted' -} - -/** Shared response shape for TikTok publish and draft initialization tools. */ +/** Shared response shape for TikTok draft initialization tools. */ export interface TikTokPublishResponse extends ToolResponse { output: { publishId: string } } -export type TikTokDirectPostVideoResponse = TikTokPublishResponse - /** * Upload Video Draft - Send a video to the user's TikTok inbox for manual editing/posting */ @@ -200,6 +181,5 @@ export type TikTokResponse = | TikTokListVideosResponse | TikTokQueryVideosResponse | TikTokQueryCreatorInfoResponse - | TikTokDirectPostVideoResponse | TikTokUploadVideoDraftResponse | TikTokGetPostStatusResponse diff --git a/apps/sim/tools/tiktok/upload_video_draft.ts b/apps/sim/tools/tiktok/upload_video_draft.ts index 3104539049f..4e340cd2afa 100644 --- a/apps/sim/tools/tiktok/upload_video_draft.ts +++ b/apps/sim/tools/tiktok/upload_video_draft.ts @@ -44,7 +44,6 @@ export const tiktokUploadVideoDraftTool: ToolConfig< }), body: (params: TikTokUploadVideoDraftParams) => ({ accessToken: params.accessToken, - mode: 'draft', file: params.file, }), }, diff --git a/apps/sim/tools/tiktok/utils.test.ts b/apps/sim/tools/tiktok/utils.test.ts index 6e5426cc398..674820b31f5 100644 --- a/apps/sim/tools/tiktok/utils.test.ts +++ b/apps/sim/tools/tiktok/utils.test.ts @@ -61,12 +61,25 @@ describe('TikTok tool utilities', () => { data: null, error: { code: 'invalid_response', - message: 'TikTok response exceeded the maximum supported size or could not be read', + message: 'TikTok response exceeded the maximum supported size', }, rawBody: '', }) }) + it('surfaces TikTok error codes when the provider omits a message', async () => { + const response = Response.json({ + data: {}, + error: { code: 'UnknownError' }, + }) + + await expect(readTikTokPublishInitResponse(response)).resolves.toEqual({ + success: false, + publishId: '', + error: 'UnknownError', + }) + }) + it('normalizes direct TikTok publish responses', async () => { const response = Response.json({ data: { publish_id: 'publish-1', upload_url: 'https://upload.example/video' }, diff --git a/apps/sim/tools/tiktok/utils.ts b/apps/sim/tools/tiktok/utils.ts index 56617c3435e..f579e841324 100644 --- a/apps/sim/tools/tiktok/utils.ts +++ b/apps/sim/tools/tiktok/utils.ts @@ -1,6 +1,10 @@ import { truncate } from '@sim/utils/string' +import { getErrorMessage } from '@sim/utils/errors' import type { ZodType } from 'zod' -import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' +import { + isPayloadSizeLimitError, + readResponseTextWithLimit, +} from '@/lib/core/utils/stream-limits' import { type TikTokApiVideo, tiktokPublishInitApiDataSchema } from '@/tools/tiktok/api-schemas' import type { TikTokApiError, TikTokPublishResponse, TikTokVideo } from '@/tools/tiktok/types' @@ -120,13 +124,16 @@ async function readJsonObject( label: 'TikTok API response', signal: options.signal, }) - } catch { + } catch (error) { options.signal?.throwIfAborted() + const message = isPayloadSizeLimitError(error) + ? 'TikTok response exceeded the maximum supported size' + : `TikTok response could not be read: ${getErrorMessage(error, 'unknown read error')}` return { body: null, error: { code: 'invalid_response', - message: 'TikTok response exceeded the maximum supported size or could not be read', + message, }, rawBody: '', } @@ -254,7 +261,10 @@ export async function readTikTokPublishInitResponse( return { success: false, publishId: '', - error: result.error.message ?? 'Failed to initiate post', + error: + result.error.message || + result.error.code || + 'Failed to initiate post', } } diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index de118f173c8..2dea0ccbd72 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -401,8 +401,6 @@ import { tiktokPostPubliclyAvailableTrigger, tiktokPostPublishCompleteTrigger, tiktokPostPublishFailedTrigger, - tiktokVideoPublishCompletedTrigger, - tiktokVideoUploadFailedTrigger, } from '@/triggers/tiktok' import { twilioSmsReceivedTrigger, twilioSmsStatusTrigger } from '@/triggers/twilio' import { twilioVoiceWebhookTrigger } from '@/triggers/twilio_voice' @@ -711,8 +709,6 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { tiktok_post_publicly_available: tiktokPostPubliclyAvailableTrigger, tiktok_post_publish_complete: tiktokPostPublishCompleteTrigger, tiktok_post_publish_failed: tiktokPostPublishFailedTrigger, - tiktok_video_publish_completed: tiktokVideoPublishCompletedTrigger, - tiktok_video_upload_failed: tiktokVideoUploadFailedTrigger, typeform_webhook: typeformWebhookTrigger, whatsapp_webhook: whatsappWebhookTrigger, google_forms_webhook: googleFormsWebhookTrigger, diff --git a/apps/sim/triggers/tiktok/index.ts b/apps/sim/triggers/tiktok/index.ts index 2f9321fd3c5..7dd7882e115 100644 --- a/apps/sim/triggers/tiktok/index.ts +++ b/apps/sim/triggers/tiktok/index.ts @@ -4,5 +4,3 @@ export { tiktokPostNoLongerPublicTrigger } from '@/triggers/tiktok/post_no_longe export { tiktokPostPubliclyAvailableTrigger } from '@/triggers/tiktok/post_publicly_available' export { tiktokPostPublishCompleteTrigger } from '@/triggers/tiktok/post_publish_complete' export { tiktokPostPublishFailedTrigger } from '@/triggers/tiktok/post_publish_failed' -export { tiktokVideoPublishCompletedTrigger } from '@/triggers/tiktok/video_publish_completed' -export { tiktokVideoUploadFailedTrigger } from '@/triggers/tiktok/video_upload_failed' diff --git a/apps/sim/triggers/tiktok/post_publish_complete.ts b/apps/sim/triggers/tiktok/post_publish_complete.ts index e77eb2d91bc..37f4d52ef4b 100644 --- a/apps/sim/triggers/tiktok/post_publish_complete.ts +++ b/apps/sim/triggers/tiktok/post_publish_complete.ts @@ -9,7 +9,7 @@ import type { TriggerConfig } from '@/triggers/types' /** * Primary TikTok trigger — includes the trigger-type dropdown. - * Fires when Content Posting completes (direct post or inbox draft published). + * Fires when Content Posting completes (inbox draft published in TikTok). */ export const tiktokPostPublishCompleteTrigger: TriggerConfig = { id: 'tiktok_post_publish_complete', diff --git a/apps/sim/triggers/tiktok/utils.ts b/apps/sim/triggers/tiktok/utils.ts index e70d93dab81..b8a79fe4649 100644 --- a/apps/sim/triggers/tiktok/utils.ts +++ b/apps/sim/triggers/tiktok/utils.ts @@ -8,8 +8,6 @@ export const tiktokTriggerOptions = [ { label: 'Post Inbox Delivered', id: 'tiktok_post_inbox_delivered' }, { label: 'Post Publicly Available', id: 'tiktok_post_publicly_available' }, { label: 'Post No Longer Public', id: 'tiktok_post_no_longer_public' }, - { label: 'Video Publish Completed', id: 'tiktok_video_publish_completed' }, - { label: 'Video Upload Failed', id: 'tiktok_video_upload_failed' }, { label: 'Authorization Removed', id: 'tiktok_authorization_removed' }, ] @@ -20,8 +18,6 @@ export const TIKTOK_TRIGGER_EVENT_MAP: Record = { tiktok_post_inbox_delivered: 'post.publish.inbox_delivered', tiktok_post_publicly_available: 'post.publish.publicly_available', tiktok_post_no_longer_public: 'post.publish.no_longer_publicaly_available', - tiktok_video_publish_completed: 'video.publish.completed', - tiktok_video_upload_failed: 'video.upload.failed', tiktok_authorization_removed: 'authorization.removed', } @@ -123,13 +119,6 @@ export function buildTikTokPostingOutputs(options?: { return outputs } -export function buildTikTokVideoKitOutputs(): Record { - return { - ...buildCommonOutputs(), - shareId: { type: 'string', description: 'Share Kit / Video Kit share_id' }, - } -} - export function buildTikTokAuthorizationRemovedOutputs(): Record { return { ...buildCommonOutputs(), diff --git a/apps/sim/triggers/tiktok/video_publish_completed.ts b/apps/sim/triggers/tiktok/video_publish_completed.ts deleted file mode 100644 index 9cd74477337..00000000000 --- a/apps/sim/triggers/tiktok/video_publish_completed.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { TikTokIcon } from '@/components/icons' -import { - buildTikTokTriggerSubBlocks, - buildTikTokVideoKitOutputs, - TIKTOK_WEBHOOK_HEADERS, -} from '@/triggers/tiktok/utils' -import type { TriggerConfig } from '@/triggers/types' - -export const tiktokVideoPublishCompletedTrigger: TriggerConfig = { - id: 'tiktok_video_publish_completed', - name: 'TikTok Video Publish Completed', - provider: 'tiktok', - description: 'Trigger when a Share Kit / Video Kit upload is published by the user', - version: '1.0.0', - icon: TikTokIcon, - - subBlocks: buildTikTokTriggerSubBlocks( - 'tiktok_video_publish_completed', - 'video.publish.completed' - ), - - outputs: buildTikTokVideoKitOutputs(), - - webhook: { - method: 'POST', - headers: { ...TIKTOK_WEBHOOK_HEADERS }, - }, -} diff --git a/apps/sim/triggers/tiktok/video_upload_failed.ts b/apps/sim/triggers/tiktok/video_upload_failed.ts deleted file mode 100644 index 767d3b7ff1d..00000000000 --- a/apps/sim/triggers/tiktok/video_upload_failed.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TikTokIcon } from '@/components/icons' -import { - buildTikTokTriggerSubBlocks, - buildTikTokVideoKitOutputs, - TIKTOK_WEBHOOK_HEADERS, -} from '@/triggers/tiktok/utils' -import type { TriggerConfig } from '@/triggers/types' - -export const tiktokVideoUploadFailedTrigger: TriggerConfig = { - id: 'tiktok_video_upload_failed', - name: 'TikTok Video Upload Failed', - provider: 'tiktok', - description: 'Trigger when a Share Kit / Video Kit upload fails in TikTok', - version: '1.0.0', - icon: TikTokIcon, - - subBlocks: buildTikTokTriggerSubBlocks('tiktok_video_upload_failed', 'video.upload.failed'), - - outputs: buildTikTokVideoKitOutputs(), - - webhook: { - method: 'POST', - headers: { ...TIKTOK_WEBHOOK_HEADERS }, - }, -} From c91496d1816fe377fe0d15459fc3c2726b7dc5b8 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Wed, 15 Jul 2026 22:13:34 -0700 Subject: [PATCH 2/3] fix TikTok cleanup lint and Greptile review findings Reject legacy mode:direct publish requests instead of silently drafting, keep deprecated Share Kit triggers registered for saved workflows, and apply biome format/import fixes. Co-authored-by: Cursor --- .../tools/tiktok/publish-video/route.test.ts | 17 ++++++++ .../api/tools/tiktok/publish-video/route.ts | 5 +-- apps/sim/blocks/blocks/tiktok.ts | 3 ++ .../lib/api/contracts/tiktok-tools.test.ts | 43 ++++++++++++++++--- apps/sim/lib/api/contracts/tiktok-tools.ts | 21 +++++++-- .../sim/lib/webhooks/providers/tiktok.test.ts | 11 ++++- apps/sim/lib/webhooks/providers/tiktok.ts | 12 +++++- apps/sim/tools/tiktok/utils.ts | 12 ++---- apps/sim/triggers/registry.ts | 4 ++ apps/sim/triggers/tiktok/index.ts | 2 + apps/sim/triggers/tiktok/utils.ts | 9 ++++ .../tiktok/video_publish_completed.ts | 30 +++++++++++++ .../triggers/tiktok/video_upload_failed.ts | 27 ++++++++++++ 13 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 apps/sim/triggers/tiktok/video_publish_completed.ts create mode 100644 apps/sim/triggers/tiktok/video_upload_failed.ts diff --git a/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts b/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts index cb18a42b0a0..c23ddee3dc8 100644 --- a/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts +++ b/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts @@ -103,6 +103,23 @@ describe('POST /api/tools/tiktok/publish-video', () => { }) }) + it('rejects legacy Direct Post mode instead of uploading as a draft', async () => { + const fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) + + const response = await POST( + createMockRequest('POST', { + accessToken: 'access-token', + mode: 'direct', + file, + }) + ) + + expect(response.status).toBeGreaterThanOrEqual(400) + expect(mockGetStoredVideoSize).not.toHaveBeenCalled() + expect(fetchMock).not.toHaveBeenCalled() + }) + it('returns 413 when the storage object exceeds the relay limit', async () => { mockGetStoredVideoSize.mockRejectedValue( new PayloadSizeLimitError({ diff --git a/apps/sim/app/api/tools/tiktok/publish-video/route.ts b/apps/sim/app/api/tools/tiktok/publish-video/route.ts index 8e2239782be..71fe1afacb6 100644 --- a/apps/sim/app/api/tools/tiktok/publish-video/route.ts +++ b/apps/sim/app/api/tools/tiktok/publish-video/route.ts @@ -137,10 +137,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json( { success: false, - error: - initError.message || - initError.code || - 'Failed to initialize TikTok upload', + error: initError.message || initError.code || 'Failed to initialize TikTok upload', }, { status: initResponse.status >= 400 ? initResponse.status : 502 } ) diff --git a/apps/sim/blocks/blocks/tiktok.ts b/apps/sim/blocks/blocks/tiktok.ts index 15be9e5f13c..5e1ca37fcfd 100644 --- a/apps/sim/blocks/blocks/tiktok.ts +++ b/apps/sim/blocks/blocks/tiktok.ts @@ -159,6 +159,9 @@ export const TikTokBlock: BlockConfig = { ...getTrigger('tiktok_post_publicly_available').subBlocks, ...getTrigger('tiktok_post_no_longer_public').subBlocks, ...getTrigger('tiktok_authorization_removed').subBlocks, + // Deprecated Share Kit triggers: keep subBlocks so saved selectedTriggerId still renders. + ...getTrigger('tiktok_video_publish_completed').subBlocks, + ...getTrigger('tiktok_video_upload_failed').subBlocks, ], triggers: { diff --git a/apps/sim/lib/api/contracts/tiktok-tools.test.ts b/apps/sim/lib/api/contracts/tiktok-tools.test.ts index eed99eb4cd9..e7e648665ab 100644 --- a/apps/sim/lib/api/contracts/tiktok-tools.test.ts +++ b/apps/sim/lib/api/contracts/tiktok-tools.test.ts @@ -10,12 +10,43 @@ const file = { describe('tiktokPublishVideoBodySchema', () => { it('accepts a draft upload body', () => { - expect( - tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - file, - }).success - ).toBe(true) + const parsed = tiktokPublishVideoBodySchema.safeParse({ + accessToken: 'token', + file, + }) + expect(parsed.success).toBe(true) + if (parsed.success) { + expect(parsed.data).toEqual({ accessToken: 'token', file }) + } + }) + + it('accepts an explicit draft mode and strips it from the result', () => { + const parsed = tiktokPublishVideoBodySchema.safeParse({ + accessToken: 'token', + mode: 'draft', + file, + }) + expect(parsed.success).toBe(true) + if (parsed.success) { + expect(parsed.data).toEqual({ accessToken: 'token', file }) + } + }) + + it('rejects legacy Direct Post mode instead of uploading as a draft', () => { + const parsed = tiktokPublishVideoBodySchema.safeParse({ + accessToken: 'token', + mode: 'direct', + file, + musicUsageConsent: 'accepted', + postInfo: { + privacy_level: 'SELF_ONLY', + disable_duet: true, + disable_stitch: true, + disable_comment: true, + brand_content_toggle: false, + }, + }) + expect(parsed.success).toBe(false) }) it('rejects a body without an access token', () => { diff --git a/apps/sim/lib/api/contracts/tiktok-tools.ts b/apps/sim/lib/api/contracts/tiktok-tools.ts index 1a3f2e5ac66..ed17c7ed13d 100644 --- a/apps/sim/lib/api/contracts/tiktok-tools.ts +++ b/apps/sim/lib/api/contracts/tiktok-tools.ts @@ -6,10 +6,23 @@ import { } from '@/lib/api/contracts/types' import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' -export const tiktokPublishVideoBodySchema = z.object({ - accessToken: z.string().min(1, 'Access token is required'), - file: RawFileInputSchema, -}) +export const tiktokPublishVideoBodySchema = z + .object({ + accessToken: z.string().min(1, 'Access token is required'), + file: RawFileInputSchema, + /** Legacy callers may still send mode; only draft is accepted. */ + mode: z.enum(['draft', 'direct']).optional(), + }) + .superRefine((body, ctx) => { + if (body.mode === 'direct') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['mode'], + message: 'TikTok Direct Post is not available. Use Upload Video Draft instead.', + }) + } + }) + .transform(({ accessToken, file }) => ({ accessToken, file })) export const tiktokPublishVideoResponseSchema = z.object({ success: z.boolean(), diff --git a/apps/sim/lib/webhooks/providers/tiktok.test.ts b/apps/sim/lib/webhooks/providers/tiktok.test.ts index f0b82bd79ec..3d40d8060e1 100644 --- a/apps/sim/lib/webhooks/providers/tiktok.test.ts +++ b/apps/sim/lib/webhooks/providers/tiktok.test.ts @@ -276,7 +276,16 @@ describe('tiktokHandler', () => { ).toBe('post.publish.publicly_available:act.user:pub-1:post-1') }) - it('extractIdempotencyId falls back to create_time', () => { + it('extractIdempotencyId falls back to share_id then create_time', () => { + expect( + tiktokHandler.extractIdempotencyId!({ + event: 'video.publish.completed', + user_openid: 'act.user', + create_time: 99, + content: '{"share_id":"share-1"}', + }) + ).toBe('video.publish.completed:act.user:share-1') + expect( tiktokHandler.extractIdempotencyId!({ event: 'authorization.removed', diff --git a/apps/sim/lib/webhooks/providers/tiktok.ts b/apps/sim/lib/webhooks/providers/tiktok.ts index 4196a11a2fd..0abedaa7e03 100644 --- a/apps/sim/lib/webhooks/providers/tiktok.ts +++ b/apps/sim/lib/webhooks/providers/tiktok.ts @@ -201,6 +201,15 @@ export const tiktokHandler: WebhookProviderHandler = { } } + if (event === 'video.publish.completed' || event === 'video.upload.failed') { + return { + input: { + ...commonInput, + shareId: stringField(content, 'share_id') ?? null, + }, + } + } + return { input: commonInput } }, @@ -215,6 +224,7 @@ export const tiktokHandler: WebhookProviderHandler = { const content = parseTikTokContent(envelope.content) const publishId = stringField(content, 'publish_id') const postId = stringField(content, 'post_id') + const shareId = stringField(content, 'share_id') const createTime = typeof envelope.create_time === 'number' || typeof envelope.create_time === 'string' ? String(envelope.create_time) @@ -226,7 +236,7 @@ export const tiktokHandler: WebhookProviderHandler = { } else if (event === 'post.publish.complete' && publishId && createTime) { unique = `${publishId}:${createTime}` } else { - unique = publishId ?? createTime + unique = publishId ?? shareId ?? createTime } if (!unique) return null diff --git a/apps/sim/tools/tiktok/utils.ts b/apps/sim/tools/tiktok/utils.ts index f579e841324..2ec2a7d6737 100644 --- a/apps/sim/tools/tiktok/utils.ts +++ b/apps/sim/tools/tiktok/utils.ts @@ -1,10 +1,7 @@ -import { truncate } from '@sim/utils/string' import { getErrorMessage } from '@sim/utils/errors' +import { truncate } from '@sim/utils/string' import type { ZodType } from 'zod' -import { - isPayloadSizeLimitError, - readResponseTextWithLimit, -} from '@/lib/core/utils/stream-limits' +import { isPayloadSizeLimitError, readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' import { type TikTokApiVideo, tiktokPublishInitApiDataSchema } from '@/tools/tiktok/api-schemas' import type { TikTokApiError, TikTokPublishResponse, TikTokVideo } from '@/tools/tiktok/types' @@ -261,10 +258,7 @@ export async function readTikTokPublishInitResponse( return { success: false, publishId: '', - error: - result.error.message || - result.error.code || - 'Failed to initiate post', + error: result.error.message || result.error.code || 'Failed to initiate post', } } diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 2dea0ccbd72..de118f173c8 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -401,6 +401,8 @@ import { tiktokPostPubliclyAvailableTrigger, tiktokPostPublishCompleteTrigger, tiktokPostPublishFailedTrigger, + tiktokVideoPublishCompletedTrigger, + tiktokVideoUploadFailedTrigger, } from '@/triggers/tiktok' import { twilioSmsReceivedTrigger, twilioSmsStatusTrigger } from '@/triggers/twilio' import { twilioVoiceWebhookTrigger } from '@/triggers/twilio_voice' @@ -709,6 +711,8 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { tiktok_post_publicly_available: tiktokPostPubliclyAvailableTrigger, tiktok_post_publish_complete: tiktokPostPublishCompleteTrigger, tiktok_post_publish_failed: tiktokPostPublishFailedTrigger, + tiktok_video_publish_completed: tiktokVideoPublishCompletedTrigger, + tiktok_video_upload_failed: tiktokVideoUploadFailedTrigger, typeform_webhook: typeformWebhookTrigger, whatsapp_webhook: whatsappWebhookTrigger, google_forms_webhook: googleFormsWebhookTrigger, diff --git a/apps/sim/triggers/tiktok/index.ts b/apps/sim/triggers/tiktok/index.ts index 7dd7882e115..2f9321fd3c5 100644 --- a/apps/sim/triggers/tiktok/index.ts +++ b/apps/sim/triggers/tiktok/index.ts @@ -4,3 +4,5 @@ export { tiktokPostNoLongerPublicTrigger } from '@/triggers/tiktok/post_no_longe export { tiktokPostPubliclyAvailableTrigger } from '@/triggers/tiktok/post_publicly_available' export { tiktokPostPublishCompleteTrigger } from '@/triggers/tiktok/post_publish_complete' export { tiktokPostPublishFailedTrigger } from '@/triggers/tiktok/post_publish_failed' +export { tiktokVideoPublishCompletedTrigger } from '@/triggers/tiktok/video_publish_completed' +export { tiktokVideoUploadFailedTrigger } from '@/triggers/tiktok/video_upload_failed' diff --git a/apps/sim/triggers/tiktok/utils.ts b/apps/sim/triggers/tiktok/utils.ts index b8a79fe4649..4e29a218893 100644 --- a/apps/sim/triggers/tiktok/utils.ts +++ b/apps/sim/triggers/tiktok/utils.ts @@ -18,6 +18,8 @@ export const TIKTOK_TRIGGER_EVENT_MAP: Record = { tiktok_post_inbox_delivered: 'post.publish.inbox_delivered', tiktok_post_publicly_available: 'post.publish.publicly_available', tiktok_post_no_longer_public: 'post.publish.no_longer_publicaly_available', + tiktok_video_publish_completed: 'video.publish.completed', + tiktok_video_upload_failed: 'video.upload.failed', tiktok_authorization_removed: 'authorization.removed', } @@ -119,6 +121,13 @@ export function buildTikTokPostingOutputs(options?: { return outputs } +export function buildTikTokVideoKitOutputs(): Record { + return { + ...buildCommonOutputs(), + shareId: { type: 'string', description: 'Share Kit / Video Kit share_id' }, + } +} + export function buildTikTokAuthorizationRemovedOutputs(): Record { return { ...buildCommonOutputs(), diff --git a/apps/sim/triggers/tiktok/video_publish_completed.ts b/apps/sim/triggers/tiktok/video_publish_completed.ts new file mode 100644 index 00000000000..ec069a7b1e6 --- /dev/null +++ b/apps/sim/triggers/tiktok/video_publish_completed.ts @@ -0,0 +1,30 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokTriggerSubBlocks, + buildTikTokVideoKitOutputs, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Deprecated Share Kit / Video Kit trigger kept registered so existing + * workflows with this selectedTriggerId continue to resolve. + */ +export const tiktokVideoPublishCompletedTrigger: TriggerConfig = { + id: 'tiktok_video_publish_completed', + name: 'TikTok Video Publish Completed', + provider: 'tiktok', + description: 'Trigger when a Share Kit / Video Kit upload is published by the user', + version: '1.0.0', + deprecated: true, + icon: TikTokIcon, + subBlocks: buildTikTokTriggerSubBlocks( + 'tiktok_video_publish_completed', + 'video.publish.completed' + ), + outputs: buildTikTokVideoKitOutputs(), + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} diff --git a/apps/sim/triggers/tiktok/video_upload_failed.ts b/apps/sim/triggers/tiktok/video_upload_failed.ts new file mode 100644 index 00000000000..2a163d8028f --- /dev/null +++ b/apps/sim/triggers/tiktok/video_upload_failed.ts @@ -0,0 +1,27 @@ +import { TikTokIcon } from '@/components/icons' +import { + buildTikTokTriggerSubBlocks, + buildTikTokVideoKitOutputs, + TIKTOK_WEBHOOK_HEADERS, +} from '@/triggers/tiktok/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Deprecated Share Kit / Video Kit trigger kept registered so existing + * workflows with this selectedTriggerId continue to resolve. + */ +export const tiktokVideoUploadFailedTrigger: TriggerConfig = { + id: 'tiktok_video_upload_failed', + name: 'TikTok Video Upload Failed', + provider: 'tiktok', + description: 'Trigger when a Share Kit / Video Kit upload fails in TikTok', + version: '1.0.0', + deprecated: true, + icon: TikTokIcon, + subBlocks: buildTikTokTriggerSubBlocks('tiktok_video_upload_failed', 'video.upload.failed'), + outputs: buildTikTokVideoKitOutputs(), + webhook: { + method: 'POST', + headers: { ...TIKTOK_WEBHOOK_HEADERS }, + }, +} From cac1d824058e833606d5cdb943d174922d8c66f0 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 16 Jul 2026 10:52:53 -0700 Subject: [PATCH 3/3] Finish TikTok greenfield draft-only surface Remove Query Creator Info and video.publish, drop Share Kit trigger stubs, rename publish-video to upload-video-draft, and strip leftover Direct Post mode from the contract. Co-authored-by: Cursor --- .../route.test.ts | 23 +-- .../route.ts | 14 +- .../upload.test.ts | 2 +- .../upload.ts | 0 apps/sim/blocks/blocks/tiktok.test.ts | 1 - apps/sim/blocks/blocks/tiktok.ts | 68 +------- .../lib/api/contracts/tiktok-tools.test.ts | 39 +---- apps/sim/lib/api/contracts/tiktok-tools.ts | 37 ++--- apps/sim/lib/oauth/oauth.ts | 3 +- apps/sim/lib/oauth/utils.ts | 1 - .../sim/lib/webhooks/providers/tiktok.test.ts | 11 +- apps/sim/lib/webhooks/providers/tiktok.ts | 12 +- apps/sim/tools/registry.ts | 2 - apps/sim/tools/tiktok/api-schemas.test.ts | 18 --- apps/sim/tools/tiktok/api-schemas.ts | 12 -- apps/sim/tools/tiktok/get_post_status.test.ts | 3 +- apps/sim/tools/tiktok/get_post_status.ts | 1 + apps/sim/tools/tiktok/index.ts | 2 - .../tools/tiktok/query_creator_info.test.ts | 67 -------- apps/sim/tools/tiktok/query_creator_info.ts | 151 ------------------ apps/sim/tools/tiktok/types.ts | 26 +-- apps/sim/tools/tiktok/upload_video_draft.ts | 8 +- apps/sim/tools/tiktok/utils.test.ts | 10 +- apps/sim/tools/tiktok/utils.ts | 18 +-- apps/sim/triggers/registry.ts | 4 - apps/sim/triggers/tiktok/index.ts | 2 - apps/sim/triggers/tiktok/utils.ts | 9 -- .../tiktok/video_publish_completed.ts | 30 ---- .../triggers/tiktok/video_upload_failed.ts | 27 ---- 29 files changed, 63 insertions(+), 538 deletions(-) rename apps/sim/app/api/tools/tiktok/{publish-video => upload-video-draft}/route.test.ts (84%) rename apps/sim/app/api/tools/tiktok/{publish-video => upload-video-draft}/route.ts (92%) rename apps/sim/app/api/tools/tiktok/{publish-video => upload-video-draft}/upload.test.ts (99%) rename apps/sim/app/api/tools/tiktok/{publish-video => upload-video-draft}/upload.ts (100%) delete mode 100644 apps/sim/tools/tiktok/query_creator_info.test.ts delete mode 100644 apps/sim/tools/tiktok/query_creator_info.ts delete mode 100644 apps/sim/triggers/tiktok/video_publish_completed.ts delete mode 100644 apps/sim/triggers/tiktok/video_upload_failed.ts diff --git a/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts b/apps/sim/app/api/tools/tiktok/upload-video-draft/route.test.ts similarity index 84% rename from apps/sim/app/api/tools/tiktok/publish-video/route.test.ts rename to apps/sim/app/api/tools/tiktok/upload-video-draft/route.test.ts index c23ddee3dc8..4eafdea3693 100644 --- a/apps/sim/app/api/tools/tiktok/publish-video/route.test.ts +++ b/apps/sim/app/api/tools/tiktok/upload-video-draft/route.test.ts @@ -24,14 +24,14 @@ vi.mock('@/app/api/files/authorization', () => ({ assertToolFileAccess: mockAssertToolFileAccess, })) -vi.mock('@/app/api/tools/tiktok/publish-video/upload', () => ({ +vi.mock('@/app/api/tools/tiktok/upload-video-draft/upload', () => ({ computeTikTokChunkPlan: mockComputeTikTokChunkPlan, getStoredVideoSize: mockGetStoredVideoSize, streamStoredVideoToTikTok: mockStreamStoredVideoToTikTok, TIKTOK_MAX_VIDEO_BYTES: 250 * 1024 * 1024, })) -import { POST } from '@/app/api/tools/tiktok/publish-video/route' +import { POST } from '@/app/api/tools/tiktok/upload-video-draft/route' const file = { key: 'workspace/workspace-1/video.mp4', @@ -40,7 +40,7 @@ const file = { type: 'video/mp4', } -describe('POST /api/tools/tiktok/publish-video', () => { +describe('POST /api/tools/tiktok/upload-video-draft', () => { beforeEach(() => { vi.clearAllMocks() hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ @@ -103,23 +103,6 @@ describe('POST /api/tools/tiktok/publish-video', () => { }) }) - it('rejects legacy Direct Post mode instead of uploading as a draft', async () => { - const fetchMock = vi.fn() - vi.stubGlobal('fetch', fetchMock) - - const response = await POST( - createMockRequest('POST', { - accessToken: 'access-token', - mode: 'direct', - file, - }) - ) - - expect(response.status).toBeGreaterThanOrEqual(400) - expect(mockGetStoredVideoSize).not.toHaveBeenCalled() - expect(fetchMock).not.toHaveBeenCalled() - }) - it('returns 413 when the storage object exceeds the relay limit', async () => { mockGetStoredVideoSize.mockRejectedValue( new PayloadSizeLimitError({ diff --git a/apps/sim/app/api/tools/tiktok/publish-video/route.ts b/apps/sim/app/api/tools/tiktok/upload-video-draft/route.ts similarity index 92% rename from apps/sim/app/api/tools/tiktok/publish-video/route.ts rename to apps/sim/app/api/tools/tiktok/upload-video-draft/route.ts index 71fe1afacb6..e65b7fe1671 100644 --- a/apps/sim/app/api/tools/tiktok/publish-video/route.ts +++ b/apps/sim/app/api/tools/tiktok/upload-video-draft/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' -import { tiktokPublishVideoContract } from '@/lib/api/contracts/tiktok-tools' +import { tiktokUploadVideoDraftContract } from '@/lib/api/contracts/tiktok-tools' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' @@ -19,7 +19,7 @@ import { getStoredVideoSize, streamStoredVideoToTikTok, TIKTOK_MAX_VIDEO_BYTES, -} from '@/app/api/tools/tiktok/publish-video/upload' +} from '@/app/api/tools/tiktok/upload-video-draft/upload' import type { UserFile } from '@/executor/types' import { tiktokPublishInitApiDataSchema } from '@/tools/tiktok/api-schemas' import { readTikTokApiResponse } from '@/tools/tiktok/utils' @@ -27,7 +27,7 @@ import { readTikTokApiResponse } from '@/tools/tiktok/utils' export const dynamic = 'force-dynamic' export const maxDuration = 900 -const logger = createLogger('TikTokPublishVideoAPI') +const logger = createLogger('TikTokUploadVideoDraftAPI') const TIKTOK_VIDEO_MIME_TYPES = new Set(['video/mp4', 'video/quicktime', 'video/webm']) @@ -43,14 +43,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { - logger.warn(`[${requestId}] Unauthorized TikTok publish-video attempt: ${authResult.error}`) + logger.warn( + `[${requestId}] Unauthorized TikTok upload-video-draft attempt: ${authResult.error}` + ) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, { status: 401 } ) } - const parsed = await parseRequest(tiktokPublishVideoContract, request, {}) + const parsed = await parseRequest(tiktokUploadVideoDraftContract, request, {}) if (!parsed.success) return parsed.response const data = parsed.data.body @@ -195,7 +197,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { { status: 499 } ) } - logger.error(`[${requestId}] Error publishing video to TikTok:`, error) + logger.error(`[${requestId}] Error uploading TikTok video draft:`, error) return NextResponse.json( { success: false, error: getErrorMessage(error, 'Internal server error') }, { status: 500 } diff --git a/apps/sim/app/api/tools/tiktok/publish-video/upload.test.ts b/apps/sim/app/api/tools/tiktok/upload-video-draft/upload.test.ts similarity index 99% rename from apps/sim/app/api/tools/tiktok/publish-video/upload.test.ts rename to apps/sim/app/api/tools/tiktok/upload-video-draft/upload.test.ts index bc4f402f176..d32cee7852f 100644 --- a/apps/sim/app/api/tools/tiktok/publish-video/upload.test.ts +++ b/apps/sim/app/api/tools/tiktok/upload-video-draft/upload.test.ts @@ -28,7 +28,7 @@ import { getStoredVideoSize, streamStoredVideoToTikTok, TIKTOK_MAX_VIDEO_BYTES, -} from '@/app/api/tools/tiktok/publish-video/upload' +} from '@/app/api/tools/tiktok/upload-video-draft/upload' const baseStreamOptions = { key: 'workspace/workspace-1/video.mp4', diff --git a/apps/sim/app/api/tools/tiktok/publish-video/upload.ts b/apps/sim/app/api/tools/tiktok/upload-video-draft/upload.ts similarity index 100% rename from apps/sim/app/api/tools/tiktok/publish-video/upload.ts rename to apps/sim/app/api/tools/tiktok/upload-video-draft/upload.ts diff --git a/apps/sim/blocks/blocks/tiktok.test.ts b/apps/sim/blocks/blocks/tiktok.test.ts index 81a81c40e09..6ed77ef3955 100644 --- a/apps/sim/blocks/blocks/tiktok.test.ts +++ b/apps/sim/blocks/blocks/tiktok.test.ts @@ -46,7 +46,6 @@ describe('TikTokBlock', () => { it('declares file-like outputs with canonical block output types', () => { expect(TikTokBlock.outputs.avatarFile.type).toBe('file') - expect(TikTokBlock.outputs.creatorAvatarFile.type).toBe('file') expect(TikTokBlock.outputs.videos.type).toBe('array') expect(TikTokBlock.outputs.publiclyAvailablePostId.type).toBe('array') }) diff --git a/apps/sim/blocks/blocks/tiktok.ts b/apps/sim/blocks/blocks/tiktok.ts index 5e1ca37fcfd..1b46e17af74 100644 --- a/apps/sim/blocks/blocks/tiktok.ts +++ b/apps/sim/blocks/blocks/tiktok.ts @@ -6,12 +6,10 @@ import { normalizeFileInput } from '@/blocks/utils' import type { TikTokResponse } from '@/tools/tiktok/types' import { getTrigger } from '@/triggers' -const VIDEO_UPLOAD_OPERATIONS = ['tiktok_upload_video_draft'] const TIKTOK_TOOL_IDS = new Set([ 'tiktok_get_user', 'tiktok_list_videos', 'tiktok_query_videos', - 'tiktok_query_creator_info', 'tiktok_upload_video_draft', 'tiktok_get_post_status', ]) @@ -48,7 +46,6 @@ export const TikTokBlock: BlockConfig = { { label: 'Get User Info', id: 'tiktok_get_user' }, { label: 'List Videos', id: 'tiktok_list_videos' }, { label: 'Query Videos', id: 'tiktok_query_videos' }, - { label: 'Query Creator Info', id: 'tiktok_query_creator_info' }, { label: 'Upload Video Draft', id: 'tiktok_upload_video_draft' }, { label: 'Get Post Status', id: 'tiktok_get_post_status' }, ], @@ -130,8 +127,8 @@ export const TikTokBlock: BlockConfig = { multiple: false, maxSize: 250, description: 'MP4, MOV, or WebM video up to 250 MB.', - condition: { field: 'operation', value: VIDEO_UPLOAD_OPERATIONS }, - required: { field: 'operation', value: VIDEO_UPLOAD_OPERATIONS }, + condition: { field: 'operation', value: 'tiktok_upload_video_draft' }, + required: { field: 'operation', value: 'tiktok_upload_video_draft' }, }, { id: 'videoFileRef', @@ -140,8 +137,8 @@ export const TikTokBlock: BlockConfig = { canonicalParamId: 'file', mode: 'advanced', placeholder: 'Reference a video from a previous block', - condition: { field: 'operation', value: VIDEO_UPLOAD_OPERATIONS }, - required: { field: 'operation', value: VIDEO_UPLOAD_OPERATIONS }, + condition: { field: 'operation', value: 'tiktok_upload_video_draft' }, + required: { field: 'operation', value: 'tiktok_upload_video_draft' }, }, { @@ -159,9 +156,6 @@ export const TikTokBlock: BlockConfig = { ...getTrigger('tiktok_post_publicly_available').subBlocks, ...getTrigger('tiktok_post_no_longer_public').subBlocks, ...getTrigger('tiktok_authorization_removed').subBlocks, - // Deprecated Share Kit triggers: keep subBlocks so saved selectedTriggerId still renders. - ...getTrigger('tiktok_video_publish_completed').subBlocks, - ...getTrigger('tiktok_video_upload_failed').subBlocks, ], triggers: { @@ -181,7 +175,6 @@ export const TikTokBlock: BlockConfig = { 'tiktok_get_user', 'tiktok_list_videos', 'tiktok_query_videos', - 'tiktok_query_creator_info', 'tiktok_upload_video_draft', 'tiktok_get_post_status', ], @@ -216,8 +209,6 @@ export const TikTokBlock: BlockConfig = { .map((id: string) => id.trim()) .filter(Boolean) break - case 'tiktok_query_creator_info': - break case 'tiktok_upload_video_draft': { result.file = normalizeFileInput(params.file, { single: true }) break @@ -329,59 +320,12 @@ export const TikTokBlock: BlockConfig = { condition: { field: 'operation', value: 'tiktok_list_videos' }, }, - creatorAvatarUrl: { - type: 'string', - description: 'Provider URL of the creator avatar; expires after two hours', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - creatorAvatarFile: { - type: 'file', - description: - 'Canonical workflow file containing the creator avatar, suitable for downstream attachments', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - creatorUsername: { - type: 'string', - description: 'TikTok username of the creator', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - creatorNickname: { - type: 'string', - description: 'Display name/nickname of the creator', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - privacyLevelOptions: { - type: 'array', - description: 'Available privacy levels for posting (array of strings)', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - commentDisabled: { - type: 'boolean', - description: 'Whether the creator disabled comments by default', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - duetDisabled: { - type: 'boolean', - description: 'Whether the creator disabled duets by default', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - stitchDisabled: { - type: 'boolean', - description: 'Whether the creator disabled stitches by default', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - maxVideoPostDurationSec: { - type: 'number', - description: 'Maximum allowed video duration in seconds', - condition: { field: 'operation', value: 'tiktok_query_creator_info' }, - }, - publishId: { type: 'string', - description: 'Publish ID for tracking post status', + description: 'Draft upload ID for tracking post status', condition: { field: 'operation', - value: VIDEO_UPLOAD_OPERATIONS, + value: 'tiktok_upload_video_draft', }, }, diff --git a/apps/sim/lib/api/contracts/tiktok-tools.test.ts b/apps/sim/lib/api/contracts/tiktok-tools.test.ts index e7e648665ab..5edd2a7cb63 100644 --- a/apps/sim/lib/api/contracts/tiktok-tools.test.ts +++ b/apps/sim/lib/api/contracts/tiktok-tools.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { tiktokPublishVideoBodySchema } from '@/lib/api/contracts/tiktok-tools' +import { tiktokUploadVideoDraftBodySchema } from '@/lib/api/contracts/tiktok-tools' const file = { key: 'workspace/test/video.mp4', @@ -8,9 +8,9 @@ const file = { type: 'video/mp4', } -describe('tiktokPublishVideoBodySchema', () => { +describe('tiktokUploadVideoDraftBodySchema', () => { it('accepts a draft upload body', () => { - const parsed = tiktokPublishVideoBodySchema.safeParse({ + const parsed = tiktokUploadVideoDraftBodySchema.safeParse({ accessToken: 'token', file, }) @@ -20,38 +20,9 @@ describe('tiktokPublishVideoBodySchema', () => { } }) - it('accepts an explicit draft mode and strips it from the result', () => { - const parsed = tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - mode: 'draft', - file, - }) - expect(parsed.success).toBe(true) - if (parsed.success) { - expect(parsed.data).toEqual({ accessToken: 'token', file }) - } - }) - - it('rejects legacy Direct Post mode instead of uploading as a draft', () => { - const parsed = tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - mode: 'direct', - file, - musicUsageConsent: 'accepted', - postInfo: { - privacy_level: 'SELF_ONLY', - disable_duet: true, - disable_stitch: true, - disable_comment: true, - brand_content_toggle: false, - }, - }) - expect(parsed.success).toBe(false) - }) - it('rejects a body without an access token', () => { expect( - tiktokPublishVideoBodySchema.safeParse({ + tiktokUploadVideoDraftBodySchema.safeParse({ file, }).success ).toBe(false) @@ -59,7 +30,7 @@ describe('tiktokPublishVideoBodySchema', () => { it('rejects a body without a file', () => { expect( - tiktokPublishVideoBodySchema.safeParse({ + tiktokUploadVideoDraftBodySchema.safeParse({ accessToken: 'token', }).success ).toBe(false) diff --git a/apps/sim/lib/api/contracts/tiktok-tools.ts b/apps/sim/lib/api/contracts/tiktok-tools.ts index ed17c7ed13d..b14c5eda42c 100644 --- a/apps/sim/lib/api/contracts/tiktok-tools.ts +++ b/apps/sim/lib/api/contracts/tiktok-tools.ts @@ -6,36 +6,25 @@ import { } from '@/lib/api/contracts/types' import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' -export const tiktokPublishVideoBodySchema = z - .object({ - accessToken: z.string().min(1, 'Access token is required'), - file: RawFileInputSchema, - /** Legacy callers may still send mode; only draft is accepted. */ - mode: z.enum(['draft', 'direct']).optional(), - }) - .superRefine((body, ctx) => { - if (body.mode === 'direct') { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['mode'], - message: 'TikTok Direct Post is not available. Use Upload Video Draft instead.', - }) - } - }) - .transform(({ accessToken, file }) => ({ accessToken, file })) +export const tiktokUploadVideoDraftBodySchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + file: RawFileInputSchema, +}) -export const tiktokPublishVideoResponseSchema = z.object({ +export const tiktokUploadVideoDraftResponseSchema = z.object({ success: z.boolean(), output: z.object({ publishId: z.string() }).optional(), error: z.string().optional(), }) -export const tiktokPublishVideoContract = defineRouteContract({ +export const tiktokUploadVideoDraftContract = defineRouteContract({ method: 'POST', - path: '/api/tools/tiktok/publish-video', - body: tiktokPublishVideoBodySchema, - response: { mode: 'json', schema: tiktokPublishVideoResponseSchema }, + path: '/api/tools/tiktok/upload-video-draft', + body: tiktokUploadVideoDraftBodySchema, + response: { mode: 'json', schema: tiktokUploadVideoDraftResponseSchema }, }) -export type TikTokPublishVideoBody = ContractBodyInput -export type TikTokPublishVideoResponse = ContractJsonResponse +export type TikTokUploadVideoDraftBody = ContractBodyInput +export type TikTokUploadVideoDraftResponse = ContractJsonResponse< + typeof tiktokUploadVideoDraftContract +> diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index d1a0bb00dbe..ea98ba9ce41 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -441,7 +441,7 @@ export const OAUTH_PROVIDERS: Record = { services: { tiktok: { name: 'TikTok', - description: 'Read profile info and videos, and publish content to TikTok.', + description: 'Read profile info and videos, and upload drafts to the TikTok inbox.', providerId: 'tiktok', icon: TikTokIcon, baseProviderIcon: TikTokIcon, @@ -449,7 +449,6 @@ export const OAUTH_PROVIDERS: Record = { 'user.info.basic', 'user.info.profile', 'user.info.stats', - 'video.publish', 'video.upload', 'video.list', ], diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 45035d4603b..befbc8b5065 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -126,7 +126,6 @@ export const SCOPE_DESCRIPTIONS: Record = { 'user.info.basic': "Read a user's profile info (open id, avatar, display name)", 'user.info.profile': "Read a user's profile info (bio, verification status, username)", 'user.info.stats': "Read a user's stats (follower, following, likes, and video counts)", - 'video.publish': "Query creator posting capabilities for a user's TikTok account", 'video.upload': "Share content to a creator's account as a draft for further edit and post", 'video.list': "Read a user's public TikTok videos", diff --git a/apps/sim/lib/webhooks/providers/tiktok.test.ts b/apps/sim/lib/webhooks/providers/tiktok.test.ts index 3d40d8060e1..f0b82bd79ec 100644 --- a/apps/sim/lib/webhooks/providers/tiktok.test.ts +++ b/apps/sim/lib/webhooks/providers/tiktok.test.ts @@ -276,16 +276,7 @@ describe('tiktokHandler', () => { ).toBe('post.publish.publicly_available:act.user:pub-1:post-1') }) - it('extractIdempotencyId falls back to share_id then create_time', () => { - expect( - tiktokHandler.extractIdempotencyId!({ - event: 'video.publish.completed', - user_openid: 'act.user', - create_time: 99, - content: '{"share_id":"share-1"}', - }) - ).toBe('video.publish.completed:act.user:share-1') - + it('extractIdempotencyId falls back to create_time', () => { expect( tiktokHandler.extractIdempotencyId!({ event: 'authorization.removed', diff --git a/apps/sim/lib/webhooks/providers/tiktok.ts b/apps/sim/lib/webhooks/providers/tiktok.ts index 0abedaa7e03..4196a11a2fd 100644 --- a/apps/sim/lib/webhooks/providers/tiktok.ts +++ b/apps/sim/lib/webhooks/providers/tiktok.ts @@ -201,15 +201,6 @@ export const tiktokHandler: WebhookProviderHandler = { } } - if (event === 'video.publish.completed' || event === 'video.upload.failed') { - return { - input: { - ...commonInput, - shareId: stringField(content, 'share_id') ?? null, - }, - } - } - return { input: commonInput } }, @@ -224,7 +215,6 @@ export const tiktokHandler: WebhookProviderHandler = { const content = parseTikTokContent(envelope.content) const publishId = stringField(content, 'publish_id') const postId = stringField(content, 'post_id') - const shareId = stringField(content, 'share_id') const createTime = typeof envelope.create_time === 'number' || typeof envelope.create_time === 'string' ? String(envelope.create_time) @@ -236,7 +226,7 @@ export const tiktokHandler: WebhookProviderHandler = { } else if (event === 'post.publish.complete' && publishId && createTime) { unique = `${publishId}:${createTime}` } else { - unique = publishId ?? shareId ?? createTime + unique = publishId ?? createTime } if (!unique) return null diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f7eecc8741a..7651cb1172a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -4133,7 +4133,6 @@ import { tiktokGetPostStatusTool, tiktokGetUserTool, tiktokListVideosTool, - tiktokQueryCreatorInfoTool, tiktokQueryVideosTool, tiktokUploadVideoDraftTool, } from '@/tools/tiktok' @@ -7296,7 +7295,6 @@ export const tools: Record = { tiktok_get_post_status: tiktokGetPostStatusTool, tiktok_get_user: tiktokGetUserTool, tiktok_list_videos: tiktokListVideosTool, - tiktok_query_creator_info: tiktokQueryCreatorInfoTool, tiktok_query_videos: tiktokQueryVideosTool, tiktok_upload_video_draft: tiktokUploadVideoDraftTool, tinybird_events: tinybirdEventsTool, diff --git a/apps/sim/tools/tiktok/api-schemas.test.ts b/apps/sim/tools/tiktok/api-schemas.test.ts index 4509e10e3e5..199c28d75df 100644 --- a/apps/sim/tools/tiktok/api-schemas.test.ts +++ b/apps/sim/tools/tiktok/api-schemas.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest' import { - tiktokCreatorInfoApiDataSchema, tiktokGetUserApiDataSchema, tiktokListVideosApiDataSchema, tiktokPostStatusApiDataSchema, @@ -32,23 +31,6 @@ describe('TikTok documented response schemas', () => { expect(tiktokQueryVideosApiDataSchema.safeParse({ videos: [{}] }).success).toBe(false) }) - it('requires the documented creator capability fields', () => { - const complete = { - creator_avatar_url: 'https://example.com/avatar', - creator_username: 'creator', - creator_nickname: 'Creator', - privacy_level_options: ['SELF_ONLY'], - comment_disabled: false, - duet_disabled: false, - stitch_disabled: false, - max_video_post_duration_sec: 180, - } - - expect(tiktokCreatorInfoApiDataSchema.safeParse(complete).success).toBe(true) - const { privacy_level_options: _privacyLevelOptions, ...incomplete } = complete - expect(tiktokCreatorInfoApiDataSchema.safeParse(incomplete).success).toBe(false) - }) - it('requires publish initialization identifiers and post status', () => { expect( tiktokPublishInitApiDataSchema.safeParse({ diff --git a/apps/sim/tools/tiktok/api-schemas.ts b/apps/sim/tools/tiktok/api-schemas.ts index 1e244a98f5e..b373fd1a7f9 100644 --- a/apps/sim/tools/tiktok/api-schemas.ts +++ b/apps/sim/tools/tiktok/api-schemas.ts @@ -53,18 +53,6 @@ export const tiktokQueryVideosApiDataSchema = z.object({ videos: z.array(tiktokApiVideoSchema), }) -/** Data payload returned by TikTok's creator info API. */ -export const tiktokCreatorInfoApiDataSchema = z.object({ - creator_avatar_url: z.string(), - creator_username: z.string(), - creator_nickname: z.string(), - privacy_level_options: z.array(z.string()), - comment_disabled: z.boolean(), - duet_disabled: z.boolean(), - stitch_disabled: z.boolean(), - max_video_post_duration_sec: z.number(), -}) - /** Data payload returned by TikTok's publish initialization APIs. */ export const tiktokPublishInitApiDataSchema = z.object({ publish_id: z.string(), diff --git a/apps/sim/tools/tiktok/get_post_status.test.ts b/apps/sim/tools/tiktok/get_post_status.test.ts index 960a6e64d67..db6764a2b1d 100644 --- a/apps/sim/tools/tiktok/get_post_status.test.ts +++ b/apps/sim/tools/tiktok/get_post_status.test.ts @@ -2,10 +2,11 @@ import { describe, expect, it } from 'vitest' import { tiktokGetPostStatusTool } from '@/tools/tiktok/get_post_status' describe('TikTok Get Post Status OAuth scopes', () => { - it("does not model TikTok's publish-or-upload authorization as all-of scopes", () => { + it('requires the draft-upload scope used by the supported posting flow', () => { expect(tiktokGetPostStatusTool.oauth).toEqual({ required: true, provider: 'tiktok', + requiredScopes: ['video.upload'], }) }) }) diff --git a/apps/sim/tools/tiktok/get_post_status.ts b/apps/sim/tools/tiktok/get_post_status.ts index a883e97f423..302eb90ae8d 100644 --- a/apps/sim/tools/tiktok/get_post_status.ts +++ b/apps/sim/tools/tiktok/get_post_status.ts @@ -26,6 +26,7 @@ export const tiktokGetPostStatusTool: ToolConfig< oauth: { required: true, provider: 'tiktok', + requiredScopes: ['video.upload'], }, params: { diff --git a/apps/sim/tools/tiktok/index.ts b/apps/sim/tools/tiktok/index.ts index 92fc56d1835..9239f23a5db 100644 --- a/apps/sim/tools/tiktok/index.ts +++ b/apps/sim/tools/tiktok/index.ts @@ -1,7 +1,6 @@ import { tiktokGetPostStatusTool } from '@/tools/tiktok/get_post_status' import { tiktokGetUserTool } from '@/tools/tiktok/get_user' import { tiktokListVideosTool } from '@/tools/tiktok/list_videos' -import { tiktokQueryCreatorInfoTool } from '@/tools/tiktok/query_creator_info' import { tiktokQueryVideosTool } from '@/tools/tiktok/query_videos' import { tiktokUploadVideoDraftTool } from '@/tools/tiktok/upload_video_draft' @@ -10,6 +9,5 @@ export * from '@/tools/tiktok/types' export { tiktokGetUserTool } export { tiktokListVideosTool } export { tiktokQueryVideosTool } -export { tiktokQueryCreatorInfoTool } export { tiktokUploadVideoDraftTool } export { tiktokGetPostStatusTool } diff --git a/apps/sim/tools/tiktok/query_creator_info.test.ts b/apps/sim/tools/tiktok/query_creator_info.test.ts deleted file mode 100644 index 7102736409b..00000000000 --- a/apps/sim/tools/tiktok/query_creator_info.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @vitest-environment node - */ -import { describe, expect, it } from 'vitest' -import { tiktokQueryCreatorInfoTool } from '@/tools/tiktok/query_creator_info' - -describe('TikTok Query Creator Info', () => { - it('exposes the temporary avatar as a composable file output', async () => { - const response = Response.json({ - data: { - creator_avatar_url: 'https://p16-sign.tiktokcdn-us.com/avatar.jpeg', - creator_username: 'creator', - creator_nickname: 'Creator', - privacy_level_options: ['SELF_ONLY'], - comment_disabled: true, - duet_disabled: false, - stitch_disabled: true, - max_video_post_duration_sec: 180, - }, - error: { code: 'ok' }, - }) - - await expect(tiktokQueryCreatorInfoTool.transformResponse?.(response)).resolves.toMatchObject({ - success: true, - output: { - creatorAvatarUrl: 'https://p16-sign.tiktokcdn-us.com/avatar.jpeg', - creatorAvatarFile: { - name: 'creator-avatar.jpg', - mimeType: 'image/jpeg', - url: 'https://p16-sign.tiktokcdn-us.com/avatar.jpeg', - }, - privacyLevelOptions: ['SELF_ONLY'], - }, - }) - expect(tiktokQueryCreatorInfoTool.outputs?.creatorAvatarFile).toMatchObject({ - type: 'file', - optional: true, - }) - expect(tiktokQueryCreatorInfoTool.outputs?.creatorAvatarUrl?.description).toMatch(/two hours/i) - }) - - it('omits the file descriptor when TikTok returns an empty avatar URL', async () => { - const response = Response.json({ - data: { - creator_avatar_url: '', - creator_username: 'creator', - creator_nickname: 'Creator', - privacy_level_options: ['SELF_ONLY'], - comment_disabled: false, - duet_disabled: false, - stitch_disabled: false, - max_video_post_duration_sec: 180, - }, - error: { code: 'ok' }, - }) - - const result = await tiktokQueryCreatorInfoTool.transformResponse?.(response) - - expect(result?.success).toBe(true) - expect(result?.output.creatorAvatarUrl).toBe('') - expect(result?.output).not.toHaveProperty('creatorAvatarFile') - expect(tiktokQueryCreatorInfoTool.outputs?.privacyLevelOptions?.items).toEqual({ - type: 'string', - description: 'Privacy level currently available to the authenticated creator', - }) - }) -}) diff --git a/apps/sim/tools/tiktok/query_creator_info.ts b/apps/sim/tools/tiktok/query_creator_info.ts deleted file mode 100644 index 8395319bd8c..00000000000 --- a/apps/sim/tools/tiktok/query_creator_info.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { tiktokCreatorInfoApiDataSchema } from '@/tools/tiktok/api-schemas' -import type { - TikTokQueryCreatorInfoParams, - TikTokQueryCreatorInfoResponse, -} from '@/tools/tiktok/types' -import { readTikTokApiResponse } from '@/tools/tiktok/utils' -import type { ToolConfig } from '@/tools/types' - -function emptyCreatorInfoOutput(): TikTokQueryCreatorInfoResponse['output'] { - return { - creatorAvatarUrl: null, - creatorUsername: null, - creatorNickname: null, - privacyLevelOptions: [], - commentDisabled: false, - duetDisabled: false, - stitchDisabled: false, - maxVideoPostDurationSec: null, - } -} - -export const tiktokQueryCreatorInfoTool: ToolConfig< - TikTokQueryCreatorInfoParams, - TikTokQueryCreatorInfoResponse -> = { - id: 'tiktok_query_creator_info', - name: 'TikTok Query Creator Info', - description: - "Inspect the authenticated creator's privacy options, interaction settings, and maximum video duration.", - version: '1.0.0', - - oauth: { - required: true, - provider: 'tiktok', - requiredScopes: ['video.publish'], - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'TikTok OAuth access token', - }, - }, - - request: { - url: () => 'https://open.tiktokapis.com/v2/post/publish/creator_info/query/', - method: 'POST', - headers: (params: TikTokQueryCreatorInfoParams) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json; charset=UTF-8', - }), - body: () => ({}), - }, - - transformResponse: async (response: Response): Promise => { - const { data: creatorInfo, error } = await readTikTokApiResponse( - response, - tiktokCreatorInfoApiDataSchema - ) - - if (error) { - return { - success: false, - output: emptyCreatorInfoOutput(), - error: error.message || 'Failed to query creator info', - } - } - - if (!creatorInfo) { - return { - success: false, - output: emptyCreatorInfoOutput(), - error: 'No creator info returned', - } - } - - return { - success: true, - output: { - creatorAvatarUrl: creatorInfo.creator_avatar_url ?? null, - ...(creatorInfo.creator_avatar_url && { - creatorAvatarFile: { - name: `${creatorInfo.creator_username || 'tiktok-creator'}-avatar.jpg`, - mimeType: 'image/jpeg', - url: creatorInfo.creator_avatar_url, - }, - }), - creatorUsername: creatorInfo.creator_username ?? null, - creatorNickname: creatorInfo.creator_nickname ?? null, - privacyLevelOptions: creatorInfo.privacy_level_options ?? [], - commentDisabled: creatorInfo.comment_disabled ?? false, - duetDisabled: creatorInfo.duet_disabled ?? false, - stitchDisabled: creatorInfo.stitch_disabled ?? false, - maxVideoPostDurationSec: creatorInfo.max_video_post_duration_sec ?? null, - }, - } - }, - - outputs: { - creatorAvatarUrl: { - type: 'string', - description: - 'Temporary URL of the current creator avatar. TikTok documents this URL as expiring two hours after it is returned.', - optional: true, - }, - creatorAvatarFile: { - type: 'file', - description: - 'Durable workflow-file copy of the current creator avatar, suitable for chaining into file-consuming blocks such as email attachments.', - optional: true, - }, - creatorUsername: { - type: 'string', - description: 'TikTok username of the creator', - optional: true, - }, - creatorNickname: { - type: 'string', - description: 'Display name/nickname of the creator', - optional: true, - }, - privacyLevelOptions: { - type: 'array', - description: - 'Available privacy levels for posting (e.g., PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY)', - items: { - type: 'string', - description: 'Privacy level currently available to the authenticated creator', - }, - }, - commentDisabled: { - type: 'boolean', - description: 'Whether the creator has disabled comments by default', - }, - duetDisabled: { - type: 'boolean', - description: 'Whether the creator has disabled duets by default', - }, - stitchDisabled: { - type: 'boolean', - description: 'Whether the creator has disabled stitches by default', - }, - maxVideoPostDurationSec: { - type: 'number', - description: 'Maximum allowed video duration in seconds', - optional: true, - }, - }, -} diff --git a/apps/sim/tools/tiktok/types.ts b/apps/sim/tools/tiktok/types.ts index 9a2b2a7059b..c1172179f31 100644 --- a/apps/sim/tools/tiktok/types.ts +++ b/apps/sim/tools/tiktok/types.ts @@ -121,27 +121,8 @@ export interface TikTokQueryVideosResponse extends ToolResponse { } } -/** - * Query Creator Info - Check posting permissions and get privacy options - */ -export interface TikTokQueryCreatorInfoParams extends TikTokBaseParams {} - -export interface TikTokQueryCreatorInfoResponse extends ToolResponse { - output: { - creatorAvatarUrl: string | null - creatorAvatarFile?: ToolFileData - creatorUsername: string | null - creatorNickname: string | null - privacyLevelOptions: string[] - commentDisabled: boolean - duetDisabled: boolean - stitchDisabled: boolean - maxVideoPostDurationSec: number | null - } -} - -/** Shared response shape for TikTok draft initialization tools. */ -export interface TikTokPublishResponse extends ToolResponse { +/** Response shape for TikTok inbox draft initialization. */ +export interface TikTokDraftInitResponse extends ToolResponse { output: { publishId: string } @@ -154,7 +135,7 @@ export interface TikTokUploadVideoDraftParams extends TikTokBaseParams { file: UserFile } -export type TikTokUploadVideoDraftResponse = TikTokPublishResponse +export type TikTokUploadVideoDraftResponse = TikTokDraftInitResponse /** * Get Post Status - Check status of a published/uploaded post @@ -180,6 +161,5 @@ export type TikTokResponse = | TikTokGetUserResponse | TikTokListVideosResponse | TikTokQueryVideosResponse - | TikTokQueryCreatorInfoResponse | TikTokUploadVideoDraftResponse | TikTokGetPostStatusResponse diff --git a/apps/sim/tools/tiktok/upload_video_draft.ts b/apps/sim/tools/tiktok/upload_video_draft.ts index 4e340cd2afa..ca478dd32dc 100644 --- a/apps/sim/tools/tiktok/upload_video_draft.ts +++ b/apps/sim/tools/tiktok/upload_video_draft.ts @@ -2,7 +2,7 @@ import type { TikTokUploadVideoDraftParams, TikTokUploadVideoDraftResponse, } from '@/tools/tiktok/types' -import { readTikTokPublishInitResponse, toTikTokPublishToolResponse } from '@/tools/tiktok/utils' +import { readTikTokDraftInitResponse, toTikTokDraftInitToolResponse } from '@/tools/tiktok/utils' import type { ToolConfig } from '@/tools/types' export const tiktokUploadVideoDraftTool: ToolConfig< @@ -37,7 +37,7 @@ export const tiktokUploadVideoDraftTool: ToolConfig< }, request: { - url: () => '/api/tools/tiktok/publish-video', + url: () => '/api/tools/tiktok/upload-video-draft', method: 'POST', headers: () => ({ 'Content-Type': 'application/json', @@ -49,8 +49,8 @@ export const tiktokUploadVideoDraftTool: ToolConfig< }, transformResponse: async (response: Response): Promise => { - const result = await readTikTokPublishInitResponse(response) - return toTikTokPublishToolResponse(result) + const result = await readTikTokDraftInitResponse(response) + return toTikTokDraftInitToolResponse(result) }, outputs: { diff --git a/apps/sim/tools/tiktok/utils.test.ts b/apps/sim/tools/tiktok/utils.test.ts index 674820b31f5..4ad79dcb90f 100644 --- a/apps/sim/tools/tiktok/utils.test.ts +++ b/apps/sim/tools/tiktok/utils.test.ts @@ -7,7 +7,7 @@ import { assertTikTokArrayLength, mapTikTokVideo, readTikTokApiResponse, - readTikTokPublishInitResponse, + readTikTokDraftInitResponse, TIKTOK_API_RESPONSE_MAX_BYTES, } from '@/tools/tiktok/utils' @@ -31,7 +31,7 @@ describe('TikTok tool utilities', () => { { status: 400 } ) - await expect(readTikTokPublishInitResponse(response)).resolves.toEqual({ + await expect(readTikTokDraftInitResponse(response)).resolves.toEqual({ success: false, publishId: '', error: 'Upload failed', @@ -73,20 +73,20 @@ describe('TikTok tool utilities', () => { error: { code: 'UnknownError' }, }) - await expect(readTikTokPublishInitResponse(response)).resolves.toEqual({ + await expect(readTikTokDraftInitResponse(response)).resolves.toEqual({ success: false, publishId: '', error: 'UnknownError', }) }) - it('normalizes direct TikTok publish responses', async () => { + it('normalizes TikTok draft initialization responses', async () => { const response = Response.json({ data: { publish_id: 'publish-1', upload_url: 'https://upload.example/video' }, error: { code: 'ok' }, }) - await expect(readTikTokPublishInitResponse(response)).resolves.toEqual({ + await expect(readTikTokDraftInitResponse(response)).resolves.toEqual({ success: true, publishId: 'publish-1', }) diff --git a/apps/sim/tools/tiktok/utils.ts b/apps/sim/tools/tiktok/utils.ts index 2ec2a7d6737..4fc6320e4c7 100644 --- a/apps/sim/tools/tiktok/utils.ts +++ b/apps/sim/tools/tiktok/utils.ts @@ -3,7 +3,7 @@ import { truncate } from '@sim/utils/string' import type { ZodType } from 'zod' import { isPayloadSizeLimitError, readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' import { type TikTokApiVideo, tiktokPublishInitApiDataSchema } from '@/tools/tiktok/api-schemas' -import type { TikTokApiError, TikTokPublishResponse, TikTokVideo } from '@/tools/tiktok/types' +import type { TikTokApiError, TikTokDraftInitResponse, TikTokVideo } from '@/tools/tiktok/types' export const TIKTOK_API_RESPONSE_MAX_BYTES = 1024 * 1024 @@ -69,7 +69,7 @@ interface ParsedJsonObject { rawBody: string } -interface TikTokPublishInitResult { +interface TikTokDraftInitResult { success: boolean publishId: string error?: string @@ -225,9 +225,9 @@ export function assertTikTokArrayLength(values: unknown[], label: string, maximu * The internal Sim upload route and TikTok both return publish-init envelopes. * Reading and normalization share one boundary. */ -export async function readTikTokPublishInitResponse( +export async function readTikTokDraftInitResponse( response: Response -): Promise { +): Promise { const parsed = await readJsonObject(response) if (!parsed.body) { return { @@ -267,15 +267,15 @@ export async function readTikTokPublishInitResponse( : { success: false, publishId: '', error: 'No publish ID returned' } } -/** Converts a normalized publish result into the shared tool response shape. */ -export function toTikTokPublishToolResponse( - result: TikTokPublishInitResult -): TikTokPublishResponse { +/** Converts a normalized draft-init result into the tool response shape. */ +export function toTikTokDraftInitToolResponse( + result: TikTokDraftInitResult +): TikTokDraftInitResponse { return result.success ? { success: true, output: { publishId: result.publishId } } : { success: false, output: { publishId: '' }, - error: result.error ?? 'Failed to initiate TikTok publish', + error: result.error ?? 'Failed to initiate TikTok draft upload', } } diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index de118f173c8..2dea0ccbd72 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -401,8 +401,6 @@ import { tiktokPostPubliclyAvailableTrigger, tiktokPostPublishCompleteTrigger, tiktokPostPublishFailedTrigger, - tiktokVideoPublishCompletedTrigger, - tiktokVideoUploadFailedTrigger, } from '@/triggers/tiktok' import { twilioSmsReceivedTrigger, twilioSmsStatusTrigger } from '@/triggers/twilio' import { twilioVoiceWebhookTrigger } from '@/triggers/twilio_voice' @@ -711,8 +709,6 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { tiktok_post_publicly_available: tiktokPostPubliclyAvailableTrigger, tiktok_post_publish_complete: tiktokPostPublishCompleteTrigger, tiktok_post_publish_failed: tiktokPostPublishFailedTrigger, - tiktok_video_publish_completed: tiktokVideoPublishCompletedTrigger, - tiktok_video_upload_failed: tiktokVideoUploadFailedTrigger, typeform_webhook: typeformWebhookTrigger, whatsapp_webhook: whatsappWebhookTrigger, google_forms_webhook: googleFormsWebhookTrigger, diff --git a/apps/sim/triggers/tiktok/index.ts b/apps/sim/triggers/tiktok/index.ts index 2f9321fd3c5..7dd7882e115 100644 --- a/apps/sim/triggers/tiktok/index.ts +++ b/apps/sim/triggers/tiktok/index.ts @@ -4,5 +4,3 @@ export { tiktokPostNoLongerPublicTrigger } from '@/triggers/tiktok/post_no_longe export { tiktokPostPubliclyAvailableTrigger } from '@/triggers/tiktok/post_publicly_available' export { tiktokPostPublishCompleteTrigger } from '@/triggers/tiktok/post_publish_complete' export { tiktokPostPublishFailedTrigger } from '@/triggers/tiktok/post_publish_failed' -export { tiktokVideoPublishCompletedTrigger } from '@/triggers/tiktok/video_publish_completed' -export { tiktokVideoUploadFailedTrigger } from '@/triggers/tiktok/video_upload_failed' diff --git a/apps/sim/triggers/tiktok/utils.ts b/apps/sim/triggers/tiktok/utils.ts index 4e29a218893..b8a79fe4649 100644 --- a/apps/sim/triggers/tiktok/utils.ts +++ b/apps/sim/triggers/tiktok/utils.ts @@ -18,8 +18,6 @@ export const TIKTOK_TRIGGER_EVENT_MAP: Record = { tiktok_post_inbox_delivered: 'post.publish.inbox_delivered', tiktok_post_publicly_available: 'post.publish.publicly_available', tiktok_post_no_longer_public: 'post.publish.no_longer_publicaly_available', - tiktok_video_publish_completed: 'video.publish.completed', - tiktok_video_upload_failed: 'video.upload.failed', tiktok_authorization_removed: 'authorization.removed', } @@ -121,13 +119,6 @@ export function buildTikTokPostingOutputs(options?: { return outputs } -export function buildTikTokVideoKitOutputs(): Record { - return { - ...buildCommonOutputs(), - shareId: { type: 'string', description: 'Share Kit / Video Kit share_id' }, - } -} - export function buildTikTokAuthorizationRemovedOutputs(): Record { return { ...buildCommonOutputs(), diff --git a/apps/sim/triggers/tiktok/video_publish_completed.ts b/apps/sim/triggers/tiktok/video_publish_completed.ts deleted file mode 100644 index ec069a7b1e6..00000000000 --- a/apps/sim/triggers/tiktok/video_publish_completed.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TikTokIcon } from '@/components/icons' -import { - buildTikTokTriggerSubBlocks, - buildTikTokVideoKitOutputs, - TIKTOK_WEBHOOK_HEADERS, -} from '@/triggers/tiktok/utils' -import type { TriggerConfig } from '@/triggers/types' - -/** - * Deprecated Share Kit / Video Kit trigger kept registered so existing - * workflows with this selectedTriggerId continue to resolve. - */ -export const tiktokVideoPublishCompletedTrigger: TriggerConfig = { - id: 'tiktok_video_publish_completed', - name: 'TikTok Video Publish Completed', - provider: 'tiktok', - description: 'Trigger when a Share Kit / Video Kit upload is published by the user', - version: '1.0.0', - deprecated: true, - icon: TikTokIcon, - subBlocks: buildTikTokTriggerSubBlocks( - 'tiktok_video_publish_completed', - 'video.publish.completed' - ), - outputs: buildTikTokVideoKitOutputs(), - webhook: { - method: 'POST', - headers: { ...TIKTOK_WEBHOOK_HEADERS }, - }, -} diff --git a/apps/sim/triggers/tiktok/video_upload_failed.ts b/apps/sim/triggers/tiktok/video_upload_failed.ts deleted file mode 100644 index 2a163d8028f..00000000000 --- a/apps/sim/triggers/tiktok/video_upload_failed.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TikTokIcon } from '@/components/icons' -import { - buildTikTokTriggerSubBlocks, - buildTikTokVideoKitOutputs, - TIKTOK_WEBHOOK_HEADERS, -} from '@/triggers/tiktok/utils' -import type { TriggerConfig } from '@/triggers/types' - -/** - * Deprecated Share Kit / Video Kit trigger kept registered so existing - * workflows with this selectedTriggerId continue to resolve. - */ -export const tiktokVideoUploadFailedTrigger: TriggerConfig = { - id: 'tiktok_video_upload_failed', - name: 'TikTok Video Upload Failed', - provider: 'tiktok', - description: 'Trigger when a Share Kit / Video Kit upload fails in TikTok', - version: '1.0.0', - deprecated: true, - icon: TikTokIcon, - subBlocks: buildTikTokTriggerSubBlocks('tiktok_video_upload_failed', 'video.upload.failed'), - outputs: buildTikTokVideoKitOutputs(), - webhook: { - method: 'POST', - headers: { ...TIKTOK_WEBHOOK_HEADERS }, - }, -}