mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-10 16:05:32 -04:00

the #shared alias used by Nuxt cannot be easily disabled and to prevent breackage with jiti, we make use of it
241 lines
7.3 KiB
TypeScript
241 lines
7.3 KiB
TypeScript
import './setup.ts';
|
|
|
|
import fs from 'fs';
|
|
import type { ReadStream } from 'node:fs';
|
|
|
|
import { AtpAgent, RichText } from '@atproto/api';
|
|
import * as Sentry from '@sentry/node';
|
|
// @ts-expect-error Mastodon doesn't have type declarations
|
|
import Mastodon from 'mastodon';
|
|
import Twitter from 'twitter';
|
|
|
|
import buildLocaleList from '#shared/buildLocaleList.ts';
|
|
import { newDate } from '#shared/helpers.ts';
|
|
|
|
const __dirname = new URL('.', import.meta.url).pathname;
|
|
|
|
const locales = buildLocaleList('_');
|
|
|
|
type BlueskyId = { uri: string; cid: string };
|
|
|
|
type PublisherIds = {
|
|
twitter: string;
|
|
mastodon: string;
|
|
bluesky: BlueskyId;
|
|
};
|
|
type Publishers = {
|
|
[P in keyof PublisherIds]: (
|
|
tweet: string,
|
|
image: ReadStream | null,
|
|
rootId: PublisherIds[P] | undefined,
|
|
previousId: PublisherIds[P] | undefined,
|
|
locale: string
|
|
) => Promise<PublisherIds[P] | undefined>;
|
|
};
|
|
|
|
const publishers: Publishers = {
|
|
async twitter(
|
|
tweet: string,
|
|
_image: ReadStream | null,
|
|
_rootId: string | undefined,
|
|
previousId: string | undefined,
|
|
_locale: string,
|
|
) {
|
|
const client = new Twitter({
|
|
consumer_key: process.env.TWITTER_CALENDAR_CONSUMER_KEY!,
|
|
consumer_secret: process.env.TWITTER_CALENDAR_CONSUMER_SECRET!,
|
|
access_token_key: process.env.TWITTER_CALENDAR_ACCESS_TOKEN_KEY!,
|
|
access_token_secret: process.env.TWITTER_CALENDAR_ACCESS_TOKEN_SECRET!,
|
|
});
|
|
|
|
try {
|
|
const tweetResponse = await client.post('statuses/update', {
|
|
status: tweet,
|
|
...previousId ? { in_reply_to_status_id: previousId } : {},
|
|
});
|
|
console.log(tweetResponse);
|
|
|
|
return tweetResponse.id_str;
|
|
} catch (error) {
|
|
console.error(error);
|
|
Sentry.captureException(error);
|
|
}
|
|
},
|
|
async mastodon(
|
|
tweet: string,
|
|
image: ReadStream | null,
|
|
_rootId: string | undefined,
|
|
previousId: string | undefined,
|
|
locale: string,
|
|
) {
|
|
const client = new Mastodon({
|
|
access_token: process.env.MASTODON_CALENDAR_ACCESS_TOKEN,
|
|
api_url: `https://${process.env.MASTODON_CALENDAR_INSTANCE}/api/v1/`,
|
|
});
|
|
|
|
const mediaIds = [];
|
|
if (image) {
|
|
try {
|
|
const mediaResponse = await client.post('media', {
|
|
file: image,
|
|
description: 'Screenshot of the link above',
|
|
});
|
|
console.log(mediaResponse);
|
|
mediaIds.push(mediaResponse.data.id);
|
|
} catch (error) {
|
|
console.error(error);
|
|
Sentry.captureException(error);
|
|
}
|
|
}
|
|
|
|
const language = tweet.match(/^\[(.*)]/)![1];
|
|
|
|
try {
|
|
const tweetResponse = await client.post('statuses', {
|
|
status: tweet,
|
|
media_ids: mediaIds,
|
|
visibility: 'unlisted',
|
|
...previousId
|
|
? {
|
|
in_reply_to_id: previousId,
|
|
spoiler_text: language,
|
|
}
|
|
: {},
|
|
language: locale,
|
|
});
|
|
console.log(tweetResponse.data);
|
|
return tweetResponse.data.id;
|
|
} catch (error) {
|
|
console.error(error);
|
|
Sentry.captureException(error);
|
|
}
|
|
},
|
|
async bluesky(
|
|
tweet: string,
|
|
image: ReadStream | null,
|
|
rootId: BlueskyId | undefined,
|
|
previousId: BlueskyId | undefined,
|
|
locale: string,
|
|
) {
|
|
const agent = new AtpAgent({ service: 'https://bsky.social' });
|
|
await agent.login({
|
|
identifier: process.env.BLUESKY_CALENDAR_IDENTIFIER!,
|
|
password: process.env.BLUESKY_CALENDAR_PASSWORD!,
|
|
});
|
|
|
|
const media = [];
|
|
if (image) {
|
|
try {
|
|
const uploadResponse = await agent.uploadBlob(image as unknown as Blob, { encoding: 'image/png' });
|
|
console.log(uploadResponse);
|
|
media.push(uploadResponse.data.blob);
|
|
} catch (error) {
|
|
console.error(error);
|
|
Sentry.captureException(error);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const rt = new RichText({
|
|
text: tweet,
|
|
});
|
|
await rt.detectFacets(agent);
|
|
|
|
const postResponse = await agent.post({
|
|
$type: 'app.bsky.feed.post',
|
|
text: rt.text,
|
|
facets: rt.facets,
|
|
embed: {
|
|
$type: 'app.bsky.embed.images',
|
|
images: media.map((blob) => ({ image: blob, alt: '' })),
|
|
},
|
|
...(rootId && previousId)
|
|
? {
|
|
reply: {
|
|
root: rootId,
|
|
parent: previousId,
|
|
},
|
|
}
|
|
: {},
|
|
langs: [locale],
|
|
createdAt: newDate().toISOString(),
|
|
});
|
|
console.log(postResponse);
|
|
return {
|
|
uri: postResponse.uri,
|
|
cid: postResponse.cid,
|
|
};
|
|
} catch (error) {
|
|
console.error(error);
|
|
Sentry.captureException(error);
|
|
}
|
|
},
|
|
};
|
|
|
|
const isPublisher = (value: string): value is keyof PublisherIds => {
|
|
return value in publishers;
|
|
};
|
|
|
|
const tmpDir = `${__dirname}/../cache/tmp`;
|
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
const imageTmpPath = `${tmpDir}/calendar-tmp.png`;
|
|
|
|
const rootPostId: Partial<PublisherIds> = {};
|
|
const previousId: Partial<PublisherIds> = {};
|
|
|
|
const publish = async <P extends keyof PublisherIds>(publisher: P, message: string, locale: string) => {
|
|
console.log(`Publishing: ${publisher}`);
|
|
|
|
let imageStream: ReadStream | null = null;
|
|
try {
|
|
imageStream = fs.createReadStream(imageTmpPath);
|
|
} catch { void 0; }
|
|
|
|
const postId = await publishers[publisher](
|
|
message,
|
|
imageStream,
|
|
rootPostId[publisher],
|
|
previousId[publisher],
|
|
locale,
|
|
);
|
|
console.log(postId);
|
|
if (!rootPostId[publisher]) {
|
|
rootPostId[publisher] = postId;
|
|
}
|
|
previousId[publisher] = postId;
|
|
};
|
|
|
|
(async () => {
|
|
if (process.argv.length !== 4) {
|
|
console.error('Missing parameters. Usage: node server/calendarBot.ts <locales> <publishers>');
|
|
return;
|
|
}
|
|
for (const locale of process.argv[2].split(',')) {
|
|
console.log('------------');
|
|
console.log(locales[locale].name);
|
|
|
|
try {
|
|
const { message, image } = await (await fetch(`${locales[locale].url}/api/calendar/today`)).json();
|
|
console.log('<<<', message, '>>>');
|
|
if (!message) {
|
|
continue;
|
|
}
|
|
|
|
fs.writeFileSync(
|
|
imageTmpPath,
|
|
Buffer.from(await (await fetch(image)).arrayBuffer()),
|
|
{ encoding: 'binary' },
|
|
);
|
|
|
|
for (const publisher of process.argv[3].split(',')) {
|
|
if (isPublisher(publisher)) {
|
|
await publish(publisher, message, locale);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
Sentry.captureException(error);
|
|
}
|
|
}
|
|
})();
|