postiz/libraries/nestjs-libraries/src/integrations/social/facebook.provider.ts

436 lines
11 KiB
TypeScript

import {
AnalyticsData,
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';
export class FacebookProvider extends SocialAbstract implements SocialProvider {
identifier = 'facebook';
name = 'Facebook Page';
isBetweenSteps = true;
scopes = [
'pages_show_list',
'business_management',
'pages_manage_posts',
'pages_manage_engagement',
'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<AuthTokenDetails> {
return {
refreshToken: '',
expiresIn: 0,
accessToken: '',
id: '',
name: '',
picture: '',
username: '',
};
}
async generateAuthUrl() {
const state = makeId(6);
return {
url:
'https://www.facebook.com/v20.0/dialog/oauth' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/facebook`
)}` +
`&state=${state}` +
`&scope=${this.scopes.join(',')}`,
codeVerifier: makeId(10),
state,
};
}
async reConnect(
id: string,
requiredId: string,
accessToken: string
): Promise<AuthTokenDetails> {
const information = await this.fetchPageInformation(
accessToken,
requiredId
);
return {
id: information.id,
name: information.name,
accessToken: information.access_token,
refreshToken: information.access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: information.picture,
username: information.username,
};
}
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const getAccessToken = await (
await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
`?client_id=${process.env.FACEBOOK_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
`${process.env.FRONTEND_URL}/integrations/social/facebook${
params.refresh ? `?refresh=${params.refresh}` : ''
}`
)}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&code=${params.code}`
)
).json();
const { access_token } = await (
await this.fetch(
'https://graph.facebook.com/v20.0/oauth/access_token' +
'?grant_type=fb_exchange_token' +
`&client_id=${process.env.FACEBOOK_APP_ID}` +
`&client_secret=${process.env.FACEBOOK_APP_SECRET}` +
`&fb_exchange_token=${getAccessToken.access_token}&fields=access_token,expires_in`
)
).json();
const { data } = await (
await this.fetch(
`https://graph.facebook.com/v20.0/me/permissions?access_token=${access_token}`
)
).json();
const permissions = data
.filter((d: any) => d.status === 'granted')
.map((p: any) => p.permission);
this.checkScopes(this.scopes, permissions);
const {
id,
name,
picture: {
data: { url },
},
} = await (
await this.fetch(
`https://graph.facebook.com/v20.0/me?fields=id,name,picture&access_token=${access_token}`
)
).json();
return {
id,
name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: url,
username: '',
};
}
async pages(accessToken: string) {
const { data } = await (
await this.fetch(
`https://graph.facebook.com/v20.0/me/accounts?fields=id,username,name,picture.type(large)&access_token=${accessToken}`
)
).json();
return data;
}
async fetchPageInformation(accessToken: string, pageId: string) {
const {
id,
name,
access_token,
username,
picture: {
data: { url },
},
} = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${pageId}?fields=username,access_token,name,picture.type(large)&access_token=${accessToken}`
)
).json();
return {
id,
name,
access_token,
picture: url,
username,
};
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails[]
): Promise<PostResponse[]> {
const [firstPost, ...comments] = postDetails;
let finalId = '';
let finalUrl = '';
if ((firstPost?.media?.[0]?.path?.indexOf('mp4') || -2) > -1) {
const {
id: videoId,
permalink_url,
...all
} = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${id}/videos?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
file_url: firstPost?.media?.[0]?.path!,
description: firstPost.message,
published: true,
}),
},
'upload mp4'
)
).json();
finalUrl = 'https://www.facebook.com/reel/' + videoId;
finalId = videoId;
} else {
const uploadPhotos = !firstPost?.media?.length
? []
: await Promise.all(
firstPost.media.map(async (media) => {
const { id: photoId } = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${id}/photos?access_token=${accessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: media.path,
published: false,
}),
},
'upload images slides'
)
).json();
return { media_fbid: photoId };
})
);
const {
id: postId,
permalink_url,
...all
} = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${id}/feed?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(uploadPhotos?.length ? { attached_media: uploadPhotos } : {}),
message: firstPost.message,
published: true,
}),
},
'finalize upload'
)
).json();
finalUrl = permalink_url;
finalId = postId;
}
const postsArray = [];
let commentId = finalId;
for (const comment of comments) {
const data = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${commentId}/comments?access_token=${accessToken}&fields=id,permalink_url`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(comment.media?.length
? { attachment_url: comment.media[0].path }
: {}),
message: comment.message,
}),
},
'add comment'
)
).json();
commentId = data.id;
postsArray.push({
id: comment.id,
postId: data.id,
releaseURL: data.permalink_url,
status: 'success',
});
}
return [
{
id: firstPost.id,
postId: finalId,
releaseURL: finalUrl,
status: 'success',
},
...postsArray,
];
}
async analytics(
id: string,
accessToken: string,
date: number
): Promise<AnalyticsData[]> {
const until = dayjs().endOf('day').unix();
const since = dayjs().subtract(date, 'day').unix();
const { data } = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${id}/insights?metric=page_impressions_unique,page_posts_impressions_unique,page_post_engagements,page_daily_follows,page_video_views&access_token=${accessToken}&period=day&since=${since}&until=${until}`
)
).json();
return (
data?.map((d: any) => ({
label:
d.name === 'page_impressions_unique'
? 'Page Impressions'
: d.name === 'page_post_engagements'
? 'Posts Engagement'
: d.name === 'page_daily_follows'
? 'Page followers'
: d.name === 'page_video_views'
? 'Videos views'
: 'Posts Impressions',
percentageChange: 5,
data: d?.values?.map((v: any) => ({
total: v.value,
date: dayjs(v.end_time).format('YYYY-MM-DD'),
})),
})) || []
);
}
}