generated from nhcarrigan/template
128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
/**
|
|
* @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}`,
|
|
});
|
|
},
|
|
);
|
|
};
|