204 lines
8.7 KiB
TypeScript
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;
|
|
}
|
|
}
|