From 1a39593b2938adcd1321f8788385a5563bf5e982 Mon Sep 17 00:00:00 2001 From: knuthtimo-lab Date: Thu, 9 Jul 2026 18:18:17 +0200 Subject: [PATCH] TikTok V3 --- docs/automations/social-accounts-and-jobs.md | 11 +++++++-- src/app/(main)/api/tiktok/connect/route.ts | 2 +- src/app/(main)/api/tiktok/upload/route.ts | 24 +++++++++++++------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/docs/automations/social-accounts-and-jobs.md b/docs/automations/social-accounts-and-jobs.md index db4cffb..aa171b9 100644 --- a/docs/automations/social-accounts-and-jobs.md +++ b/docs/automations/social-accounts-and-jobs.md @@ -56,12 +56,19 @@ - Do not treat a generic approval like "Go" as permission for automatic/direct TikTok publishing. ## TikTok Analytics (Display API, read-only) -- Both apps request the scopes `user.info.basic,user.info.stats,video.list,video.upload,video.publish`. +- The requested scopes MUST exactly match what each app has approved in the TikTok Developer Portal — requesting an unapproved scope aborts the whole OAuth login with a generic "scope" error. +- Approved scopes per app (as of 2026-07-09): + - QRMaster: `user.info.basic, user.info.profile, user.info.stats, video.list, video.upload` (NO `video.publish` — upload/draft only). + - GreenLens Pro: `user.info.basic, video.upload, video.publish` (NO analytics scopes yet — a portal revision adding `user.info.stats` + `video.list` is required before GreenLens analytics works). - Admin-key-protected live analytics endpoints (no data is stored, every call reads fresh from TikTok): - QRMaster: `GET /api/tiktok/analytics?key=&max_videos=50` - GreenLens: `GET /api/tiktok/analytics` (guarded by the plant import admin key) - Response: account stats (followers, total likes, video count) plus per-video views/likes/comments/shares with computed `engagement_rate`, `posted_weekday_utc` and `posted_hour_utc`, and a summary block (totals, average/median views). -- The new scopes must be enabled for the app in the TikTok Developer Portal, and accounts connected before the scope change must re-authorize via `/api/tiktok/connect` — otherwise TikTok returns a scope error. +- After any scope change the account must re-authorize via `/api/tiktok/connect`, otherwise the stored token keeps the old permissions. + +## TikTok Photo/Carousel Upload Notes +- Photo posts use `POST /v2/post/publish/content/init/` with `post_mode: MEDIA_UPLOAD` (draft in the creator's inbox, needs only `video.upload`). Valid post modes are only `MEDIA_UPLOAD` and `DIRECT_POST` — `DRAFT` is not a valid value. +- Photos are delivered via `source_info.photo_images` as public URLs (`PULL_FROM_URL`). TikTok only pulls from **verified domains** — verify the hosting domain (e.g. `greenlenspro.com` for MinIO storage URLs) under Content Posting API → "Verify domains" in the Developer Portal, otherwise the upload fails. ## Refresh Behavior - Meta user/page tokens werden automatisch refreshed durch `python C:\Users\timo\Documents\meta_token_refresh.py`. diff --git a/src/app/(main)/api/tiktok/connect/route.ts b/src/app/(main)/api/tiktok/connect/route.ts index a1be41a..301ab82 100644 --- a/src/app/(main)/api/tiktok/connect/route.ts +++ b/src/app/(main)/api/tiktok/connect/route.ts @@ -24,7 +24,7 @@ export async function GET(request: NextRequest) { const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/'); authUrl.searchParams.set('client_key', clientKey); - authUrl.searchParams.set('scope', 'user.info.basic,user.info.stats,video.list,video.upload,video.publish'); + authUrl.searchParams.set('scope', 'user.info.basic,user.info.profile,user.info.stats,video.list,video.upload'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('redirect_uri', redirectUri); authUrl.searchParams.set('state', oauthState); diff --git a/src/app/(main)/api/tiktok/upload/route.ts b/src/app/(main)/api/tiktok/upload/route.ts index b167fb5..6d2a103 100644 --- a/src/app/(main)/api/tiktok/upload/route.ts +++ b/src/app/(main)/api/tiktok/upload/route.ts @@ -92,16 +92,24 @@ export async function POST(request: NextRequest) { ); } + const title = typeof json?.title === 'string' ? json.title.trim() : ''; + const description = typeof json?.description === 'string' ? json.description.trim() : ''; + const initBody = { media_type: 'PHOTO', - photo_cover_index: 0, - file_paths: photoUrls, - file_extensions: photoUrls.map((url) => { - const fileName = String(new URL(url).pathname).split('/').pop() || 'photo.jpg'; - const extension = fileName.split('.').pop() || 'jpg'; - return extension.startsWith('.') ? extension.slice(1) : extension; - }), - post_mode: 'DIRECT_POST', + // MEDIA_UPLOAD = draft in the creator's TikTok inbox (posting + // policy is upload/draft only) and only needs the video.upload + // scope — QRMaster has no video.publish. + post_mode: 'MEDIA_UPLOAD', + post_info: { + ...(title ? { title } : {}), + ...(description ? { description } : {}), + }, + source_info: { + source: 'PULL_FROM_URL', + photo_cover_index: 0, + photo_images: photoUrls, + }, }; const initResult = await tiktokApi(