/** * The comment routes. A private dump's comments exist only for its poster, and * changes to them aren't broadcast. * * - `GET /api/dumps/:dumpId/comments` — optional sign-in. Answers every comment * on a dump, oldest first. * - `POST /api/dumps/:dumpId/comments` — signed in. Posts a comment, replying * to `parentId` if given. * - `PATCH /api/comments/:commentId` — signed in, as its author or with * `comment:moderate`. Edits a comment. * - `GET /api/comments/:commentId/likers?page=&limit=` — optional sign-in. * Answers a page of the users who like a comment. * - `DELETE /api/comments/:commentId` — signed in, as its author or with * `comment:moderate`. Deletes a comment, leaving its place in the thread. * * @module */ import { Router } from "@oak/oak"; import { APIErrorCode, type APIResponse, type PaginatedData, } from "../../shared/api.ts"; import { APIException } from "../lib/api-exception.ts"; import type { Comment } from "../../shared/comment.ts"; import { isCreateCommentRequest, isUpdateCommentRequest, } from "../validators/comment.ts"; import type { PublicUser } from "../../shared/user.ts"; import { toPublicUser } from "../model/user.ts"; import { authMiddleware } from "../middleware/auth.ts"; import { can } from "../lib/permissions.ts"; import { parseOptionalAuth } from "../lib/auth.ts"; import { parsePagination } from "../lib/pagination.ts"; import { createComment, deleteComment, getCommentDumpId, getComments, updateComment, } from "../services/comment-service.ts"; import { getCommentLikers } from "../services/comment-like-service.ts"; import { getDump } from "../services/dump-service.ts"; import { broadcastCommentCreated, broadcastCommentDeleted, broadcastCommentUpdated, } from "../services/ws-service.ts"; /** The router serving the endpoints the module doc lists. */ const router: Router = new Router({ prefix: "/api" }); router.get("/dumps/:dumpId/comments", async (ctx) => { const requestingUserId = await parseOptionalAuth(ctx) ?? undefined; const dump = getDump(ctx.params.dumpId, requestingUserId); const comments = getComments(dump.id); const responseBody: APIResponse = { success: true, data: comments, }; ctx.response.body = responseBody; }); router.post("/dumps/:dumpId/comments", authMiddleware, async (ctx) => { const userId = ctx.state.user.userId; const dump = getDump(ctx.params.dumpId, userId); const body: unknown = await ctx.request.body.json(); if (!isCreateCommentRequest(body)) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, "Invalid comment data", ); } const comment = createComment( dump.id, userId, body.body, body.parentId ?? undefined, ); if (!dump.isPrivate) broadcastCommentCreated(comment); const responseBody: APIResponse = { success: true, data: comment }; ctx.response.status = 201; ctx.response.body = responseBody; }); router.patch("/comments/:commentId", authMiddleware, async (ctx) => { const userId = ctx.state.user.userId; const canModerate = can(ctx.state.user, "comment:moderate"); const body: unknown = await ctx.request.body.json(); if (!isUpdateCommentRequest(body)) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, "Invalid comment data", ); } const { comment, isPrivate } = updateComment( ctx.params.commentId, body.body, userId, canModerate, ); if (!isPrivate) broadcastCommentUpdated(comment); const responseBody: APIResponse = { success: true, data: comment }; ctx.response.body = responseBody; }); router.get("/comments/:commentId/likers", async (ctx) => { const requestingUserId = await parseOptionalAuth(ctx) ?? undefined; const dumpId = getCommentDumpId(ctx.params.commentId); getDump(dumpId, requestingUserId); // enforces privacy, 404s if not visible const { page, limit } = parsePagination(ctx.request.url.searchParams); const { items, hasMore } = getCommentLikers( ctx.params.commentId, page, limit, ); const responseBody: APIResponse> = { success: true, data: { items: items.map(toPublicUser), hasMore, }, }; ctx.response.body = responseBody; }); router.delete("/comments/:commentId", authMiddleware, (ctx) => { const userId = ctx.state.user.userId; const canModerate = can(ctx.state.user, "comment:moderate"); const { dumpId, isPrivate } = deleteComment( ctx.params.commentId, userId, canModerate, ); if (!isPrivate) broadcastCommentDeleted(ctx.params.commentId, dumpId); const responseBody: APIResponse = { success: true, data: null }; ctx.response.body = responseBody; }); export default router;