93 lines
4.0 KiB
TypeScript
93 lines
4.0 KiB
TypeScript
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 };
|
||
}
|