Files
electric-cherry/src/lib/ui/moderation.ts
T

204 lines
8.7 KiB
TypeScript

import {
moderationCaseStatusLabels,
moderationCaseRelationLabels,
type ModerationCaseRelation,
type ModerationCaseStatus,
type ModerationGame
} from '$lib/moderation.js';
import { serverTitle } from './server-labels.js';
export function moderationCaseHref(id: string): string {
return `/moderation/cases/${encodeURIComponent(id)}`;
}
export function moderationHref(context: {
game?: string;
guid?: string;
q?: string;
server?: string;
matchId?: number;
message?: string;
}): string {
const params = new URLSearchParams();
if (context.game === 't5' || context.game === 't6') params.set('game', context.game);
if (context.guid) params.set('guid', context.guid);
if (context.q) params.set('q', context.q);
if (context.server) params.set('server', context.server);
if (context.matchId !== undefined && context.message) {
params.set('matchId', String(context.matchId));
params.set('message', context.message);
}
return `/moderation?${params}`;
}
export function moderationGameName(game: ModerationGame): string {
return game === 't5' ? 'Black Ops I' : 'Black Ops II';
}
export const moderationActionLabels: Record<string, string> = {
warn: 'Warning',
message: 'Private message',
kick: 'Kick',
announce: 'Announcement',
'create-sanction': 'Restriction created',
'revoke-sanction': 'Restriction revoked',
'create-case': 'Case opened',
'case-note': 'Case note added',
'resolve-case': 'Case reviewed',
'reopen-case': 'Case reopened',
'update-case': 'Case updated',
'attach-evidence': 'Case evidence attached',
note: 'Private note',
'restriction.created': 'Restriction created',
'restriction.revoked': 'Restriction revoked',
'action.queued': 'Live action requested',
'action.confirmed': 'Live action confirmed',
'action.failed': 'Live action failed',
'action.expired': 'Live action expired',
'action.unknown': 'Live action outcome unknown',
'case.opened': 'Case opened',
'case.note': 'Case note added',
'case.resolved': 'Case resolved',
'case.dismissed': 'Case dismissed',
'case.in_review': 'Case under review',
'case.escalated': 'Case escalated',
'case.warning_issued': 'Warning issued',
'case.restriction_applied': 'Restriction applied',
'case.no_action': 'Closed without action',
'case.duplicate': 'Closed as duplicate',
'case.appeal_upheld': 'Appeal upheld',
'case.appeal_denied': 'Appeal denied',
'case.reopened': 'Case reopened',
'case.updated': 'Case updated',
'case.assessed': 'Assessment revised',
'case.review_scheduled': 'Follow-up updated',
'schedule-case-review': 'Follow-up updated',
'case.linked': 'Case relationship added',
'case.unlinked': 'Case relationship removed',
'link-case': 'Case relationship added',
'unlink-case': 'Case relationship removed',
'assess-case': 'Assessment revised',
'case.evidence_attached': 'Evidence attached',
'case.restriction': 'Restriction linked to case',
'case.action_linked': 'Existing action linked to case',
'link-case-action': 'Existing action linked to case',
'player.note': 'Private player note',
'runtime.configure': 'Integration configuration requested'
};
export function moderationActionName(kind: string): string {
return (
moderationActionLabels[kind] ?? (kind === 'ban' ? 'Ban' : kind === 'mute' ? 'Mute' : kind)
);
}
export function moderationEventDetail(detail: string, action: string): string {
if (action.startsWith('case.')) {
try {
const record: unknown = JSON.parse(detail);
if (!record || typeof record !== 'object') return detail;
const value = record as Record<string, unknown>;
const display = (item: unknown, status = false): string =>
item === null
? 'Unassigned'
: typeof item === 'string'
? status && Object.hasOwn(moderationCaseStatusLabels, item)
? moderationCaseStatusLabels[item as ModerationCaseStatus]
: item
: '';
if (
(action === 'case.linked' || action === 'case.unlinked') &&
typeof value.relation === 'string' &&
Object.hasOwn(moderationCaseRelationLabels, value.relation) &&
typeof value.relatedCaseId === 'string' &&
typeof value.reason === 'string'
) {
const label =
value.direction === 'incoming' && value.relation !== 'same_incident'
? value.relation === 'appeal_of'
? 'Appealed by'
: 'Has duplicate'
: moderationCaseRelationLabels[value.relation as ModerationCaseRelation];
return `${label}: ${value.relatedCaseId}\n${value.reason}`;
}
if (action === 'case.review_scheduled' && typeof value.reason === 'string')
return `${typeof value.reviewAt === 'string' ? `Review scheduled for ${value.reviewAt}` : 'Follow-up cleared'}\n${value.reason}`;
if (action === 'case.assessed' && typeof value.summary === 'string')
return `Assessment revision ${value.assessmentRevision}: ${value.summary}`;
if (action === 'case.updated') {
const changes = ['title', 'priority', 'assignee', 'status'].flatMap((key) => {
const change = value[key];
if (!change || typeof change !== 'object') return [];
const { from, to } = change as { from?: unknown; to?: unknown };
return [
`${key === 'assignee' ? 'Assigned to' : key.charAt(0).toUpperCase() + key.slice(1)}: ${display(from, key === 'status')} → ${display(to, key === 'status')}`
];
});
if (typeof value.reason === 'string') changes.push(value.reason);
return changes.join('\n') || detail;
}
if (action === 'case.reopened' && typeof value.reason === 'string')
return `Previous outcome: ${display(value.from, true)}\n${value.reason}`;
if (
action === 'case.evidence_attached' &&
typeof value.count === 'number' &&
typeof value.matchId === 'number'
)
return `${value.count} archived chat ${value.count === 1 ? 'entry' : 'entries'} attached from match #${value.matchId}.`;
if (action === 'case.restriction' && typeof value.reason === 'string')
return `${value.kind === 'ban' ? 'Ban' : 'Mute'} · ${display(value.scope)} scope\n${value.reason}`;
if (
typeof value.reason === 'string' &&
value.actionReference &&
typeof value.actionReference === 'object'
) {
const reference = value.actionReference as { type?: unknown; id?: unknown };
if (
(reference.type === 'restriction' || reference.type === 'action') &&
typeof reference.id === 'string'
)
return `${value.reason}\nReferenced ${reference.type === 'restriction' ? 'restriction' : 'action'}: ${reference.id}`;
}
} catch {
// Older case events and decision reasons are plain staff-authored text.
}
return detail;
}
if (!['restriction.created', 'action.queued'].includes(action)) return detail;
try {
const value: unknown = JSON.parse(detail);
if (
!value ||
typeof value !== 'object' ||
!('reason' in value) ||
typeof value.reason !== 'string'
)
return detail;
const record = value as Record<string, unknown>;
const context = [
typeof record.kind === 'string'
? (moderationActionLabels[record.kind] ??
(record.kind === 'ban' ? 'Ban' : record.kind === 'mute' ? 'Mute' : ''))
: '',
typeof record.server === 'string'
? serverTitle(record.server)
: record.scope === 'fleet'
? 'Entire fleet'
: record.scope === 'game'
? 'Game scope'
: ''
]
.filter(Boolean)
.join(' · ');
return [
context,
record.reason,
typeof record.note === 'string' && record.note ? `Private note: ${record.note}` : ''
]
.filter(Boolean)
.join('\n');
} catch {
return detail;
}
}