From 256481f2792afd6b000a9d4b2b9938f4abf94561 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Sat, 19 Jul 2025 17:08:46 -0700 Subject: [PATCH] feat: auto-post to reddit --- server/prod.env | 6 +- server/src/modules/announceOnReddit.ts | 96 ++++++++++++++++++++++++++ server/src/routes/announcement.ts | 4 +- 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 server/src/modules/announceOnReddit.ts diff --git a/server/prod.env b/server/prod.env index 547fe45..839dff8 100644 --- a/server/prod.env +++ b/server/prod.env @@ -2,4 +2,8 @@ LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth" MONGO_URI="op://Environment Variables - Naomi/Hikari/mongo_uri" DISCORD_TOKEN="op://Environment Variables - Naomi/Hikari/discord_token" FORUM_API_KEY="op://Environment Variables - Naomi/Hikari/discourse_key" -ANNOUNCEMENT_TOKEN="op://Environment Variables - Naomi/Hikari/announcement_token" \ No newline at end of file +ANNOUNCEMENT_TOKEN="op://Environment Variables - Naomi/Hikari/announcement_token" +REDDIT_CLIENT_ID="op://Environment Variables - Naomi/Hikari/reddit_client_id" +REDDIT_CLIENT_SECRET="op://Environment Variables - Naomi/Hikari/reddit_client_secret" +REDDIT_PASSWORD="op://Environment Variables - Naomi/Hikari/reddit_password" +REDDIT_USERNAME="op://Environment Variables - Naomi/Hikari/reddit_username" diff --git a/server/src/modules/announceOnReddit.ts b/server/src/modules/announceOnReddit.ts new file mode 100644 index 0000000..f38423d --- /dev/null +++ b/server/src/modules/announceOnReddit.ts @@ -0,0 +1,96 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +/* eslint-disable @typescript-eslint/naming-convention -- we are making raw API calls. */ +/* eslint-disable max-lines-per-function -- Big logic here. */ + +const flairIds = { + community: "7a01a5a6-0f29-11ef-a0c4-c6fb085f7c8f", + products: "335e57b6-083f-11ef-96b3-0202af2d9d99", +}; + +/** + * Posts an announcement to a specific subreddit as a self-post. + * @param title - The title of the announcement. + * @param content - The main body of the announcement. + * @param type - Whether the announcement is for a product or community. + * @returns A message indicating the success or failure of the operation. + */ +export const announceOnReddit = async( + title: string, + content: string, + type: "products" | "community", +): Promise => { + if ( + process.env.REDDIT_CLIENT_ID === undefined + || process.env.REDDIT_CLIENT_SECRET === undefined + || process.env.REDDIT_USERNAME === undefined + || process.env.REDDIT_PASSWORD === undefined + ) { + return "Reddit credentials are not set."; + } + const tokenResponse = await fetch( + "https://www.reddit.com/api/v1/access_token", + { + body: new URLSearchParams({ + grant_type: "password", + password: process.env.REDDIT_PASSWORD, + username: process.env.REDDIT_USERNAME, + }), + headers: { + "Authorization": `Basic ${Buffer.from( + `${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`, + ).toString("base64")}`, + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "HikariBot/1.0 by nhcarrigan", + }, + method: "POST", + }, + ); + + if (tokenResponse.status !== 200) { + return `Failed to obtain Reddit access token. Status: ${tokenResponse.status.toString()} ${tokenResponse.statusText}`; + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Fetch does not accept generic. + const tokenData = (await tokenResponse.json()) as { access_token?: string }; + + if (tokenData.access_token === undefined) { + return `Failed to obtain Reddit access token. ${JSON.stringify(tokenData)}`; + } + + const redditPost = await fetch("https://oauth.reddit.com/api/submit", { + body: new URLSearchParams({ + api_type: "json", + flair_id: flairIds[type], + flair_text: type, + kind: "self", + sr: "nhcarrigan", + text: content, + title: title, + }), + headers: { + "Authorization": `bearer ${tokenData.access_token}`, + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "HikariBot/1.0 by nhcarrigan", + }, + method: "POST", + }); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Fetch does not accept generic. + const redditData = (await redditPost.json()) as { + json: { + errors: Array; + }; + }; + + if (redditData.json.errors.length > 0) { + return `Failed to post to Reddit: ${JSON.stringify( + redditData.json.errors, + )}`; + } + + return "Successfully posted announcement to Reddit~! ✨"; +}; diff --git a/server/src/routes/announcement.ts b/server/src/routes/announcement.ts index 9b71efe..7619b24 100644 --- a/server/src/routes/announcement.ts +++ b/server/src/routes/announcement.ts @@ -8,6 +8,7 @@ import { blockedIps } from "../cache/blockedIps.js"; import { database } from "../db/database.js"; import { announceOnDiscord } from "../modules/announceOnDiscord.js"; import { announceOnForum } from "../modules/announceOnForum.js"; +import { announceOnReddit } from "../modules/announceOnReddit.js"; import { getIpFromRequest } from "../modules/getIpFromRequest.js"; import type { FastifyPluginAsync } from "fastify"; @@ -102,8 +103,9 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => { const discord = await announceOnDiscord(title, content, type); const forum = await announceOnForum(title, content, type); + const reddit = await announceOnReddit(title, content, type); return await reply.status(201).send({ - message: `Announcement processed. Discord: ${discord}, Forum: ${forum}`, + message: `Announcement processed. Discord: ${discord}, Forum: ${forum}, Reddit: ${reddit}`, }); }, );