436 lines
11 KiB
TypeScript
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'),
|
|
})),
|
|
})) || []
|
|
);
|
|
}
|
|
}
|