-- 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);