TTikotok V3

This commit is contained in:
2026-07-09 18:18:53 +02:00
parent d81938060b
commit 1a9548cf25
2 changed files with 41 additions and 12 deletions

View File

@@ -1223,7 +1223,7 @@ app.get('/api/tiktok/connect', (request, response) => {
const authUrl = new URL('https://www.tiktok.com/v2/auth/authorize/');
authUrl.searchParams.set('client_key', clientKey);
authUrl.searchParams.set('scope', 'user.info.basic,user.info.stats,video.list,video.upload,video.publish');
authUrl.searchParams.set('scope', 'user.info.basic,video.publish,video.upload');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', redirectUri);
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.' });
}
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) {
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.' });
}
const fileInfos = [];
const photoUrls = [];
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 mimeType = file.contentType || file.mimeType || 'image/jpeg';
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);
fileInfos.push({
url: uploadRes.url,
mime_type: mimeType,
});
photoUrls.push(uploadRes.url);
}
const title = typeof request.body?.title === 'string' ? request.body.title.trim() : '';
const description = typeof request.body?.description === 'string' ? request.body.description.trim() : '';
const initBody = {
media_type: 'PHOTO',
photo_cover_index: 0,
file_paths: fileInfos.map((info) => info.url),
file_extensions: fileInfos.map((info) => (info.mime_type || 'image/jpeg').split('/').pop()),
post_mode: 'DIRECT_POST',
// MEDIA_UPLOAD = draft in the creator's TikTok inbox (posting policy is
// upload/draft only) and only needs the video.upload scope.
post_mode: 'MEDIA_UPLOAD',
post_info: {
...(title ? { title } : {}),
...(description ? { description } : {}),
},
source_info: {
source: 'PULL_FROM_URL',
photo_cover_index: 0,
photo_images: photoUrls,
},
};
const initResult = await tiktokApi('https://open.tiktokapis.com/v2/post/publish/content/init/', {

View File

@@ -1248,6 +1248,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0",