feat: tiktok photos
This commit is contained in:
parent
3de66b7e68
commit
8399aee295
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ export const useValues = (
|
|||
criteriaMode: 'all',
|
||||
});
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
const getValues = useMemo(() => {
|
||||
return () => ({ ...form.getValues(), __type: identifier });
|
||||
}, [form, integration]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export interface SocialProvider
|
|||
ISocialMediaIntegration {
|
||||
identifier: string;
|
||||
refreshWait?: boolean;
|
||||
convertToJPEG?: boolean;
|
||||
isWeb3?: boolean;
|
||||
customFields?: () => Promise<
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue