import { moderatorMaximumRestrictionMs, type AuthUser } from './auth.js'; export type ModerationGame = 't5' | 't6'; export type ModerationScope = 'server' | 'game' | 'fleet'; export type ModerationActionKind = 'warn' | 'message' | 'kick' | 'announce'; export type ModerationState = 'queued' | 'delivered' | 'confirmed' | 'failed' | 'expired' | 'unknown'; /** Legacy decisions retained for historical events and older API clients, never lifecycle states. */ export const moderationCaseOutcomes = [ 'resolved', 'dismissed', 'warning_issued', 'restriction_applied', 'no_action', 'duplicate', 'appeal_upheld', 'appeal_denied' ] as const; export const moderationCaseStatuses = ['open', 'in_review', 'closed'] as const; export type ModerationCaseStatus = (typeof moderationCaseStatuses)[number]; export type ModerationCasePriority = 'low' | 'normal' | 'high' | 'urgent'; export const moderationCaseRelations = ['same_incident', 'appeal_of', 'duplicate_of'] as const; export type ModerationCaseRelation = (typeof moderationCaseRelations)[number]; export const moderationCaseRelationLabels: Record = { same_incident: 'Same incident', appeal_of: 'Appeal of', duplicate_of: 'Duplicate of' }; export const moderationCaseStatusLabels: Record< ModerationCaseStatus | (typeof moderationCaseOutcomes)[number] | 'escalated', string > = { open: 'Open', in_review: 'In progress', closed: 'Closed', escalated: 'Escalated', resolved: 'Resolved', dismissed: 'Dismissed', warning_issued: 'Warning issued', restriction_applied: 'Restriction applied', no_action: 'No action needed', duplicate: 'Duplicate', appeal_upheld: 'Appeal upheld', appeal_denied: 'Appeal denied' }; export function isModerationCaseOpen(status: ModerationCaseStatus): boolean { return status === 'open' || status === 'in_review'; } export const moderationConcernCategories = [ 'hate_speech', 'harassment', 'threats', 'safeguarding', 'spam', 'cheating', 'other' ] as const; export type ModerationConcernCategory = (typeof moderationConcernCategories)[number]; export const moderationConcernLabels: Record = { hate_speech: 'Hate speech', harassment: 'Harassment', threats: 'Threats', safeguarding: 'Safeguarding', spam: 'Spam', cheating: 'Cheating', other: 'Other' }; export const moderationFindingOutcomes = [ 'substantiated', 'inconclusive', 'unsubstantiated' ] as const; export type ModerationFindingOutcome = (typeof moderationFindingOutcomes)[number]; export const moderationFindingLabels: Record = { substantiated: 'Substantiated', inconclusive: 'Inconclusive', unsubstantiated: 'Unsubstantiated' }; export interface ModerationCaseAssessment { revision: number; summary: string; rules: string[]; findings: { category: ModerationConcernCategory; finding: ModerationFindingOutcome; detail: string; }[]; actor: string; createdAt: string; } export interface ModerationPlayer { game: ModerationGame; guid: string; name: string; aliases: string[]; firstSeen: string | null; lastSeen: string | null; servers: string[]; restrictions: number; } export interface ModerationSanction { id: string; caseId?: string | null; game: ModerationGame; guid: string; kind: 'ban' | 'mute'; scope: ModerationScope; server: string | null; reason: string; note: string; createdAt: string; expiresAt: string | null; revokedAt: string | null; actorId: string; actor: string; revision: number; active: boolean; } export function canRevokeModerationRestriction( actor: Pick | null | undefined, restriction: Pick ): boolean { if (actor?.role === 'admin') return true; if ( actor?.role !== 'moderator' || actor.id !== restriction.actorId || (restriction.scope !== 'server' && restriction.scope !== 'game') || !restriction.expiresAt ) return false; const duration = Date.parse(restriction.expiresAt) - Date.parse(restriction.createdAt); return duration > 0 && duration <= moderatorMaximumRestrictionMs; } export interface ModerationAction { id: string; game: ModerationGame; guid: string | null; server: string; kind: ModerationActionKind; reason: string; actor: string; createdAt: string; expiresAt: string; state: ModerationState; detail: string | null; } export interface ModerationCase { id: string; game: ModerationGame; guid: string; title: string; status: ModerationCaseStatus; priority: ModerationCasePriority; assigneeId: string | null; assignee: string | null; actor: string; createdAt: string; updatedAt: string; revision: number; assessment?: ModerationCaseAssessment | null; reviewAt?: string | null; reviewReason?: string; reviewSanctionId?: string | null; unreadUpdates?: number; evidence: { id: string; text: string; source: string; snapshot?: string | null; createdAt: string; actor: string; }[]; } export interface ModerationCaseDetail { case: ModerationCase; player: ModerationPlayer; sanctions: ModerationSanction[]; linkedActions?: ModerationCaseAction[]; availableActions?: ModerationCaseAction[]; assessment?: ModerationCaseAssessment | null; assessmentHistory?: ModerationCaseAssessment[]; relatedCases?: ModerationCaseRelationship[]; candidateCases?: ModerationCaseCandidate[]; notifications?: ModerationCaseNotification[]; events: ModerationEvent[]; coverage: ModerationCoverage[]; assignees: { id: string; username: string }[]; checkedAt: string; } export type ModerationCaseReference = Pick< ModerationCase, 'id' | 'title' | 'status' | 'game' | 'guid' >; export interface ModerationCaseRelationship { id: string; relation: ModerationCaseRelation; direction: 'outgoing' | 'incoming'; case: ModerationCaseReference; actor: string; createdAt: string; } export interface ModerationCaseCandidate extends ModerationCaseReference { sharedMatch: boolean; } export interface ModerationCaseNotification { id: number; caseId: string; caseTitle: string; kind: 'evidence' | 'assignment'; createdAt: string; readAt: string | null; } export interface ModerationCaseAction { id: string; type: 'restriction' | 'action'; kind: string; reason: string; actor: string; createdAt: string; state: string; expiresAt: string | null; } export interface ModerationEvent { id: number; game: ModerationGame | null; guid: string | null; action: string; subjectId: string | null; actor: string; detail: string; createdAt: string; } export interface ModerationCoverage { server: string; game: ModerationGame; connectedAt: string | null; revision: number | null; desiredRevision: number; state: 'current' | 'pending' | 'offline' | 'unavailable'; capabilities: string[]; } export interface ModerationQuery { view: 'players' | 'restrictions' | 'cases' | 'history'; q: string; game: '' | ModerationGame; guid: string; offset: number; caseStatus?: '' | 'active' | 'closed' | ModerationCaseStatus; priority?: '' | ModerationCasePriority; assignedTo?: string; review?: '' | 'due' | 'scheduled' | 'updates'; } export interface ModerationOverview { query: ModerationQuery; players: ModerationPlayer[]; sanctions: ModerationSanction[]; cases: ModerationCase[]; events: ModerationEvent[]; actions: ModerationAction[]; coverage: ModerationCoverage[]; total: number; limit: number; checkedAt: string; notifications?: ModerationCaseNotification[]; unreadCount?: number; } /** RCON T6 IDs are hexadecimal, while the game scripts send decimal strings. */ export function moderationGuid( game: ModerationGame, guid: string, source: 'game' | 'rcon' = 'game' ): string { const value = guid.trim(); if (game === 't6' && source === 'rcon' && /^(?:0x)?[0-9a-f]+$/i.test(value)) return BigInt(`0x${value.replace(/^0x/i, '')}`).toString(); if (/^\d+$/.test(value)) return BigInt(value).toString(); return value; }