mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-03 02:56:45 -04:00
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 '../src/buildLocaleList.ts';
|
|
import { newDate } from '../src/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);
|
|
}
|
|
}
|
|
})();
|