Preserve conversation excerpts and deduplicate case evidence

This commit is contained in:
2026-10-17 19:35:52 -04:00
parent 83f1c91877
commit ef96a2c308
13 changed files with 1222 additions and 50 deletions
+6 -37
View File
@@ -1,5 +1,6 @@
import { randomUUID } from 'node:crypto';
import { db } from '../db/index.js';
import { archivedEvidenceSnapshots } from './evidence.js';
import { AuthError } from '../auth/index.js';
import { moderatorMaximumRestrictionMs, type AuthUser } from '../../auth.js';
import {
@@ -7,7 +8,6 @@ import {
isModerationCaseOpen,
moderationCaseOutcomes,
moderationCaseStatusLabels,
moderationGuid,
type ModerationCaseStatus,
type ModerationCasePriority,
type ModerationGame,
@@ -404,42 +404,10 @@ function archivedEvidence(
value: unknown,
now: string
): number {
const requested = value as { matchId: unknown; message: unknown };
if (!requested || !Number.isSafeInteger(requested.matchId))
throw new AuthError('Choose a recorded chat message.');
textField(requested.message, 'Chat message', 4096);
// Evidence must match the archived bytes, including intentional whitespace.
const message = requested.message as string;
const match = db()
.prepare('SELECT game,server,started_at FROM leaderboard_matches WHERE id=?')
.get(requested.matchId) as { game: string; server: string; started_at: string } | undefined;
if (!match || match.game !== target.game)
throw new AuthError('The source match is unavailable.', 404);
const entries = db()
.prepare('SELECT entry FROM match_chat WHERE match_id=?')
.all(requested.matchId) as { entry: string }[];
const found = entries
.filter((row) => {
const entry = JSON.parse(row.entry);
return (
entry.type === 'message' &&
moderationGuid(target.game, entry.message.guid) === target.guid &&
entry.message.message === message
);
})
.slice(0, 10);
if (!found.length)
throw new AuthError('The original chat message could not be verified in the archive.', 409);
for (const row of found)
evidence(
actor,
id,
message,
`Archived chat · ${match.server} · match ${requested.matchId}`,
JSON.stringify({ matchId: requested.matchId, match, entry: JSON.parse(row.entry) }),
now
);
return found.length;
const snapshots = archivedEvidenceSnapshots(id, target, value);
for (const snapshot of snapshots)
evidence(actor, id, snapshot.text, snapshot.source, snapshot.snapshot, now);
return snapshots.length;
}
function updateCase(actor: AuthUser, input: Record<string, unknown>, now: string) {
const row = caseRow(input.id);
@@ -548,6 +516,7 @@ export function mutateModeration(actorId: string, input: Record<string, unknown>
const row = caseRow(input.id);
checkCaseRevision(row, input.revision);
const count = archivedEvidence(actor, row.id, row, input.evidence, now);
if (!count) return { id: row.id };
touchCase(row.id, now);
moderationEvent(
actor,