From 4ddea7977fd440facc1f561d3caee3f84d9a9c6f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 9 Jul 2025 20:53:49 +0700 Subject: [PATCH] feat: better error messages, retry fix --- .../integrations/integration.service.ts | 6 +- .../database/prisma/posts/posts.service.ts | 93 +----- .../src/integrations/integration.manager.ts | 4 +- .../src/integrations/social.abstract.ts | 25 +- .../integrations/social/facebook.provider.ts | 104 +++++++ .../integrations/social/instagram.provider.ts | 191 ++++++++++-- .../social/instagram.standalone.provider.ts | 4 + .../integrations/social/tiktok.provider.ts | 294 ++++++++++++------ .../integrations/social/youtube.provider.ts | 121 ++++--- 9 files changed, 570 insertions(+), 272 deletions(-) diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 60be9a2d..d7a2abc0 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -163,11 +163,11 @@ export class IntegrationService { await this.informAboutRefreshError(orgId, integration); } - async informAboutRefreshError(orgId: string, integration: Integration) { + async informAboutRefreshError(orgId: string, integration: Integration, err = '') { await this._notificationService.inAppNotification( orgId, - `Could not refresh your ${integration.providerIdentifier} channel`, - `Could not refresh your ${integration.providerIdentifier} channel. Please go back to the system and connect it again ${process.env.FRONTEND_URL}/launches`, + `Could not refresh your ${integration.providerIdentifier} channel ${err}`, + `Could not refresh your ${integration.providerIdentifier} channel ${err}. Please go back to the system and connect it again ${process.env.FRONTEND_URL}/launches`, true ); } 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 9c3a5494..3bdc60d3 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -299,16 +299,10 @@ export class PostsService { } try { - const finalPost = - firstPost.integration?.type === 'article' - ? await this.postArticle(firstPost.integration!, [ - firstPost, - ...morePosts, - ]) - : await this.postSocial(firstPost.integration!, [ - firstPost, - ...morePosts, - ]); + const finalPost = await this.postSocial(firstPost.integration!, [ + firstPost, + ...morePosts, + ]); if (firstPost?.intervalInDays) { this._workerServiceProducer.emit('post', { @@ -333,31 +327,16 @@ export class PostsService { return; } - - if (firstPost.submittedForOrderId) { - this._workerServiceProducer.emit('submit', { - payload: { - id: firstPost.id, - releaseURL: finalPost.releaseURL, - }, - }); - } } catch (err: any) { await this._postRepository.changeState(firstPost.id, 'ERROR', err); - await this._notificationService.inAppNotification( - firstPost.organizationId, - `Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`, - `An error occurred while posting on ${ - firstPost.integration?.providerIdentifier - } ${ - !process.env.NODE_ENV || process.env.NODE_ENV === 'development' - ? err - : '' - }`, - true - ); - if (err instanceof BadBody) { + await this._notificationService.inAppNotification( + firstPost.organizationId, + `Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`, + `An error occurred while posting on ${firstPost.integration?.providerIdentifier}${err?.message ? `: ${err?.message}` : ``}`, + true + ); + console.error( '[Error] posting on', firstPost.integration?.providerIdentifier, @@ -366,15 +345,9 @@ export class PostsService { err.body, err ); - - return; } - console.error( - '[Error] posting on', - firstPost.integration?.providerIdentifier, - err - ); + return; } } @@ -403,7 +376,8 @@ export class PostsService { private async postSocial( integration: Integration, posts: Post[], - forceRefresh = false + forceRefresh = false, + err = '' ): Promise> { const getIntegration = this._integrationManager.getSocialIntegration( integration.providerIdentifier @@ -533,7 +507,7 @@ export class PostsService { }; } catch (err) { if (err instanceof RefreshToken) { - return this.postSocial(integration, posts, true); + return this.postSocial(integration, posts, true, err?.message || ''); } throw err; @@ -627,41 +601,6 @@ export class PostsService { } } - private async postArticle( - integration: Integration, - posts: Post[] - ): Promise { - const getIntegration = this._integrationManager.getArticlesIntegration( - integration.providerIdentifier - ); - if (!getIntegration) { - return; - } - - const newPosts = await this.updateTags(integration.organizationId, posts); - - const { postId, releaseURL } = await getIntegration.post( - integration.token, - newPosts.map((p) => p.content).join('\n\n'), - JSON.parse(newPosts[0].settings || '{}') - ); - - await this._notificationService.inAppNotification( - integration.organizationId, - `Your article has been published on ${capitalize( - integration.providerIdentifier - )}`, - `Your article has been published at ${releaseURL}`, - true - ); - await this._postRepository.updatePost(newPosts[0].id, postId, releaseURL); - - return { - postId, - releaseURL, - }; - } - async deletePost(orgId: string, group: string) { const post = await this._postRepository.deletePost(orgId, group); if (post?.id) { @@ -738,7 +677,7 @@ export class PostsService { ); if (!posts?.length) { - return; + return [] as any[]; } await this._workerServiceProducer.delete( diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index ea821d2e..95f257af 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -89,7 +89,7 @@ export class IntegrationManager { plugs: ( Reflect.getMetadata('custom:plug', p.constructor.prototype) || [] ) - .filter((f) => !f.disabled) + .filter((f: any) => !f.disabled) .map((p: any) => ({ ...p, fields: p.fields.map((c: any) => ({ @@ -111,7 +111,7 @@ export class IntegrationManager { 'custom:internal_plug', p.constructor.prototype ) || [] - ).filter((f) => !f.disabled) || [], + ).filter((f: any) => !f.disabled) || [], }; } diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index fe7725b2..a92b6252 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -6,7 +6,7 @@ export class RefreshToken { public identifier: string, public json: string, public body: BodyInit, - public message = 'refresh account', + public message = '', ) {} } export class BadBody { @@ -14,7 +14,7 @@ export class BadBody { public identifier: string, public json: string, public body: BodyInit, - public message = 'error occurred' + public message = '' ) {} } @@ -24,7 +24,7 @@ export class NotEnoughScopes { const pThrottleInstance = pThrottle({ limit: 1, - interval: 2000 + interval: 5000 }); export abstract class SocialAbstract { @@ -32,7 +32,7 @@ export abstract class SocialAbstract { (url: RequestInfo, options?: RequestInit) => fetch(url, options) ); - protected handleErrors(body: string): {type: 'refresh-token' | 'bad-body', value: string}|undefined { + public handleErrors(body: string): {type: 'refresh-token' | 'bad-body', value: string}|undefined { return {type: 'bad-body', value: 'bad request'}; } @@ -61,7 +61,7 @@ export abstract class SocialAbstract { } if (json.includes('rate_limit_exceeded') || json.includes('Rate limit')) { - await timer(2000); + await timer(5000); return this.fetch(url, options, identifier, totalRetries + 1); } @@ -71,24 +71,11 @@ export abstract class SocialAbstract { throw new RefreshToken(identifier, json, options.body!, handleError?.value); } - // if ( - // request.status === 401 || - // (json.includes('OAuthException') && - // !json.includes('The user is not an Instagram Business') && - // !json.includes('Unsupported format') && - // !json.includes('2207018') && - // !json.includes('352') && - // !json.includes('REVOKED_ACCESS_TOKEN')) - // ) { - // throw new RefreshToken(identifier, json, options.body!); - // } - if (totalRetries < 2) { - await timer(2000); return this.fetch(url, options, identifier, totalRetries + 1); } - throw new BadBody(identifier, json, options.body!, handleError.value); + throw new BadBody(identifier, json, options.body!, handleError?.value); } checkScopes(required: string[], got: string | string[]) { diff --git a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts index b86eaf71..0771578e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts @@ -21,6 +21,110 @@ export class FacebookProvider extends SocialAbstract implements SocialProvider { 'pages_read_engagement', 'read_insights', ]; + + override handleErrors(body: string): { + type: 'refresh-token' | 'bad-body'; + value: string; + } | undefined { + // Access token validation errors - require re-authentication + if (body.indexOf('Error validating access token') > -1) { + return { + type: 'refresh-token' as const, + value: 'Please re-authenticate your Facebook account', + }; + } + + if (body.indexOf('490') > -1) { + return { + type: 'refresh-token' as const, + value: 'Access token expired, please re-authenticate', + }; + } + + if (body.indexOf('REVOKED_ACCESS_TOKEN') > -1) { + return { + type: 'refresh-token' as const, + value: 'Access token has been revoked, please re-authenticate', + }; + } + + if (body.indexOf('1390008') > -1) { + return { + type: 'bad-body' as const, + value: 'You are posting too fast, please slow down', + }; + } + + // Content policy violations + if (body.indexOf('1346003') > -1) { + return { + type: 'bad-body' as const, + value: 'Content flagged as abusive by Facebook', + }; + } + + if (body.indexOf('1404102') > -1) { + return { + type: 'bad-body' as const, + value: 'Content violates Facebook Community Standards', + }; + } + + // Permission errors + if (body.indexOf('1404078') > -1) { + return { + type: 'refresh-token' as const, + value: 'Page publishing authorization required, please re-authenticate', + }; + } + + if (body.indexOf('1609008') > -1) { + return { + type: 'bad-body' as const, + value: 'Cannot post Facebook.com links', + }; + } + + // Parameter validation errors + if (body.indexOf('2061006') > -1) { + return { + type: 'bad-body' as const, + value: 'Invalid URL format in post content', + }; + } + + if (body.indexOf('1349125') > -1) { + return { + type: 'bad-body' as const, + value: 'Invalid content format', + }; + } + + if (body.indexOf('Name parameter too long') > -1) { + return { + type: 'bad-body' as const, + value: 'Post content is too long', + }; + } + + // Service errors - checking specific subcodes first + if (body.indexOf('1363047') > -1) { + return { + type: 'bad-body' as const, + value: 'Facebook service temporarily unavailable', + }; + } + + if (body.indexOf('1609010') > -1) { + return { + type: 'bad-body' as const, + value: 'Facebook service temporarily unavailable', + }; + } + + return undefined; + } + async refreshToken(refresh_token: string): Promise { return { refreshToken: '', diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 3e64b4f5..bbaacd5a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -43,17 +43,28 @@ export class InstagramProvider }; } - protected override handleErrors(body: string): { - type: 'refresh-token' | 'bad-body'; - value: string; - } | undefined { - if (body.indexOf('2207018') > -1) { + public override handleErrors(body: string): + | { + type: 'refresh-token' | 'bad-body'; + value: string; + } + | undefined { + if (body.indexOf('2207018') > -1 || body.indexOf('REVOKED_ACCESS_TOKEN')) { return { type: 'refresh-token' as const, value: 'Something is wrong with your connected user, please re-authenticate', }; } + + if (body.indexOf('The user is not an Instagram Business') > -1) { + return { + type: 'refresh-token' as const, + value: + 'Your Instagram account is not a business account, please convert it to a business account', + }; + } + if (body.indexOf('Error validating access token') > -1) { return { type: 'refresh-token' as const, @@ -68,6 +79,143 @@ export class InstagramProvider }; } + // Media download/upload errors + if (body.indexOf('2207003') > -1) { + return { + type: 'bad-body' as const, + value: 'Timeout downloading media, please try again', + }; + } + + if (body.indexOf('2207020') > -1) { + return { + type: 'bad-body' as const, + value: 'Media expired, please upload again', + }; + } + + if (body.indexOf('2207032') > -1) { + return { + type: 'bad-body' as const, + value: 'Failed to create media, please try again', + }; + } + + if (body.indexOf('2207053') > -1) { + return { + type: 'bad-body' as const, + value: 'Unknown upload error, please try again', + }; + } + + if (body.indexOf('2207052') > -1) { + return { + type: 'bad-body' as const, + value: 'Media fetch failed, please try again', + }; + } + + if (body.indexOf('2207057') > -1) { + return { + type: 'bad-body' as const, + value: 'Invalid thumbnail offset for video', + }; + } + + if (body.indexOf('2207026') > -1) { + return { + type: 'bad-body' as const, + value: 'Unsupported video format', + }; + } + + if (body.indexOf('2207023') > -1) { + return { + type: 'bad-body' as const, + value: 'Unknown media type', + }; + } + + if (body.indexOf('2207006') > -1) { + return { + type: 'bad-body' as const, + value: 'Media not found, please upload again', + }; + } + + if (body.indexOf('2207008') > -1) { + return { + type: 'bad-body' as const, + value: 'Media builder expired, please try again', + }; + } + + // Content validation errors + if (body.indexOf('2207028') > -1) { + return { + type: 'bad-body' as const, + value: 'Carousel validation failed', + }; + } + + if (body.indexOf('2207010') > -1) { + return { + type: 'bad-body' as const, + value: 'Caption is too long', + }; + } + + // Product tagging errors + if (body.indexOf('2207035') > -1) { + return { + type: 'bad-body' as const, + value: 'Product tag positions not supported for videos', + }; + } + + if (body.indexOf('2207036') > -1) { + return { + type: 'bad-body' as const, + value: 'Product tag positions required for photos', + }; + } + + if (body.indexOf('2207037') > -1) { + return { + type: 'bad-body' as const, + value: 'Product tag validation failed', + }; + } + + if (body.indexOf('2207040') > -1) { + return { + type: 'bad-body' as const, + value: 'Too many product tags', + }; + } + + // Image format/size errors + if (body.indexOf('2207004') > -1) { + return { + type: 'bad-body' as const, + value: 'Image is too large', + }; + } + + if (body.indexOf('2207005') > -1) { + return { + type: 'bad-body' as const, + value: 'Unsupported image format', + }; + } + + if (body.indexOf('2207009') > -1) { + return { + type: 'bad-body' as const, + value: 'Aspect ratio not supported, must be between 4:5 to 1.91:1', + }; + } + if (body.indexOf('Page request limit reached') > -1) { return { type: 'bad-body' as const, @@ -75,6 +223,14 @@ export class InstagramProvider }; } + if (body.indexOf('2207042') > -1) { + return { + type: 'bad-body' as const, + value: + 'You have reached the maximum of 25 posts per day, allowed for your account', + }; + } + if (body.indexOf('Not enough permissions to post') > -1) { return { type: 'bad-body' as const, @@ -96,26 +252,10 @@ export class InstagramProvider }; } - if (body.indexOf('2207027') > -1) { - return { - type: 'bad-body' as const, - value: 'Unknown error, please try again later or contact support', - }; - } - - if (body.indexOf('2207042') > -1) { - return { - type: 'bad-body' as const, - value: - 'You have reached the maximum of 25 posts per day, allowed for your account', - }; - } - if (body.indexOf('2207051') > -1) { return { type: 'bad-body' as const, - value: - 'Instagram blocked your request', + value: 'Instagram blocked your request', }; } @@ -127,6 +267,13 @@ export class InstagramProvider }; } + if (body.indexOf('2207027') > -1) { + return { + type: 'bad-body' as const, + value: 'Unknown error, please try again later or contact support', + }; + } + return undefined; } diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts index 8de6cf54..578ee393 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts @@ -27,6 +27,10 @@ export class InstagramStandaloneProvider 'instagram_business_manage_insights', ]; + public override handleErrors(body: string): { type: "refresh-token" | "bad-body"; value: string } | undefined { + return instagramProvider.handleErrors(body); + } + async refreshToken(refresh_token: string): Promise { const { access_token } = await ( await this.fetch( diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 4e1c516a..d3390533 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -25,6 +25,125 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { 'user.info.profile', ]; + override handleErrors(body: string): + | { + type: 'refresh-token' | 'bad-body'; + value: string; + } + | undefined { + // Authentication/Authorization errors - require re-authentication + if (body.indexOf('access_token_invalid') > -1) { + return { + type: 'refresh-token' as const, + value: + 'Access token invalid, please re-authenticate your TikTok account', + }; + } + + if (body.indexOf('scope_not_authorized') > -1) { + return { + type: 'refresh-token' as const, + value: + 'Missing required permissions, please re-authenticate with all scopes', + }; + } + + if (body.indexOf('scope_permission_missed') > -1) { + return { + type: 'refresh-token' as const, + value: 'Additional permissions required, please re-authenticate', + }; + } + + // Rate limiting errors + if (body.indexOf('rate_limit_exceeded') > -1) { + return { + type: 'bad-body' as const, + value: 'TikTok API rate limit exceeded, please try again later', + }; + } + + // Spam/Policy errors + if (body.indexOf('spam_risk_too_many_posts') > -1) { + return { + type: 'bad-body' as const, + value: 'Daily post limit reached, please try again tomorrow', + }; + } + + if (body.indexOf('spam_risk_user_banned_from_posting') > -1) { + return { + type: 'bad-body' as const, + value: + 'Account banned from posting, please check TikTok account status', + }; + } + + if (body.indexOf('reached_active_user_cap') > -1) { + return { + type: 'bad-body' as const, + value: 'Daily active user quota reached, please try again later', + }; + } + + if ( + body.indexOf('unaudited_client_can_only_post_to_private_accounts') > -1 + ) { + return { + type: 'bad-body' as const, + value: 'App not approved for public posting, contact support', + }; + } + + if (body.indexOf('url_ownership_unverified') > -1) { + return { + type: 'bad-body' as const, + value: 'URL ownership not verified, please verify domain ownership', + }; + } + + if (body.indexOf('privacy_level_option_mismatch') > -1) { + return { + type: 'bad-body' as const, + value: 'Privacy level mismatch, please check privacy settings', + }; + } + + // Content/Format validation errors + if (body.indexOf('invalid_file_upload') > -1) { + return { + type: 'bad-body' as const, + value: 'Invalid file format or specifications not met', + }; + } + + if (body.indexOf('invalid_params') > -1) { + return { + type: 'bad-body' as const, + value: 'Invalid request parameters, please check content format', + }; + } + + // Server errors + if (body.indexOf('internal_error') > -1) { + return { + type: 'bad-body' as const, + value: 'TikTok server error, please try again later', + }; + } + + // Generic TikTok API errors + if (body.indexOf('TikTok API error') > -1) { + return { + type: 'bad-body' as const, + value: 'TikTok API error, please try again', + }; + } + + // Fall back to parent class error handling + return undefined; + } + async refreshToken(refreshToken: string): Promise { const value = { client_key: process.env.TIKTOK_CLIENT_ID!, @@ -240,108 +359,85 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { integration: Integration ): Promise { const [firstPost, ...comments] = postDetails; - const maxRetries = 3; - let lastError: Error | null = null; - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - console.log(`TikTok post attempt ${attempt}/${maxRetries}`, firstPost); - - const { - data: { publish_id }, - } = await ( - await this.fetch( - `https://open.tiktokapis.com/v2/post/publish${this.postingMethod( - firstPost.settings.content_posting_method, - (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1 - )}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=UTF-8', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - ...((firstPost?.settings?.content_posting_method || - 'DIRECT_POST') === 'DIRECT_POST' - ? { - post_info: { - title: firstPost.message, - privacy_level: - firstPost.settings.privacy_level || 'PUBLIC_TO_EVERYONE', - disable_duet: !firstPost.settings.duet || false, - disable_comment: !firstPost.settings.comment || false, - disable_stitch: !firstPost.settings.stitch || false, - brand_content_toggle: - firstPost.settings.brand_content_toggle || false, - brand_organic_toggle: - firstPost.settings.brand_organic_toggle || false, - ...((firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === - -1 - ? { - auto_add_music: - firstPost.settings.autoAddMusic === 'yes', - } - : {}), - }, - } - : {}), - ...((firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) > -1 - ? { - source_info: { - source: 'PULL_FROM_URL', - video_url: firstPost?.media?.[0]?.path!, - ...(firstPost?.media?.[0]?.thumbnailTimestamp! - ? { - video_cover_timestamp_ms: - firstPost?.media?.[0]?.thumbnailTimestamp!, - } - : {}), - }, - } - : { - source_info: { - source: 'PULL_FROM_URL', - photo_cover_index: 0, - photo_images: firstPost.media?.map((p) => p.path), - }, - post_mode: 'DIRECT_POST', - media_type: 'PHOTO', - }), - }), - } - ) - ).json(); - - const { url, id: videoId } = await this.uploadedVideoSuccess( - integration.profile!, - publish_id, - accessToken - ); - - return [ - { - id: firstPost.id, - releaseURL: url, - postId: String(videoId), - status: 'success', + const { + data: { publish_id }, + } = await ( + await this.fetch( + `https://open.tiktokapis.com/v2/post/publish${this.postingMethod( + firstPost.settings.content_posting_method, + (firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === -1 + )}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Authorization: `Bearer ${accessToken}`, }, - ]; - } catch (error) { - lastError = error as Error; - console.log(`TikTok post attempt ${attempt} failed:`, error); - - // If it's the last attempt, throw the error - if (attempt === maxRetries) { - throw error; + body: JSON.stringify({ + ...((firstPost?.settings?.content_posting_method || + 'DIRECT_POST') === 'DIRECT_POST' + ? { + post_info: { + title: firstPost.message, + privacy_level: firstPost.settings.privacy_level || 'PUBLIC_TO_EVERYONE', + disable_duet: !firstPost.settings.duet || false, + disable_comment: !firstPost.settings.comment || false, + disable_stitch: !firstPost.settings.stitch || false, + brand_content_toggle: + firstPost.settings.brand_content_toggle || false, + brand_organic_toggle: + firstPost.settings.brand_organic_toggle || false, + ...((firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) === + -1 + ? { + auto_add_music: + firstPost.settings.autoAddMusic === 'yes', + } + : {}), + }, + } + : {}), + ...((firstPost?.media?.[0]?.path?.indexOf('mp4') || -1) > -1 + ? { + source_info: { + source: 'PULL_FROM_URL', + video_url: firstPost?.media?.[0]?.path!, + ...(firstPost?.media?.[0]?.thumbnailTimestamp! + ? { + video_cover_timestamp_ms: + firstPost?.media?.[0]?.thumbnailTimestamp!, + } + : {}), + }, + } + : { + source_info: { + source: 'PULL_FROM_URL', + photo_cover_index: 0, + photo_images: firstPost.media?.map((p) => p.path), + }, + post_mode: 'DIRECT_POST', + media_type: 'PHOTO', + }), + }), } - - // Wait before retrying (exponential backoff) - await timer(attempt * 2000); - } - } + ) + ).json(); - // This should never be reached, but just in case - throw lastError || new Error('TikTok post failed after all retries'); + const { url, id: videoId } = await this.uploadedVideoSuccess( + integration.profile!, + publish_id, + accessToken + ); + + return [ + { + id: firstPost.id, + releaseURL: url, + postId: String(videoId), + status: 'success', + }, + ]; } } diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts index c2763ab1..39a1f502 100644 --- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts @@ -6,14 +6,18 @@ import { SocialProvider, } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; -import { google } from 'googleapis'; +import { google, youtube_v3 } from 'googleapis'; import { OAuth2Client } from 'google-auth-library/build/src/auth/oauth2client'; import axios from 'axios'; import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto'; -import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import { + BadBody, + SocialAbstract, +} from '@gitroom/nestjs-libraries/integrations/social.abstract'; import * as process from 'node:process'; import dayjs from 'dayjs'; -import { GaxiosError } from 'gaxios/build/src/common'; +import { GaxiosResponse } from 'gaxios/build/src/common'; +import Schema$Video = youtube_v3.Schema$Video; const clientAndYoutube = () => { const client = new google.auth.OAuth2({ @@ -147,8 +151,9 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { responseType: 'stream', }); + let all: GaxiosResponse; try { - const all = await youtubeClient.videos.insert({ + all = await youtubeClient.videos.insert({ part: ['id', 'snippet', 'status'], notifySubscribers: true, requestBody: { @@ -158,15 +163,6 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { ...(settings?.tags?.length ? { tags: settings.tags.map((p) => p.label) } : {}), - // ...(settings?.thumbnail?.url - // ? { - // thumbnails: { - // default: { - // url: settings?.thumbnail?.url, - // }, - // }, - // } - // : {}), }, status: { privacyStatus: settings.type, @@ -176,58 +172,83 @@ export class YoutubeProvider extends SocialAbstract implements SocialProvider { body: response.data, }, }); - - if (settings?.thumbnail?.path) { - try { - const allb = await youtubeClient.thumbnails.set({ - videoId: all?.data?.id!, - media: { - body: ( - await axios({ - url: settings?.thumbnail?.path, - method: 'GET', - responseType: 'stream', - }) - ).data, - }, - }); - } catch (err: any) { - if ( - err.response?.data?.error?.errors?.[0]?.domain === - 'youtube.thumbnail' - ) { - throw 'Your account is not verified, we have uploaded your video but we could not set the thumbnail. Please verify your account and try again.'; - } - } - } - - return [ - { - id: firstPost.id, - releaseURL: `https://www.youtube.com/watch?v=${all?.data?.id}`, - postId: all?.data?.id!, - status: 'success', - }, - ]; } catch (err: any) { if ( err.response?.data?.error?.errors?.[0]?.reason === 'failedPrecondition' ) { - throw 'We have uploaded your video but we could not set the thumbnail. Thumbnail size is too large'; + throw new BadBody( + 'youtube', + JSON.stringify(err.response.data), + JSON.stringify(err.response.data), + 'We have uploaded your video but we could not set the thumbnail. Thumbnail size is too large.' + ); } if ( err.response?.data?.error?.errors?.[0]?.reason === 'uploadLimitExceeded' ) { - throw 'You have reached your daily upload limit, please try again tomorrow.'; + throw new BadBody( + 'youtube', + JSON.stringify(err.response.data), + JSON.stringify(err.response.data), + 'You have reached your daily upload limit, please try again tomorrow.' + ); } if ( err.response?.data?.error?.errors?.[0]?.reason === 'youtubeSignupRequired' ) { - throw 'You have to link your youtube account to your google account first.'; + throw new BadBody( + 'youtube', + JSON.stringify(err.response.data), + JSON.stringify(err.response.data), + 'You have to link your youtube account to your google account first.' + ); + } + + throw new BadBody( + 'youtube', + JSON.stringify(err.response.data), + JSON.stringify(err.response.data), + 'An error occurred while uploading your video, please try again later.' + ); + } + + if (settings?.thumbnail?.path) { + try { + await youtubeClient.thumbnails.set({ + videoId: all?.data?.id!, + media: { + body: ( + await axios({ + url: settings?.thumbnail?.path, + method: 'GET', + responseType: 'stream', + }) + ).data, + }, + }); + } catch (err: any) { + if ( + err.response?.data?.error?.errors?.[0]?.domain === 'youtube.thumbnail' + ) { + throw new BadBody( + '', + JSON.stringify(err.response.data), + JSON.stringify(err.response.data), + 'Your account is not verified, we have uploaded your video but we could not set the thumbnail. Please verify your account and try again.' + ); + } } } - return []; + + return [ + { + id: firstPost.id, + releaseURL: `https://www.youtube.com/watch?v=${all?.data?.id}`, + postId: all?.data?.id!, + status: 'success', + }, + ]; } async analytics(