/** * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { blockedIps } from "../cache/blockedIps.js"; import { database } from "../db/database.js"; import { announceOnBluesky } from "../modules/announceOnBluesky.js"; import { announceOnDiscord } from "../modules/announceOnDiscord.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"; const oneDay = 24 * 60 * 60 * 1000; /** * Mounts the entry routes for the application. These routes * should not require CORS, as they are used by external services * such as our uptime monitor. * @param server - The Fastify server instance. */ export const announcementRoutes: FastifyPluginAsync = async(server) => { server.get("/announcements", async(_request, reply) => { const announcements = await database.getInstance().announcements.findMany({ orderBy: { createdAt: "desc", }, take: 10, }); return await reply.status(200).type("application/json"). send(announcements.map((announcement) => { return { content: announcement.content, createdAt: announcement.createdAt, title: announcement.title, type: announcement.type, }; })); }); // eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify requires Body instead of body. server.post<{ Body: { title: string; content: string; type: string } }>( "/announcement", // eslint-disable-next-line complexity, max-statements -- This is a complex route, but it is necessary to validate the announcement. async(request, reply) => { const token = request.headers.authorization; if (token === undefined || token !== process.env.ANNOUNCEMENT_TOKEN) { blockedIps.push({ ip: getIpFromRequest(request), ttl: new Date(Date.now() + oneDay), }); return await reply.status(401).send({ error: // eslint-disable-next-line stylistic/max-len -- Big boi string. "This endpoint requires a special auth token. If you believe you should have access, please contact Naomi. To protect our services, your IP has been blocked from all routes for 24 hours.", }); } const { title, content, type } = request.body; if ( typeof title !== "string" || typeof content !== "string" || typeof type !== "string" || title.length === 0 || content.length === 0 || type.length === 0 ) { return await reply.status(400).send({ error: "Missing required fields.", }); } if (title.length < 20) { return await reply.status(400).send({ error: // eslint-disable-next-line stylistic/max-len -- Big boi string. "Title must be at least 20 characters long so that it may be posted on our forum.", }); } if (content.length < 50) { return await reply.status(400).send({ error: // eslint-disable-next-line stylistic/max-len -- Big boi string. "Content must be at least 50 characters long so that it may be posted on our forum.", }); } if (type !== "products" && type !== "community") { return await reply.status(400).send({ error: "Invalid announcement type.", }); } await database.getInstance().announcements.create({ data: { content, title, type, }, }); const discord = await announceOnDiscord(title, content, type); const reddit = await announceOnReddit(title, content, type); const summary = await summarisePost(title, content); if (summary === null) { return await reply.status(201).send({ message: `Announcement processed. Discord: ${discord}, 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}, 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}, Reddit: ${reddit}, Bluesky: ${bluesky}, Twitter: ${twitter}`, }); }, ); };