138 lines
7 KiB
SQL
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);
|