TTikotok V3
This commit is contained in:
@@ -1223,7 +1223,7 @@ app.get('/api/tiktok/connect', (request, response) => {
|
|||||||
|
|
||||||
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
|
||||||
authUrl.searchParams.set('client_key', clientKey);
|
authUrl.searchParams.set('client_key', clientKey);
|
||||||
authUrl.searchParams.set('scope', 'user.info.basic,user.info.stats,video.list,video.upload,video.publish');
|
authUrl.searchParams.set('scope', 'user.info.basic,video.publish,video.upload');
|
||||||
authUrl.searchParams.set('response_type', 'code');
|
authUrl.searchParams.set('response_type', 'code');
|
||||||
authUrl.searchParams.set('redirect_uri', redirectUri);
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
||||||
authUrl.searchParams.set('state', oauthState);
|
authUrl.searchParams.set('state', oauthState);
|
||||||
@@ -1468,7 +1468,20 @@ app.post('/api/tiktok/upload/photo', async (request, response) => {
|
|||||||
return response.status(401).json({ code: 'UNAUTHORIZED', message: 'Invalid or missing admin key.' });
|
return response.status(401).json({ code: 'UNAUTHORIZED', message: 'Invalid or missing admin key.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = Array.isArray(request.body?.files) ? request.body.files : [];
|
let files = Array.isArray(request.body?.files)
|
||||||
|
? request.body.files
|
||||||
|
: Array.isArray(request.body?.photos)
|
||||||
|
? request.body.photos.map((photo) => {
|
||||||
|
if (Buffer.isBuffer(photo?.buffer)) {
|
||||||
|
return { buffer: photo.buffer, contentType: photo.mimeType || photo.contentType || 'image/jpeg' };
|
||||||
|
}
|
||||||
|
if (typeof photo?.url === 'string' && /^https?:\/\//i.test(photo.url)) {
|
||||||
|
return { buffer: Buffer.from(photo.url), contentType: photo.mimeType || photo.contentType || 'image/jpeg', url: photo.url };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
if (!files.length) {
|
if (!files.length) {
|
||||||
return response.status(400).json({ code: 'BAD_REQUEST', message: 'files must be a non-empty array.' });
|
return response.status(400).json({ code: 'BAD_REQUEST', message: 'files must be a non-empty array.' });
|
||||||
}
|
}
|
||||||
@@ -1477,28 +1490,43 @@ app.post('/api/tiktok/upload/photo', async (request, response) => {
|
|||||||
return response.status(400).json({ code: 'BAD_REQUEST', message: 'TikTok allows up to 35 photos per carousel post.' });
|
return response.status(400).json({ code: 'BAD_REQUEST', message: 'TikTok allows up to 35 photos per carousel post.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileInfos = [];
|
const photoUrls = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
// Photos given as public URLs are passed to TikTok directly; buffers
|
||||||
|
// are first uploaded to MinIO so TikTok can pull them by URL.
|
||||||
|
if (typeof file.url === 'string' && /^https?:\/\//i.test(file.url)) {
|
||||||
|
photoUrls.push(file.url);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const buffer = file.buffer || file.data;
|
const buffer = file.buffer || file.data;
|
||||||
const mimeType = file.contentType || file.mimeType || 'image/jpeg';
|
const mimeType = file.contentType || file.mimeType || 'image/jpeg';
|
||||||
|
|
||||||
if (!Buffer.isBuffer(buffer)) {
|
if (!Buffer.isBuffer(buffer)) {
|
||||||
return response.status(400).json({ code: 'BAD_REQUEST', message: 'Each file must include a binary buffer.' });
|
return response.status(400).json({ code: 'BAD_REQUEST', message: 'Each file must include a binary buffer or a public url.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadRes = await uploadImage(buffer.toString('base64'), mimeType);
|
const uploadRes = await uploadImage(buffer.toString('base64'), mimeType);
|
||||||
fileInfos.push({
|
photoUrls.push(uploadRes.url);
|
||||||
url: uploadRes.url,
|
|
||||||
mime_type: mimeType,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const title = typeof request.body?.title === 'string' ? request.body.title.trim() : '';
|
||||||
|
const description = typeof request.body?.description === 'string' ? request.body.description.trim() : '';
|
||||||
|
|
||||||
const initBody = {
|
const initBody = {
|
||||||
media_type: 'PHOTO',
|
media_type: 'PHOTO',
|
||||||
photo_cover_index: 0,
|
// MEDIA_UPLOAD = draft in the creator's TikTok inbox (posting policy is
|
||||||
file_paths: fileInfos.map((info) => info.url),
|
// upload/draft only) and only needs the video.upload scope.
|
||||||
file_extensions: fileInfos.map((info) => (info.mime_type || 'image/jpeg').split('/').pop()),
|
post_mode: 'MEDIA_UPLOAD',
|
||||||
post_mode: 'DIRECT_POST',
|
post_info: {
|
||||||
|
...(title ? { title } : {}),
|
||||||
|
...(description ? { description } : {}),
|
||||||
|
},
|
||||||
|
source_info: {
|
||||||
|
source: 'PULL_FROM_URL',
|
||||||
|
photo_cover_index: 0,
|
||||||
|
photo_images: photoUrls,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const initResult = await tiktokApi('https://open.tiktokapis.com/v2/post/publish/content/init/', {
|
const initResult = await tiktokApi('https://open.tiktokapis.com/v2/post/publish/content/init/', {
|
||||||
|
|||||||
1
server/package-lock.json
generated
1
server/package-lock.json
generated
@@ -1248,6 +1248,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.12.0",
|
"pg-connection-string": "^2.12.0",
|
||||||
"pg-pool": "^3.13.0",
|
"pg-pool": "^3.13.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user