@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,13 @@
|
|
|
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 } from '@volter/world-core';
|
|
4
|
+
import surface from './generated/surface.gen.json' with { type: 'json' };
|
|
5
|
+
import { manifest } from "./manifest.js";
|
|
6
|
+
import { businessSemantics } from "./semantics/index.js";
|
|
7
|
+
export function createBusinessLaneFetch(options = {}) {
|
|
8
|
+
return createPackFetch({
|
|
9
|
+
manifest,
|
|
10
|
+
surface: surface,
|
|
11
|
+
handlers: businessSemantics,
|
|
12
|
+
}, options);
|
|
13
|
+
}
|
|
@@ -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,33 @@
|
|
|
1
|
+
import { states } from "./semantics/states.js";
|
|
2
|
+
export const manifest = {
|
|
3
|
+
vendor: 'tiktok',
|
|
4
|
+
// the vendor's one store: the open lane's apps, people, codes, posts and videos are these too
|
|
5
|
+
service: 'tiktok',
|
|
6
|
+
body: { json: 'always' },
|
|
7
|
+
ids: { template: '{uuid}' },
|
|
8
|
+
time: 'unix',
|
|
9
|
+
// "Every Business API endpoint (except OAuth) ... wraps the payload in {code, message, data}" (Postiz reads it so):
|
|
10
|
+
// a refusal is a non-zero code, answered HTTP 200
|
|
11
|
+
// 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"
|
|
12
|
+
error: { code: '{code}', message: '{message}', request_id: '{id}', data: {} },
|
|
13
|
+
errorsAnsweredAs: { status: 200, below: 500 },
|
|
14
|
+
readOnly: { status: 200, message: 'This twin was started read-only; writes are refused.' },
|
|
15
|
+
notFound: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
|
|
16
|
+
// 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."
|
|
17
|
+
// Where documentation stops: token errors do not establish an authentication gate before unknown-route resolution.
|
|
18
|
+
gap: { status: 200, code: 40008, message: "The interface (endpoint) has not been implemented." },
|
|
19
|
+
deleted: {},
|
|
20
|
+
resources: {
|
|
21
|
+
publish: { idPrefix: '', ...states.publish },
|
|
22
|
+
video: { idPrefix: '' },
|
|
23
|
+
_access_token: { idPrefix: 'act_', ids: '{prefix}{n}' },
|
|
24
|
+
_refresh_token: { idPrefix: 'rft_', ids: '{prefix}{n}' },
|
|
25
|
+
_seq_publish: { idPrefix: '', ids: '{n}' },
|
|
26
|
+
_seq_video: { idPrefix: '', ids: '{n}' },
|
|
27
|
+
},
|
|
28
|
+
unmodeled: ['ttUserOauth2Revoke', 'ttUserTokenInfoGet'],
|
|
29
|
+
discovery: {
|
|
30
|
+
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",
|
|
31
|
+
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",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -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,12 @@
|
|
|
1
|
+
import * as open_api from "./open_api.js";
|
|
2
|
+
export const businessSemantics = {
|
|
3
|
+
businessGet: open_api.businessGet,
|
|
4
|
+
businessPhotoPublish: open_api.businessPhotoPublish,
|
|
5
|
+
businessPublishLocation: open_api.businessPublishLocation,
|
|
6
|
+
businessPublishStatus: open_api.businessPublishStatus,
|
|
7
|
+
businessVideoList: open_api.businessVideoList,
|
|
8
|
+
businessVideoPublish: open_api.businessVideoPublish,
|
|
9
|
+
discoveryCmlTrendingList: open_api.discoveryCmlTrendingList,
|
|
10
|
+
ttUserOauth2RefreshToken: open_api.ttUserOauth2RefreshToken,
|
|
11
|
+
ttUserOauth2Token: open_api.ttUserOauth2Token,
|
|
12
|
+
};
|
|
@@ -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,10 @@
|
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
export declare function ttUserOauth2Token(ctx: HandlerContext): Promise<Response>;
|
|
3
|
+
export declare function ttUserOauth2RefreshToken(ctx: HandlerContext): Promise<Response>;
|
|
4
|
+
export declare function businessGet(ctx: HandlerContext): Promise<Response>;
|
|
5
|
+
export declare function businessVideoList(ctx: HandlerContext): Promise<Response>;
|
|
6
|
+
export declare function businessVideoPublish(ctx: HandlerContext): Promise<Response>;
|
|
7
|
+
export declare function businessPhotoPublish(ctx: HandlerContext): Promise<Response>;
|
|
8
|
+
export declare function businessPublishStatus(ctx: HandlerContext): Promise<Response>;
|
|
9
|
+
export declare function businessPublishLocation(ctx: HandlerContext): Promise<Response>;
|
|
10
|
+
export declare function discoveryCmlTrendingList(ctx: HandlerContext): Promise<Response>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { CODE, DEFAULT, NO_PERMISSION, PARAM, PERSON, PUBLISH, REFRESH, VIDEO, appOf, authorized, bodyOf, bySha, issue, jsonArray, mediaUrl, ok, postsFields, privacyOptions, profileFields, publish, publishObj, refuse, seconds, videoPage } from "./shared.js";
|
|
2
|
+
// 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."
|
|
3
|
+
export async function ttUserOauth2Token(ctx) {
|
|
4
|
+
const app = await appOf(ctx);
|
|
5
|
+
if (app instanceof Response)
|
|
6
|
+
return app;
|
|
7
|
+
const b = bodyOf(ctx);
|
|
8
|
+
if (b.grant_type !== 'authorization_code')
|
|
9
|
+
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))
|
|
13
|
+
return refuse(ctx, 40110, 'Invalid authorization code. The auth_code is canceled, already used or expired.');
|
|
14
|
+
if (typeof b.redirect_uri === 'string' && b.redirect_uri !== code.redirect_uri)
|
|
15
|
+
return refuse(ctx, PARAM, 'redirect_uri does not match the one the code was issued for.');
|
|
16
|
+
await ctx.record(CODE, { ...code, used: true }, String(code.id));
|
|
17
|
+
return issue(ctx, app, ctx.row(PERSON, String(code.person)), String(code.scope));
|
|
18
|
+
}
|
|
19
|
+
// 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"
|
|
20
|
+
export async function ttUserOauth2RefreshToken(ctx) {
|
|
21
|
+
const app = await appOf(ctx);
|
|
22
|
+
if (app instanceof Response)
|
|
23
|
+
return app;
|
|
24
|
+
const b = bodyOf(ctx);
|
|
25
|
+
if (b.grant_type !== 'refresh_token')
|
|
26
|
+
return refuse(ctx, PARAM, 'grant_type must be refresh_token.');
|
|
27
|
+
const held = typeof b.refresh_token === 'string' ? bySha(ctx, REFRESH, b.refresh_token) : undefined;
|
|
28
|
+
// 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."
|
|
29
|
+
if (!held || held.used === true || held.kind !== 'business' || held.app !== app.id || Number(held.expires_at) <= seconds(ctx.occurredAt))
|
|
30
|
+
return refuse(ctx, 40107, 'Invalid or incorrect refresh token.');
|
|
31
|
+
await ctx.record(REFRESH, { ...held, used: true }, String(held.id));
|
|
32
|
+
return issue(ctx, app, ctx.row(PERSON, String(held.person)), String(held.scope));
|
|
33
|
+
}
|
|
34
|
+
// 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"
|
|
35
|
+
export async function businessGet(ctx) {
|
|
36
|
+
const c = authorized(ctx);
|
|
37
|
+
if (c instanceof Response)
|
|
38
|
+
return c;
|
|
39
|
+
const asked = jsonArray(ctx, 'fields');
|
|
40
|
+
if (asked instanceof Response)
|
|
41
|
+
return asked;
|
|
42
|
+
const fields = asked ?? DEFAULT;
|
|
43
|
+
const unknown = fields.filter((f) => !profileFields.includes(f));
|
|
44
|
+
if (unknown.length)
|
|
45
|
+
return refuse(ctx, PARAM, `Invalid fields: ${unknown.join(', ')}`);
|
|
46
|
+
const p = c.person;
|
|
47
|
+
const all = {
|
|
48
|
+
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)}`,
|
|
49
|
+
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),
|
|
50
|
+
total_likes: Number(p.likes_count ?? 0),
|
|
51
|
+
// the account's public videos, counted from the set
|
|
52
|
+
videos_count: ctx.rowsRaw(VIDEO).filter((v) => v.deleted !== true && v._person === p.id && v._public === true).length,
|
|
53
|
+
};
|
|
54
|
+
return ok(ctx, Object.fromEntries(fields.map((f) => [f, all[f]])));
|
|
55
|
+
}
|
|
56
|
+
// 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"
|
|
57
|
+
export async function businessVideoList(ctx) {
|
|
58
|
+
const c = authorized(ctx);
|
|
59
|
+
if (c instanceof Response)
|
|
60
|
+
return c;
|
|
61
|
+
const asked = jsonArray(ctx, 'fields');
|
|
62
|
+
if (asked instanceof Response)
|
|
63
|
+
return asked;
|
|
64
|
+
const fields = asked ?? ['item_id'];
|
|
65
|
+
if (!fields.includes('item_id') || fields.some((f) => !postsFields.includes(f)))
|
|
66
|
+
return refuse(ctx, PARAM, 'fields must include item_id and name only the supported fields.');
|
|
67
|
+
const q = new URL(ctx.call.request.url).searchParams;
|
|
68
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1762228421622786&language=ENGLISH "Maximum value"
|
|
69
|
+
const max = q.get('max_count') === null ? 10 : Number(q.get('max_count'));
|
|
70
|
+
if (!Number.isInteger(max) || max < 1 || max > 20)
|
|
71
|
+
return refuse(ctx, PARAM, 'max_count must be between 1 and 20.');
|
|
72
|
+
const cursor = q.get('cursor') === null ? Infinity : Number(q.get('cursor'));
|
|
73
|
+
const { page, cursor: after, more } = videoPage(ctx, String(c.person.id), cursor, max);
|
|
74
|
+
const post = (v) => {
|
|
75
|
+
const all = {
|
|
76
|
+
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,
|
|
77
|
+
video_duration: v.duration, likes: v.like_count, comments: v.comment_count, shares: v.share_count, video_views: v.view_count,
|
|
78
|
+
};
|
|
79
|
+
return Object.fromEntries(fields.map((f) => [f, all[f]]));
|
|
80
|
+
};
|
|
81
|
+
return ok(ctx, { videos: page.map(post), cursor: after, has_more: more });
|
|
82
|
+
}
|
|
83
|
+
// 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"
|
|
84
|
+
export async function businessVideoPublish(ctx) {
|
|
85
|
+
const c = authorized(ctx, 'video.publish');
|
|
86
|
+
if (c instanceof Response)
|
|
87
|
+
return c;
|
|
88
|
+
const b = bodyOf(ctx);
|
|
89
|
+
if (typeof b.video_url !== 'string' || !/^https?:\/\//.test(b.video_url))
|
|
90
|
+
return refuse(ctx, PARAM, 'video_url must be an http(s) URL.');
|
|
91
|
+
if (b.post_info === undefined || typeof b.post_info !== 'object')
|
|
92
|
+
return refuse(ctx, PARAM, 'post_info is required.');
|
|
93
|
+
const post = publishObj(b.post_info);
|
|
94
|
+
if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean')
|
|
95
|
+
return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
|
|
96
|
+
return publish(ctx, 'VIDEO', [b.video_url], post, post.upload_to_draft === true, c.person);
|
|
97
|
+
}
|
|
98
|
+
// 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."
|
|
99
|
+
export async function businessPhotoPublish(ctx) {
|
|
100
|
+
const c = authorized(ctx, 'video.publish');
|
|
101
|
+
if (c instanceof Response)
|
|
102
|
+
return c;
|
|
103
|
+
const b = bodyOf(ctx);
|
|
104
|
+
const images = Array.isArray(b.photo_images) ? b.photo_images.map(String) : [];
|
|
105
|
+
if (images.length < 1 || images.length > 35)
|
|
106
|
+
return refuse(ctx, PARAM, 'photo_images must hold 1 to 35 URLs.');
|
|
107
|
+
const cover = Number(b.photo_cover_index ?? 0);
|
|
108
|
+
if (!Number.isInteger(cover) || cover < 0 || cover >= images.length)
|
|
109
|
+
return refuse(ctx, PARAM, 'photo_cover_index must index photo_images.');
|
|
110
|
+
const post = publishObj(b.post_info);
|
|
111
|
+
const draft = post.is_draft === true;
|
|
112
|
+
if (typeof post.is_brand_organic !== 'boolean' || typeof post.is_branded_content !== 'boolean')
|
|
113
|
+
return refuse(ctx, PARAM, 'post_info.is_brand_organic and post_info.is_branded_content are required.');
|
|
114
|
+
// 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)."
|
|
115
|
+
if (typeof post.title === 'string' && post.title.length > 90)
|
|
116
|
+
return refuse(ctx, PARAM, 'post_info.title is over 90 characters.');
|
|
117
|
+
if (!draft && !privacyOptions(c.person).includes(String(post.privacy_level)))
|
|
118
|
+
return refuse(ctx, PARAM, 'post_info.privacy_level must be one of the account\'s privacy levels.');
|
|
119
|
+
return publish(ctx, 'PHOTO', images, post, draft, c.person);
|
|
120
|
+
}
|
|
121
|
+
// 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."
|
|
122
|
+
// post_ids are "Returned only when" the status is PUBLISH_COMPLETE, the reason when FAILED
|
|
123
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1816387106635778&language=ENGLISH "Returned only when"
|
|
124
|
+
export async function businessPublishStatus(ctx) {
|
|
125
|
+
const c = authorized(ctx);
|
|
126
|
+
if (c instanceof Response)
|
|
127
|
+
return c;
|
|
128
|
+
const id = String(ctx.params.publish_id ?? '');
|
|
129
|
+
const p = ctx.row(PUBLISH, id);
|
|
130
|
+
if (!p || p.deleted === true)
|
|
131
|
+
return refuse(ctx, PARAM, 'publish_id does not exist.');
|
|
132
|
+
if (p._person !== c.person.id)
|
|
133
|
+
return refuse(ctx, NO_PERMISSION, 'No permission to read this publishing task.');
|
|
134
|
+
return ok(ctx, { status: p.status, ...(p.status === 'PUBLISH_COMPLETE' ? { post_ids: p.post_ids ?? [] } : {}), ...(p.status === 'FAILED' ? { reason: p.reason } : {}) });
|
|
135
|
+
}
|
|
136
|
+
// 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."
|
|
137
|
+
export async function businessPublishLocation(ctx) {
|
|
138
|
+
const c = authorized(ctx);
|
|
139
|
+
if (c instanceof Response)
|
|
140
|
+
return c;
|
|
141
|
+
const q = String(ctx.params.search_query ?? '');
|
|
142
|
+
if (!q || q.length > 100)
|
|
143
|
+
return refuse(ctx, PARAM, 'search_query must be 1 to 100 characters.');
|
|
144
|
+
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
145
|
+
const found = ctx.rowsRaw('_location').filter((l) => words.every((w) => `${String(l.location_name)} ${String(l.location_address)}`.toLowerCase().includes(w)));
|
|
146
|
+
return ok(ctx, { locations: found.map((l) => ({ location_id: l.id, location_name: l.location_name, location_address: l.location_address })) });
|
|
147
|
+
}
|
|
148
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Commercial Music Library"
|
|
149
|
+
export async function discoveryCmlTrendingList(ctx) {
|
|
150
|
+
const c = authorized(ctx);
|
|
151
|
+
if (c instanceof Response)
|
|
152
|
+
return c;
|
|
153
|
+
// source: https://business-api.tiktok.com/gateway/api/doc/client/node/get/v2/?identify_key=c0138ffadd90a955c1f0670a56fe348d1d40680b3c89461e09f78ed26785164b&doc_id=1825119063013505&language=ENGLISH "Default value"
|
|
154
|
+
const genre = typeof ctx.params.genre === 'string' && ctx.params.genre.toUpperCase() !== 'ALL' ? ctx.params.genre.toUpperCase() : undefined;
|
|
155
|
+
const tracks = ctx.rowsRaw('_track').filter((t) => !genre || t.genre === genre);
|
|
156
|
+
return ok(ctx, {
|
|
157
|
+
list: tracks.map((t) => ({
|
|
158
|
+
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,
|
|
159
|
+
full_duration_song_clip: { song_clip_id: t.song_clip_id, duration: t.duration, preview_url: t.preview_url },
|
|
160
|
+
})),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -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
|
+
}
|