115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
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<typeof reactionBody> }>(
|
|
"/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 });
|
|
}
|
|
);
|
|
}
|