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

168 lines
4.6 KiB
TypeScript

import { TwitterApi } from 'twitter-api-v2';
import {
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';
export class XProvider implements SocialProvider {
identifier = 'x';
name = 'X';
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
const startingClient = new TwitterApi({
clientId: process.env.TWITTER_CLIENT_ID!,
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
});
const {
accessToken,
refreshToken: newRefreshToken,
expiresIn,
client,
} = await startingClient.refreshOAuth2Token(refreshToken);
const {
data: { id, name, profile_image_url },
} = await client.v2.me();
return {
id,
name,
accessToken,
refreshToken: newRefreshToken,
expiresIn,
picture: profile_image_url,
};
}
async generateAuthUrl() {
const client = new TwitterApi({
// clientId: process.env.TWITTER_CLIENT_ID!,
// clientSecret: process.env.TWITTER_CLIENT_SECRET!,
appKey: process.env.X_API_KEY!,
appSecret: process.env.X_API_SECRET!,
});
const { url, oauth_token, oauth_token_secret } =
await client.generateAuthLink(
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 { id, name, profile_image_url_https } = await client.currentUser(
true
);
return {
id: String(id),
accessToken: accessToken + ':' + accessSecret,
name,
refreshToken: '',
expiresIn: 999999999,
picture: profile_image_url_https,
};
}
async post(
id: string,
accessToken: string,
postDetails: PostDetails[]
): 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(
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);
const { data }: { data: { id: string } } = await client.v2.tweet({
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}`,
});
}
return ids.map((p) => ({
...p,
status: 'posted',
}));
}
}