@volter/twin-tiktok 0.1.37 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -308
- package/business/spec/SOURCE.md +17 -0
- package/business/spec/openapi.json +1045 -0
- package/business/spec/sources.json +84 -0
- package/business/src/fetch.ts +14 -0
- package/business/src/generated/surface.gen.json +1 -0
- package/business/src/generated/ui.gen.json +1 -0
- package/business/src/index.ts +3 -0
- package/business/src/manifest.ts +38 -0
- package/business/src/semantics/index.ts +16 -0
- package/business/src/semantics/open_api.ts +142 -0
- package/business/src/semantics/shared.ts +119 -0
- package/business/src/semantics/states.ts +3 -0
- package/dist/business/spec/SOURCE.md +17 -0
- package/dist/business/spec/openapi.json +1045 -0
- package/dist/business/spec/sources.json +84 -0
- package/dist/business/src/fetch.d.ts +2 -0
- package/dist/business/src/fetch.js +13 -0
- package/dist/business/src/fetch.ts +14 -0
- package/dist/business/src/generated/surface.gen.json +1 -0
- package/dist/business/src/generated/ui.gen.json +1 -0
- package/dist/business/src/index.d.ts +2 -0
- package/dist/business/src/index.js +3 -0
- package/dist/business/src/index.ts +3 -0
- package/dist/business/src/manifest.d.ts +2 -0
- package/dist/business/src/manifest.js +33 -0
- package/dist/business/src/manifest.ts +38 -0
- package/dist/business/src/semantics/index.d.ts +2 -0
- package/dist/business/src/semantics/index.js +12 -0
- package/dist/business/src/semantics/index.ts +16 -0
- package/dist/business/src/semantics/open_api.d.ts +10 -0
- package/dist/business/src/semantics/open_api.js +162 -0
- package/dist/business/src/semantics/open_api.ts +142 -0
- package/dist/business/src/semantics/shared.d.ts +31 -0
- package/dist/business/src/semantics/shared.js +109 -0
- package/dist/business/src/semantics/shared.ts +119 -0
- package/dist/business/src/semantics/states.d.ts +8 -0
- package/dist/business/src/semantics/states.js +3 -0
- package/dist/business/src/semantics/states.ts +3 -0
- package/dist/generated/pack-facts.json +55 -0
- package/dist/journeys/coverage.json +220 -0
- package/dist/journeys/customer-life.json +2950 -0
- package/dist/journeys/customer-life.outline.md +59 -0
- package/dist/journeys/decisions.json +161 -0
- package/dist/journeys/demand.json +142 -0
- package/dist/journeys/score.json +294 -0
- package/dist/journeys/unreachable.json +13 -0
- package/dist/open/spec/SOURCE.md +20 -0
- package/dist/open/spec/openapi.json +1417 -0
- package/dist/open/spec/sources.json +124 -0
- package/dist/open/src/engine/mp4.d.ts +6 -0
- package/dist/open/src/engine/mp4.js +55 -0
- package/dist/open/src/engine/mp4.ts +48 -0
- package/dist/open/src/fetch.d.ts +2 -0
- package/dist/open/src/fetch.js +17 -0
- package/dist/open/src/fetch.ts +18 -0
- package/dist/open/src/generated/surface.gen.json +1 -0
- package/dist/open/src/generated/ui.gen.json +1 -0
- package/dist/open/src/index.d.ts +2 -0
- package/dist/open/src/index.js +3 -0
- package/dist/open/src/index.ts +3 -0
- package/dist/open/src/manifest.d.ts +2 -0
- package/dist/open/src/manifest.js +53 -0
- package/dist/open/src/manifest.ts +59 -0
- package/dist/open/src/screens/authorize.d.ts +2 -0
- package/dist/open/src/screens/authorize.js +53 -0
- package/dist/open/src/screens/authorize.ts +53 -0
- package/dist/open/src/semantics/doors.d.ts +10 -0
- package/dist/open/src/semantics/doors.js +118 -0
- package/dist/open/src/semantics/doors.ts +121 -0
- package/dist/open/src/semantics/index.d.ts +2 -0
- package/dist/open/src/semantics/index.js +16 -0
- package/dist/open/src/semantics/index.ts +20 -0
- package/dist/open/src/semantics/oauth.d.ts +2 -0
- package/dist/open/src/semantics/oauth.js +30 -0
- package/dist/open/src/semantics/oauth.ts +28 -0
- package/dist/open/src/semantics/post.d.ts +6 -0
- package/dist/open/src/semantics/post.js +66 -0
- package/dist/open/src/semantics/post.ts +60 -0
- package/dist/open/src/semantics/shared.d.ts +51 -0
- package/dist/open/src/semantics/shared.js +196 -0
- package/dist/open/src/semantics/shared.ts +197 -0
- package/dist/open/src/semantics/states.d.ts +20 -0
- package/dist/open/src/semantics/states.js +15 -0
- package/dist/open/src/semantics/states.ts +23 -0
- package/dist/open/src/semantics/user.d.ts +2 -0
- package/dist/open/src/semantics/user.js +24 -0
- package/dist/open/src/semantics/user.ts +24 -0
- package/dist/open/src/semantics/video.d.ts +4 -0
- package/dist/open/src/semantics/video.js +112 -0
- package/dist/open/src/semantics/video.ts +97 -0
- package/dist/src/cli.js +11 -34
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +9 -0
- package/dist/src/index.d.ts +5 -22
- package/dist/src/index.js +13 -167
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +28 -0
- package/dist/src/semantics/shared.d.ts +49 -0
- package/dist/src/semantics/shared.js +61 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +55 -0
- package/journeys/coverage.json +220 -0
- package/journeys/customer-life.json +2950 -0
- package/journeys/customer-life.outline.md +59 -0
- package/journeys/decisions.json +161 -0
- package/journeys/demand.json +142 -0
- package/journeys/score.json +294 -0
- package/journeys/unreachable.json +13 -0
- package/open/spec/SOURCE.md +20 -0
- package/open/spec/openapi.json +1417 -0
- package/open/spec/sources.json +124 -0
- package/open/src/engine/mp4.ts +48 -0
- package/open/src/fetch.ts +18 -0
- package/open/src/generated/surface.gen.json +1 -0
- package/open/src/generated/ui.gen.json +1 -0
- package/open/src/index.ts +3 -0
- package/open/src/manifest.ts +59 -0
- package/open/src/screens/authorize.ts +53 -0
- package/open/src/semantics/doors.ts +121 -0
- package/open/src/semantics/index.ts +20 -0
- package/open/src/semantics/oauth.ts +28 -0
- package/open/src/semantics/post.ts +60 -0
- package/open/src/semantics/shared.ts +197 -0
- package/open/src/semantics/states.ts +23 -0
- package/open/src/semantics/user.ts +24 -0
- package/open/src/semantics/video.ts +97 -0
- package/package.json +13 -41
- package/src/cli.ts +11 -32
- package/src/fetch.ts +10 -0
- package/src/index.ts +15 -270
- package/src/manifest.ts +33 -0
- package/src/semantics/shared.ts +78 -0
- package/src/server.ts +8 -0
- package/client/tiktok-consent.tsx +0 -154
- package/client/tiktok-mirror.css +0 -137
- package/client/tiktok-mirror.tsx +0 -497
- package/dist/client/tiktok-consent.bundle.js +0 -18
- package/dist/client/tiktok-consent.d.ts +0 -47
- package/dist/client/tiktok-consent.js +0 -20
- package/dist/client/tiktok-consent.tsx +0 -154
- package/dist/client/tiktok-mirror.bundle.js +0 -484
- package/dist/client/tiktok-mirror.css +0 -137
- package/dist/client/tiktok-mirror.d.ts +0 -42
- package/dist/client/tiktok-mirror.js +0 -325
- package/dist/client/tiktok-mirror.tsx +0 -497
- package/dist/src/tiktok-blobs.d.ts +0 -66
- package/dist/src/tiktok-blobs.js +0 -161
- package/dist/src/tiktok-budget.d.ts +0 -56
- package/dist/src/tiktok-budget.js +0 -136
- package/dist/src/tiktok-capabilities.d.ts +0 -7
- package/dist/src/tiktok-capabilities.js +0 -1855
- package/dist/src/tiktok-conformance.d.ts +0 -11
- package/dist/src/tiktok-conformance.js +0 -498
- package/dist/src/tiktok-connector.d.ts +0 -158
- package/dist/src/tiktok-connector.js +0 -600
- package/dist/src/tiktok-consent-ui.d.ts +0 -19
- package/dist/src/tiktok-consent-ui.js +0 -127
- package/dist/src/tiktok-errors.d.ts +0 -78
- package/dist/src/tiktok-errors.js +0 -175
- package/dist/src/tiktok-ids.d.ts +0 -16
- package/dist/src/tiktok-ids.js +0 -48
- package/dist/src/tiktok-media.d.ts +0 -7
- package/dist/src/tiktok-media.js +0 -86
- package/dist/src/tiktok-mirror-ui.d.ts +0 -61
- package/dist/src/tiktok-mirror-ui.js +0 -187
- package/dist/src/tiktok-pkce.d.ts +0 -25
- package/dist/src/tiktok-pkce.js +0 -56
- package/dist/src/tiktok-posting.d.ts +0 -100
- package/dist/src/tiktok-posting.js +0 -599
- package/dist/src/tiktok-sample-mp4.d.ts +0 -10
- package/dist/src/tiktok-sample-mp4.js +0 -55
- package/dist/src/tiktok-scopes.d.ts +0 -29
- package/dist/src/tiktok-scopes.js +0 -106
- package/dist/src/tiktok-server.d.ts +0 -28
- package/dist/src/tiktok-server.js +0 -99
- package/dist/src/tiktok-store.d.ts +0 -164
- package/dist/src/tiktok-store.js +0 -451
- package/dist/src/tiktok-twin.d.ts +0 -70
- package/dist/src/tiktok-twin.js +0 -1197
- package/dist/src/tiktok-user.d.ts +0 -28
- package/dist/src/tiktok-user.js +0 -174
- package/src/tiktok-blobs.ts +0 -217
- package/src/tiktok-budget.ts +0 -163
- package/src/tiktok-capabilities.ts +0 -2022
- package/src/tiktok-conformance.ts +0 -526
- package/src/tiktok-connector.ts +0 -637
- package/src/tiktok-consent-ui.ts +0 -146
- package/src/tiktok-errors.ts +0 -197
- package/src/tiktok-ids.ts +0 -51
- package/src/tiktok-journey.uitest.ts +0 -305
- package/src/tiktok-media.ts +0 -89
- package/src/tiktok-mirror-ui.ts +0 -193
- package/src/tiktok-pkce.ts +0 -61
- package/src/tiktok-posting.ts +0 -617
- package/src/tiktok-sample-mp4.ts +0 -54
- package/src/tiktok-scopes.ts +0 -122
- package/src/tiktok-server.ts +0 -110
- package/src/tiktok-store.ts +0 -543
- package/src/tiktok-twin.ts +0 -1361
- package/src/tiktok-user.ts +0 -137
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH": {
|
|
3
|
+
"status": 200,
|
|
4
|
+
"checked": "2026-10-03",
|
|
5
|
+
"quotes": {
|
|
6
|
+
"These return codes take precedence over HTTP status codes": true,
|
|
7
|
+
"The interface (endpoint) has not been implemented.": true,
|
|
8
|
+
"Invalid authorization code.": true,
|
|
9
|
+
"Invalid or incorrect refresh token.": true,
|
|
10
|
+
"Successful": true,
|
|
11
|
+
"Invalid or incorrect access token.": true,
|
|
12
|
+
"The access token is empty.": true,
|
|
13
|
+
"The access token has expired.": true,
|
|
14
|
+
"No permission to perform the related operation.": true,
|
|
15
|
+
"App ID or secret is incorrect.": true
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1738084387220481&language=ENGLISH": {
|
|
19
|
+
"status": 200,
|
|
20
|
+
"checked": "2026-10-03",
|
|
21
|
+
"quotes": {
|
|
22
|
+
"The code is valid for 10 minutes and can only be used once.": true,
|
|
23
|
+
"Use this endpoint to renew a TikTok account access token by": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228399168514&language=ENGLISH": {
|
|
27
|
+
"status": 200,
|
|
28
|
+
"checked": "2026-10-03",
|
|
29
|
+
"quotes": {
|
|
30
|
+
"Requested fields. If not set, returns the": true,
|
|
31
|
+
"Use this endpoint to access detailed analytics and insights about a TikTok account": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH": {
|
|
35
|
+
"status": 200,
|
|
36
|
+
"checked": "2026-10-03",
|
|
37
|
+
"quotes": {
|
|
38
|
+
"you need to at least include": true,
|
|
39
|
+
"Maximum value": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228496095234&language=ENGLISH": {
|
|
43
|
+
"status": 200,
|
|
44
|
+
"checked": "2026-10-03",
|
|
45
|
+
"quotes": {
|
|
46
|
+
"This field is required, even if you don't want to use any of the additional parameters": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1803630424390658&language=ENGLISH": {
|
|
50
|
+
"status": 200,
|
|
51
|
+
"checked": "2026-10-03",
|
|
52
|
+
"quotes": {
|
|
53
|
+
"A list of up to 35 publicly accessible HTTP(s) URLs for the photo content to be published.": true,
|
|
54
|
+
"Length limit: 90 runes (UTF-16 code units).": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH": {
|
|
58
|
+
"status": 200,
|
|
59
|
+
"checked": "2026-10-03",
|
|
60
|
+
"quotes": {
|
|
61
|
+
"The publishing status of the TikTok post.": true,
|
|
62
|
+
"Returned only when": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH": {
|
|
66
|
+
"status": 200,
|
|
67
|
+
"checked": "2026-10-03",
|
|
68
|
+
"quotes": {
|
|
69
|
+
"Commercial Music Library": true,
|
|
70
|
+
"Default value": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1870580030749698&language=ENGLISH": {
|
|
74
|
+
"status": 200,
|
|
75
|
+
"checked": "2026-10-03",
|
|
76
|
+
"quotes": {
|
|
77
|
+
"Length limit: 100 characters.": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"https://developers.tiktok.com/doc/content-posting-api-reference-get-video-status": {
|
|
81
|
+
"status": 200,
|
|
82
|
+
"checked": "2026-10-03"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// GENERATED by scripts/create-pack.ts (--init, --index): a fixed file. The kernel serves the discovery door, the clock,
|
|
2
|
+
// the World's doors, the vendor's screens and the API from the pack's parts (architecture, "The pack contract").
|
|
3
|
+
import { createPackFetch, type DerivedFetch, type DerivedSurface, type PackFetchOptions } from '@volter/world-core';
|
|
4
|
+
import surface from './generated/surface.gen.json' with { type: 'json' };
|
|
5
|
+
import { manifest } from './manifest.ts';
|
|
6
|
+
import { businessSemantics } from './semantics/index.ts';
|
|
7
|
+
|
|
8
|
+
export function createBusinessLaneFetch(options: PackFetchOptions = {}): DerivedFetch {
|
|
9
|
+
return createPackFetch({
|
|
10
|
+
manifest,
|
|
11
|
+
surface: surface as DerivedSurface,
|
|
12
|
+
handlers: businessSemantics,
|
|
13
|
+
}, options);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"generatedBy":"scripts/derive-pack.ts","spec":"openapi.json","format":"openapi","version":"v1.3","resources":[],"operations":[{"id":"businessGet","method":"get","path":"/open_api/v1.3/business/get/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"fields","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessPhotoPublish","method":"post","path":"/open_api/v1.3/business/photo/publish/","pathParams":[],"query":[],"body":[{"name":"business_id","type":"string","required":false},{"name":"photo_cover_index","type":"integer","required":false},{"name":"photo_images","type":"array","required":false},{"name":"post_info","type":"object","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"businessPublishLocation","method":"get","path":"/open_api/v1.3/business/publish/location/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"search_query","type":"string","required":true}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessPublishStatus","method":"get","path":"/open_api/v1.3/business/publish/status/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"publish_id","type":"string","required":true}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessVideoList","method":"get","path":"/open_api/v1.3/business/video/list/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"cursor","type":"integer","required":false},{"name":"fields","type":"string","required":false},{"name":"max_count","type":"integer","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"businessVideoPublish","method":"post","path":"/open_api/v1.3/business/video/publish/","pathParams":[],"query":[],"body":[{"name":"business_id","type":"string","required":false},{"name":"custom_thumbnail_url","type":"string","required":false},{"name":"post_info","type":"object","required":false},{"name":"video_url","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"discoveryCmlTrendingList","method":"get","path":"/open_api/v1.3/discovery/cml/trending_list/","pathParams":[],"query":[{"name":"business_id","type":"string","required":true},{"name":"genre","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"ttUserOauth2RefreshToken","method":"post","path":"/open_api/v1.3/tt_user/oauth2/refresh_token/","pathParams":[],"query":[],"body":[{"name":"client_id","type":"string","required":false},{"name":"client_secret","type":"string","required":false},{"name":"grant_type","type":"string","required":false},{"name":"refresh_token","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"ttUserOauth2Revoke","method":"post","path":"/open_api/v1.3/tt_user/oauth2/revoke/","pathParams":[],"query":[],"body":[{"name":"access_token","type":"string","required":false},{"name":"client_id","type":"string","required":false},{"name":"client_secret","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"ttUserOauth2Token","method":"post","path":"/open_api/v1.3/tt_user/oauth2/token/","pathParams":[],"query":[],"body":[{"name":"auth_code","type":"string","required":false},{"name":"client_id","type":"string","required":false},{"name":"client_secret","type":"string","required":false},{"name":"grant_type","type":"string","required":false},{"name":"redirect_uri","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"ttUserTokenInfoGet","method":"post","path":"/open_api/v1.3/tt_user/token_info/get/","pathParams":[],"query":[],"body":[{"name":"access_token","type":"string","required":false},{"name":"app_id","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"}]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"generatedBy":"scripts/derive-pack.ts","resources":{"publish":{"actions":[],"states":{"status":["FAILED","PROCESSING_DOWNLOAD","PROCESSING_UPLOAD","PUBLISH_COMPLETE","SEND_TO_USER_INBOX"]},"embeds":{}},"video":{"actions":[],"states":{},"embeds":{}},"_access_token":{"actions":[],"states":{},"embeds":{}},"_refresh_token":{"actions":[],"states":{},"embeds":{}},"_seq_publish":{"actions":[],"states":{},"embeds":{}},"_seq_video":{"actions":[],"states":{},"embeds":{}}}}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The business lane's manifest: the vendor facts its spec (../spec, written from the API for Business v1.3 pages) does
|
|
2
|
+
// not carry. Served is what Postiz's TikTok Business provider calls (../../journeys/demand.json,
|
|
3
|
+
// ../../journeys/decisions.json); revoking and the token's scopes are `unmodeled`.
|
|
4
|
+
import type { DerivedManifest } from '@volter/world-core';
|
|
5
|
+
import { states } from './semantics/states.ts';
|
|
6
|
+
|
|
7
|
+
export const manifest: DerivedManifest = {
|
|
8
|
+
vendor: 'tiktok',
|
|
9
|
+
// the vendor's one store: the open lane's apps, people, codes, posts and videos are these too
|
|
10
|
+
service: 'tiktok',
|
|
11
|
+
body: { json: 'always' },
|
|
12
|
+
ids: { template: '{uuid}' },
|
|
13
|
+
time: 'unix',
|
|
14
|
+
// "Every Business API endpoint (except OAuth) ... wraps the payload in {code, message, data}" (Postiz reads it so):
|
|
15
|
+
// a refusal is a non-zero code, answered HTTP 200
|
|
16
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "These return codes take precedence over HTTP status codes"
|
|
17
|
+
error: { code: '{code}', message: '{message}', request_id: '{id}', data: {} },
|
|
18
|
+
errorsAnsweredAs: { status: 200, below: 500 },
|
|
19
|
+
readOnly: { status: 200, message: 'This twin was started read-only; writes are refused.' },
|
|
20
|
+
notFound: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
|
|
21
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "The interface (endpoint) has not been implemented."
|
|
22
|
+
// Where documentation stops: token errors do not establish an authentication gate before unknown-route resolution.
|
|
23
|
+
gap: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
|
|
24
|
+
deleted: {},
|
|
25
|
+
resources: {
|
|
26
|
+
publish: { idPrefix: '', ...states.publish },
|
|
27
|
+
video: { idPrefix: '' },
|
|
28
|
+
_access_token: { idPrefix: 'act_', ids: '{prefix}{n}' },
|
|
29
|
+
_refresh_token: { idPrefix: 'rft_', ids: '{prefix}{n}' },
|
|
30
|
+
_seq_publish: { idPrefix: '', ids: '{n}' },
|
|
31
|
+
_seq_video: { idPrefix: '', ids: '{n}' },
|
|
32
|
+
},
|
|
33
|
+
unmodeled: ['ttUserOauth2Revoke', 'ttUserTokenInfoGet'],
|
|
34
|
+
discovery: {
|
|
35
|
+
twinOf: "TikTok's API for Business v1.3 as a TikTok account's owner uses it: its tokens, its profile, its posts, publishing videos and photos by URL, the publishing status, location tags and the Commercial Music Library's popular tracks",
|
|
36
|
+
stores: "the vendor's one store (the open lane's): apps, people, codes and tokens, posts and videos, and the World's location tags and tracks",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
|
|
2
|
+
// handler named as its operation). Do not edit; add a handler to its family's file and generate again.
|
|
3
|
+
import type { Handler } from '@volter/world-core';
|
|
4
|
+
import * as open_api from './open_api.ts';
|
|
5
|
+
|
|
6
|
+
export const businessSemantics: Record<string, Handler> = {
|
|
7
|
+
businessGet: open_api.businessGet,
|
|
8
|
+
businessPhotoPublish: open_api.businessPhotoPublish,
|
|
9
|
+
businessPublishLocation: open_api.businessPublishLocation,
|
|
10
|
+
businessPublishStatus: open_api.businessPublishStatus,
|
|
11
|
+
businessVideoList: open_api.businessVideoList,
|
|
12
|
+
businessVideoPublish: open_api.businessVideoPublish,
|
|
13
|
+
discoveryCmlTrendingList: open_api.discoveryCmlTrendingList,
|
|
14
|
+
ttUserOauth2RefreshToken: open_api.ttUserOauth2RefreshToken,
|
|
15
|
+
ttUserOauth2Token: open_api.ttUserOauth2Token,
|
|
16
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
import { ACCESS, ACCESS_TTL, APP, CODE, DEFAULT, NO_PERMISSION, PARAM, PERSON, PUBLISH, REFRESH, REFRESH_TTL, VIDEO, appOf, authorized, bodyOf, bySha, clientSecret, credential, issue, jsonArray, madeVideo, mediaUrl, next, ok, openIdOf, postsFields, privacyOptions, profileFields, publish, publishObj, pullAll, refuse, seconds, type Row, videoMoment, videoPage } from './shared.ts';
|
|
3
|
+
|
|
4
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1738084387220481&language=ENGLISH "The code is valid for 10 minutes and can only be used once."
|
|
5
|
+
export async function ttUserOauth2Token(ctx: HandlerContext): Promise<Response> {
|
|
6
|
+
const app = await appOf(ctx);
|
|
7
|
+
if (app instanceof Response) return app;
|
|
8
|
+
const b = bodyOf(ctx);
|
|
9
|
+
if (b.grant_type !== 'authorization_code') return refuse(ctx, PARAM, 'grant_type must be authorization_code.');
|
|
10
|
+
const code = typeof b.auth_code === 'string' ? bySha(ctx, CODE, b.auth_code) : undefined;
|
|
11
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Invalid authorization code."
|
|
12
|
+
if (!code || code.used === true || code.app !== app.id || Number(code.expires_at) <= seconds(ctx.occurredAt)) return refuse(ctx, 40110, 'Invalid authorization code. The auth_code is canceled, already used or expired.');
|
|
13
|
+
if (typeof b.redirect_uri === 'string' && b.redirect_uri !== code.redirect_uri) return refuse(ctx, PARAM, 'redirect_uri does not match the one the code was issued for.');
|
|
14
|
+
await ctx.record(CODE, { ...code, used: true }, String(code.id));
|
|
15
|
+
return issue(ctx, app, ctx.row(PERSON, String(code.person))!, String(code.scope));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1738084387220481&language=ENGLISH "Use this endpoint to renew a TikTok account access token by"
|
|
19
|
+
export async function ttUserOauth2RefreshToken(ctx: HandlerContext): Promise<Response> {
|
|
20
|
+
const app = await appOf(ctx);
|
|
21
|
+
if (app instanceof Response) return app;
|
|
22
|
+
const b = bodyOf(ctx);
|
|
23
|
+
if (b.grant_type !== 'refresh_token') return refuse(ctx, PARAM, 'grant_type must be refresh_token.');
|
|
24
|
+
const held = typeof b.refresh_token === 'string' ? bySha(ctx, REFRESH, b.refresh_token) : undefined;
|
|
25
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Invalid or incorrect refresh token."
|
|
26
|
+
if (!held || held.used === true || held.kind !== 'business' || held.app !== app.id || Number(held.expires_at) <= seconds(ctx.occurredAt)) return refuse(ctx, 40107, 'Invalid or incorrect refresh token.');
|
|
27
|
+
await ctx.record(REFRESH, { ...held, used: true }, String(held.id));
|
|
28
|
+
return issue(ctx, app, ctx.row(PERSON, String(held.person))!, String(held.scope));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228399168514&language=ENGLISH "Use this endpoint to access detailed analytics and insights about a TikTok account"
|
|
32
|
+
export async function businessGet(ctx: HandlerContext): Promise<Response> {
|
|
33
|
+
const c = authorized(ctx);
|
|
34
|
+
if (c instanceof Response) return c;
|
|
35
|
+
const asked = jsonArray(ctx, 'fields');
|
|
36
|
+
if (asked instanceof Response) return asked;
|
|
37
|
+
const fields = asked ?? DEFAULT;
|
|
38
|
+
const unknown = fields.filter((f) => !profileFields.includes(f));
|
|
39
|
+
if (unknown.length) return refuse(ctx, PARAM, `Invalid fields: ${unknown.join(', ')}`);
|
|
40
|
+
const p = c.person;
|
|
41
|
+
const all: Row = {
|
|
42
|
+
display_name: p.display_name, profile_image: mediaUrl(ctx, `avatar-${String(p.id)}`), username: p.username, profile_deep_link: `https://www.tiktok.com/@${String(p.username)}`,
|
|
43
|
+
bio_description: p.bio_description ?? '', is_verified: p.is_verified === true, followers_count: Number(p.follower_count ?? 0), following_count: Number(p.following_count ?? 0),
|
|
44
|
+
total_likes: Number(p.likes_count ?? 0),
|
|
45
|
+
// the account's public videos, counted from the set
|
|
46
|
+
videos_count: ctx.rowsRaw(VIDEO).filter((v) => v.deleted !== true && v._person === p.id && v._public === true).length,
|
|
47
|
+
};
|
|
48
|
+
return ok(ctx, Object.fromEntries(fields.map((f) => [f, all[f]])));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH "you need to at least include"
|
|
52
|
+
export async function businessVideoList(ctx: HandlerContext): Promise<Response> {
|
|
53
|
+
const c = authorized(ctx);
|
|
54
|
+
if (c instanceof Response) return c;
|
|
55
|
+
const asked = jsonArray(ctx, 'fields');
|
|
56
|
+
if (asked instanceof Response) return asked;
|
|
57
|
+
const fields = asked ?? ['item_id'];
|
|
58
|
+
if (!fields.includes('item_id') || fields.some((f) => !postsFields.includes(f))) return refuse(ctx, PARAM, 'fields must include item_id and name only the supported fields.');
|
|
59
|
+
const q = new URL(ctx.call.request.url).searchParams;
|
|
60
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH "Maximum value"
|
|
61
|
+
const max = q.get('max_count') === null ? 10 : Number(q.get('max_count'));
|
|
62
|
+
if (!Number.isInteger(max) || max < 1 || max > 20) return refuse(ctx, PARAM, 'max_count must be between 1 and 20.');
|
|
63
|
+
const cursor = q.get('cursor') === null ? Infinity : Number(q.get('cursor'));
|
|
64
|
+
const { page, cursor: after, more } = videoPage(ctx, String(c.person.id), cursor, max);
|
|
65
|
+
const post = (v: Row): Row => {
|
|
66
|
+
const all: Row = {
|
|
67
|
+
item_id: v.id, create_time: v.create_time, thumbnail_url: v.cover_image_url, share_url: v.share_url, embed_url: v.embed_link, caption: v.video_description,
|
|
68
|
+
video_duration: v.duration, likes: v.like_count, comments: v.comment_count, shares: v.share_count, video_views: v.view_count,
|
|
69
|
+
};
|
|
70
|
+
return Object.fromEntries(fields.map((f) => [f, all[f]]));
|
|
71
|
+
};
|
|
72
|
+
return ok(ctx, { videos: page.map(post), cursor: after, has_more: more });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228496095234&language=ENGLISH "This field is required, even if you don't want to use any of the additional parameters"
|
|
76
|
+
export async function businessVideoPublish(ctx: HandlerContext): Promise<Response> {
|
|
77
|
+
const c = authorized(ctx, 'video.publish');
|
|
78
|
+
if (c instanceof Response) return c;
|
|
79
|
+
const b = bodyOf(ctx);
|
|
80
|
+
if (typeof b.video_url !== 'string' || !/^https?:\/\//.test(b.video_url)) return refuse(ctx, PARAM, 'video_url must be an http(s) URL.');
|
|
81
|
+
if (b.post_info === undefined || typeof b.post_info !== 'object') return refuse(ctx, PARAM, 'post_info is required.');
|
|
82
|
+
const post = publishObj(b.post_info);
|
|
83
|
+
if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean') return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
|
|
84
|
+
return publish(ctx, 'VIDEO', [b.video_url], post, post.upload_to_draft === true, c.person);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1803630424390658&language=ENGLISH "A list of up to 35 publicly accessible HTTP(s) URLs for the photo content to be published."
|
|
88
|
+
export async function businessPhotoPublish(ctx: HandlerContext): Promise<Response> {
|
|
89
|
+
const c = authorized(ctx, 'video.publish');
|
|
90
|
+
if (c instanceof Response) return c;
|
|
91
|
+
const b = bodyOf(ctx);
|
|
92
|
+
const images = Array.isArray(b.photo_images) ? (b.photo_images as unknown[]).map(String) : [];
|
|
93
|
+
if (images.length < 1 || images.length > 35) return refuse(ctx, PARAM, 'photo_images must hold 1 to 35 URLs.');
|
|
94
|
+
const cover = Number(b.photo_cover_index ?? 0);
|
|
95
|
+
if (!Number.isInteger(cover) || cover < 0 || cover >= images.length) return refuse(ctx, PARAM, 'photo_cover_index must index photo_images.');
|
|
96
|
+
const post = publishObj(b.post_info);
|
|
97
|
+
const draft = post.is_draft === true;
|
|
98
|
+
if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean') return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
|
|
99
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1803630424390658&language=ENGLISH "Length limit: 90 runes (UTF-16 code units)."
|
|
100
|
+
if (typeof post.title === 'string' && post.title.length > 90) return refuse(ctx, PARAM, 'post_info.title is over 90 characters.');
|
|
101
|
+
if (!draft && !privacyOptions(c.person).includes(String(post.privacy_level))) return refuse(ctx, PARAM, 'post_info.privacy_level must be one of the account\'s privacy levels.');
|
|
102
|
+
return publish(ctx, 'PHOTO', images, post, draft, c.person);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH "The publishing status of the TikTok post."
|
|
106
|
+
// post_ids are "Returned only when" the status is PUBLISH_COMPLETE, the reason when FAILED
|
|
107
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH "Returned only when"
|
|
108
|
+
export async function businessPublishStatus(ctx: HandlerContext): Promise<Response> {
|
|
109
|
+
const c = authorized(ctx);
|
|
110
|
+
if (c instanceof Response) return c;
|
|
111
|
+
const id = String(ctx.params.publish_id ?? '');
|
|
112
|
+
const p = ctx.row(PUBLISH, id);
|
|
113
|
+
if (!p || p.deleted === true) return refuse(ctx, PARAM, 'publish_id does not exist.');
|
|
114
|
+
if (p._person !== c.person.id) return refuse(ctx, NO_PERMISSION, 'No permission to read this publishing task.');
|
|
115
|
+
return ok(ctx, { status: p.status, ...(p.status === 'PUBLISH_COMPLETE' ? { post_ids: p.post_ids ?? [] } : {}), ...(p.status === 'FAILED' ? { reason: p.reason } : {}) });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1870580030749698&language=ENGLISH "Length limit: 100 characters."
|
|
119
|
+
export async function businessPublishLocation(ctx: HandlerContext): Promise<Response> {
|
|
120
|
+
const c = authorized(ctx);
|
|
121
|
+
if (c instanceof Response) return c;
|
|
122
|
+
const q = String(ctx.params.search_query ?? '');
|
|
123
|
+
if (!q || q.length > 100) return refuse(ctx, PARAM, 'search_query must be 1 to 100 characters.');
|
|
124
|
+
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
125
|
+
const found = ctx.rowsRaw('_location').filter((l) => words.every((w) => `${String(l.location_name)} ${String(l.location_address)}`.toLowerCase().includes(w)));
|
|
126
|
+
return ok(ctx, { locations: found.map((l) => ({ location_id: l.id, location_name: l.location_name, location_address: l.location_address })) });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Commercial Music Library"
|
|
130
|
+
export async function discoveryCmlTrendingList(ctx: HandlerContext): Promise<Response> {
|
|
131
|
+
const c = authorized(ctx);
|
|
132
|
+
if (c instanceof Response) return c;
|
|
133
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Default value"
|
|
134
|
+
const genre = typeof ctx.params.genre === 'string' && ctx.params.genre.toUpperCase() !== 'ALL' ? ctx.params.genre.toUpperCase() : undefined;
|
|
135
|
+
const tracks = ctx.rowsRaw('_track').filter((t) => !genre || t.genre === genre);
|
|
136
|
+
return ok(ctx, {
|
|
137
|
+
list: tracks.map((t) => ({
|
|
138
|
+
commercial_music_id: t.id, commercial_music_name: t.name, artist: t.artist, thumbnail_url: t.thumbnail_url, duration: t.duration, preview_url: t.preview_url, genre: t.genre,
|
|
139
|
+
full_duration_song_clip: { song_clip_id: t.song_clip_id, duration: t.duration, preview_url: t.preview_url },
|
|
140
|
+
})),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// What every family of the business lane shares beyond the vendor's (../../../src/semantics/shared.ts): the API for
|
|
2
|
+
// Business's envelope (`{code, message, request_id, data}`, a refusal a non-zero code at HTTP 200) and who a request is
|
|
3
|
+
// (its Access-Token, a business app's, and the business_id it names).
|
|
4
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
5
|
+
import { ACCESS, ACCESS_TTL, APP, CODE, PERSON, PUBLISH, REFRESH, REFRESH_TTL, VIDEO, bodyOf, bySha, clientSecret, credential, mediaUrl, next, openIdOf, privacyOptions, seconds, type Row, videoMoment, videoPage } from '../../../src/semantics/shared.ts';
|
|
6
|
+
|
|
7
|
+
export * from '../../../src/semantics/shared.ts';
|
|
8
|
+
export type { HandlerContext };
|
|
9
|
+
|
|
10
|
+
/** A request's id, as the API answers it. */
|
|
11
|
+
const requestId = (ctx: Pick<HandlerContext, 'crypto' | 'occurredAt'>, what: string): string =>
|
|
12
|
+
`${ctx.occurredAt.replace(/\D/g, '').slice(0, 14)}${ctx.crypto.digest('sha256', `${ctx.occurredAt}:business:${what}`, 'hex').slice(0, 18).toUpperCase()}`;
|
|
13
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Successful"
|
|
14
|
+
export const ok = (ctx: Pick<HandlerContext, 'crypto' | 'occurredAt'>, data: unknown): Response =>
|
|
15
|
+
Response.json({ code: 0, message: 'OK', request_id: requestId(ctx, JSON.stringify(data)), data });
|
|
16
|
+
/** A refusal: its code and message, at HTTP 200. */
|
|
17
|
+
export const refuse = (ctx: Pick<HandlerContext, 'crypto' | 'occurredAt'>, code: number, message: string): Response =>
|
|
18
|
+
Response.json({ code, message, request_id: requestId(ctx, `${code}:${message}`), data: {} });
|
|
19
|
+
|
|
20
|
+
// the codes the lane answers (Appendix - Return codes)
|
|
21
|
+
export const PARAM = 40002;
|
|
22
|
+
export const NO_PERMISSION = 40001;
|
|
23
|
+
|
|
24
|
+
/** Who a request is: the business access token its Access-Token header bears, live, with its app and its person. */
|
|
25
|
+
export type Caller = { token: Row; app: Row; person: Row; scopes: string[]; openId: string };
|
|
26
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "Invalid or incorrect access token."
|
|
27
|
+
export function authorized(ctx: HandlerContext, scope?: string): Caller | Response {
|
|
28
|
+
const value = ctx.call.request.headers.get('access-token') ?? '';
|
|
29
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "The access token is empty."
|
|
30
|
+
if (!value) return refuse(ctx, 40104, 'The access token is empty.');
|
|
31
|
+
const token = bySha(ctx, ACCESS, value);
|
|
32
|
+
if (!token || token.kind !== 'business' || token.revoked === true) return refuse(ctx, 40105, 'Invalid or incorrect access token.');
|
|
33
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "The access token has expired."
|
|
34
|
+
if (Number(token.expires_at) <= seconds(ctx.occurredAt)) return refuse(ctx, 40102, 'The access token has expired.');
|
|
35
|
+
const app = ctx.row(APP, String(token.app))!;
|
|
36
|
+
const person = ctx.row(PERSON, String(token.person))!;
|
|
37
|
+
const openId = openIdOf(ctx, app, person);
|
|
38
|
+
const businessId = String(ctx.params.business_id ?? '');
|
|
39
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "No permission to perform the related operation."
|
|
40
|
+
if (businessId !== openId) return refuse(ctx, NO_PERMISSION, 'No permission to operate this TikTok account: business_id is not the access token\'s.');
|
|
41
|
+
const scopes = String(token.scope).split(',');
|
|
42
|
+
if (scope && !scopes.includes(scope)) return refuse(ctx, NO_PERMISSION, `No permission: the TikTok account did not grant ${scope}.`);
|
|
43
|
+
return { token, app, person, scopes, openId };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A JSON-array query parameter (`fields`, as Postiz sends it), or the refusal. */
|
|
47
|
+
export function jsonArray(ctx: HandlerContext, name: string): string[] | undefined | Response {
|
|
48
|
+
const raw = new URL(ctx.call.request.url).searchParams.get(name);
|
|
49
|
+
if (raw === null) return undefined;
|
|
50
|
+
try {
|
|
51
|
+
const v = JSON.parse(raw) as unknown;
|
|
52
|
+
if (Array.isArray(v) && v.every((x) => typeof x === 'string')) return v as string[];
|
|
53
|
+
} catch { /* the refusal below */ }
|
|
54
|
+
return refuse(ctx, PARAM, `${name} must be a JSON array of strings.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function appOf(ctx: HandlerContext): Promise<Row | Response> {
|
|
58
|
+
const b = bodyOf(ctx);
|
|
59
|
+
const app = ctx.rowsRaw(APP).find((a) => a.deleted !== true && a.kind === 'business' && a.client_key === b.client_id);
|
|
60
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1737172488964097&language=ENGLISH "App ID or secret is incorrect."
|
|
61
|
+
if (!app || b.client_secret !== await clientSecret(ctx, app)) return refuse(ctx, 40131, 'App ID or secret is incorrect.');
|
|
62
|
+
return app;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function issue(ctx: HandlerContext, app: Row, person: Row, scope: string): Promise<Response> {
|
|
66
|
+
const now = seconds(ctx.occurredAt);
|
|
67
|
+
const access = await credential(ctx, ACCESS, 'act', { kind: 'business', app: app.id, person: person.id, scope, expires_at: now + ACCESS_TTL, revoked: false });
|
|
68
|
+
const refresh = await credential(ctx, REFRESH, 'rft', { kind: 'business', app: app.id, person: person.id, scope, access: access.id, expires_at: now + REFRESH_TTL, used: false });
|
|
69
|
+
return ok(ctx, { access_token: access.value, token_type: 'Bearer', scope, expires_in: ACCESS_TTL, refresh_token: refresh.value, refresh_token_expires_in: REFRESH_TTL, open_id: openIdOf(ctx, app, person) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228399168514&language=ENGLISH "Requested fields. If not set, returns the"
|
|
73
|
+
export const DEFAULT = ['display_name', 'profile_image'];
|
|
74
|
+
|
|
75
|
+
export const profileFields = ['display_name', 'profile_image', 'username', 'profile_deep_link', 'bio_description', 'is_verified', 'followers_count', 'following_count', 'total_likes', 'videos_count'];
|
|
76
|
+
|
|
77
|
+
export const postsFields = ['item_id', 'create_time', 'thumbnail_url', 'share_url', 'embed_url', 'caption', 'video_duration', 'likes', 'comments', 'shares', 'video_views'];
|
|
78
|
+
|
|
79
|
+
export const publishObj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v) ? v as Row : {});
|
|
80
|
+
|
|
81
|
+
/** Pull each URL through the World; the bytes pulled, or undefined when one fails. */
|
|
82
|
+
export async function pullAll(ctx: HandlerContext, urls: string[]): Promise<number | undefined> {
|
|
83
|
+
let n = 0;
|
|
84
|
+
for (const url of urls) {
|
|
85
|
+
try {
|
|
86
|
+
const r = await ctx.vendorFetch(url, { method: 'GET' });
|
|
87
|
+
if (!r.ok) return undefined;
|
|
88
|
+
n += (await r.arrayBuffer()).byteLength;
|
|
89
|
+
} catch { return undefined; }
|
|
90
|
+
}
|
|
91
|
+
return n;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The video a completed pull makes (the Video Object, as the open lane makes them), public; its id. */
|
|
95
|
+
export async function madeVideo(ctx: HandlerContext, publishId: string, post: Row, person: Row): Promise<string> {
|
|
96
|
+
const videoId = String(7_400_000_000_000_000_000 + (await next(ctx, 'video')) * 1024);
|
|
97
|
+
const caption = typeof post.caption === 'string' ? post.caption : '';
|
|
98
|
+
await ctx.write(VIDEO, videoId, {
|
|
99
|
+
id: videoId, create_time: seconds(ctx.occurredAt), _moment: videoMoment(ctx, String(person.id)), cover_image_url: mediaUrl(ctx, `cover-${videoId}`), share_url: `https://www.tiktok.com/@${String(person.username)}/video/${videoId}`,
|
|
100
|
+
video_description: caption.slice(0, 150), duration: 0, height: 0, width: 0, title: caption.slice(0, 150),
|
|
101
|
+
embed_html: `<blockquote class="tiktok-embed" cite="https://www.tiktok.com/@${String(person.username)}/video/${videoId}" data-video-id="${videoId}"></blockquote>`,
|
|
102
|
+
embed_link: `https://www.tiktok.com/player/v1/${videoId}`, like_count: 0, comment_count: 0, share_count: 0, view_count: 0, is_aigc: post.is_ai_generated === true,
|
|
103
|
+
_person: person.id, _public: true, _publish: publishId,
|
|
104
|
+
}, 'video.publish');
|
|
105
|
+
return videoId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function publish(ctx: HandlerContext, media: 'VIDEO' | 'PHOTO', urls: string[], post: Row, draft: boolean, person: Row): Promise<Response> {
|
|
109
|
+
const n = await next(ctx, 'publish');
|
|
110
|
+
const id = `${media === 'VIDEO' ? 'v' : 'p'}_pub_url~v2.${n}`;
|
|
111
|
+
await ctx.write(PUBLISH, id, { status: 'PROCESSING_DOWNLOAD', post_ids: [], _person: person.id, _media: media, _urls: urls, _post_info: post, _draft: draft, _lane: 'business' }, media === 'VIDEO' ? 'businessVideoPublish' : 'businessPhotoPublish');
|
|
112
|
+
const pulled = await pullAll(ctx, urls);
|
|
113
|
+
const to = pulled === undefined ? 'FAILED' : draft ? 'SEND_TO_USER_INBOX' : 'PUBLISH_COMPLETE';
|
|
114
|
+
const refused = ctx.legal(PUBLISH, 'status', 'publish.pull', 'PROCESSING_DOWNLOAD', to, id, 'vendor');
|
|
115
|
+
if (refused) throw new Error(refused.message);
|
|
116
|
+
const postIds = to === 'PUBLISH_COMPLETE' && media === 'VIDEO' ? [await madeVideo(ctx, id, post, person)] : [];
|
|
117
|
+
await ctx.write(PUBLISH, id, { status: to, post_ids: postIds, ...(to === 'FAILED' ? { reason: media === 'VIDEO' ? 'video_pull_failed' : 'photo_pull_failed' } : {}) }, 'publish.pull');
|
|
118
|
+
return ok(ctx, { share_id: id });
|
|
119
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# TikTok API for Business v1.3: the Accounts API and Discovery
|
|
2
|
+
|
|
3
|
+
- **Why this document:** TikTok publishes no OpenAPI document of these endpoints (its SDK repository's yml files,
|
|
4
|
+
tiktok/tiktok-business-api-sdk @ f809c39652, are the Marketing API's). This document is written from the v1.3
|
|
5
|
+
documentation, one operation per endpoint. The portal (business-api.tiktok.com/portal/docs) renders in the browser;
|
|
6
|
+
each page's text is its docs gateway's answer, which each operation names (`externalDocs`) and the pack cites:
|
|
7
|
+
`https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=<v1.3>&doc_id=<page>&language=ENGLISH`
|
|
8
|
+
(the v1.3 key `c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b`, from the portal's own bundle). Pages: Accounts API Authentication (1738084387220481),
|
|
9
|
+
Authorization (1738083939371009), Get profile data (1762228399168514), Get post data (1762228421622786), Publish a
|
|
10
|
+
public video post (1762228496095234), Publish a photo post (1803630424390658), Get the publishing status
|
|
11
|
+
(1816387106635778), Get the location tags (1870580030749698), Get popular tracks from the Commercial Music Library
|
|
12
|
+
(1825119063013505), Get TikTok account permission scopes (1765927978092545), Appendix - Return codes
|
|
13
|
+
(1737172488964097), fetched 2026-09-30.
|
|
14
|
+
- **Its host:** business-api.tiktok.com.
|
|
15
|
+
- **Corrections:** none.
|
|
16
|
+
- **Read by:** the twin orchestrator, 2026-09-30.
|
|
17
|
+
- **Vendored bytes (sha256):** `openapi.json` `ad0a7a19e49fba66c359b3ef12dbe8daeb112a36e1cc0a14f0e28f94fb26d4f9`.
|