Add public agent profiles and community moderation
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
import { avatarUrl } from '$lib/avatar';
|
||||
import { recordIsacAudit } from '$lib/server/audit';
|
||||
import { getPermissions, requirePermission } from '$lib/server/auth/authorization';
|
||||
import { getSessionUser } from '$lib/server/auth/session';
|
||||
import { getDatabase } from '$lib/server/database';
|
||||
import { renderUserMarkdown } from '$lib/markdown';
|
||||
import { createNotification } from '$lib/server/notifications';
|
||||
import { getPagination, getRegistrySearch, getRegistrySort } from '$lib/server/pagination';
|
||||
import { error, fail } from '@sveltejs/kit';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
const COMMENT_PERMISSION = 'community.comments.moderate';
|
||||
const PROFILE_PERMISSION = 'community.profiles.moderate';
|
||||
const REMOVED_RETENTION_DAYS = 30;
|
||||
|
||||
const has = (permissions: string[], permission: string) =>
|
||||
permissions.includes('*') || permissions.includes(permission);
|
||||
|
||||
const actionFailure = (status: number, message: string) => fail(status, {
|
||||
communityError: message,
|
||||
communityResultToken: randomUUID()
|
||||
});
|
||||
|
||||
const requiredReason = (value: FormDataEntryValue | null) => {
|
||||
const reason = String(value ?? '').normalize('NFKC').trim().slice(0, 500);
|
||||
return reason.length >= 3 ? reason : null;
|
||||
};
|
||||
|
||||
const targetHref = (targetType: string, targetId: unknown, slug: unknown) =>
|
||||
targetType === 'profile'
|
||||
? `/agents/${encodeURIComponent(String(targetId))}#profile-comments`
|
||||
: `/builds/${encodeURIComponent(String(slug ?? ''))}#comments`;
|
||||
|
||||
const parseAuditSnapshot = (value: unknown) => {
|
||||
if (typeof value !== 'string') return null;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? parsed as Record<string, unknown>
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const operator = await getSessionUser(cookies);
|
||||
if (!operator) error(401, 'Sign in required');
|
||||
const granted = await getPermissions(operator);
|
||||
const canModerateComments = has(granted, COMMENT_PERMISSION);
|
||||
const canModerateProfiles = has(granted, PROFILE_PERMISSION);
|
||||
if (!canModerateComments && !canModerateProfiles) error(403, 'Community Oversight permission required');
|
||||
|
||||
const requestedTab = url.searchParams.get('tab');
|
||||
const tab = requestedTab === 'bios'
|
||||
? 'bios'
|
||||
: requestedTab === 'comments'
|
||||
? 'comments'
|
||||
: canModerateComments ? 'comments' : 'bios';
|
||||
if (tab === 'comments' && !canModerateComments) error(403, 'Comment moderation permission required');
|
||||
if (tab === 'bios' && !canModerateProfiles) error(403, 'Profile moderation permission required');
|
||||
|
||||
const allowedStatuses = tab === 'comments' ? ['all', 'visible', 'removed'] : ['all', 'visible', 'hidden'];
|
||||
const status = allowedStatuses.includes(String(url.searchParams.get('status')))
|
||||
? String(url.searchParams.get('status'))
|
||||
: 'visible';
|
||||
const { search, pattern } = getRegistrySearch(url);
|
||||
const database = await getDatabase();
|
||||
const removedCutoff = new Date(Date.now() - REMOVED_RETENTION_DAYS * 86_400_000).toISOString();
|
||||
|
||||
const commentSummaryRows = canModerateComments
|
||||
? await (await database.prepare(`SELECT status,COUNT(*) AS count FROM comments
|
||||
WHERE status='visible' OR (status='removed' AND removed_at>=?)
|
||||
GROUP BY status`)).all(removedCutoff)
|
||||
: [];
|
||||
const visibleBios = canModerateProfiles
|
||||
? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM user_profiles profile
|
||||
LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
|
||||
WHERE profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0
|
||||
AND length(trim(profile.bio_markdown))>0`)).get())?.count ?? 0)
|
||||
: 0;
|
||||
const hiddenBios = canModerateProfiles
|
||||
? Number((await (await database.prepare(`SELECT COUNT(*) AS count FROM user_profiles profile
|
||||
JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
|
||||
WHERE moderation.bio_hidden=1 AND length(trim(profile.bio_markdown))>0`)).get())?.count ?? 0)
|
||||
: 0;
|
||||
const commentSummary = Object.fromEntries(
|
||||
commentSummaryRows.map((row) => [String(row.status), Number(row.count)])
|
||||
);
|
||||
|
||||
if (tab === 'comments') {
|
||||
const sort = getRegistrySort(url, {
|
||||
created: {
|
||||
asc: 'comments.created_at ASC',
|
||||
desc: 'comments.created_at DESC',
|
||||
defaultDirection: 'desc'
|
||||
},
|
||||
updated: {
|
||||
asc: 'comments.updated_at ASC',
|
||||
desc: 'comments.updated_at DESC',
|
||||
defaultDirection: 'desc'
|
||||
},
|
||||
author: {
|
||||
asc: 'display_name COLLATE NOCASE ASC',
|
||||
desc: 'display_name COLLATE NOCASE DESC'
|
||||
}
|
||||
}, 'created', 'desc');
|
||||
const conditions: string[] = [];
|
||||
const args: unknown[] = [];
|
||||
if (status === 'all') {
|
||||
conditions.push("(comments.status='visible' OR (comments.status='removed' AND comments.removed_at>=?))");
|
||||
args.push(removedCutoff);
|
||||
} else {
|
||||
conditions.push('comments.status=?');
|
||||
args.push(status);
|
||||
}
|
||||
if (status === 'removed') {
|
||||
conditions.push('comments.removed_at>=?');
|
||||
args.push(removedCutoff);
|
||||
}
|
||||
if (search) {
|
||||
conditions.push(`(comments.body_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE
|
||||
OR COALESCE(profile.display_name,users.username,'Deleted Agent') LIKE ? ESCAPE '\\' COLLATE NOCASE
|
||||
OR COALESCE(builds.title,target_profile.display_name,target_user.username,'') LIKE ? ESCAPE '\\' COLLATE NOCASE)`);
|
||||
args.push(pattern, pattern, pattern);
|
||||
}
|
||||
const joins = `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
|
||||
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`;
|
||||
const where = `WHERE ${conditions.join(' AND ')}`;
|
||||
const total = Number((await (await database.prepare(
|
||||
`SELECT COUNT(*) AS count ${joins} ${where}`
|
||||
)).get(...args))?.count ?? 0);
|
||||
const pagination = getPagination(url, total);
|
||||
const rows = await (await database.prepare(`SELECT comments.id,comments.body_markdown,comments.status,
|
||||
comments.created_at,comments.updated_at,comments.removal_reason,
|
||||
comments.author_id,COALESCE(profile.display_name,users.username,'Deleted Agent') AS display_name,
|
||||
avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at,
|
||||
thread.target_type,thread.target_id,builds.slug,
|
||||
COALESCE(builds.title,target_profile.display_name,target_user.username,'Community content') AS target_title
|
||||
${joins} ${where} ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`))
|
||||
.all(...args, pagination.perPage, pagination.offset);
|
||||
return {
|
||||
tab,
|
||||
status,
|
||||
search,
|
||||
summary: {
|
||||
comments: { visible: Number(commentSummary.visible ?? 0), removed: Number(commentSummary.removed ?? 0) },
|
||||
bios: { visible: visibleBios, hidden: hiddenBios }
|
||||
},
|
||||
items: rows.map((row) => ({
|
||||
id: String(row.id),
|
||||
bodyHtml: renderUserMarkdown(row.body_markdown),
|
||||
status: String(row.status),
|
||||
createdAt: String(row.created_at),
|
||||
updatedAt: String(row.updated_at),
|
||||
moderationReason: row.removal_reason ? String(row.removal_reason) : null,
|
||||
targetType: String(row.target_type),
|
||||
targetTitle: String(row.target_title),
|
||||
targetHref: targetHref(String(row.target_type), row.target_id, row.slug),
|
||||
author: {
|
||||
id: String(row.author_id),
|
||||
displayName: String(row.display_name),
|
||||
avatarUrl: avatarUrl(row.avatar_source, row.avatar_value, String(row.author_id), row.avatar_updated_at)
|
||||
}
|
||||
})),
|
||||
pagination,
|
||||
sort: { key: sort.key, direction: sort.direction },
|
||||
permissions: { comments: canModerateComments, profiles: canModerateProfiles }
|
||||
};
|
||||
}
|
||||
|
||||
const sort = getRegistrySort(url, {
|
||||
updated: {
|
||||
asc: 'profile.updated_at ASC',
|
||||
desc: 'profile.updated_at DESC',
|
||||
defaultDirection: 'desc'
|
||||
},
|
||||
agent: {
|
||||
asc: 'display_name COLLATE NOCASE ASC',
|
||||
desc: 'display_name COLLATE NOCASE DESC'
|
||||
}
|
||||
}, 'updated', 'desc');
|
||||
const conditions = [
|
||||
status === 'all'
|
||||
? "((profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0) OR moderation.bio_hidden=1)"
|
||||
: status === 'visible'
|
||||
? 'profile.is_public=1 AND COALESCE(moderation.bio_hidden,0)=0'
|
||||
: 'moderation.bio_hidden=1',
|
||||
'length(trim(profile.bio_markdown))>0'
|
||||
];
|
||||
const args: unknown[] = [];
|
||||
if (search) {
|
||||
conditions.push(`(profile.bio_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE
|
||||
OR COALESCE(profile.display_name,users.username) LIKE ? ESCAPE '\\' COLLATE NOCASE)`);
|
||||
args.push(pattern, pattern);
|
||||
}
|
||||
const where = `WHERE ${conditions.join(' AND ')}`;
|
||||
const total = Number((await (await database.prepare(`SELECT COUNT(*) AS count
|
||||
FROM user_profiles profile JOIN users ON users.id=profile.user_id
|
||||
LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
|
||||
${where}`)).get(...args))?.count ?? 0);
|
||||
const pagination = getPagination(url, total);
|
||||
const rows = await (await database.prepare(`SELECT profile.user_id,profile.bio_markdown,profile.is_public,
|
||||
profile.updated_at,COALESCE(profile.display_name,users.username) AS display_name,
|
||||
avatar.source AS avatar_source,avatar.value AS avatar_value,avatar.updated_at AS avatar_updated_at,
|
||||
moderation.reason AS moderation_reason
|
||||
FROM user_profiles profile
|
||||
JOIN users ON users.id=profile.user_id
|
||||
LEFT JOIN user_avatars avatar ON avatar.user_id=users.id
|
||||
LEFT JOIN profile_moderation moderation ON moderation.user_id=users.id
|
||||
${where} ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`))
|
||||
.all(...args, pagination.perPage, pagination.offset);
|
||||
return {
|
||||
tab,
|
||||
status,
|
||||
search,
|
||||
summary: {
|
||||
comments: { visible: Number(commentSummary.visible ?? 0), removed: Number(commentSummary.removed ?? 0) },
|
||||
bios: { visible: visibleBios, hidden: hiddenBios }
|
||||
},
|
||||
items: rows.map((row) => ({
|
||||
id: String(row.user_id),
|
||||
bodyHtml: renderUserMarkdown(row.bio_markdown),
|
||||
status,
|
||||
updatedAt: String(row.updated_at),
|
||||
moderationReason: row.moderation_reason ? String(row.moderation_reason) : null,
|
||||
agent: {
|
||||
id: String(row.user_id),
|
||||
displayName: String(row.display_name),
|
||||
avatarUrl: avatarUrl(row.avatar_source, row.avatar_value, String(row.user_id), row.avatar_updated_at)
|
||||
}
|
||||
})),
|
||||
pagination,
|
||||
sort: { key: sort.key, direction: sort.direction },
|
||||
permissions: { comments: canModerateComments, profiles: canModerateProfiles }
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
removeComment: async ({ cookies, request }) => {
|
||||
const operator = await requirePermission(await getSessionUser(cookies), COMMENT_PERMISSION);
|
||||
const form = await request.formData();
|
||||
const id = String(form.get('id') ?? '').trim();
|
||||
const reason = requiredReason(form.get('reason'));
|
||||
if (!id || !reason) return actionFailure(400, 'Choose a comment and provide a brief removal reason.');
|
||||
const database = await getDatabase();
|
||||
const comment = await (await database.prepare(`SELECT comments.*,thread.target_type,thread.target_id,builds.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')
|
||||
WHERE comments.id=? AND comments.status='visible'`)).get(id);
|
||||
if (!comment) return actionFailure(404, 'That visible comment no longer exists.');
|
||||
const before = {
|
||||
status: 'visible',
|
||||
bodyMarkdown: String(comment.body_markdown),
|
||||
authorId: String(comment.author_id),
|
||||
threadId: String(comment.thread_id),
|
||||
targetType: String(comment.target_type),
|
||||
targetId: String(comment.target_id),
|
||||
updatedAt: String(comment.updated_at)
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
const result = await (await database.prepare(`UPDATE comments SET status='removed',removed_at=?,
|
||||
removed_by=?,removal_reason=?,updated_at=? WHERE id=? AND status='visible'`))
|
||||
.run(now, operator.id, reason, now, id);
|
||||
if (!result.changes) return actionFailure(409, 'That comment changed before it could be removed.');
|
||||
const after = { ...before, status: 'removed', removedAt: now, removedBy: operator.id, reason, updatedAt: now };
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'community_moderation',
|
||||
action: 'comment.removed',
|
||||
entityId: id,
|
||||
before,
|
||||
after
|
||||
});
|
||||
await createNotification({
|
||||
userId: String(comment.author_id),
|
||||
category: 'isac',
|
||||
source: 'ISAC COMMUNITY OVERSIGHT',
|
||||
title: 'Comment removed',
|
||||
body: `Community Oversight removed one of your comments. Reason: ${reason}`,
|
||||
link: targetHref(String(comment.target_type), comment.target_id, comment.slug),
|
||||
actions: [{
|
||||
label: 'VIEW DISCUSSION',
|
||||
href: targetHref(String(comment.target_type), comment.target_id, comment.slug)
|
||||
}],
|
||||
mandatory: true
|
||||
});
|
||||
return { communitySaved: true, communityResultToken: randomUUID() };
|
||||
},
|
||||
|
||||
restoreComment: async ({ cookies, request }) => {
|
||||
const operator = await requirePermission(await getSessionUser(cookies), COMMENT_PERMISSION);
|
||||
const form = await request.formData();
|
||||
const id = String(form.get('id') ?? '').trim();
|
||||
const reason = requiredReason(form.get('reason'));
|
||||
if (!id || !reason) return actionFailure(400, 'Choose a comment and provide a brief restoration reason.');
|
||||
const database = await getDatabase();
|
||||
const comment = await (await database.prepare(`SELECT * FROM comments WHERE id=? AND status='removed'`)).get(id);
|
||||
if (!comment) return actionFailure(404, 'That removed comment no longer exists.');
|
||||
const audit = await (await database.prepare(`SELECT after_json FROM audit_log
|
||||
WHERE entity_type='community_moderation' AND entity_id=? AND action='comment.removed'
|
||||
ORDER BY created_at DESC LIMIT 1`)).get(id);
|
||||
const expected = parseAuditSnapshot(audit?.after_json);
|
||||
if (
|
||||
!expected
|
||||
|| expected.status !== 'removed'
|
||||
|| String(expected.removedAt ?? '') !== String(comment.removed_at ?? '')
|
||||
|| String(expected.updatedAt ?? '') !== String(comment.updated_at ?? '')
|
||||
) return actionFailure(409, 'This comment was not removed by Community Oversight or changed after removal.');
|
||||
const before = {
|
||||
status: 'removed',
|
||||
bodyMarkdown: String(comment.body_markdown),
|
||||
authorId: String(comment.author_id),
|
||||
threadId: String(comment.thread_id),
|
||||
removedAt: String(comment.removed_at),
|
||||
removedBy: String(comment.removed_by),
|
||||
removalReason: String(comment.removal_reason),
|
||||
updatedAt: String(comment.updated_at)
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
const result = await (await database.prepare(`UPDATE comments SET status='visible',removed_at=NULL,
|
||||
removed_by=NULL,removal_reason=NULL,updated_at=? WHERE id=? AND status='removed' AND updated_at=?`))
|
||||
.run(now, id, comment.updated_at);
|
||||
if (!result.changes) return actionFailure(409, 'That comment changed before it could be restored.');
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'community_moderation',
|
||||
action: 'comment.restored',
|
||||
entityId: id,
|
||||
before,
|
||||
after: {
|
||||
status: 'visible',
|
||||
bodyMarkdown: String(comment.body_markdown),
|
||||
authorId: String(comment.author_id),
|
||||
threadId: String(comment.thread_id),
|
||||
restorationReason: reason,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
return { communitySaved: true, communityResultToken: randomUUID() };
|
||||
},
|
||||
|
||||
hideBio: async ({ cookies, request }) => {
|
||||
const operator = await requirePermission(await getSessionUser(cookies), PROFILE_PERMISSION);
|
||||
const form = await request.formData();
|
||||
const id = String(form.get('id') ?? '').trim();
|
||||
const reason = requiredReason(form.get('reason'));
|
||||
if (!id || !reason) return actionFailure(400, 'Choose a profile and provide a brief reason for hiding it.');
|
||||
const database = await getDatabase();
|
||||
const profile = await (await database.prepare(`SELECT profile.user_id,profile.bio_markdown,
|
||||
profile.is_public,profile.bio_updated_at,profile.updated_at,
|
||||
COALESCE(moderation.bio_hidden,0) AS bio_hidden,moderation.reason,
|
||||
moderation.moderated_at,moderation.moderated_by
|
||||
FROM user_profiles profile
|
||||
LEFT JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
|
||||
WHERE profile.user_id=? AND profile.is_public=1
|
||||
AND COALESCE(moderation.bio_hidden,0)=0 AND length(trim(profile.bio_markdown))>0`)).get(id);
|
||||
if (!profile) return actionFailure(404, 'That visible profile biography no longer exists.');
|
||||
const before = {
|
||||
bioMarkdown: String(profile.bio_markdown),
|
||||
isPublic: true,
|
||||
bioHidden: false,
|
||||
bioUpdatedAt: profile.bio_updated_at ? String(profile.bio_updated_at) : null,
|
||||
profileUpdatedAt: String(profile.updated_at),
|
||||
moderation: profile.moderated_at ? {
|
||||
reason: profile.reason ? String(profile.reason) : null,
|
||||
moderatedAt: String(profile.moderated_at),
|
||||
moderatedBy: profile.moderated_by ? String(profile.moderated_by) : null
|
||||
} : null
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
const result = await (await database.prepare(`INSERT INTO profile_moderation
|
||||
(user_id,bio_hidden,reason,moderated_at,moderated_by) VALUES(?,1,?,?,?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET bio_hidden=1,reason=excluded.reason,
|
||||
moderated_at=excluded.moderated_at,moderated_by=excluded.moderated_by
|
||||
WHERE profile_moderation.bio_hidden=0`)).run(id, reason, now, operator.id);
|
||||
if (!result.changes) return actionFailure(409, 'That profile changed before it could be hidden.');
|
||||
const after = {
|
||||
...before,
|
||||
bioHidden: true,
|
||||
reason,
|
||||
moderatedAt: now,
|
||||
moderatedBy: operator.id
|
||||
};
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'community_moderation',
|
||||
action: 'profile.hidden',
|
||||
entityId: id,
|
||||
before,
|
||||
after
|
||||
});
|
||||
await createNotification({
|
||||
userId: id,
|
||||
category: 'isac',
|
||||
source: 'ISAC COMMUNITY OVERSIGHT',
|
||||
title: 'Agent profile hidden',
|
||||
body: `Community Oversight hid your public Agent profile. Your biography is preserved but is not publicly visible. Reason: ${reason}`,
|
||||
link: '/account',
|
||||
actions: [{ label: 'REVIEW PROFILE', href: '/account' }],
|
||||
mandatory: true
|
||||
});
|
||||
return { communitySaved: true, communityResultToken: randomUUID() };
|
||||
},
|
||||
|
||||
restoreBio: async ({ cookies, request }) => {
|
||||
const operator = await requirePermission(await getSessionUser(cookies), PROFILE_PERMISSION);
|
||||
const form = await request.formData();
|
||||
const id = String(form.get('id') ?? '').trim();
|
||||
const reason = requiredReason(form.get('reason'));
|
||||
if (!id || !reason) return actionFailure(400, 'Choose a profile and provide a brief restoration reason.');
|
||||
const database = await getDatabase();
|
||||
const profile = await (await database.prepare(`SELECT profile.user_id,profile.bio_markdown,
|
||||
profile.is_public,profile.bio_updated_at,profile.updated_at,
|
||||
moderation.bio_hidden,moderation.reason,moderation.moderated_at,moderation.moderated_by
|
||||
FROM user_profiles profile
|
||||
JOIN profile_moderation moderation ON moderation.user_id=profile.user_id
|
||||
WHERE profile.user_id=? AND moderation.bio_hidden=1
|
||||
AND length(trim(profile.bio_markdown))>0`)).get(id);
|
||||
if (!profile) return actionFailure(404, 'That hidden profile biography no longer exists.');
|
||||
const audit = await (await database.prepare(`SELECT after_json FROM audit_log
|
||||
WHERE entity_type='community_moderation' AND entity_id=? AND action='profile.hidden'
|
||||
ORDER BY created_at DESC LIMIT 1`)).get(id);
|
||||
const expected = parseAuditSnapshot(audit?.after_json);
|
||||
if (
|
||||
!expected
|
||||
|| expected.bioHidden !== true
|
||||
|| String(expected.bioMarkdown ?? '') !== String(profile.bio_markdown)
|
||||
|| String(expected.bioUpdatedAt ?? '') !== String(profile.bio_updated_at ?? '')
|
||||
|| String(expected.profileUpdatedAt ?? '') !== String(profile.updated_at)
|
||||
|| String(expected.moderatedAt ?? '') !== String(profile.moderated_at)
|
||||
|| String(expected.moderatedBy ?? '') !== String(profile.moderated_by)
|
||||
) return actionFailure(409, 'This profile was not hidden by Community Oversight or changed after it was hidden.');
|
||||
const before = {
|
||||
bioMarkdown: String(profile.bio_markdown),
|
||||
isPublic: Boolean(profile.is_public),
|
||||
bioHidden: true,
|
||||
bioUpdatedAt: profile.bio_updated_at ? String(profile.bio_updated_at) : null,
|
||||
profileUpdatedAt: String(profile.updated_at),
|
||||
reason: String(profile.reason),
|
||||
moderatedAt: String(profile.moderated_at),
|
||||
moderatedBy: String(profile.moderated_by)
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
const result = await (await database.prepare(`UPDATE profile_moderation
|
||||
SET bio_hidden=0,reason=?,moderated_at=?,moderated_by=?
|
||||
WHERE user_id=? AND bio_hidden=1 AND moderated_at=?`))
|
||||
.run(reason, now, operator.id, id, profile.moderated_at);
|
||||
if (!result.changes) return actionFailure(409, 'That profile changed before it could be restored.');
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'community_moderation',
|
||||
action: 'profile.restored',
|
||||
entityId: id,
|
||||
before,
|
||||
after: {
|
||||
...before,
|
||||
bioHidden: false,
|
||||
restorationReason: reason,
|
||||
moderatedAt: now,
|
||||
moderatedBy: operator.id
|
||||
}
|
||||
});
|
||||
return { communitySaved: true, communityResultToken: randomUUID() };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user