diff --git a/apps/backend/src/api/routes/marketplace.controller.ts b/apps/backend/src/api/routes/marketplace.controller.ts index a9d500e5..afb961e9 100644 --- a/apps/backend/src/api/routes/marketplace.controller.ts +++ b/apps/backend/src/api/routes/marketplace.controller.ts @@ -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') diff --git a/apps/frontend/src/components/launches/helpers/use.values.ts b/apps/frontend/src/components/launches/helpers/use.values.ts index a863e776..3b1e0492 100644 --- a/apps/frontend/src/components/launches/helpers/use.values.ts +++ b/apps/frontend/src/components/launches/helpers/use.values.ts @@ -38,6 +38,8 @@ export const useValues = ( criteriaMode: 'all', }); + console.log(form.formState.errors); + const getValues = useMemo(() => { return () => ({ ...form.getValues(), __type: identifier }); }, [form, integration]); diff --git a/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx b/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx index 58d0db42..ffed977a 100644 --- a/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx +++ b/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx @@ -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'); -
+
{`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.`}
+ +
+ This feature available only for photos, it will add a default music that + you can change later. +

Allow User To:
-
{ 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 diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts index 00d54911..0354b3f3 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts @@ -80,7 +80,7 @@ export class BullMqClient extends ClientProxy { async dispatchEvent(packet: ReadPacket): Promise { console.log('event to dispatch: ', packet); const queue = this.getQueue(packet.pattern); - if (packet.data.options.every) { + if (packet?.data?.options?.every) { const { every, immediately } = packet.data.options; const id = packet.data.id ?? v4(); await queue.upsertJobScheduler( diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 71db47c0..1404b1ab 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -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 diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts index 91eece6c..1c0c4de3 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts @@ -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() diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index 5d43bc84..673e2fd2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -110,6 +110,7 @@ export interface SocialProvider ISocialMediaIntegration { identifier: string; refreshWait?: boolean; + convertToJPEG?: boolean; isWeb3?: boolean; customFields?: () => Promise< { diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 14b4b0c5..45a3e6a8 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -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 { 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', + }), }), } )