Merge pull request #522 from gitroomhq/feat/instagram-standalone

Add instagram as a standalone without a business
This commit is contained in:
Nevo David 2025-01-05 18:59:39 +07:00 committed by GitHub
commit bf77ab71db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 189 additions and 17 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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'
}
>
<div>
@ -458,7 +465,24 @@ export const AddProviderComponent: FC<{
/>
)}
</div>
<div>{item.name}</div>
<div className="whitespace-pre-wrap text-center">
{item.name}
{!!item.toolTip && (
<svg
width="15"
height="15"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute top-[10px] right-[10px]"
>
<path
d="M13 0C10.4288 0 7.91543 0.762437 5.77759 2.1909C3.63975 3.61935 1.97351 5.64968 0.989572 8.02512C0.0056327 10.4006 -0.251811 13.0144 0.249797 15.5362C0.751405 18.0579 1.98953 20.3743 3.80762 22.1924C5.6257 24.0105 7.94208 25.2486 10.4638 25.7502C12.9856 26.2518 15.5995 25.9944 17.9749 25.0104C20.3503 24.0265 22.3807 22.3603 23.8091 20.2224C25.2376 18.0846 26 15.5712 26 13C25.9964 9.5533 24.6256 6.24882 22.1884 3.81163C19.7512 1.37445 16.4467 0.00363977 13 0ZM13 21C12.7033 21 12.4133 20.912 12.1667 20.7472C11.92 20.5824 11.7277 20.3481 11.6142 20.074C11.5007 19.7999 11.471 19.4983 11.5288 19.2074C11.5867 18.9164 11.7296 18.6491 11.9393 18.4393C12.1491 18.2296 12.4164 18.0867 12.7074 18.0288C12.9983 17.9709 13.2999 18.0007 13.574 18.1142C13.8481 18.2277 14.0824 18.42 14.2472 18.6666C14.412 18.9133 14.5 19.2033 14.5 19.5C14.5 19.8978 14.342 20.2794 14.0607 20.5607C13.7794 20.842 13.3978 21 13 21ZM14 14.91V15C14 15.2652 13.8946 15.5196 13.7071 15.7071C13.5196 15.8946 13.2652 16 13 16C12.7348 16 12.4804 15.8946 12.2929 15.7071C12.1054 15.5196 12 15.2652 12 15V14C12 13.7348 12.1054 13.4804 12.2929 13.2929C12.4804 13.1054 12.7348 13 13 13C14.6538 13 16 11.875 16 10.5C16 9.125 14.6538 8 13 8C11.3463 8 10 9.125 10 10.5V11C10 11.2652 9.89465 11.5196 9.70711 11.7071C9.51958 11.8946 9.26522 12 9.00001 12C8.73479 12 8.48044 11.8946 8.2929 11.7071C8.10536 11.5196 8.00001 11.2652 8.00001 11V10.5C8.00001 8.01875 10.2425 6 13 6C15.7575 6 18 8.01875 18 10.5C18 12.6725 16.28 14.4913 14 14.91Z"
fill="currentColor"
/>
</svg>
)}
</div>
</div>
))}
</div>

View File

@ -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},

View File

@ -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() } : {}),
}))

View File

@ -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<InstagramDto>[]
postDetails: PostDetails<InstagramDto>[],
integration: Integration,
type = 'graph.facebook.com'
): Promise<PostResponse[]> {
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}`,
{

View File

@ -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<AuthTokenDetails> {
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<InstagramDto>[],
integration: Integration
): Promise<PostResponse[]> {
return instagramProvider.post(id, accessToken, postDetails, integration, 'graph.instagram.com');
}
}

View File

@ -120,6 +120,7 @@ export interface SocialProvider
}[]
>;
name: string;
toolTip?: string;
oneTimeToken?: boolean;
isBetweenSteps: boolean;
scopes: string[];

View File

@ -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 {