Link prior moderation actions to case decisions

This commit is contained in:
2026-10-17 19:42:05 -04:00
parent ef96a2c308
commit aca728b557
13 changed files with 948 additions and 17 deletions
+92
View File
@@ -0,0 +1,92 @@
import { db } from '../db/index.js';
import { AuthError } from '../auth/index.js';
import { textField } from './core.js';
import type { ModerationCaseAction, ModerationGame } from '../../moderation.js';
type Subject = { game: ModerationGame; guid: string };
export type CaseActionReference = Pick<ModerationCaseAction, 'type' | 'id'>;
export function caseActionReference(value: unknown): CaseActionReference {
if (!value || typeof value !== 'object') throw new AuthError('Choose a recorded action.');
const reference = value as Record<string, unknown>;
if (reference.type !== 'action' && reference.type !== 'restriction')
throw new AuthError('Choose a recorded warning, action, or restriction.');
return { type: reference.type, id: textField(reference.id, 'Action ID', 128) };
}
const restrictionColumns = `id,'restriction' AS type,kind,reason,actor,
created_at AS createdAt,expires_at AS expiresAt,
CASE WHEN revoked_at IS NOT NULL THEN 'revoked'
WHEN expires_at IS NOT NULL AND expires_at<=? THEN 'expired' ELSE 'active' END AS state`;
const actionColumns = `id,'action' AS type,kind,reason,actor,
created_at AS createdAt,expires_at AS expiresAt,state`;
/** Only actual records for this account can become case evidence. */
export function recordedCaseAction(
subject: Subject,
reference: CaseActionReference,
now: string
): ModerationCaseAction {
const handle = db();
const action =
reference.type === 'restriction'
? handle
.prepare(
`SELECT ${restrictionColumns} FROM moderation_sanctions
WHERE id=? AND guid=? AND (game=? OR scope='fleet')`
)
.get(now, reference.id, subject.guid, subject.game)
: handle
.prepare(
`SELECT ${actionColumns} FROM moderation_actions
WHERE id=? AND guid=? AND game=?`
)
.get(reference.id, subject.guid, subject.game);
if (!action)
throw new AuthError('The recorded action is unavailable for this case’s player.', 404);
return action as ModerationCaseAction;
}
export function isCaseActionLinked(caseId: string, reference: CaseActionReference): boolean {
return Boolean(
db()
.prepare(
`SELECT 1 FROM moderation_case_action_links
WHERE case_id=? AND action_type=? AND action_id=?
UNION ALL SELECT 1 FROM moderation_sanctions
WHERE ?='restriction' AND id=? AND case_id=? LIMIT 1`
)
.get(caseId, reference.type, reference.id, reference.type, reference.id, caseId)
);
}
export function caseActionLists(caseId: string, subject: Subject, now: string) {
const handle = db();
const records = handle
.prepare(
`SELECT ${restrictionColumns},
(case_id=? OR EXISTS (SELECT 1 FROM moderation_case_action_links l
WHERE l.case_id=? AND l.action_type='restriction' AND l.action_id=moderation_sanctions.id)) AS linked
FROM moderation_sanctions WHERE guid=? AND (game=? OR scope='fleet')
UNION ALL SELECT ${actionColumns},
EXISTS (SELECT 1 FROM moderation_case_action_links l
WHERE l.case_id=? AND l.action_type='action' AND l.action_id=moderation_actions.id) AS linked
FROM moderation_actions WHERE guid=? AND game=?
ORDER BY createdAt DESC,id`
)
.all(
now,
caseId,
caseId,
subject.guid,
subject.game,
caseId,
subject.guid,
subject.game
) as Array<ModerationCaseAction & { linked: number | null }>;
const linkedActions: ModerationCaseAction[] = [];
const availableActions: ModerationCaseAction[] = [];
for (const { linked, ...record } of records)
(linked ? linkedActions : availableActions).push(record);
return { linkedActions, availableActions };
}