-- ── NIP-56 reports (kind:1984 ingest) ───────────────────────────────────────── -- One row per accepted report event. Reports are signals, never triggers: -- nothing auto-actions on them (NIP-56). Reporter identity is visible to -- moderators in the queue but never revealed to the reported author. CREATE TABLE moderation_reports ( community_id UUID NOT NULL REFERENCES communities(id), id UUID NOT NULL DEFAULT gen_random_uuid(), -- The signed kind:1984 event id (stored for audit/idempotency). report_event_id BYTEA NOT NULL CHECK (length(report_event_id) = 32), reporter_pubkey BYTEA NOT NULL CHECK (length(reporter_pubkey) = 32), -- What was reported. Exactly one target class per row (CHECK-enforced below). target_kind TEXT NOT NULL CHECK (target_kind IN ('event', 'pubkey', 'blob')), target_event_id BYTEA CHECK (target_event_id IS NULL OR length(target_event_id) = 32), target_pubkey BYTEA CHECK (target_pubkey IS NULL OR length(target_pubkey) = 32), target_blob_sha256 BYTEA CHECK (target_blob_sha256 IS NULL OR length(target_blob_sha256) = 32), -- Channel inferred from an in-tenant target event row, when resolvable. channel_id UUID, -- NIP-56 report type: illegal|nudity|malware|spam|impersonation|profanity|other. report_type TEXT NOT NULL, -- Reporter's optional free-text context (mod-queue-only; never public). note TEXT, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'processing', 'resolved', 'dismissed', 'escalated')), -- Non-null when status='processing': the relay_admin_actions row that claimed this report. active_action_id UUID, resolved_by BYTEA, resolved_at TIMESTAMPTZ, -- moderation_actions row that resolved this report, if any. action_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (community_id, id), -- Exactly one target class per row: target_kind is authoritative and the -- matching column (only) is populated. Queue/action code never guesses. CHECK ( (target_kind = 'event' AND target_event_id IS NOT NULL AND target_pubkey IS NULL AND target_blob_sha256 IS NULL) OR (target_kind = 'pubkey' AND target_event_id IS NULL AND target_pubkey IS NOT NULL AND target_blob_sha256 IS NULL) OR (target_kind = 'blob' AND target_event_id IS NULL AND target_pubkey IS NULL AND target_blob_sha256 IS NOT NULL) ), -- Same-community channel provenance (channels are soft-deleted, never -- hard-deleted, so this FK cannot dangle). FOREIGN KEY (community_id, channel_id) REFERENCES channels (community_id, id), -- Same-community resolution provenance: a report can only be resolved by an -- action row in its own community. FOREIGN KEY (community_id, action_id) REFERENCES moderation_actions (community_id, id) ); -- Queue reads: open reports, newest first, per community. CREATE INDEX idx_moderation_reports_status ON moderation_reports (community_id, status, created_at DESC); -- Group-by-target for triage aggregation. CREATE INDEX idx_moderation_reports_target_event ON moderation_reports (community_id, target_event_id) WHERE target_event_id IS NOT NULL; CREATE INDEX idx_moderation_reports_target_pubkey ON moderation_reports (community_id, target_pubkey) WHERE target_pubkey IS NOT NULL; -- Idempotency: one row per report event per community. CREATE UNIQUE INDEX idx_moderation_reports_event ON moderation_reports (community_id, report_event_id);