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

505 lines
14 KiB
TypeScript

import { TweetV2, TwitterApi } from 'twitter-api-v2';
import {
AnalyticsData,
AuthTokenDetails,
PostDetails,
PostResponse,
SocialProvider,
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
import { lookup } from 'mime-types';
import sharp from 'sharp';
import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
import { Integration } from '@prisma/client';
import { timer } from '@gitroom/helpers/utils/timer';
import { PostPlug } from '@gitroom/helpers/decorators/post.plug';
import dayjs from 'dayjs';
import { uniqBy } from 'lodash';
export class XProvider extends SocialAbstract implements SocialProvider {
identifier = 'x';
name = 'X';
isBetweenSteps = false;
scopes = [] as string[];
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',
title: 'Auto Repost Posts',
disabled: !!process.env.DISABLE_X_ANALYTICS,
description:
'When a post reached a certain number of likes, repost it to increase engagement (1 week old posts)',
runEveryMilliseconds: 21600000,
totalRuns: 3,
fields: [
{
name: 'likesAmount',
type: 'number',
placeholder: 'Amount of likes',
description: 'The amount of likes to trigger the repost',
validation: /^\d+$/,
},
],
})
async autoRepostPost(
integration: Integration,
id: string,
fields: { likesAmount: string }
) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: accessTokenSplit,
accessSecret: accessSecretSplit,
});
if (
(await client.v2.tweetLikedBy(id)).meta.result_count >=
+fields.likesAmount
) {
await timer(2000);
await client.v2.retweet(integration.internalId, id);
return true;
}
return false;
}
@PostPlug({
identifier: 'x-repost-post-users',
title: 'Add Re-posters',
disabled: !!process.env.DISABLE_X_ANALYTICS,
description: 'Add accounts to repost your post',
pickIntegration: ['x'],
fields: [],
})
async repostPostUsers(
integration: Integration,
originalIntegration: Integration,
postId: string,
information: any
) {
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: accessTokenSplit,
accessSecret: accessSecretSplit,
});
const {
data: { id },
} = await client.v2.me();
try {
await client.v2.retweet(id, postId);
} catch (err) {
/** nothing **/
}
}
@Plug({
identifier: 'x-autoPlugPost',
title: 'Auto plug post',
disabled: !!process.env.DISABLE_X_ANALYTICS,
description:
'When a post reached a certain number of likes, add another post to it so you followers get a notification about your promotion',
runEveryMilliseconds: 21600000,
totalRuns: 3,
fields: [
{
name: 'likesAmount',
type: 'number',
placeholder: 'Amount of likes',
description: 'The amount of likes to trigger the repost',
validation: /^\d+$/,
},
{
name: 'post',
type: 'richtext',
placeholder: 'Post to plug',
description: 'Message content to plug',
validation: /^[\s\S]{3,}$/g,
},
],
})
async autoPlugPost(
integration: Integration,
id: string,
fields: { likesAmount: string; post: string }
) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
const [accessTokenSplit, accessSecretSplit] = integration.token.split(':');
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: accessTokenSplit,
accessSecret: accessSecretSplit,
});
if (
(await client.v2.tweetLikedBy(id)).meta.result_count >=
+fields.likesAmount
) {
await timer(2000);
await client.v2.tweet({
text: fields.post,
reply: { in_reply_to_tweet_id: id },
});
return true;
}
return false;
}
async refreshToken(): Promise<AuthTokenDetails> {
return {
id: '',
name: '',
accessToken: '',
refreshToken: '',
expiresIn: 0,
picture: '',
username: '',
};
}
async generateAuthUrl() {
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
});
const { url, oauth_token, oauth_token_secret } =
await client.generateAuthLink(
(process.env.X_URL || process.env.FRONTEND_URL) +
`/integrations/social/x`,
{
authAccessType: 'write',
linkMode: 'authenticate',
forceLogin: false,
}
);
return {
url,
codeVerifier: oauth_token + ':' + oauth_token_secret,
state: oauth_token,
};
}
async authenticate(params: { code: string; codeVerifier: string }) {
const { code, codeVerifier } = params;
const [oauth_token, oauth_token_secret] = codeVerifier.split(':');
const startingClient = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: oauth_token,
accessSecret: oauth_token_secret,
});
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',
],
});
return {
id: String(id),
accessToken: accessToken + ':' + accessSecret,
name,
refreshToken: '',
expiresIn: 999999999,
picture: profile_image_url,
username,
additionalSettings: [
{
title: 'Verified',
description: 'Is this a verified user? (Premium)',
type: 'checkbox' as const,
value: verified,
},
],
};
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails<{
active_thread_finisher: boolean;
thread_finisher: string;
community?: string;
who_can_reply_post:
| 'everyone'
| 'following'
| 'mentionedUsers'
| 'subscribers'
| 'verified';
}>[]
): Promise<PostResponse[]> {
const [accessTokenSplit, accessSecretSplit] = accessToken.split(':');
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: accessTokenSplit,
accessSecret: accessSecretSplit,
});
const {
data: { username },
} = await client.v2.me({
'user.fields': 'username',
});
// upload everything before, you don't want it to fail between the posts
const uploadAll = (
await Promise.all(
postDetails.flatMap((p) =>
p?.media?.flatMap(async (m) => {
return {
id: await client.v1.uploadMedia(
m.path.indexOf('mp4') > -1
? Buffer.from(await readOrFetch(m.path))
: await sharp(await readOrFetch(m.path), {
animated: lookup(m.path) === 'image/gif',
})
.resize({
width: 1000,
})
.gif()
.toBuffer(),
{
mimeType: lookup(m.path) || '',
}
),
postId: p.id,
};
})
)
)
).reduce((acc, val) => {
if (!val?.id) {
return acc;
}
acc[val.postId] = acc[val.postId] || [];
acc[val.postId].push(val.id);
return acc;
}, {} as Record<string, string[]>);
const ids: Array<{ postId: string; id: string; releaseURL: string }> = [];
for (const post of postDetails) {
const media_ids = (uploadAll[post.id] || []).filter((f) => f);
// @ts-ignore
const { data }: { data: { id: string } } = await client.v2.tweet({
...(!postDetails?.[0]?.settings?.who_can_reply_post ||
postDetails?.[0]?.settings?.who_can_reply_post === 'everyone'
? {}
: {
reply_settings: postDetails?.[0]?.settings?.who_can_reply_post,
}),
...(postDetails?.[0]?.settings?.community
? {
community_id:
postDetails?.[0]?.settings?.community?.split('/').pop() || '',
}
: {}),
text: post.message,
...(media_ids.length ? { media: { media_ids } } : {}),
...(ids.length
? { reply: { in_reply_to_tweet_id: ids[ids.length - 1].postId } }
: {}),
});
ids.push({
postId: data.id,
id: post.id,
releaseURL: `https://twitter.com/${username}/status/${data.id}`,
});
}
if (postDetails?.[0]?.settings?.active_thread_finisher) {
try {
await client.v2.tweet({
text:
postDetails?.[0]?.settings?.thread_finisher! +
'\n' +
ids[0].releaseURL,
reply: { in_reply_to_tweet_id: ids[ids.length - 1].postId },
});
} catch (err) {}
}
return ids.map((p) => ({
...p,
status: 'posted',
}));
}
communities(accessToken: string, data: { search: string }) {
const [accessTokenSplit, accessSecretSplit] = accessToken.split(':');
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: accessTokenSplit,
accessSecret: accessSecretSplit,
});
return client.v2.searchCommunities(data.search);
// })).data.map(p => {
// return {
// id: p.id,
// name: p.name,
// accessToken
// }
// })
}
private loadAllTweets = async (
client: TwitterApi,
id: string,
until: string,
since: string,
token = ''
): Promise<TweetV2[]> => {
const tweets = await client.v2.userTimeline(id, {
'tweet.fields': ['id'],
'user.fields': [],
'poll.fields': [],
'place.fields': [],
'media.fields': [],
exclude: ['replies', 'retweets'],
start_time: since,
end_time: until,
max_results: 100,
...(token ? { pagination_token: token } : {}),
});
return [
...tweets.data.data,
...(tweets.data.data.length === 100
? await this.loadAllTweets(
client,
id,
until,
since,
tweets.meta.next_token
)
: []),
];
};
async analytics(
id: string,
accessToken: string,
date: number
): Promise<AnalyticsData[]> {
if (process.env.DISABLE_X_ANALYTICS) {
return [];
}
const until = dayjs().endOf('day');
const since = dayjs().subtract(date, 'day');
const [accessTokenSplit, accessSecretSplit] = accessToken.split(':');
const client = new TwitterApi({
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
accessToken: accessTokenSplit,
accessSecret: accessSecretSplit,
});
try {
const tweets = uniqBy(
await this.loadAllTweets(
client,
id,
until.format('YYYY-MM-DDTHH:mm:ssZ'),
since.format('YYYY-MM-DDTHH:mm:ssZ')
),
(p) => p.id
);
if (tweets.length === 0) {
return [];
}
console.log(tweets.map((p) => p.id));
const data = await client.v2.tweets(
tweets.map((p) => p.id),
{
'tweet.fields': ['public_metrics'],
}
);
const metrics = data.data.reduce(
(all, current) => {
all.impression_count =
(all.impression_count || 0) +
+current.public_metrics.impression_count;
all.bookmark_count =
(all.bookmark_count || 0) + +current.public_metrics.bookmark_count;
all.like_count =
(all.like_count || 0) + +current.public_metrics.like_count;
all.quote_count =
(all.quote_count || 0) + +current.public_metrics.quote_count;
all.reply_count =
(all.reply_count || 0) + +current.public_metrics.reply_count;
all.retweet_count =
(all.retweet_count || 0) + +current.public_metrics.retweet_count;
return all;
},
{
impression_count: 0,
bookmark_count: 0,
like_count: 0,
quote_count: 0,
reply_count: 0,
retweet_count: 0,
}
);
console.log(metrics);
console.log(JSON.stringify(data, null, 2));
return Object.entries(metrics).map(([key, value]) => ({
label: key.replace('_count', '').replace('_', ' ').toUpperCase(),
percentageChange: 5,
data: [
{
total: String(0),
date: since.format('YYYY-MM-DD'),
},
{
total: String(value),
date: until.format('YYYY-MM-DD'),
},
],
}));
} catch (err) {
console.log(err);
}
return [];
}
}