import { db } from '../db/index.js'; import { currentCaseAssessment } from './assessments.js'; import { moderationActor, moderationNow, policyRevision } from './core.js'; import { runtimeFreshMs } from './actions.js'; import { refreshActionStates } from './runtime.js'; import { moderationCaseStatuses, moderationGuid, type ModerationOverview, type ModerationQuery, type ModerationPlayer, type ModerationSanction, type ModerationCase, type ModerationEvent, type ModerationAction, type ModerationCoverage } from '../../moderation.js'; export function moderationQuery(params: URLSearchParams): ModerationQuery { const view = params.get('view'); const game = params.get('game'); const guid = (params.get('guid') ?? '').slice(0, 128); const offset = Number(params.get('offset') ?? 0); const caseStatus = params.get('caseStatus') ?? ''; const priority = params.get('priority') ?? ''; return { view: view === 'restrictions' || view === 'cases' || view === 'history' ? view : 'players', q: (params.get('q') ?? '').trim().slice(0, 200), game: game === 't5' || game === 't6' ? game : '', guid: game === 't5' || game === 't6' ? moderationGuid(game, guid) : guid, offset: Number.isSafeInteger(offset) && offset >= 0 ? offset : 0, caseStatus: ['active', 'closed', ...moderationCaseStatuses].includes(caseStatus) ? (caseStatus as ModerationQuery['caseStatus']) : '', priority: ['low', 'normal', 'high', 'urgent'].includes(priority) ? (priority as ModerationQuery['priority']) : '', assignedTo: (params.get('assignedTo') ?? '').slice(0, 128) }; } export function moderationOverview(actorId: string, query: ModerationQuery): ModerationOverview { moderationActor(actorId); return db().transaction(() => { const handle = db(), now = moderationNow(), limit = 25; refreshActionStates(now); const revision = policyRevision(); const runtimeRows = handle .prepare( `SELECT s.name AS server,s.game,n.id,r.connected_at AS connectedAt,r.revision,r.capabilities,r.players FROM servers s LEFT JOIN native_instances n ON n.server=s.name AND n.retired_at IS NULL LEFT JOIN moderation_runtimes r ON r.instance_id=n.id AND r.generation=n.generation WHERE s.destroyed_at IS NULL AND s.game IN ('t5','t6') ORDER BY s.name` ) .all() as { server: string; game: 't5' | 't6'; id: string | null; connectedAt: string | null; revision: number | null; capabilities: string | null; players: string | null; }[]; const coverage: ModerationCoverage[] = runtimeRows.map((row) => ({ server: row.server, game: row.game, connectedAt: row.connectedAt, revision: row.revision, desiredRevision: revision, state: !row.connectedAt ? 'unavailable' : Date.parse(now) - Date.parse(row.connectedAt) > runtimeFreshMs ? 'offline' : row.revision === revision && ['ban', 'mute'].every((capability) => (JSON.parse(row.capabilities ?? '[]') as string[]).includes(capability) ) ? 'current' : 'pending', capabilities: row.capabilities ? JSON.parse(row.capabilities) : [] })); const result: ModerationOverview = { query: { ...query }, players: [], sanctions: [], cases: [], events: [], actions: [], coverage, total: 0, limit, checkedAt: now }; const where: string[] = [], values: (string | number)[] = []; if (query.game) { where.push( query.view === 'restrictions' ? "(p.game=? OR p.scope='fleet')" : 'p.game=?' ); values.push(query.game); } if (query.guid) { where.push('p.guid=?'); values.push(query.guid); } if (query.view === 'cases') { if (query.caseStatus === 'active' || query.caseStatus === 'closed') { where.push( `p.status ${query.caseStatus === 'closed' ? 'NOT ' : ''}IN ('open','in_review','escalated')` ); } else if (query.caseStatus) { where.push('p.status=?'); values.push(query.caseStatus); } if (query.priority) { where.push('p.priority=?'); values.push(query.priority); } if (query.assignedTo === 'unassigned') where.push('p.assignee_id IS NULL'); else if (query.assignedTo) { where.push('p.assignee_id=?'); values.push(query.assignedTo === 'me' ? actorId : query.assignedTo); } } if (query.q) { const extra = query.view === 'restrictions' ? 'p.reason' : query.view === 'cases' ? 'p.title' : query.view === 'history' ? 'p.detail' : 'p.name'; where.push( `(instr(lower(p.guid),lower(?))>0 OR instr(lower(${extra}),lower(?))>0 OR EXISTS (SELECT 1 FROM moderation_aliases a WHERE a.game=p.game AND a.guid=p.guid AND instr(lower(a.name),lower(?))>0))` ); values.push(query.q, query.q, query.q); } const clause = where.length ? ' WHERE ' + where.join(' AND ') : ''; function rows(table: string, select: string, order: string): T[] { result.total = ( handle .prepare(`SELECT COUNT(*) AS count FROM ${table} p${clause}`) .get(...values) as { count: number } ).count; result.query.offset = Math.min( query.offset, Math.max(0, Math.floor((result.total - 1) / limit) * limit) ); return handle .prepare( `SELECT ${select} FROM ${table} p${clause} ORDER BY ${order} LIMIT ? OFFSET ?` ) .all(...values, limit, result.query.offset) as T[]; } if (query.view === 'players') { result.players = rows>( 'moderation_players', "p.game,p.guid,p.name,NULLIF(p.first_seen_at,'') AS firstSeen,NULLIF(p.last_seen_at,'') AS lastSeen", 'p.last_seen_at DESC,p.game,p.guid' ).map((player) => ({ ...player, aliases: ( handle .prepare( 'SELECT name FROM moderation_aliases WHERE game=? AND guid=? ORDER BY last_seen_at DESC LIMIT 30' ) .all(player.game, player.guid) as { name: string }[] ).map((a) => a.name), servers: runtimeRows .filter( (r) => r.game === player.game && r.connectedAt && Date.parse(now) - Date.parse(r.connectedAt) <= runtimeFreshMs && r.players && (JSON.parse(r.players) as { guid: string }[]).some( (p) => p.guid === player.guid ) ) .map((r) => r.server), restrictions: ( handle .prepare( "SELECT COUNT(*) AS count FROM moderation_sanctions WHERE guid=? AND (game=? OR scope='fleet') AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)" ) .get(player.guid, player.game, now) as { count: number } ).count })); } else if (query.view === 'restrictions') { result.sanctions = rows>( 'moderation_sanctions', 'p.id,p.game,p.guid,p.kind,p.scope,p.server,p.reason,p.note,p.created_at AS createdAt,p.expires_at AS expiresAt,p.revoked_at AS revokedAt,p.actor_id AS actorId,p.actor,p.revision,p.case_id AS caseId', 'p.created_at DESC,p.id' ).map((s) => ({ ...s, active: !s.revokedAt && (!s.expiresAt || s.expiresAt > now) })); } else if (query.view === 'cases') { result.cases = rows>( 'moderation_cases', 'p.id,p.game,p.guid,p.title,p.status,p.priority,p.assignee_id AS assigneeId,(SELECT username FROM auth_users WHERE id=p.assignee_id) AS assignee,p.actor,p.created_at AS createdAt,p.updated_at AS updatedAt,p.revision', "CASE WHEN p.status IN ('open','in_review','escalated') THEN 0 ELSE 1 END,CASE p.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,p.updated_at DESC,p.id" ).map((c) => ({ ...c, assessment: currentCaseAssessment(c.id), evidence: handle .prepare( 'SELECT id,text,source,created_at AS createdAt,actor FROM moderation_evidence WHERE case_id=? ORDER BY created_at,id' ) .all(c.id) as ModerationCase['evidence'] })); } else { result.events = rows( 'moderation_events', 'p.id,p.game,p.guid,p.action,p.subject_id AS subjectId,p.actor,p.detail,p.created_at AS createdAt', 'p.id DESC' ); } const actionWhere: string[] = [], actionValues: string[] = []; if (query.game) { actionWhere.push('game=?'); actionValues.push(query.game); } if (query.guid) { actionWhere.push('guid=?'); actionValues.push(query.guid); } if (query.q) { actionWhere.push( '(instr(lower(guid),lower(?))>0 OR instr(lower(reason),lower(?))>0 OR EXISTS (SELECT 1 FROM moderation_aliases a WHERE a.game=moderation_actions.game AND a.guid=moderation_actions.guid AND instr(lower(a.name),lower(?))>0))' ); actionValues.push(query.q, query.q, query.q); } result.actions = handle .prepare( `SELECT id,game,guid,server,kind,reason,actor,created_at AS createdAt,expires_at AS expiresAt,state,detail FROM moderation_actions${actionWhere.length ? ' WHERE ' + actionWhere.join(' AND ') : ''} ORDER BY created_at DESC,id LIMIT 25` ) .all(...actionValues) as ModerationAction[]; return result; })(); }