feat: tiktok photos

This commit is contained in:
Nevo David 2025-03-15 00:57:32 +07:00
parent 3de66b7e68
commit 8399aee295
7 changed files with 221 additions and 87 deletions

View File

@ -56,8 +56,12 @@ export class MarketplaceController {
connectBankAccount(
@GetUserFromRequest() user: User,
@Query('country') country: string
) {
return this._stripeService.createAccountProcess(user.id, user.email, country);
) {
return this._stripeService.createAccountProcess(
user.id,
user.email,
country
);
}
@Post('/item')
@ -126,12 +130,19 @@ export class MarketplaceController {
@GetOrgFromRequest() organization: Organization,
@Param('id') id: string
) {
const getPost = await this._messagesService.getPost(user.id, organization.id, id);
const getPost = await this._messagesService.getPost(
user.id,
organization.id,
id
);
if (!getPost) {
return ;
return;
}
return {...await this._postsService.getPost(getPost.organizationId, id), providerId: getPost.integration.providerIdentifier};
return {
...(await this._postsService.getPost(getPost.organizationId, id)),
providerId: getPost.integration.providerIdentifier,
};
}
@Post('/posts/:id/revision')

View File

@ -38,6 +38,8 @@ export const useValues = (
criteriaMode: 'all',
});
console.log(form.formState.errors);
const getValues = useMemo(() => {
return () => ({ ...form.getValues(), __type: identifier });
}, [form, integration]);

View File

