import { avatarUrl } from '$lib/avatar'; import { getSessionUser } from '$lib/server/auth/session'; import { getDatabase } from '$lib/server/database'; import { normalizeUserMarkdown, renderUserMarkdown } from '$lib/markdown'; import { createNotification } from '$lib/server/notifications'; import { getCommunityModerationPolicy, moderateCommunityText, createUserContentReport, recordCommunityModerationScan } from '$lib/server/community-moderation'; import { error, fail, redirect } from '@sveltejs/kit'; import { randomUUID } from 'node:crypto'; import { queueCurrentAvatarReview } from '$lib/server/avatars'; import { agentProfileHref } from '$lib/agents'; import { resolveAgentProfile } from '$lib/server/agent-profiles'; import { orderProfileCommentThread, profileCommentNotificationRecipient } from '$lib/server/profile-comment-thread'; import { notifyProfileCommentFollowers, profileCommentConversation } from '$lib/server/profile-comment-follows'; import { loadAccountControlData } from '$lib/server/account-control-data'; import { saveProfileBio, updateProfileVisibility } from '$lib/server/profile-settings'; import type { Actions, PageServerLoad } from './$types'; const COMMENT_MAX_CHARACTERS = 2_000; const RAPID_COMMENT_SECONDS = 10; const unavailable = (row: Record | null | undefined) => { const banned = typeof row?.banned_until === 'string' && new Date(row.banned_until) > new Date(); return !row || Boolean(row.deleted_at || row.blocked_at || banned); }; const commentHref = ( targetType: string, slug: unknown, targetId: string, targetPublicSlug: unknown, commentId: string ) => { if (targetType === 'profile') { return agentProfileHref({ id: targetId, publicSlug: typeof targetPublicSlug === 'string' ? targetPublicSlug : null }, `profile-comment-${commentId}`); } return `/builds/${encodeURIComponent(String(slug ?? ''))}#comment-${encodeURIComponent(commentId)}`; }; export const load: PageServerLoad = async ({ params, cookies, url }) => { const database = await getDatabase(); const viewer = await getSessionUser(cookies); const resolved = await resolveAgentProfile(database, params.id); if (!resolved) error(404, 'Agent profile not found'); const agentId = resolved.id; const agent = await (await database.prepare(`SELECT users.id, COALESCE(profile.display_name,users.username) AS display_name, profile.public_slug,profile.bio_markdown,profile.is_public,profile.bio_updated_at, COALESCE(moderation.bio_hidden,0) AS bio_hidden,moderation.reason AS moderation_reason, avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at, status.blocked_at,status.banned_until,deleted.deleted_at FROM users LEFT JOIN user_profiles profile ON profile.user_id=users.id LEFT JOIN profile_moderation moderation ON moderation.user_id=users.id LEFT JOIN user_avatars avatar ON avatar.user_id=users.id LEFT JOIN user_status status ON status.user_id=users.id LEFT JOIN deleted_accounts deleted ON deleted.user_id=users.id WHERE users.id=?`)).get(agentId); const isSelf = viewer?.id === agentId; if ( unavailable(agent) || (!Boolean(agent?.is_public) && !isSelf) ) error(404, 'Agent profile not found'); if (params.id !== String(agent.public_slug)) { redirect(308, agentProfileHref({ id: agentId, publicSlug: String(agent.public_slug) })); } const [pendingBioRevision, accountControl] = isSelf ? await Promise.all([ (await database.prepare(`SELECT revision.body_markdown,revision.created_at,scan.reason FROM moderated_content_revisions revision JOIN content_moderation_scans scan ON scan.revision_id=revision.id LEFT JOIN content_moderation_reviews review ON review.scan_id=scan.id WHERE revision.content_type='profile_bio' AND revision.content_id=? AND scan.status IN ('review','priority_review','error') AND review.id IS NULL ORDER BY revision.created_at DESC LIMIT 1`)).get(agentId), loadAccountControlData(database, agentId, url.searchParams.get('control')) ]) : [null, null]; const [roleRows, buildRows, guideRows, authoredCommentRows, profileCommentRows, authoredCount, profileCommentCount, contentCounts] = await Promise.all([ (await database.prepare(`SELECT roles.name FROM user_roles JOIN roles ON roles.id=user_roles.role_id WHERE user_roles.user_id=? ORDER BY roles.name COLLATE NOCASE,roles.id`)).all(agentId), (await database.prepare(`SELECT id,slug,title,COALESCE(summary,'') AS summary,updated_at FROM builds WHERE owner_id=? AND visibility='public' AND (guide_body IS NULL OR length(trim(guide_body))=0) ORDER BY updated_at DESC LIMIT 60`)).all(agentId), (await database.prepare(`SELECT id,slug,title,COALESCE(summary,'') AS summary,updated_at FROM builds WHERE owner_id=? AND visibility='public' AND guide_body IS NOT NULL AND length(trim(guide_body))>0 ORDER BY updated_at DESC LIMIT 60`)).all(agentId), (await database.prepare(`SELECT comments.id,comments.created_at, thread.target_type,thread.target_id,builds.title AS build_title,builds.slug, COALESCE(target_profile.display_name,target_user.username) AS target_display_name, target_profile.public_slug AS target_public_slug FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN builds ON builds.id=thread.target_id AND thread.target_type IN ('build','guide') LEFT JOIN users target_user ON target_user.id=thread.target_id AND thread.target_type='profile' LEFT JOIN user_profiles target_profile ON target_profile.user_id=target_user.id LEFT JOIN deleted_accounts target_deleted ON target_deleted.user_id=target_user.id LEFT JOIN user_status target_status ON target_status.user_id=target_user.id WHERE comments.author_id=? AND comments.status='visible' AND ( (thread.target_type IN ('build','guide') AND builds.visibility='public') OR (thread.target_type='profile' AND COALESCE(target_profile.is_public,0)=1 AND target_deleted.user_id IS NULL AND target_status.blocked_at IS NULL AND (target_status.banned_until IS NULL OR target_status.banned_until<=?)) ) ORDER BY comments.created_at DESC LIMIT 50`)).all(agentId, new Date().toISOString()), (await database.prepare(`SELECT comments.id,comments.author_id,comments.parent_id,comments.body_markdown, comments.status,comments.created_at, COALESCE(profile.display_name,users.username,'Deleted Agent') AS display_name, profile.public_slug, avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at FROM comment_threads thread JOIN comments ON comments.thread_id=thread.id AND comments.status='visible' LEFT JOIN users ON users.id=comments.author_id LEFT JOIN user_profiles profile ON profile.user_id=users.id LEFT JOIN user_avatars avatar ON avatar.user_id=users.id WHERE thread.target_type='profile' AND thread.target_id=? ORDER BY comments.created_at DESC LIMIT 100`)).all(agentId), (await database.prepare(`SELECT COUNT(*) AS count FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN builds ON builds.id=thread.target_id AND thread.target_type IN ('build','guide') LEFT JOIN user_profiles target_profile ON target_profile.user_id=thread.target_id AND thread.target_type='profile' LEFT JOIN deleted_accounts target_deleted ON target_deleted.user_id=thread.target_id LEFT JOIN user_status target_status ON target_status.user_id=thread.target_id WHERE comments.author_id=? AND comments.status='visible' AND ( (thread.target_type IN ('build','guide') AND builds.visibility='public') OR (thread.target_type='profile' AND COALESCE(target_profile.is_public,0)=1 AND target_deleted.user_id IS NULL AND target_status.blocked_at IS NULL AND (target_status.banned_until IS NULL OR target_status.banned_until<=?)) )`)).get(agentId, new Date().toISOString()), (await database.prepare(`SELECT COUNT(*) AS count FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id WHERE thread.target_type='profile' AND thread.target_id=? AND comments.status='visible'`)) .get(agentId), (await database.prepare(`SELECT SUM(CASE WHEN guide_body IS NULL OR length(trim(guide_body))=0 THEN 1 ELSE 0 END) AS builds, SUM(CASE WHEN guide_body IS NOT NULL AND length(trim(guide_body))>0 THEN 1 ELSE 0 END) AS guides FROM builds WHERE owner_id=? AND visibility='public'`)).get(agentId) ]); const loadedCommentIds = new Set(profileCommentRows.map((comment) => String(comment.id))); let missingParentIds = [...new Set(profileCommentRows .map((comment) => comment.parent_id ? String(comment.parent_id) : '') .filter((parentId) => parentId && !loadedCommentIds.has(parentId)))]; for (let level = 0; missingParentIds.length && level < 100; level += 1) { const placeholders = missingParentIds.map(() => '?').join(','); const ancestors = await (await database.prepare(`SELECT comments.id,comments.author_id, comments.parent_id,comments.body_markdown,comments.status,comments.created_at, COALESCE(profile.display_name,users.username,'Deleted Agent') AS display_name, profile.public_slug, avatar.source AS avatar_source,avatar.value AS avatar_value, avatar.updated_at AS avatar_updated_at FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id LEFT JOIN users ON users.id=comments.author_id LEFT JOIN user_profiles profile ON profile.user_id=users.id LEFT JOIN user_avatars avatar ON avatar.user_id=users.id WHERE comments.id IN (${placeholders}) AND comments.status IN ('visible','removed') AND thread.target_type='profile' AND thread.target_id=?`)) .all(...missingParentIds, agentId); if (!ancestors.length) break; profileCommentRows.push(...ancestors); for (const ancestor of ancestors) loadedCommentIds.add(String(ancestor.id)); missingParentIds = [...new Set(ancestors .map((ancestor) => ancestor.parent_id ? String(ancestor.parent_id) : '') .filter((parentId) => parentId && !loadedCommentIds.has(parentId)))]; } const commentAuthorIds = [...new Set(profileCommentRows .map((comment) => comment.author_id ? String(comment.author_id) : '') .filter(Boolean))]; const commentRoleRows = commentAuthorIds.length ? await (await database.prepare(`SELECT user_roles.user_id,roles.name FROM user_roles JOIN roles ON roles.id=user_roles.role_id WHERE user_roles.user_id IN (${commentAuthorIds.map(() => '?').join(',')}) ORDER BY roles.name COLLATE NOCASE,roles.id`)).all(...commentAuthorIds) : []; const rolesByCommentAuthor = new Map(); for (const row of commentRoleRows) { const userId = String(row.user_id); const roles = rolesByCommentAuthor.get(userId) ?? []; roles.push(String(row.name)); rolesByCommentAuthor.set(userId, roles); } const threadedProfileComments = orderProfileCommentThread(profileCommentRows.map((comment) => { const status = String(comment.status) === 'removed' ? 'removed' as const : 'visible' as const; return { id: String(comment.id), parentId: comment.parent_id ? String(comment.parent_id) : null, status, bodyHtml: status === 'visible' ? renderUserMarkdown(comment.body_markdown) : '', createdAt: String(comment.created_at), author: { id: String(comment.author_id), displayName: String(comment.display_name), publicSlug: comment.public_slug ? String(comment.public_slug) : null, avatarUrl: avatarUrl( comment.avatar_source, comment.avatar_value, String(comment.author_id), comment.avatar_updated_at ), roles: rolesByCommentAuthor.get(String(comment.author_id)) ?? [] }, canDelete: status === 'visible' && viewer?.id === String(comment.author_id), canReport: status === 'visible' && Boolean(viewer) && viewer?.id !== String(comment.author_id) }; })); const followedRootIds = viewer && threadedProfileComments.length ? new Set((await (await database.prepare(`SELECT root_comment_id FROM comment_thread_follows WHERE user_id=? AND root_comment_id IN (${[...new Set(threadedProfileComments.map((comment) => comment.rootId))] .map(() => '?').join(',')})`)).all( viewer.id, ...[...new Set(threadedProfileComments.map((comment) => comment.rootId))] )).map((row) => String(row.root_comment_id))) : new Set(); return { agent: { id: String(agent.id), displayName: String(agent.display_name), publicSlug: String(agent.public_slug), avatarUrl: avatarUrl( agent.avatar_source, agent.avatar_value, String(agent.id), agent.avatar_updated_at ), bioHtml: agent.bio_hidden ? '' : renderUserMarkdown(agent.bio_markdown), bioMarkdown: isSelf ? String(pendingBioRevision?.body_markdown ?? agent.bio_markdown ?? '') : '', isPublic: Boolean(agent.is_public), bioHidden: Boolean(agent.bio_hidden), bioModerationReason: isSelf && agent.moderation_reason ? String(agent.moderation_reason) : null, bioReviewPending: Boolean(pendingBioRevision), bioReviewReason: pendingBioRevision?.reason ? String(pendingBioRevision.reason) : null, roles: roleRows.map((role) => String(role.name)) }, counts: { builds: Number(contentCounts?.builds ?? 0), guides: Number(contentCounts?.guides ?? 0), authoredComments: Number(authoredCount?.count ?? 0), profileComments: Number(profileCommentCount?.count ?? 0) }, builds: buildRows.map((build) => ({ id: String(build.id), slug: String(build.slug), title: String(build.title), summary: String(build.summary ?? ''), updatedAt: String(build.updated_at) })), guides: guideRows.map((guide) => ({ id: String(guide.id), slug: String(guide.slug), title: String(guide.title), summary: String(guide.summary ?? ''), updatedAt: String(guide.updated_at) })), authoredComments: authoredCommentRows.map((comment) => { const targetType = String(comment.target_type); const targetId = String(comment.target_id); return { id: String(comment.id), createdAt: String(comment.created_at), targetType, targetTitle: targetType === 'profile' ? `${String(comment.target_display_name ?? 'Agent')} profile` : String(comment.build_title ?? 'Community content'), targetHref: commentHref( targetType, comment.slug, targetId, comment.target_public_slug, String(comment.id) ) }; }), profileComments: threadedProfileComments.map((comment) => ({ ...comment, isFollowing: followedRootIds.has(comment.rootId) })), viewer: { id: viewer?.id ?? null, canComment: Boolean(viewer) && Boolean(agent.is_public), canReportProfile: Boolean(viewer) && !isSelf, isSelf }, accountControl }; }; export const actions: Actions = { saveBio: async ({ params, cookies, request }) => { const viewer = await getSessionUser(cookies); if (!viewer) return fail(401, { bioError: 'Sign in to edit your profile.', bioResultToken: randomUUID() }); const database = await getDatabase(); const owner = await resolveAgentProfile(database, params.id); if (!owner || owner.id !== viewer.id) { return fail(403, { bioError: 'You can edit only your own profile.', bioResultToken: randomUUID() }); } const result = await saveProfileBio(viewer, (await request.formData()).get('bio')); if (!result.ok) { return fail(400, { bioError: result.error, bioResultToken: randomUUID() }); } return { bioSaved: true, bioReviewQueued: result.reviewQueued, bioMarkdown: result.bio, bioResultToken: randomUUID() }; }, setProfileVisibility: async ({ params, cookies, request }) => { const viewer = await getSessionUser(cookies); if (!viewer) return fail(401, { profileVisibilityError: 'Sign in to change profile visibility.', profileVisibilityResultToken: randomUUID() }); const database = await getDatabase(); const owner = await resolveAgentProfile(database, params.id); if (!owner || owner.id !== viewer.id) { return fail(403, { profileVisibilityError: 'You can change only your own profile.', profileVisibilityResultToken: randomUUID() }); } const result = await updateProfileVisibility( viewer, (await request.formData()).get('isPublic') ); if (!result.ok) { return fail(400, { profileVisibilityError: result.error, profileVisibilityResultToken: randomUUID() }); } return { profileVisibilitySaved: true, profileIsPublic: result.isPublic, profileVisibilityResultToken: randomUUID() }; }, commentProfile: async ({ params, cookies, request }) => { const commenter = await getSessionUser(cookies); const formData = await request.formData(); const parentId = String(formData.get('parentId') ?? '').trim() || null; const commentFailure = (status: number, profileCommentError: string) => fail(status, { profileCommentError, profileCommentParentId: parentId, profileCommentResultToken: randomUUID() }); if (!commenter) return commentFailure(401, 'Sign in to post a profile comment.'); const body = normalizeUserMarkdown( formData.get('body'), COMMENT_MAX_CHARACTERS ); if (body === null || Array.from(body).length < 2) { return commentFailure( 400, `Comments must contain 2–${COMMENT_MAX_CHARACTERS.toLocaleString()} visible characters.` ); } const database = await getDatabase(); const owner = await resolveAgentProfile(database, params.id); if (!owner) return commentFailure(404, 'Agent profile not found.'); const agentId = owner.id; const moderationPolicy = await getCommunityModerationPolicy(); const now = new Date(); const rows = await (await database.prepare(`SELECT users.id, COALESCE(profile.display_name,users.username) AS display_name,profile.is_public, status.blocked_at,status.banned_until,deleted.deleted_at FROM users LEFT JOIN user_profiles profile ON profile.user_id=users.id LEFT JOIN user_status status ON status.user_id=users.id LEFT JOIN deleted_accounts deleted ON deleted.user_id=users.id WHERE users.id IN (?,?)`)).all(commenter.id, agentId); const commenterState = rows.find((row) => row.id === commenter.id); const ownerState = rows.find((row) => row.id === agentId); if (unavailable(commenterState)) { return commentFailure(403, 'This account cannot post comments.'); } if (unavailable(ownerState) || !Boolean(ownerState?.is_public)) { return commentFailure(404, 'Agent profile not found.'); } const recent = await (await database.prepare(`SELECT comments.body_markdown,comments.created_at FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id WHERE comments.author_id=? AND comments.status IN ('visible','pending_review') AND thread.target_type='profile' AND thread.target_id=? ORDER BY comments.created_at DESC LIMIT 1`)).get(commenter.id, agentId); if (typeof recent?.created_at === 'string') { const elapsed = (now.getTime() - new Date(recent.created_at).getTime()) / 1_000; if (elapsed < RAPID_COMMENT_SECONDS) { return commentFailure(429, 'Wait a few seconds before posting again.'); } if (elapsed < moderationPolicy.duplicateWindowSeconds && recent.body_markdown === body) { return commentFailure(409, 'That comment was already posted.'); } } const moderation = await moderateCommunityText({ contentType: 'comment', text: body, policy: moderationPolicy }); const commentStatus = moderation.decision === 'approved' ? 'visible' : 'pending_review'; const createdAt = now.toISOString(); const proposedThreadId = randomUUID(); await (await database.prepare(`INSERT OR IGNORE INTO comment_threads (id,target_type,target_id,created_at) VALUES(?,'profile',?,?)`)) .run(proposedThreadId, agentId, createdAt); const thread = await (await database.prepare( "SELECT id FROM comment_threads WHERE target_type='profile' AND target_id=?" )).get(agentId); if (!thread?.id) return commentFailure(500, 'The profile thread could not be opened.'); const parent = parentId ? await (await database.prepare(`SELECT parent.author_id FROM comments parent JOIN comment_threads thread ON thread.id=parent.thread_id WHERE parent.id=? AND parent.status='visible' AND thread.id=? AND thread.target_type='profile' AND thread.target_id=?`)) .get(parentId, thread.id, agentId) : null; if (parentId && !parent) { return commentFailure(404, 'That message is no longer available for replies.'); } const commentId = randomUUID(); const inserted = parentId ? await (await database.prepare(`INSERT INTO comments (id,thread_id,author_id,parent_id,body_markdown,status,created_at,updated_at) SELECT ?,parent.thread_id,?,parent.id,?,?,?,? FROM comments parent JOIN comment_threads thread ON thread.id=parent.thread_id WHERE parent.id=? AND parent.status='visible' AND thread.id=? AND thread.target_type='profile' AND thread.target_id=?`)) .run( commentId, commenter.id, body, commentStatus, createdAt, createdAt, parentId, thread.id, agentId ) : await (await database.prepare(`INSERT INTO comments (id,thread_id,author_id,parent_id,body_markdown,status,created_at,updated_at) VALUES(?,?,?,NULL,?,?,?,?)`)) .run(commentId, thread.id, commenter.id, body, commentStatus, createdAt, createdAt); if (!inserted.changes) { return commentFailure(409, 'That conversation changed before the reply could be posted.'); } await recordCommunityModerationScan({ contentType: 'comment', contentId: commentId, authorId: commenter.id, result: moderation }); const parentAuthorId = parent?.author_id ? String(parent.author_id) : null; const recipientId = profileCommentNotificationRecipient( commenter.id, parentAuthorId, agentId ); if (commentStatus === 'visible' && recipientId) { const profileLink = agentProfileHref(owner, `profile-comment-${commentId}`); const isDirectReplyRecipient = Boolean(parentAuthorId && recipientId === parentAuthorId); await createNotification({ userId: recipientId, category: 'comments', source: 'agent', title: parentId ? 'New profile reply' : 'New profile comment', body: parentId ? isDirectReplyRecipient ? `${commenter.displayName} replied to your message on ${owner.displayName}'s Agent profile.` : `${commenter.displayName} replied in a conversation on your Agent profile.` : `${commenter.displayName} posted a comment on your Agent profile.`, link: profileLink, actions: [{ label: parentId ? 'VIEW REPLY' : 'VIEW COMMENT', href: profileLink }] }); } if (commentStatus === 'visible' && parentId) { await notifyProfileCommentFollowers({ commentId, authorId: commenter.id, authorDisplayName: commenter.displayName, profileOwnerId: agentId, profileOwnerDisplayName: owner.displayName, profileOwnerPublicSlug: owner.publicSlug, directRecipientId: recipientId }); } return { profileCommentSaved: true, profileCommentReviewQueued: commentStatus === 'pending_review', profileCommentId: commentId, profileCommentParentId: parentId, profileCommentResultToken: randomUUID() }; }, toggleProfileCommentFollow: async ({ params, cookies, request }) => { const viewer = await getSessionUser(cookies); const formData = await request.formData(); const commentId = String(formData.get('commentId') ?? '').trim(); const intent = String(formData.get('intent') ?? '').trim(); const failure = (status: number, message: string) => fail(status, { profileCommentFollowError: message, profileCommentFollowId: commentId, profileCommentFollowResultToken: randomUUID() }); if (!viewer) return failure(401, 'Sign in to follow a conversation.'); if (!commentId || !['follow', 'unfollow'].includes(intent)) { return failure(400, 'Choose a conversation and follow preference.'); } const database = await getDatabase(); const owner = await resolveAgentProfile(database, params.id); if (!owner) return failure(404, 'Agent profile not found.'); const conversation = await profileCommentConversation(commentId, owner.id); if (!conversation) return failure(404, 'That conversation is no longer available.'); if (intent === 'follow') { await (await database.prepare(`INSERT OR IGNORE INTO comment_thread_follows (user_id,root_comment_id,created_at) VALUES(?,?,?)`)) .run(viewer.id, conversation.rootId, new Date().toISOString()); } else { await (await database.prepare( 'DELETE FROM comment_thread_follows WHERE user_id=? AND root_comment_id=?' )).run(viewer.id, conversation.rootId); } return { profileCommentFollowSaved: true, profileCommentFollowing: intent === 'follow', profileCommentFollowId: commentId, profileCommentFollowResultToken: randomUUID() }; }, deleteProfileComment: async ({ params, cookies, request }) => { const viewer = await getSessionUser(cookies); if (!viewer) return fail(401, { profileCommentError: 'Sign in to remove a comment.' }); const commentId = String((await request.formData()).get('commentId') ?? '').trim(); if (!commentId) return fail(400, { profileCommentError: 'Choose a comment to remove.' }); const database = await getDatabase(); const owner = await resolveAgentProfile(database, params.id); if (!owner) return fail(404, { profileCommentError: 'Agent profile not found.' }); const comment = await (await database.prepare(`SELECT comments.id,comments.author_id FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id WHERE comments.id=? AND comments.status='visible' AND thread.target_type='profile' AND thread.target_id=?`)).get(commentId, owner.id); if (!comment) return fail(404, { profileCommentError: 'That comment is no longer available.' }); if (comment.author_id !== viewer.id) { return fail(403, { profileCommentError: 'You can remove only your own comment.' }); } const removedAt = new Date().toISOString(); const removed = await (await database.prepare(`UPDATE comments SET status='removed',removed_at=?,removed_by=?, removal_reason='Removed by author',updated_at=? WHERE id=? AND status='visible'`)) .run(removedAt, viewer.id, removedAt, commentId); if (!removed.changes) { return fail(409, { profileCommentError: 'That comment changed before it could be removed.' }); } return { profileCommentDeleted: true, profileCommentId: commentId, profileCommentResultToken: randomUUID() }; }, reportContent: async ({ params, cookies, request }) => { const reporter = await getSessionUser(cookies); const form = await request.formData(); const contentType = String(form.get('contentType') ?? ''); const contentId = String(form.get('contentId') ?? '').trim(); const category = String(form.get('category') ?? ''); const details = String(form.get('details') ?? '').normalize('NFKC').trim(); const failure = (status: number, message: string) => fail(status, { userReportError: message, userReportContentId: contentId, userReportContentType: contentType, userReportResultToken: randomUUID() }); if (!reporter) return failure(401, 'Sign in to request an Oversight review.'); if (!['comment', 'profile_bio', 'avatar'].includes(contentType)) { return failure(400, 'Choose reportable content.'); } if (!['spam', 'harassment', 'hate', 'sexual', 'violence', 'self_harm', 'privacy', 'other'].includes(category)) { return failure(400, 'Choose the policy concern that best fits this report.'); } if (details.length > 500 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(details)) { return failure(400, 'Report details may contain up to 500 visible characters.'); } const database = await getDatabase(); const owner = await resolveAgentProfile(database, params.id); if (!owner) return failure(404, 'Agent profile not found.'); const agentId = owner.id; if (contentType === 'comment') { const comment = await (await database.prepare(`SELECT comments.author_id FROM comments JOIN comment_threads thread ON thread.id=comments.thread_id WHERE comments.id=? AND comments.status='visible' AND thread.target_type='profile' AND thread.target_id=?`)).get(contentId, agentId); if (!comment) return failure(404, 'That comment is no longer available.'); if (String(comment.author_id) === reporter.id) return failure(400, 'You cannot report your own comment.'); } else { if (contentId !== agentId) return failure(400, 'That profile content does not match this Agent.'); if (reporter.id === agentId) return failure(400, 'You cannot report your own profile content.'); const profile = await (await database.prepare(`SELECT profile.bio_markdown,profile.is_public, COALESCE(moderation.bio_hidden,0) AS bio_hidden,avatar.source AS avatar_source FROM user_profiles profile LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id LEFT JOIN user_avatars avatar ON avatar.user_id=profile.user_id WHERE profile.user_id=?`)).get(agentId); if (!profile?.is_public) return failure(404, 'That profile is no longer available.'); if (contentType === 'profile_bio' && (profile.bio_hidden || !String(profile.bio_markdown ?? '').trim())) { return failure(404, 'That biography is no longer available.'); } if (contentType === 'avatar' && profile.avatar_source !== 'upload') { return failure(400, 'Preset SHD avatars are managed site assets and do not need an Agent report.'); } } const report = await createUserContentReport({ contentType: contentType as 'comment' | 'profile_bio' | 'avatar', contentId, reporterId: reporter.id, category: category as 'spam' | 'harassment' | 'hate' | 'sexual' | 'violence' | 'self_harm' | 'privacy' | 'other', details }); if (!report) return failure(409, 'You already have a pending report for this content.'); if (contentType === 'avatar') { await queueCurrentAvatarReview(database, agentId, { origin: 'user_report' }); } return { userReportSaved: true, userReportContentId: contentId, userReportContentType: contentType, userReportResultToken: randomUUID() }; } };