import { APIErrorCode, APIException, type ChatMessage, } from "../model/interfaces.ts"; import { chatMessageRowToApi, db, isChatMessageRow } from "../model/db.ts"; export const MAX_CHAT_LENGTH = 2000; const DEFAULT_LIMIT = 50; const MAX_LIMIT = 100; const SELECT_COLS = `m.id, m.user_id, m.body, m.created_at, m.updated_at, u.username as author_username, u.avatar_mime as author_avatar_mime`; function fetchMessage(id: string): ChatMessage { const row = db.prepare( `SELECT ${SELECT_COLS} FROM chat_messages m JOIN users u ON m.user_id = u.id WHERE m.id = ?;`, ).get(id); if (!row || !isChatMessageRow(row)) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "Message not found"); } return chatMessageRowToApi(row); } export function createChatMessage(userId: string, body: string): ChatMessage { const trimmed = body.trim(); if (!trimmed) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, "Message cannot be empty", ); } if (trimmed.length > MAX_CHAT_LENGTH) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, `Message exceeds ${MAX_CHAT_LENGTH} characters`, ); } const id = crypto.randomUUID(); const createdAt = new Date().toISOString(); db.prepare( `INSERT INTO chat_messages (id, user_id, body, created_at) VALUES (?, ?, ?, ?);`, ).run(id, userId, trimmed, createdAt); return fetchMessage(id); } function getMessageOwner(id: string): string { const row = db.prepare(`SELECT user_id FROM chat_messages WHERE id = ?;`).get( id, ) as { user_id: string } | undefined; if (!row) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "Message not found"); } return row.user_id; } // Owners may edit their own messages; moderators may edit any. export function updateChatMessage( id: string, body: string, requestingUserId: string, canModerate: boolean, ): ChatMessage { const ownerId = getMessageOwner(id); if (ownerId !== requestingUserId && !canModerate) { throw new APIException( APIErrorCode.UNAUTHORIZED, 401, "Not authorized to edit this message", ); } const trimmed = body.trim(); if (!trimmed) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, "Message cannot be empty", ); } if (trimmed.length > MAX_CHAT_LENGTH) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, `Message exceeds ${MAX_CHAT_LENGTH} characters`, ); } db.prepare(`UPDATE chat_messages SET body = ?, updated_at = ? WHERE id = ?;`) .run(trimmed, new Date().toISOString(), id); return fetchMessage(id); } // Deletion is a moderation action only (owners can edit but not delete). export function deleteChatMessage(id: string, canModerate: boolean): void { if (!canModerate) { throw new APIException( APIErrorCode.UNAUTHORIZED, 401, "Not authorized to delete this message", ); } getMessageOwner(id); // 404s if the message doesn't exist db.prepare(`DELETE FROM chat_messages WHERE id = ?;`).run(id); } /** * Returns up to `limit` most recent messages in ascending (display) order. * When `before` (a message created_at ISO string) is given, returns the page * of older messages immediately preceding it — used for "load older" history. */ export function getRecentChatMessages( limit = DEFAULT_LIMIT, before?: string, ): ChatMessage[] { const capped = Math.min(Math.max(limit, 1), MAX_LIMIT); const rows = before ? db.prepare( `SELECT ${SELECT_COLS} FROM chat_messages m JOIN users u ON m.user_id = u.id WHERE m.created_at < ? ORDER BY m.created_at DESC LIMIT ?;`, ).all(before, capped) : db.prepare( `SELECT ${SELECT_COLS} FROM chat_messages m JOIN users u ON m.user_id = u.id ORDER BY m.created_at DESC LIMIT ?;`, ).all(capped); if (!rows.every(isChatMessageRow)) { throw new APIException( APIErrorCode.SERVER_ERROR, 500, "Malformed chat message data", ); } // Newest-first from SQL; reverse to ascending for display. return rows.map(chatMessageRowToApi).reverse(); }