This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { moderationActor, moderationNow, policyRevision } from './core.js';
|
||||
import { runtimeFreshMs } from './actions.js';
|
||||
import { refreshActionStates } from './runtime.js';
|
||||
import {
|
||||
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);
|
||||
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
|
||||
};
|
||||
}
|
||||
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.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<T>(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<Omit<ModerationPlayer, 'aliases' | 'servers' | 'restrictions'>>(
|
||||
'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<Omit<ModerationSanction, 'active'>>(
|
||||
'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.created_at DESC,p.id'
|
||||
).map((s) => ({ ...s, active: !s.revokedAt && (!s.expiresAt || s.expiresAt > now) }));
|
||||
} else if (query.view === 'cases') {
|
||||
result.cases = rows<Omit<ModerationCase, 'evidence'>>(
|
||||
'moderation_cases',
|
||||
'p.id,p.game,p.guid,p.title,p.status,p.actor,p.created_at AS createdAt,p.updated_at AS updatedAt,p.revision',
|
||||
'p.updated_at DESC,p.id'
|
||||
).map((c) => ({
|
||||
...c,
|
||||
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<ModerationEvent>(
|
||||
'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;
|
||||
})();
|
||||
}
|
||||
Reference in New Issue
Block a user