import { APIErrorCode, APIException, type Comment, } from "../model/interfaces.ts"; import { type SQLOutputValue } from "node:sqlite"; import { commentRowToApi, db, isCommentRow } from "../model/db.ts"; import { notifyMentions } from "./notification-service.ts"; const SELECT_COLS = `c.id, c.dump_id, c.user_id, c.parent_id, c.body, c.created_at, c.updated_at, c.deleted, u.username as author_username, u.avatar_mime as author_avatar_mime`; function fetchComment(commentId: string): Comment { const row = db.prepare( `SELECT ${SELECT_COLS} FROM comments c JOIN users u ON c.user_id = u.id WHERE c.id = ?;`, ).get(commentId); if (!row || !isCommentRow(row as Record)) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "Comment not found"); } if (!isCommentRow(row)) { throw new APIException( APIErrorCode.SERVER_ERROR, 500, "Malformed comment data", ); } return commentRowToApi(row); } export function getComments(dumpId: string): Comment[] { const rows = db.prepare( `SELECT ${SELECT_COLS} FROM comments c JOIN users u ON c.user_id = u.id WHERE c.dump_id = ? ORDER BY c.created_at ASC;`, ).all(dumpId); if (!rows.every(isCommentRow)) { throw new APIException( APIErrorCode.SERVER_ERROR, 500, "Malformed comment data", ); } return rows.map(commentRowToApi); } export function createComment( dumpId: string, userId: string, body: string, parentId?: string, ): Comment { const id = crypto.randomUUID(); const createdAt = new Date(); db.prepare( `INSERT INTO comments (id, dump_id, user_id, parent_id, body, created_at) VALUES (?, ?, ?, ?, ?, ?);`, ).run( id, dumpId, userId, parentId ?? null, body.trim(), createdAt.toISOString(), ); const comment = fetchComment(id); const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get( dumpId, ) as { title: string } | undefined; notifyMentions(userId, body, "comment", id, dumpRow?.title ?? "", dumpId); return comment; } export function updateComment( commentId: string, body: string, requestingUserId: string, isAdmin: boolean, ): { comment: Comment; dumpId: string; isPrivate: boolean } { const row = db.prepare( `SELECT c.dump_id, d.is_private FROM comments c JOIN dumps d ON c.dump_id = d.id WHERE c.id = ?;`, ).get(commentId) as { dump_id: string; is_private: number } | undefined; if (!row) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "Comment not found"); } const existing = fetchComment(commentId); if (existing.deleted) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, "Cannot edit a deleted comment", ); } if (existing.userId !== requestingUserId && !isAdmin) { throw new APIException( APIErrorCode.UNAUTHORIZED, 401, "Not authorized to edit this comment", ); } const now = new Date().toISOString(); db.prepare(`UPDATE comments SET body = ?, updated_at = ? WHERE id = ?;`).run( body.trim(), now, commentId, ); const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get( row.dump_id, ) as { title: string } | undefined; notifyMentions( requestingUserId, body, "comment", commentId, dumpRow?.title ?? "", row.dump_id, ); return { comment: fetchComment(commentId), dumpId: row.dump_id, isPrivate: Boolean(row.is_private), }; } export function deleteComment( commentId: string, requestingUserId: string, isAdmin: boolean, ): { dumpId: string; isPrivate: boolean } { const row = db.prepare( `SELECT c.dump_id, d.is_private FROM comments c JOIN dumps d ON c.dump_id = d.id WHERE c.id = ?;`, ).get(commentId) as { dump_id: string; is_private: number } | undefined; if (!row) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "Comment not found"); } const comment = fetchComment(commentId); if (comment.userId !== requestingUserId && !isAdmin) { throw new APIException( APIErrorCode.UNAUTHORIZED, 401, "Not authorized to delete this comment", ); } db.prepare(`UPDATE comments SET deleted = 1, body = '' WHERE id = ?;`).run( commentId, ); return { dumpId: row.dump_id, isPrivate: Boolean(row.is_private) }; }