veza/veza-backend-api/migrations/947_moderation_advanced_v0112.sql
senke 0002af1a3a feat(v0.11.2): F411-F420 database migrations and models for advanced moderation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:41:38 +01:00

138 lines
7 KiB
SQL

-- Migration 947: Advanced Moderation v0.11.2 (F411-F420)
-- Extends moderation system with queue, spam detection, strikes, and fingerprinting
-- F411: Enhance reports table with moderation queue features
ALTER TABLE reports ADD COLUMN IF NOT EXISTS category VARCHAR(50) DEFAULT 'other';
ALTER TABLE reports ADD COLUMN IF NOT EXISTS priority VARCHAR(20) DEFAULT 'normal';
ALTER TABLE reports ADD COLUMN IF NOT EXISTS resolution_note TEXT DEFAULT '';
ALTER TABLE reports ADD COLUMN IF NOT EXISTS resolution_action VARCHAR(50) DEFAULT '';
ALTER TABLE reports ADD COLUMN IF NOT EXISTS assigned_to UUID REFERENCES users(id);
ALTER TABLE reports ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
CREATE INDEX IF NOT EXISTS idx_reports_status_priority ON reports(status, priority);
CREATE INDEX IF NOT EXISTS idx_reports_assigned_to ON reports(assigned_to);
CREATE INDEX IF NOT EXISTS idx_reports_category ON reports(category);
CREATE INDEX IF NOT EXISTS idx_reports_created_at ON reports(created_at);
-- F413: Spam detection rules table (deterministic, no ML)
CREATE TABLE IF NOT EXISTS spam_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
description TEXT,
rule_type VARCHAR(50) NOT NULL, -- 'duplicate_content', 'excessive_links', 'bot_pattern', 'keyword'
config JSONB NOT NULL DEFAULT '{}', -- rule-specific configuration
is_active BOOLEAN NOT NULL DEFAULT TRUE,
severity VARCHAR(20) NOT NULL DEFAULT 'low', -- 'low', 'medium', 'high'
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Seed default spam rules
INSERT INTO spam_rules (name, description, rule_type, config, severity) VALUES
('Duplicate Title', 'Flag tracks with identical titles uploaded within 24h', 'duplicate_content',
'{"field": "title", "window_hours": 24, "min_duplicates": 3}', 'medium'),
('Excessive Links', 'Flag content with too many URLs', 'excessive_links',
'{"max_links": 5, "fields": ["description", "bio"]}', 'low'),
('Rapid Upload', 'Flag accounts uploading too many tracks too fast', 'bot_pattern',
'{"max_uploads_per_hour": 10, "max_uploads_per_day": 50}', 'high'),
('Rapid Comments', 'Flag accounts posting too many comments too fast', 'bot_pattern',
'{"max_comments_per_minute": 5, "max_comments_per_hour": 60}', 'medium')
ON CONFLICT DO NOTHING;
-- F413: Spam detection log
CREATE TABLE IF NOT EXISTS spam_detections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
rule_id UUID NOT NULL REFERENCES spam_rules(id),
content_type VARCHAR(50) NOT NULL, -- 'track', 'comment', 'profile'
content_id UUID,
details JSONB DEFAULT '{}',
action_taken VARCHAR(50) DEFAULT 'flagged', -- 'flagged', 'auto_hidden', 'none'
reviewed BOOLEAN DEFAULT FALSE,
reviewed_by UUID REFERENCES users(id),
reviewed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_spam_detections_user_id ON spam_detections(user_id);
CREATE INDEX IF NOT EXISTS idx_spam_detections_reviewed ON spam_detections(reviewed) WHERE reviewed = FALSE;
CREATE INDEX IF NOT EXISTS idx_spam_detections_created_at ON spam_detections(created_at);
-- F414: Audio fingerprint results
CREATE TABLE IF NOT EXISTS audio_fingerprints (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 'pending', 'clean', 'matched', 'error'
matched_title VARCHAR(255),
matched_artist VARCHAR(255),
matched_album VARCHAR(255),
confidence DECIMAL(5,2), -- match confidence 0-100
external_id VARCHAR(255), -- ACRCloud reference
raw_response JSONB DEFAULT '{}',
reviewed BOOLEAN DEFAULT FALSE,
reviewed_by UUID REFERENCES users(id),
reviewed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT uq_audio_fingerprint_track UNIQUE(track_id)
);
CREATE INDEX IF NOT EXISTS idx_audio_fingerprints_status ON audio_fingerprints(status);
CREATE INDEX IF NOT EXISTS idx_audio_fingerprints_track_id ON audio_fingerprints(track_id);
-- F415: Strike system
CREATE TABLE IF NOT EXISTS user_strikes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
report_id UUID REFERENCES reports(id),
reason TEXT NOT NULL,
severity VARCHAR(20) NOT NULL DEFAULT 'warning', -- 'warning', 'minor', 'major'
issued_by UUID NOT NULL REFERENCES users(id),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
appealed BOOLEAN NOT NULL DEFAULT FALSE,
appeal_text TEXT,
appeal_resolved BOOLEAN NOT NULL DEFAULT FALSE,
appeal_result VARCHAR(20), -- 'upheld', 'overturned'
appeal_resolved_by UUID REFERENCES users(id),
appeal_resolved_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE, -- NULL = permanent
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_strikes_user_id ON user_strikes(user_id);
CREATE INDEX IF NOT EXISTS idx_strikes_active ON user_strikes(user_id, is_active) WHERE is_active = TRUE;
CREATE INDEX IF NOT EXISTS idx_strikes_appealed ON user_strikes(appealed) WHERE appealed = TRUE AND appeal_resolved = FALSE;
-- F415: User suspensions (triggered by strikes)
CREATE TABLE IF NOT EXISTS user_suspensions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
reason TEXT NOT NULL,
suspended_by UUID NOT NULL REFERENCES users(id),
suspended_until TIMESTAMP WITH TIME ZONE, -- NULL = permanent
is_active BOOLEAN NOT NULL DEFAULT TRUE,
lifted_by UUID REFERENCES users(id),
lifted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_suspensions_user_id ON user_suspensions(user_id);
CREATE INDEX IF NOT EXISTS idx_suspensions_active ON user_suspensions(user_id) WHERE is_active = TRUE;
-- Moderation action log for audit trail
CREATE TABLE IF NOT EXISTS moderation_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
moderator_id UUID NOT NULL REFERENCES users(id),
target_user_id UUID REFERENCES users(id),
target_content_type VARCHAR(50),
target_content_id UUID,
action VARCHAR(50) NOT NULL, -- 'approve', 'reject', 'ban_temp', 'ban_perm', 'warn', 'strike', 'suspend', 'unsuspend'
reason TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_mod_actions_moderator ON moderation_actions(moderator_id);
CREATE INDEX IF NOT EXISTS idx_mod_actions_target_user ON moderation_actions(target_user_id);
CREATE INDEX IF NOT EXISTS idx_mod_actions_created_at ON moderation_actions(created_at);