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 76% 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 4bd583a2a92..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({ @@ -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/upload-video-draft/route.ts similarity index 88% 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 ec1cf21c55b..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,28 +43,19 @@ 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 - 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 +137,10 @@ 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 } ) } @@ -203,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 a9aec3ca9eb..6ed77ef3955 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,21 +36,16 @@ 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() }) 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 0cddbde45d6..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', ]) @@ -22,16 +20,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 +35,7 @@ export const TikTokBlock: BlockConfig = { bgColor: '#000000', icon: TikTokIcon, triggerAllowed: true, - hideFromToolbar: true, + hideFromToolbar: false, subBlocks: [ { @@ -58,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' }, ], @@ -140,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', @@ -150,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' }, }, { @@ -168,8 +155,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 +166,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', ], }, @@ -192,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', ], @@ -227,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 @@ -340,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 ee24ba93bd3..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,78 +8,30 @@ const file = { type: 'video/mp4', } -describe('tiktokPublishVideoBodySchema', () => { - it('accepts a draft without direct-post metadata', () => { - expect( - tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - mode: 'draft', - file, - }).success - ).toBe(true) +describe('tiktokUploadVideoDraftBodySchema', () => { + it('accepts a draft upload body', () => { + const parsed = tiktokUploadVideoDraftBodySchema.safeParse({ + accessToken: 'token', + file, + }) + expect(parsed.success).toBe(true) + if (parsed.success) { + expect(parsed.data).toEqual({ accessToken: 'token', file }) + } }) - it('requires direct-post privacy and commercial-content disclosure', () => { + it('rejects a body without an access token', () => { expect( - tiktokPublishVideoBodySchema.safeParse({ - accessToken: 'token', - mode: 'direct', + tiktokUploadVideoDraftBodySchema.safeParse({ file, - postInfo: { privacy_level: 'SELF_ONLY' }, }).success ).toBe(false) }) - it('accepts documented direct-post metadata', () => { + it('rejects a body without a file', () => { expect( - tiktokPublishVideoBodySchema.safeParse({ + tiktokUploadVideoDraftBodySchema.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', () => { - 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..b14c5eda42c 100644 --- a/apps/sim/lib/api/contracts/tiktok-tools.ts +++ b/apps/sim/lib/api/contracts/tiktok-tools.ts @@ -6,51 +6,25 @@ 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 tiktokUploadVideoDraftBodySchema = 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({ +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/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/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 7960aad288e..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': "Directly post content to a user's TikTok profile", '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..7651cb1172a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -4130,11 +4130,9 @@ import { thriveUpdateUserTool, } from '@/tools/thrive' import { - tiktokDirectPostVideoTool, tiktokGetPostStatusTool, tiktokGetUserTool, tiktokListVideosTool, - tiktokQueryCreatorInfoTool, tiktokQueryVideosTool, tiktokUploadVideoDraftTool, } from '@/tools/tiktok' @@ -7294,11 +7292,9 @@ 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, - 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/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.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 99ff8ee84c8..302eb90ae8d 100644 --- a/apps/sim/tools/tiktok/get_post_status.ts +++ b/apps/sim/tools/tiktok/get_post_status.ts @@ -20,12 +20,13 @@ 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: { 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 d20db2ae69a..9239f23a5db 100644 --- a/apps/sim/tools/tiktok/index.ts +++ b/apps/sim/tools/tiktok/index.ts @@ -1,8 +1,6 @@ -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' -import { tiktokQueryCreatorInfoTool } from '@/tools/tiktok/query_creator_info' import { tiktokQueryVideosTool } from '@/tools/tiktok/query_videos' import { tiktokUploadVideoDraftTool } from '@/tools/tiktok/upload_video_draft' @@ -11,7 +9,5 @@ export * from '@/tools/tiktok/types' export { tiktokGetUserTool } export { tiktokListVideosTool } export { tiktokQueryVideosTool } -export { tiktokQueryCreatorInfoTool } -export { tiktokDirectPostVideoTool } 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 2b969b9aa1e..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 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.", - 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 b9320ab1622..c1172179f31 100644 --- a/apps/sim/tools/tiktok/types.ts +++ b/apps/sim/tools/tiktok/types.ts @@ -121,51 +121,13 @@ 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 - } -} - -/** - * 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. */ -export interface TikTokPublishResponse extends ToolResponse { +/** Response shape for TikTok inbox draft initialization. */ +export interface TikTokDraftInitResponse 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 */ @@ -173,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 @@ -199,7 +161,5 @@ export type TikTokResponse = | TikTokGetUserResponse | 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..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,21 +37,20 @@ 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', }), body: (params: TikTokUploadVideoDraftParams) => ({ accessToken: params.accessToken, - mode: 'draft', file: params.file, }), }, 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 6e5426cc398..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', @@ -61,19 +61,32 @@ 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('normalizes direct TikTok publish responses', async () => { + it('surfaces TikTok error codes when the provider omits a message', async () => { + const response = Response.json({ + data: {}, + error: { code: 'UnknownError' }, + }) + + await expect(readTikTokDraftInitResponse(response)).resolves.toEqual({ + success: false, + publishId: '', + error: 'UnknownError', + }) + }) + + 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 56617c3435e..4fc6320e4c7 100644 --- a/apps/sim/tools/tiktok/utils.ts +++ b/apps/sim/tools/tiktok/utils.ts @@ -1,8 +1,9 @@ +import { getErrorMessage } from '@sim/utils/errors' import { truncate } from '@sim/utils/string' 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' +import type { TikTokApiError, TikTokDraftInitResponse, TikTokVideo } from '@/tools/tiktok/types' export const TIKTOK_API_RESPONSE_MAX_BYTES = 1024 * 1024 @@ -68,7 +69,7 @@ interface ParsedJsonObject { rawBody: string } -interface TikTokPublishInitResult { +interface TikTokDraftInitResult { success: boolean publishId: string error?: string @@ -120,13 +121,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: '', } @@ -221,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 { @@ -254,7 +258,7 @@ 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', } } @@ -263,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/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 }, - }, -}