/** * Serves the comments on dumps. Comments are broadcast over the WebSocket as * they are posted, edited and deleted, to everyone, or to its poster alone for * a private dump, whose comments 404 for anyone else. * * - `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 { 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"; /** * Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware. * An `APIException` thrown by a handler becomes the JSON error response there. */ 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(400, "Invalid comment data"); } const comment = createComment( dump.id, userId, body.body, body.parentId ?? undefined, ); broadcastCommentCreated(comment, dump); 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(400, "Invalid comment data"); } const { comment, dump } = updateComment( ctx.params.commentId, body.body, userId, canModerate, ); broadcastCommentUpdated(comment, dump); 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); // 404s unless the caller may see the dump. 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 dump = deleteComment( ctx.params.commentId, userId, canModerate, ); broadcastCommentDeleted(ctx.params.commentId, dump); const responseBody: APIResponse = { success: true, data: null }; ctx.response.body = responseBody; }); export default router;