From b982f78e22768a95145affd97d0019ead0db0516 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Sat, 19 Jul 2025 18:08:51 -0700 Subject: [PATCH] feat: post to twitter --- pnpm-lock.yaml | 8 +++++ server/package.json | 3 +- server/prod.env | 7 ++++- server/src/modules/announceOnTwitter.ts | 41 +++++++++++++++++++++++++ server/src/modules/summarisePost.ts | 3 +- server/src/routes/announcement.ts | 8 +++-- 6 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 server/src/modules/announceOnTwitter.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3cbd3d..57e4275 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: gray-matter: specifier: 4.0.3 version: 4.0.3 + twitter-api-v2: + specifier: 1.24.0 + version: 1.24.0 devDependencies: '@types/node': specifier: 24.0.10 @@ -4589,6 +4592,9 @@ packages: resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==} hasBin: true + twitter-api-v2@1.24.0: + resolution: {integrity: sha512-RDEiuNwnFirvf4c5f1sysgg0rfMQgekXgKt+/UdbNu+Bs5bJ1VbXkqKzdd2a2lPMlDVDbdGUoe2pOd4n25fFVQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -10097,6 +10103,8 @@ snapshots: turbo-windows-64: 2.5.4 turbo-windows-arm64: 2.5.4 + twitter-api-v2@1.24.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/server/package.json b/server/package.json index cd90065..2776a66 100644 --- a/server/package.json +++ b/server/package.json @@ -22,7 +22,8 @@ "@nhcarrigan/logger": "1.0.0", "@prisma/client": "6.11.1", "fastify": "5.4.0", - "gray-matter": "4.0.3" + "gray-matter": "4.0.3", + "twitter-api-v2": "1.24.0" }, "devDependencies": { "@types/node": "24.0.10", diff --git a/server/prod.env b/server/prod.env index 8b59189..3e37e1c 100644 --- a/server/prod.env +++ b/server/prod.env @@ -8,4 +8,9 @@ REDDIT_CLIENT_SECRET="op://Environment Variables - Naomi/Hikari/reddit_client_se REDDIT_PASSWORD="op://Environment Variables - Naomi/Hikari/reddit_password" REDDIT_USERNAME="op://Environment Variables - Naomi/Hikari/reddit_username" BSKY_APP_PASSWORD="op://Environment Variables - Naomi/Hikari/bsky_password" -ANTHROPIC_KEY="op://Environment Variables - Naomi/Hikari/anthropic_key" \ No newline at end of file +ANTHROPIC_KEY="op://Environment Variables - Naomi/Hikari/anthropic_key" +TWITTER_TOKEN="op://Environment Variables - Naomi/Hikari/twitter_access_token" +TWITTER_SECRET="op://Environment Variables - Naomi/Hikari/twitter_access_secret" +TWITTER_CONSUMER_KEY="op://Environment Variables - Naomi/Hikari/twitter_consumer_key" +TWITTER_CONSUMER_SECRET="op://Environment Variables - Naomi/Hikari/twitter_consumer_secret" +TWITTER_BEARER_TOKEN="op://Environment Variables - Naomi/Hikari/twitter_bearer_token" \ No newline at end of file diff --git a/server/src/modules/announceOnTwitter.ts b/server/src/modules/announceOnTwitter.ts new file mode 100644 index 0000000..2473a0d --- /dev/null +++ b/server/src/modules/announceOnTwitter.ts @@ -0,0 +1,41 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { TwitterApi } from "twitter-api-v2"; + +/** + * Forwards an announcement to our Twitter account. + * @param content - The main body of the announcement. + * @returns A message indicating the success or failure of the operation. + */ +export const announceOnTwitter = async(content: string): Promise => { + if ( + process.env.TWITTER_CONSUMER_KEY === undefined + || process.env.TWITTER_CONSUMER_SECRET === undefined + || process.env.TWITTER_TOKEN === undefined + || process.env.TWITTER_SECRET === undefined + ) { + return "Twitter credentials are not set."; + } + const twitterClient = new TwitterApi({ + accessSecret: process.env.TWITTER_SECRET, + accessToken: process.env.TWITTER_TOKEN, + appKey: process.env.TWITTER_CONSUMER_KEY, + appSecret: process.env.TWITTER_CONSUMER_SECRET, + }); + + const result = await twitterClient.v2. + tweet(content). + catch((error: unknown) => { + return error instanceof Error + ? error.message + : String(error); + }); + if (typeof result === "string") { + return `Failed to send message to Twitter. ${result}`; + } + return "Successfully sent message to Twitter."; +}; diff --git a/server/src/modules/summarisePost.ts b/server/src/modules/summarisePost.ts index a8b0487..b55afa3 100644 --- a/server/src/modules/summarisePost.ts +++ b/server/src/modules/summarisePost.ts @@ -4,11 +4,12 @@ * @author Naomi Carrigan */ +// eslint-disable-next-line @typescript-eslint/naming-convention -- 'Tis a class. import Anthropic from "@anthropic-ai/sdk"; /** * Summarises an announcement using AI, to condense the content for platforms like Bluesky and Twitter. - * @param title + * @param title - The title of the announcement. * @param content - The main body of the announcement. * @returns A message indicating the success or failure of the operation. */ diff --git a/server/src/routes/announcement.ts b/server/src/routes/announcement.ts index 2fa8f7b..4f8ea56 100644 --- a/server/src/routes/announcement.ts +++ b/server/src/routes/announcement.ts @@ -10,6 +10,7 @@ import { announceOnBluesky } from "../modules/announceOnBluesky.js"; import { announceOnDiscord } from "../modules/announceOnDiscord.js"; import { announceOnForum } from "../modules/announceOnForum.js"; import { announceOnReddit } from "../modules/announceOnReddit.js"; +import { announceOnTwitter } from "../modules/announceOnTwitter.js"; import { getIpFromRequest } from "../modules/getIpFromRequest.js"; import { summarisePost } from "../modules/summarisePost.js"; import type { FastifyPluginAsync } from "fastify"; @@ -109,18 +110,19 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => { const summary = await summarisePost(title, content); if (summary === null) { return await reply.status(201).send({ - message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: Skipped (AI summarisation failed).`, + message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: Skipped (AI summarisation failed), Twitter: Skipped (AI summarisation failed).`, }); } if (summary.length > 280) { return await reply.status(201).send({ - message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: Skipped (AI summary too long).`, + message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: Skipped (AI summary too long), Twitter: Skipped (AI summary too long).`, }); } const bluesky = await announceOnBluesky(summary); + const twitter = await announceOnTwitter(summary); return await reply.status(201).send({ - message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: ${bluesky}`, + message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}, Bluesky: ${bluesky}, Twitter: ${twitter}`, }); }, );