diff --git a/apps/frontend/public/icons/platforms/instagram-standalone.png b/apps/frontend/public/icons/platforms/instagram-standalone.png new file mode 100644 index 00000000..389d7eb5 Binary files /dev/null and b/apps/frontend/public/icons/platforms/instagram-standalone.png differ diff --git a/apps/frontend/src/components/launches/add.provider.component.tsx b/apps/frontend/src/components/launches/add.provider.component.tsx index 2cdbcb30..7d9c6c12 100644 --- a/apps/frontend/src/components/launches/add.provider.component.tsx +++ b/apps/frontend/src/components/launches/add.provider.component.tsx @@ -305,6 +305,7 @@ export const AddProviderComponent: FC<{ social: Array<{ identifier: string; name: string; + toolTip?: string; isExternal: boolean; customFields?: Array<{ key: string; @@ -444,8 +445,14 @@ export const AddProviderComponent: FC<{ item.isExternal, item.customFields )} + {...(!!item.toolTip + ? { + 'data-tooltip-id': 'tooltip', + 'data-tooltip-content': item.toolTip, + } + : {})} className={ - 'w-[120px] h-[100px] bg-input text-textColor justify-center items-center flex flex-col gap-[10px] cursor-pointer' + 'w-[200px] h-[100px] text-[14px] bg-input text-textColor relative justify-center items-center flex flex-col gap-[10px] cursor-pointer' } >
@@ -458,7 +465,24 @@ export const AddProviderComponent: FC<{ /> )}
-
{item.name}
+
+ {item.name} + {!!item.toolTip && ( + + + + )} +
))} diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index 2549ca36..34afbfe8 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -29,6 +29,7 @@ export const Providers = [ {identifier: 'hashnode', component: HashnodeProvider}, {identifier: 'facebook', component: FacebookProvider}, {identifier: 'instagram', component: InstagramProvider}, + {identifier: 'instagram-standalone', component: InstagramProvider}, {identifier: 'youtube', component: YoutubeProvider}, {identifier: 'tiktok', component: TiktokProvider}, {identifier: 'pinterest', component: PinterestProvider}, diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts index bf44d558..8dd99931 100644 --- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts +++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts @@ -22,6 +22,7 @@ import { SlackProvider } from '@gitroom/nestjs-libraries/integrations/social/sla import { MastodonProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.provider'; import { BlueskyProvider } from '@gitroom/nestjs-libraries/integrations/social/bluesky.provider'; import { LemmyProvider } from '@gitroom/nestjs-libraries/integrations/social/lemmy.provider'; +import { InstagramStandaloneProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.standalone.provider'; // import { MastodonCustomProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.custom.provider'; const socialIntegrationList: SocialProvider[] = [ @@ -29,8 +30,9 @@ const socialIntegrationList: SocialProvider[] = [ new LinkedinProvider(), new LinkedinPageProvider(), new RedditProvider(), - new FacebookProvider(), new InstagramProvider(), + new InstagramStandaloneProvider(), + new FacebookProvider(), new ThreadsProvider(), new YoutubeProvider(), new TiktokProvider(), @@ -58,6 +60,7 @@ export class IntegrationManager { socialIntegrationList.map(async (p) => ({ name: p.name, identifier: p.identifier, + toolTip: p.toolTip, isExternal: !!p.externalUrl, ...(p.customFields ? { customFields: await p.customFields() } : {}), })) diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index 69d9ab23..fd787a92 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -10,14 +10,16 @@ import { timer } from '@gitroom/helpers/utils/timer'; import dayjs from 'dayjs'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto'; +import { Integration } from '@prisma/client'; export class InstagramProvider extends SocialAbstract implements SocialProvider { identifier = 'instagram'; - name = 'Instagram'; + name = 'Instagram\n(Facebook Business)'; isBetweenSteps = true; + toolTip = 'Instagram must be business and connected to a Facebook page'; scopes = [ 'instagram_basic', 'pages_show_list', @@ -204,7 +206,9 @@ export class InstagramProvider async post( id: string, accessToken: string, - postDetails: PostDetails[] + postDetails: PostDetails[], + integration: Integration, + type = 'graph.facebook.com' ): Promise { const [firstPost, ...theRest] = postDetails; console.log('in progress'); @@ -241,7 +245,7 @@ export class InstagramProvider console.log(collaborators); const { id: photoId } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}${collaborators}&access_token=${accessToken}${caption}`, + `https://${type}/v20.0/${id}/media?${mediaType}${isCarousel}${collaborators}&access_token=${accessToken}${caption}`, { method: 'POST', } @@ -253,7 +257,7 @@ export class InstagramProvider while (status === 'IN_PROGRESS') { const { status_code } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${photoId}?access_token=${accessToken}&fields=status_code` + `https://${type}/v20.0/${photoId}?access_token=${accessToken}&fields=status_code` ) ).json(); await timer(3000); @@ -272,7 +276,7 @@ export class InstagramProvider if (medias.length === 1) { const { id: mediaId } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${medias[0]}&access_token=${accessToken}&field=id`, + `https://${type}/v20.0/${id}/media_publish?creation_id=${medias[0]}&access_token=${accessToken}&field=id`, { method: 'POST', } @@ -283,7 +287,7 @@ export class InstagramProvider const { permalink } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}` + `https://${type}/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}` ) ).json(); @@ -298,7 +302,7 @@ export class InstagramProvider } else { const { id: containerId, ...all3 } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${id}/media?caption=${encodeURIComponent( + `https://${type}/v20.0/${id}/media?caption=${encodeURIComponent( firstPost?.message )}&media_type=CAROUSEL&children=${encodeURIComponent( medias.join(',') @@ -313,7 +317,7 @@ export class InstagramProvider while (status === 'IN_PROGRESS') { const { status_code } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${containerId}?fields=status_code&access_token=${accessToken}` + `https://${type}/v20.0/${containerId}?fields=status_code&access_token=${accessToken}` ) ).json(); await timer(3000); @@ -322,7 +326,7 @@ export class InstagramProvider const { id: mediaId, ...all4 } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${id}/media_publish?creation_id=${containerId}&access_token=${accessToken}&field=id`, + `https://${type}/v20.0/${id}/media_publish?creation_id=${containerId}&access_token=${accessToken}&field=id`, { method: 'POST', } @@ -333,7 +337,7 @@ export class InstagramProvider const { permalink } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}` + `https://${type}/v20.0/${mediaId}?fields=permalink&access_token=${accessToken}` ) ).json(); @@ -350,7 +354,7 @@ export class InstagramProvider for (const post of theRest) { const { id: commentId } = await ( await this.fetch( - `https://graph.facebook.com/v20.0/${containerIdGlobal}/comments?message=${encodeURIComponent( + `https://${type}/v20.0/${containerIdGlobal}/comments?message=${encodeURIComponent( post.message )}&access_token=${accessToken}`, { diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts new file mode 100644 index 00000000..f1c3cd7b --- /dev/null +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts @@ -0,0 +1,130 @@ +import { + AuthTokenDetails, + PostDetails, + PostResponse, + SocialProvider, +} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; +import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; +import dayjs from 'dayjs'; +import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; +import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto'; +import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider'; +import { Integration } from '@prisma/client'; + +const instagramProvider = new InstagramProvider(); + +export class InstagramStandaloneProvider + extends SocialAbstract + implements SocialProvider +{ + identifier = 'instagram-standalone'; + name = 'Instagram\n(Standalone)'; + isBetweenSteps = false; + scopes = [ + 'instagram_business_basic', + 'instagram_business_content_publish', + 'instagram_business_manage_comments', + ]; + toolTip = 'Standalone does not support insights or tagging'; + + async refreshToken(refresh_token: string): Promise { + return { + refreshToken: '', + expiresIn: 0, + accessToken: '', + id: '', + name: '', + picture: '', + username: '', + }; + } + + async generateAuthUrl() { + const state = makeId(6); + return { + url: + `https://www.instagram.com/oauth/authorize?enable_fb_login=0&client_id=${ + process.env.INSTAGRAM_APP_ID + }&redirect_uri=${encodeURIComponent( + `${ + process?.env.FRONTEND_URL?.indexOf('https') == -1 + ? `https://redirectmeto.com/${process?.env.FRONTEND_URL}` + : `${process?.env.FRONTEND_URL}` + }/integrations/social/instagram-standalone` + )}&response_type=code&scope=${encodeURIComponent( + this.scopes.join(',') + )}` + `&state=${state}`, + codeVerifier: makeId(10), + state, + }; + } + + async authenticate(params: { + code: string; + codeVerifier: string; + refresh: string; + }) { + const formData = new FormData(); + formData.append('client_id', process.env.INSTAGRAM_APP_ID!); + formData.append('client_secret', process.env.INSTAGRAM_APP_SECRET!); + formData.append('grant_type', 'authorization_code'); + formData.append( + 'redirect_uri', + `${ + process?.env.FRONTEND_URL?.indexOf('https') == -1 + ? `https://redirectmeto.com/${process?.env.FRONTEND_URL}` + : `${process?.env.FRONTEND_URL}` + }/integrations/social/instagram-standalone` + ); + formData.append('code', params.code); + + const getAccessToken = await ( + await this.fetch('https://api.instagram.com/oauth/access_token', { + method: 'POST', + body: formData, + }) + ).json(); + + const { access_token, expires_in, ...all } = await ( + await this.fetch( + 'https://graph.instagram.com/access_token' + + '?grant_type=ig_exchange_token' + + `&client_id=${process.env.INSTAGRAM_APP_ID}` + + `&client_secret=${process.env.INSTAGRAM_APP_SECRET}` + + `&access_token=${getAccessToken.access_token}` + ) + ).json(); + + this.checkScopes(this.scopes, getAccessToken.permissions); + + const { + user_id, + name, + username, + profile_picture_url, + } = await ( + await this.fetch( + `https://graph.instagram.com/v21.0/me?fields=user_id,username,name,profile_picture_url&access_token=${access_token}` + ) + ).json(); + + return { + id: user_id, + name, + accessToken: access_token, + refreshToken: access_token, + expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), + picture: profile_picture_url, + username, + }; + } + + async post( + id: string, + accessToken: string, + postDetails: PostDetails[], + integration: Integration + ): Promise { + return instagramProvider.post(id, accessToken, postDetails, integration, 'graph.instagram.com'); + } +} 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 bd959942..44b19bfa 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -120,6 +120,7 @@ export interface SocialProvider }[] >; name: string; + toolTip?: string; oneTimeToken?: boolean; isBetweenSteps: boolean; scopes: string[]; diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 0b2a9569..0f9ce41c 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -20,6 +20,8 @@ export class XProvider extends SocialAbstract implements SocialProvider { name = 'X'; isBetweenSteps = false; scopes = []; + toolTip = + 'You will be logged in into your current account, if you would like a different account, change it first on X'; @Plug({ identifier: 'x-autoRepostPost', @@ -199,13 +201,20 @@ export class XProvider extends SocialAbstract implements SocialProvider { accessSecret: oauth_token_secret, }); - const { accessToken, client, accessSecret } = - await startingClient.login(code); + const { accessToken, client, accessSecret } = await startingClient.login( + code + ); const { data: { username, verified, profile_image_url, name, id }, } = await client.v2.me({ - 'user.fields': ['username', 'verified', 'verified_type', 'profile_image_url', 'name'], + 'user.fields': [ + 'username', + 'verified', + 'verified_type', + 'profile_image_url', + 'name', + ], }); return {