import { FastifyInstance } from "fastify"; import { z } from "zod"; import prisma from "../lib/prisma.js"; const reactionBody = z.object({ emoji: z.string().min(1).max(8).regex(/^[\p{Extended_Pictographic}\u{FE0F}\u{200D}]+$/u, "Invalid emoji"), }); export default async function reactionRoutes(app: FastifyInstance) { app.post<{ Params: { id: string }; Body: z.infer }>( "/comments/:id/reactions", { preHandler: [app.requireUser], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } }, async (req, reply) => { const comment = await prisma.comment.findUnique({ where: { id: req.params.id } }); if (!comment) { reply.status(404).send({ error: "Comment not found" }); return; } const post = await prisma.post.findUnique({ where: { id: comment.postId }, select: { isThreadLocked: true, board: { select: { isArchived: true } } }, }); if (post?.board?.isArchived) { reply.status(403).send({ error: "Board is archived" }); return; } if (post?.isThreadLocked) { reply.status(403).send({ error: "Thread is locked" }); return; } const { emoji } = reactionBody.parse(req.body); const existing = await prisma.reaction.findUnique({ where: { commentId_userId_emoji: { commentId: comment.id, userId: req.user!.id, emoji, }, }, }); if (existing) { await prisma.reaction.delete({ where: { id: existing.id } }); reply.send({ toggled: false }); } else { const distinctCount = await prisma.reaction.count({ where: { commentId: comment.id, userId: req.user!.id }, }); if (distinctCount >= 10) { reply.status(400).send({ error: "Too many reactions" }); return; } await prisma.reaction.create({ data: { emoji, commentId: comment.id, userId: req.user!.id, }, }); reply.send({ toggled: true }); } } ); app.delete<{ Params: { id: string; emoji: string } }>( "/comments/:id/reactions/:emoji", { preHandler: [app.requireUser], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } }, async (req, reply) => { const emoji = req.params.emoji; if (!emoji || emoji.length > 8 || !/^[\p{Extended_Pictographic}\u{FE0F}\u{200D}]+$/u.test(emoji)) { reply.status(400).send({ error: "Invalid emoji" }); return; } const comment = await prisma.comment.findUnique({ where: { id: req.params.id }, select: { id: true, postId: true } }); if (!comment) { reply.status(404).send({ error: "Comment not found" }); return; } const post = await prisma.post.findUnique({ where: { id: comment.postId }, select: { isThreadLocked: true, board: { select: { isArchived: true } } }, }); if (post?.board?.isArchived) { reply.status(403).send({ error: "Board is archived" }); return; } if (post?.isThreadLocked) { reply.status(403).send({ error: "Thread is locked" }); return; } const deleted = await prisma.reaction.deleteMany({ where: { commentId: req.params.id, userId: req.user!.id, emoji: req.params.emoji, }, }); if (deleted.count === 0) { reply.status(404).send({ error: "Reaction not found" }); return; } reply.send({ ok: true }); } ); }