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

350 lines
9.6 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 { timer } from '@gitroom/helpers/utils/timer';
import dayjs from 'dayjs';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { capitalize, chunk } from 'lodash';
export class ThreadsProvider extends SocialAbstract implements SocialProvider {
identifier = 'threads';
name = 'Threads';
isBetweenSteps = false;
scopes = [
'threads_basic',
'threads_content_publish',
'threads_manage_replies',
'threads_manage_insights',
];
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
return {
refreshToken: '',
expiresIn: 0,
accessToken: '',
id: '',
name: '',
picture: '',
username: '',
};
}
async generateAuthUrl(refresh?: string) {
const state = makeId(6);
return {
url:
'https://threads.net/oauth/authorize' +
`?client_id=${process.env.THREADS_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/threads`
)}` +
`&state=${state}` +
`&scope=${encodeURIComponent(this.scopes.join(','))}`,
codeVerifier: makeId(10),
state,
};
}
async authenticate(params: {
code: string;
codeVerifier: string;
refresh?: string;
}) {
const getAccessToken = await (
await this.fetch(
'https://graph.threads.net/oauth/access_token' +
`?client_id=${process.env.THREADS_APP_ID}` +
`&redirect_uri=${encodeURIComponent(
process.env.NODE_ENV === 'development' || !process.env.NODE_ENV
? `https://integration.git.sn/integrations/social/threads`
: `${process.env.FRONTEND_URL}/integrations/social/threads${
params.refresh ? `?refresh=${params.refresh}` : ''
}`
)}` +
`&grant_type=authorization_code` +
`&client_secret=${process.env.THREADS_APP_SECRET}` +
`&code=${params.code}`
)
).json();
const { access_token } = await (
await this.fetch(
'https://graph.threads.net/access_token' +
'?grant_type=th_exchange_token' +
`&client_secret=${process.env.THREADS_APP_SECRET}` +
`&access_token=${getAccessToken.access_token}&fields=access_token,expires_in`
)
).json();
const {
id,
name,
picture: {
data: { url },
},
} = await this.fetchPageInformation(access_token);
return {
id,
name,
accessToken: access_token,
refreshToken: access_token,
expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(),
picture: url,
username: '',
};
}
private async checkLoaded(
mediaContainerId: string,
accessToken: string
): Promise<boolean> {
const { status, id, error_message } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${mediaContainerId}?fields=status,error_message&access_token=${accessToken}`
)
).json();
console.log(status, error_message);
if (status === 'ERROR') {
throw new Error(id);
}
if (status === 'FINISHED') {
await timer(2000);
return true;
}
await timer(2200);
return this.checkLoaded(mediaContainerId, accessToken);
}
async fetchPageInformation(accessToken: string) {
const { id, username, threads_profile_picture_url, access_token } = await (
await this.fetch(
`https://graph.threads.net/v1.0/me?fields=id,username,threads_profile_picture_url&access_token=${accessToken}`
)
).json();
return {
id,
name: username,
access_token,
picture: { data: { url: threads_profile_picture_url } },
username,
};
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails[]
): Promise<PostResponse[]> {
const [firstPost, ...theRest] = postDetails;
let globalThread = '';
let link = '';
if (firstPost?.media?.length! <= 1) {
const type = !firstPost?.media?.[0]?.path
? undefined
: firstPost?.media![0].path.indexOf('.mp4') > -1
? 'video_url'
: 'image_url';
const media = new URLSearchParams({
...(type === 'video_url'
? { video_url: firstPost?.media![0].path }
: {}),
...(type === 'image_url'
? { image_url: firstPost?.media![0].path }
: {}),
media_type:
type === 'video_url'
? 'VIDEO'
: type === 'image_url'
? 'IMAGE'
: 'TEXT',
text: firstPost?.message,
access_token: accessToken,
});
const { id: containerId } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${id}/threads?${media.toString()}`,
{
method: 'POST',
}
)
).json();
await this.checkLoaded(containerId, accessToken);
const { id: threadId } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${containerId}&access_token=${accessToken}`,
{
method: 'POST',
}
)
).json();
const { permalink, ...all } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}`
)
).json();
globalThread = threadId;
link = permalink;
} else {
const medias = [];
for (const mediaLoad of firstPost.media!) {
const type =
mediaLoad.path.indexOf('.mp4') > -1 ? 'video_url' : 'image_url';
const media = new URLSearchParams({
...(type === 'video_url' ? { video_url: mediaLoad.path } : {}),
...(type === 'image_url' ? { image_url: mediaLoad.path } : {}),
is_carousel_item: 'true',
media_type:
type === 'video_url'
? 'VIDEO'
: type === 'image_url'
? 'IMAGE'
: 'TEXT',
text: firstPost?.message,
access_token: accessToken,
});
const { id: mediaId } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${id}/threads?${media.toString()}`,
{
method: 'POST',
}
)
).json();
medias.push(mediaId);
}
await Promise.all(
medias.map((p: string) => this.checkLoaded(p, accessToken))
);
const { id: containerId } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${id}/threads?text=${
firstPost?.message
}&media_type=CAROUSEL&children=${medias.join(
','
)}&access_token=${accessToken}`,
{
method: 'POST',
}
)
).json();
await this.checkLoaded(containerId, accessToken);
const { id: threadId } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${containerId}&access_token=${accessToken}`,
{
method: 'POST',
}
)
).json();
const { permalink } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${threadId}?fields=id,permalink&access_token=${accessToken}`
)
).json();
globalThread = threadId;
link = permalink;
}
let lastId = globalThread;
for (const post of theRest) {
const form = new FormData();
form.append('media_type', 'TEXT');
form.append('text', post.message);
form.append('reply_to_id', lastId);
form.append('access_token', accessToken);
const { id: replyId } = await (
await this.fetch('https://graph.threads.net/v1.0/me/threads', {
method: 'POST',
body: form,
})
).json();
const { id: threadMediaId } = await (
await this.fetch(
`https://graph.threads.net/v1.0/${id}/threads_publish?creation_id=${replyId}&access_token=${accessToken}`,
{
method: 'POST',
}
)
).json();
lastId = threadMediaId;
}
return [
{
id: firstPost.id,
postId: String(globalThread),
status: 'success',
releaseURL: link,
},
...theRest.map((p) => ({
id: p.id,
postId: String(globalThread),
status: 'success',
releaseURL: link,
})),
];
}
async analytics(
id: string,
accessToken: string,
date: number
): Promise<AnalyticsData[]> {
const until = dayjs().format('YYYY-MM-DD');
const since = dayjs().subtract(date, 'day').format('YYYY-MM-DD');
const { data, ...all } = await (
await fetch(
`https://graph.threads.net/v1.0/${id}/threads_insights?metric=views,likes,replies,reposts,quotes&access_token=${accessToken}&period=day&since=${since}&until=${until}`
)
).json();
console.log(data);
return (
data?.map((d: any) => ({
label: capitalize(d.name),
percentageChange: 5,
data: d.total_value
? [{ total: d.total_value.value, date: dayjs().format('YYYY-MM-DD') }]
: d.values.map((v: any) => ({
total: v.value,
date: dayjs(v.end_time).format('YYYY-MM-DD'),
})),
})) || []
);
}
}