-- Migration 002: Messaging System -- Creates tables for conversations, messages, participants, reactions, calls, and files -- ============================================================================ -- CONVERSATIONS -- ============================================================================ CREATE TABLE IF NOT EXISTS conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), type VARCHAR(20) NOT NULL CHECK (type IN ('direct', 'group', 'channel')), title VARCHAR(200), description TEXT, avatar_url VARCHAR(500), created_by UUID REFERENCES users(id) ON DELETE SET NULL, gameforge_project_id UUID, -- For GameForge integration (future) is_archived BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_conversations_type ON conversations(type); CREATE INDEX idx_conversations_creator ON conversations(created_by); CREATE INDEX idx_conversations_project ON conversations(gameforge_project_id); CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC); -- ============================================================================ -- CONVERSATION PARTICIPANTS -- ============================================================================ CREATE TABLE IF NOT EXISTS conversation_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, identity_id UUID REFERENCES identities(id) ON DELETE SET NULL, role VARCHAR(20) DEFAULT 'member' CHECK (role IN ('admin', 'moderator', 'member')), joined_at TIMESTAMP DEFAULT NOW(), last_read_at TIMESTAMP, notification_settings JSONB DEFAULT '{"enabled": true, "mentions_only": false}'::jsonb, UNIQUE(conversation_id, user_id) ); CREATE INDEX idx_participants_conversation ON conversation_participants(conversation_id); CREATE INDEX idx_participants_user ON conversation_participants(user_id); CREATE INDEX idx_participants_identity ON conversation_participants(identity_id); -- ============================================================================ -- MESSAGES -- ============================================================================ CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, sender_identity_id UUID REFERENCES identities(id) ON DELETE SET NULL, content_encrypted TEXT NOT NULL, -- Encrypted message content content_type VARCHAR(20) DEFAULT 'text' CHECK (content_type IN ('text', 'image', 'video', 'audio', 'file')), metadata JSONB, -- Attachments, mentions, reactions, etc. reply_to_id UUID REFERENCES messages(id) ON DELETE SET NULL, edited_at TIMESTAMP, deleted_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_messages_conversation ON messages(conversation_id, created_at DESC); CREATE INDEX idx_messages_sender ON messages(sender_id); CREATE INDEX idx_messages_reply_to ON messages(reply_to_id); CREATE INDEX idx_messages_created ON messages(created_at DESC); -- ============================================================================ -- MESSAGE REACTIONS -- ============================================================================ CREATE TABLE IF NOT EXISTS message_reactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, emoji VARCHAR(20) NOT NULL, created_at TIMESTAMP DEFAULT NOW(), UNIQUE(message_id, user_id, emoji) ); CREATE INDEX idx_reactions_message ON message_reactions(message_id); CREATE INDEX idx_reactions_user ON message_reactions(user_id); -- ============================================================================ -- FILES -- ============================================================================ CREATE TABLE IF NOT EXISTS files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), uploader_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL, filename VARCHAR(255) NOT NULL, original_filename VARCHAR(255) NOT NULL, mime_type VARCHAR(100) NOT NULL, size_bytes BIGINT NOT NULL, storage_url VARCHAR(500) NOT NULL, -- GCP Cloud Storage URL or Supabase Storage thumbnail_url VARCHAR(500), -- For images/videos encryption_key TEXT, -- If file is encrypted created_at TIMESTAMP DEFAULT NOW(), expires_at TIMESTAMP -- For temporary files ); CREATE INDEX idx_files_uploader ON files(uploader_id); CREATE INDEX idx_files_conversation ON files(conversation_id); CREATE INDEX idx_files_created ON files(created_at DESC); -- ============================================================================ -- CALLS -- ============================================================================ CREATE TABLE IF NOT EXISTS calls ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL, type VARCHAR(20) NOT NULL CHECK (type IN ('voice', 'video')), initiator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, status VARCHAR(20) DEFAULT 'ringing' CHECK (status IN ('ringing', 'active', 'ended', 'missed', 'declined')), started_at TIMESTAMP, ended_at TIMESTAMP, duration_seconds INTEGER, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_calls_conversation ON calls(conversation_id); CREATE INDEX idx_calls_initiator ON calls(initiator_id); CREATE INDEX idx_calls_status ON calls(status); CREATE INDEX idx_calls_created ON calls(created_at DESC); -- ============================================================================ -- CALL PARTICIPANTS -- ============================================================================ CREATE TABLE IF NOT EXISTS call_participants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, joined_at TIMESTAMP, left_at TIMESTAMP, media_state JSONB DEFAULT '{"audio": true, "video": false, "screen_share": false}'::jsonb, UNIQUE(call_id, user_id) ); CREATE INDEX idx_call_participants_call ON call_participants(call_id); CREATE INDEX idx_call_participants_user ON call_participants(user_id); -- ============================================================================ -- FUNCTIONS AND TRIGGERS -- ============================================================================ -- Function to update conversation updated_at timestamp when messages are added CREATE OR REPLACE FUNCTION update_conversation_timestamp() RETURNS TRIGGER AS $$ BEGIN UPDATE conversations SET updated_at = NOW() WHERE id = NEW.conversation_id; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger to update conversation timestamp on new message CREATE TRIGGER trigger_update_conversation_timestamp AFTER INSERT ON messages FOR EACH ROW EXECUTE FUNCTION update_conversation_timestamp(); -- Function to automatically create direct conversation if it doesn't exist CREATE OR REPLACE FUNCTION get_or_create_direct_conversation(user1_id UUID, user2_id UUID) RETURNS UUID AS $$ DECLARE conv_id UUID; BEGIN -- Try to find existing direct conversation between these users SELECT c.id INTO conv_id FROM conversations c WHERE c.type = 'direct' AND EXISTS ( SELECT 1 FROM conversation_participants cp1 WHERE cp1.conversation_id = c.id AND cp1.user_id = user1_id ) AND EXISTS ( SELECT 1 FROM conversation_participants cp2 WHERE cp2.conversation_id = c.id AND cp2.user_id = user2_id ); -- If not found, create new direct conversation IF conv_id IS NULL THEN INSERT INTO conversations (type, created_by) VALUES ('direct', user1_id) RETURNING id INTO conv_id; -- Add both participants INSERT INTO conversation_participants (conversation_id, user_id) VALUES (conv_id, user1_id), (conv_id, user2_id); END IF; RETURN conv_id; END; $$ LANGUAGE plpgsql;