Add moderation permissions and durable player records

This commit is contained in:
2026-10-15 21:06:08 -04:00
parent c1758cce25
commit c91d49c298
17 changed files with 699 additions and 21 deletions
+101
View File
@@ -0,0 +1,101 @@
import type Database from 'better-sqlite3';
import { moderationGuid, type ModerationGame } from '../../moderation.js';
export function migrateModeration(handle: Database.Database): void {
handle.exec(`
CREATE TABLE IF NOT EXISTS moderation_players (
game TEXT NOT NULL CHECK(game IN ('t5','t6')), guid TEXT NOT NULL,
name TEXT NOT NULL, first_seen_at TEXT NOT NULL, last_seen_at TEXT NOT NULL,
PRIMARY KEY(game,guid)
);
CREATE TABLE IF NOT EXISTS moderation_aliases (
game TEXT NOT NULL, guid TEXT NOT NULL, name TEXT NOT NULL,
first_seen_at TEXT NOT NULL, last_seen_at TEXT NOT NULL,
PRIMARY KEY(game,guid,name)
);
CREATE INDEX IF NOT EXISTS moderation_alias_name ON moderation_aliases(name COLLATE NOCASE);
CREATE TABLE IF NOT EXISTS moderation_policy (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL);
INSERT OR IGNORE INTO moderation_policy VALUES(1,1);
CREATE TABLE IF NOT EXISTS moderation_sanctions (
id TEXT PRIMARY KEY, game TEXT NOT NULL CHECK(game IN ('t5','t6')), guid TEXT NOT NULL,
kind TEXT NOT NULL CHECK(kind IN ('ban','mute')),
scope TEXT NOT NULL CHECK(scope IN ('server','game','fleet')),
server TEXT, server_created_at TEXT, reason TEXT NOT NULL, note TEXT NOT NULL,
actor_id TEXT NOT NULL, actor TEXT NOT NULL,
created_at TEXT NOT NULL, expires_at TEXT, revoked_at TEXT,
revision INTEGER NOT NULL DEFAULT 1,
CHECK((scope='server' AND server IS NOT NULL AND server_created_at IS NOT NULL)
OR (scope!='server' AND server IS NULL AND server_created_at IS NULL))
);
CREATE INDEX IF NOT EXISTS moderation_sanction_subject ON moderation_sanctions(guid,game,revoked_at,expires_at);
CREATE TABLE IF NOT EXISTS moderation_actions (
id TEXT PRIMARY KEY, game TEXT NOT NULL, guid TEXT, server TEXT NOT NULL,
instance_id TEXT NOT NULL, generation INTEGER NOT NULL,
session TEXT NOT NULL, connection TEXT,
kind TEXT NOT NULL CHECK(kind IN ('warn','message','kick','announce')),
reason TEXT NOT NULL, actor_id TEXT NOT NULL, actor TEXT NOT NULL,
created_at TEXT NOT NULL, expires_at TEXT NOT NULL,
state TEXT NOT NULL CHECK(state IN ('queued','delivered','confirmed','failed','expired','unknown')),
detail TEXT, delivered_at TEXT, finished_at TEXT
);
CREATE INDEX IF NOT EXISTS moderation_action_delivery ON moderation_actions(instance_id,generation,state,expires_at);
CREATE TABLE IF NOT EXISTS moderation_cases (
id TEXT PRIMARY KEY, game TEXT NOT NULL, guid TEXT NOT NULL, title TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('open','resolved','dismissed')),
actor_id TEXT NOT NULL, actor TEXT NOT NULL,
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS moderation_evidence (
id TEXT PRIMARY KEY, case_id TEXT NOT NULL REFERENCES moderation_cases(id),
text TEXT NOT NULL, source TEXT NOT NULL, snapshot TEXT,
actor_id TEXT NOT NULL, actor TEXT NOT NULL, created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS moderation_events (
id INTEGER PRIMARY KEY AUTOINCREMENT, game TEXT, guid TEXT,
actor_id TEXT, actor TEXT NOT NULL, action TEXT NOT NULL,
subject_id TEXT, detail TEXT NOT NULL, created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS moderation_event_subject ON moderation_events(game,guid,id);
CREATE TABLE IF NOT EXISTS moderation_requests (
actor_id TEXT NOT NULL, request_key TEXT NOT NULL,
fingerprint TEXT NOT NULL, result TEXT NOT NULL, created_at TEXT NOT NULL,
PRIMARY KEY(actor_id,request_key)
);
CREATE TABLE IF NOT EXISTS moderation_credentials (
instance_id TEXT NOT NULL, generation INTEGER NOT NULL,
token TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL,
PRIMARY KEY(instance_id,generation)
);
CREATE TABLE IF NOT EXISTS moderation_runtimes (
instance_id TEXT PRIMARY KEY, generation INTEGER NOT NULL,
session TEXT NOT NULL, capabilities TEXT NOT NULL, players TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 0, connected_at TEXT NOT NULL
);
`);
const player = handle.prepare(`INSERT INTO moderation_players VALUES(?,?,?,?,?)
ON CONFLICT(game,guid) DO UPDATE SET
name=CASE WHEN excluded.last_seen_at>=last_seen_at THEN excluded.name ELSE name END,
first_seen_at=MIN(first_seen_at,excluded.first_seen_at),last_seen_at=MAX(last_seen_at,excluded.last_seen_at)`);
const alias = handle.prepare(`INSERT INTO moderation_aliases VALUES(?,?,?,?,?)
ON CONFLICT(game,guid,name) DO UPDATE SET
first_seen_at=MIN(first_seen_at,excluded.first_seen_at),last_seen_at=MAX(last_seen_at,excluded.last_seen_at)`);
for (const row of handle
.prepare("SELECT * FROM leaderboard_players WHERE game IN ('t5','t6')")
.iterate() as Iterable<{
game: ModerationGame;
guid: string;
name: string;
first_seen_at: string;
last_seen_at: string;
}>) {
const values = [
row.game,
moderationGuid(row.game, row.guid),
row.name,
row.first_seen_at,
row.last_seen_at
];
player.run(...values);
alias.run(...values);
}
}