@ -46,11 +46,11 @@ const contentPostingMethod = [
const yesNo = [
{
value: 'true',
value: 'yes',
label: 'Yes',
},
{
value: 'false',
value: 'no',
label: 'No',
},
];
@ -120,7 +120,7 @@ const TikTokSettings: FC<{ values?: any }> = (props) => {
const disclose = watch('disclose');
const brand_organic_toggle = watch('brand_organic_toggle');
const brand_content_toggle = watch('brand_content_toggle');
const content_posting_method = watch('content_posting_method');
const content_posting_method = watch('content_posting_method');
const isUploadMode = content_posting_method === 'UPLOAD';
@ -129,7 +129,8 @@ const content_posting_method = watch('content_posting_method');
<CheckTikTokValidity picture={props?.values?.[0]?.image?.[0]?.path} />
<Select
label="Who can see this video?"
disabled={isUploadMode}
hideErrors={true}
disabled={isUploadMode}
{...register('privacy_level', {
value: 'PUBLIC_TO_EVERYONE',
})}
@ -141,13 +142,13 @@ disabled={isUploadMode}
</option>
))}
</Select>
<div className="text-[14px] mb-[10px] text-balance">
<div className="text-[14px] mt-[10px] mb-[18px] text-balance">
{`Choose upload without posting if you want to review and edit your content within TikTok's app before publishing.
This gives you access to TikTok's built-in editing tools and lets you make final adjustments before posting.`}
</div>
<Select
label="Content posting method"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('content_posting_method', {
value: 'DIRECT_POST',
})}
@ -159,13 +160,31 @@ disabled={isUploadMode}
</option>
))}
</Select>
<Select
hideErrors={true}
label="Auto add music"
{...register('autoAddMusic', {
value: 'no',
})}
>
<option value="">Select</option>
{yesNo.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</Select>
<div className="text-[14px] mt-[10px] mb-[24px] text-balance">
This feature available only for photos, it will add a default music that
you can change later.
</div>
<hr className="mb-[15px] border-tableBorder" />
<div className="text-[14px] mb-[10px]">Allow User To:</div>
<div className="flex gap-[40px]">
<Checkbox
variant="hollow"
label="Duet"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('duet', {
value: false,
})}
@ -173,7 +192,7 @@ disabled={isUploadMode}
<Checkbox
label="Stitch"
variant="hollow"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('stitch', {
value: false,
})}
@ -181,7 +200,7 @@ disabled={isUploadMode}
<Checkbox
label="Comments"
variant="hollow"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('comment', {
value: false,
})}
@ -192,7 +211,7 @@ disabled={isUploadMode}
<Checkbox
variant="hollow"
label="Disclose Video Content"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('disclose', {
value: false,
})}
@ -225,12 +244,11 @@ disabled={isUploadMode}
third party, or both.
</div>
</div>
<div className={clsx(!disclose && 'invisible', 'mt-[20px]')}>
<Checkbox
variant="hollow"
label="Your brand"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('brand_organic_toggle', {
value: false,
})}
@ -243,7 +261,7 @@ disabled={isUploadMode}
<Checkbox
variant="hollow"
label="Branded content"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('brand_content_toggle', {
value: false,
})}
@ -290,19 +308,22 @@ export default withProvider(
TikTokDto,
async (items) => {
const [firstItems] = items;
if (items.length !== 1) {
return 'Tiktok items should be one';
}
if (items[0].length !== 1) {
if (
firstItems.length > 1 &&
firstItems?.some((p) => p?.path?.indexOf('mp4') > -1)
) {
return 'Only pictures are supported when selecting multiple items';
} else if (
firstItems?.length !== 1 &&
firstItems?.[0]?.path?.indexOf('mp4') > -1
) {
return 'You need one media';
}
if (firstItems[0].path.indexOf('mp4') === -1) {
return 'Item must be a video';
}
return true;
},
2200

View File

@ -24,6 +24,10 @@ import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/me
import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service';
import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service';
import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto';
import axios from 'axios';
import sharp from 'sharp';
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
import { Readable } from 'stream';
dayjs.extend(utc);
type PostWithConditionals = Post & {
@ -33,6 +37,7 @@ type PostWithConditionals = Post & {
@Injectable()
export class PostsService {
private storage = UploadFactory.createStorage();
constructor(
private _postRepository: PostsRepository,
private _workerServiceProducer: BullMqClient,
@ -92,36 +97,90 @@ export class PostsService {
return this._postRepository.getPosts(orgId, query);
}
async updateMedia(id: string, imagesList: any[]) {
async updateMedia(id: string, imagesList: any[], convertToJPEG = false) {
let imageUpdateNeeded = false;
const getImageList = (
await Promise.all(
imagesList.map(async (p: any) => {
if (!p.path && p.id) {
imageUpdateNeeded = true;
return this._mediaService.getMediaById(p.id);
const getImageList = await Promise.all(
(
await Promise.all(
imagesList.map(async (p: any) => {
if (!p.path && p.id) {
imageUpdateNeeded = true;
return this._mediaService.getMediaById(p.id);
}
return p;
})
)
)
.map((m) => {
return {
...m,
url:
m.path.indexOf('http') === -1
? process.env.FRONTEND_URL +
'/' +
process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
m.path
: m.path,
type: 'image',
path:
m.path.indexOf('http') === -1
? process.env.UPLOAD_DIRECTORY + m.path
: m.path,
};
})
.map(async (m) => {
if (!convertToJPEG) {
return m;
}
return p;
if (m.path.indexOf('.png') > -1) {
imageUpdateNeeded = true;
const response = await axios.get(m.url, {
responseType: 'arraybuffer',
});
const imageBuffer = Buffer.from(response.data);
// Use sharp to get the metadata of the image
const buffer = await sharp(imageBuffer)
.jpeg({ quality: 100 })
.toBuffer();
const { path, originalname } = await this.storage.uploadFile({
buffer,
mimetype: 'image/jpeg',
size: buffer.length,
path: '',
fieldname: '',
destination: '',
stream: new Readable(),
filename: '',
originalname: '',
encoding: '',
});
return {
...m,
name: originalname,
url:
path.indexOf('http') === -1
? process.env.FRONTEND_URL +
'/' +
process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
path
: path,
type: 'image',
path:
path.indexOf('http') === -1
? process.env.UPLOAD_DIRECTORY + path
: path,
};
}
return m;
})
)
).map((m) => {
return {
...m,
url:
m.path.indexOf('http') === -1
? process.env.FRONTEND_URL +
'/' +
process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY +
m.path
: m.path,
type: 'image',
path:
m.path.indexOf('http') === -1
? process.env.UPLOAD_DIRECTORY + m.path
: m.path,
};
});
);
if (imageUpdateNeeded) {
await this._postRepository.updateImages(id, JSON.stringify(getImageList));
@ -130,7 +189,7 @@ export class PostsService {
return getImageList;
}
async getPost(orgId: string, id: string) {
async getPost(orgId: string, id: string, convertToJPEG = false) {
const posts = await this.getPostsRecursively(id, true, orgId, true);
const list = {
group: posts?.[0]?.group,
@ -139,7 +198,8 @@ export class PostsService {
...post,
image: await this.updateMedia(
post.id,
JSON.parse(post.image || '[]')
JSON.parse(post.image || '[]'),
convertToJPEG,
),
}))
),
@ -361,7 +421,11 @@ export class PostsService {
id: p.id,
message: p.content,
settings: JSON.parse(p.settings || '{}'),
media: await this.updateMedia(p.id, JSON.parse(p.image || '[]')),
media: await this.updateMedia(
p.id,
JSON.parse(p.image || '[]'),
getIntegration.convertToJPEG
),
}))
),
integration

View File

@ -23,15 +23,18 @@ export class TikTokDto {
@IsBoolean()
comment: boolean;
@IsIn(['yes', 'no'])
autoAddMusic: 'yes' | 'no';
@IsBoolean()
brand_content_toggle: boolean;
@IsBoolean()
brand_organic_toggle: boolean;
@IsIn(['true'])
@IsDefined()
isValidVideo: boolean;
// @IsIn(['true'])
// @IsDefined()
// isValidVideo: boolean;
@IsIn(['DIRECT_POST', 'UPLOAD'])
@IsString()

View File

@ -110,6 +110,7 @@ export interface SocialProvider
ISocialMediaIntegration {
identifier: string;
refreshWait?: boolean;
convertToJPEG?: boolean;
isWeb3?: boolean;
customFields?: () => Promise<
{

View File

@ -17,6 +17,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
identifier = 'tiktok';
name = 'Tiktok';
isBetweenSteps = false;
convertToJPEG = true;
scopes = [
'user.info.basic',
'video.publish',
@ -103,10 +104,10 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
grant_type: 'authorization_code',
code_verifier: params.codeVerifier,
redirect_uri: `${
process?.env?.FRONTEND_URL?.indexOf('https') === -1
? 'https://redirectmeto.com/'
: ''
}${process?.env?.FRONTEND_URL}/integrations/social/tiktok`
process?.env?.FRONTEND_URL?.indexOf('https') === -1
? 'https://redirectmeto.com/'
: ''
}${process?.env?.FRONTEND_URL}/integrations/social/tiktok`,
};
const { access_token, refresh_token, scope } = await (
@ -208,23 +209,27 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
}
if (status === 'FAILED') {
throw new BadBody('titok-error-upload', JSON.stringify(post), {
// @ts-ignore
postDetails,
});
throw new BadBody(
'titok-error-upload',
JSON.stringify(post),
Buffer.from(JSON.stringify(post))
);
}
await timer(3000);
}
}
private postingMethod(method: TikTokDto["content_posting_method"]): string {
switch (method) {
case 'UPLOAD':
return '/inbox/video/init/';
case 'DIRECT_POST':
default:
return '/video/init/';
private postingMethod(
method: TikTokDto['content_posting_method'],
isPhoto: boolean
): string {
switch (method) {
case 'UPLOAD':
return isPhoto ? '/content/init/' : '/inbox/video/init/';
case 'DIRECT_POST':
default:
return isPhoto ? '/content/init/' : '/video/init/';
}
}
@ -235,11 +240,15 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
integration: Integration
): Promise<PostResponse[]> {
const [firstPost, ...comments] = postDetails;
const {
data: { publish_id },
} = await (
await this.fetch(
`https://open.tiktokapis.com/v2/post/publish${this.postingMethod(firstPost.settings.content_posting_method)}`,
`https://open.tiktokapis.com/v2/post/publish${this.postingMethod(
firstPost.settings.content_posting_method,
(firstPost?.media?.[0]?.url?.indexOf('mp4') || -1) === -1
)}`,
{
method: 'POST',
headers: {
@ -247,21 +256,44 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider {
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
...(firstPost.settings.content_posting_method === 'DIRECT_POST' ? {
post_info: {
title: firstPost.message,
privacy_level: firstPost.settings.privacy_level,
disable_duet: !firstPost.settings.duet,
disable_comment: !firstPost.settings.comment,
disable_stitch: !firstPost.settings.stitch,
brand_content_toggle: firstPost.settings.brand_content_toggle,
brand_organic_toggle: firstPost.settings.brand_organic_toggle,
}
} : {}),
source_info: {
source: 'PULL_FROM_URL',
video_url: firstPost?.media?.[0]?.url!,
},
...(firstPost.settings.content_posting_method === 'DIRECT_POST'
? {
post_info: {
title: firstPost.message,
privacy_level: firstPost.settings.privacy_level,
disable_duet: !firstPost.settings.duet,
disable_comment: !firstPost.settings.comment,
disable_stitch: !firstPost.settings.stitch,
brand_content_toggle:
firstPost.settings.brand_content_toggle,
brand_organic_toggle:
firstPost.settings.brand_organic_toggle,
...((firstPost?.media?.[0]?.url?.indexOf('mp4') || -1) ===
-1
? {
auto_add_music:
firstPost.settings.autoAddMusic === 'yes',
}
: {}),
},
}
: {}),
...((firstPost?.media?.[0]?.url?.indexOf('mp4') || -1) > -1
? {
source_info: {
source: 'PULL_FROM_URL',
video_url: firstPost?.media?.[0]?.url!,
},
}
: {
source_info: {
source: 'PULL_FROM_URL',
photo_cover_index: 1,
photo_images: firstPost.media?.map((p) => p.url),
},
post_mode: 'DIRECT_POST',
media_type: 'PHOTO',
}),
}),
}
)