-- 009: IM Features — Discord/Slack-class messaging support -- -- New tables: -- user_presence, user_activity, -- channel_category, channel_permission_overwrite, im_integration, -- message_attachment, message_embed, message_draft, message_pin, -- message_edit_history, saved_message, thread_read_state, -- custom_emoji -- ============================================================ -- 1. User Presence -- ============================================================ -- models/users/user_presence.rs → user_presence CREATE TABLE IF NOT EXISTS user_presence ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, status TEXT NOT NULL, custom_status_text TEXT NULL, custom_status_emoji TEXT NULL, device_type TEXT NULL, ip_address TEXT NULL, last_active_at TIMESTAMPTZ NOT NULL, last_seen_at TIMESTAMPTZ NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_user_presence_user_id UNIQUE (user_id) ); CREATE INDEX IF NOT EXISTS idx_user_presence_status ON user_presence (status); -- ============================================================ -- 2. User Activity (Rich Presence) -- ============================================================ -- models/users/user_activity.rs → user_activity CREATE TABLE IF NOT EXISTS user_activity ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, activity_type TEXT NOT NULL, name TEXT NOT NULL, details TEXT NULL, state TEXT NULL, application_id TEXT NULL, assets JSONB NULL, party_id TEXT NULL, party_current_size INTEGER NULL, party_max_size INTEGER NULL, large_image_url TEXT NULL, small_image_url TEXT NULL, start_at TIMESTAMPTZ NULL, end_at TIMESTAMPTZ NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_user_activity_user_id ON user_activity (user_id); -- ============================================================ -- 3. Channel Categories -- ============================================================ -- models/channels/channel_categories.rs → channel_category CREATE TABLE IF NOT EXISTS channel_category ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, name TEXT NOT NULL, position INTEGER NOT NULL, collapsed BOOLEAN NOT NULL, created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_channel_category_workspace_id ON channel_category (workspace_id); -- ============================================================ -- 4. ALTER channel — add category_id (after channel_category exists) -- ============================================================ ALTER TABLE channel ADD COLUMN IF NOT EXISTS category_id UUID NULL REFERENCES channel_category(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS idx_channel_category_id ON channel (category_id); -- ============================================================ -- 5. Channel Permission Overwrites -- ============================================================ -- models/channels/channel_permission_overwrites.rs → channel_permission_overwrite CREATE TABLE IF NOT EXISTS channel_permission_overwrite ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, target_type TEXT NOT NULL, target_id UUID NOT NULL, allow TEXT[] NOT NULL, deny TEXT[] NOT NULL, created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_channel_perm_overwrite UNIQUE (channel_id, target_type, target_id) ); CREATE INDEX IF NOT EXISTS idx_channel_perm_overwrite_channel_id ON channel_permission_overwrite (channel_id); CREATE INDEX IF NOT EXISTS idx_channel_perm_overwrite_target ON channel_permission_overwrite (target_type, target_id); -- ============================================================ -- 6. IM Integrations (External Bridge) -- ============================================================ -- models/channels/im_integrations.rs → im_integration CREATE TABLE IF NOT EXISTS im_integration ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, provider TEXT NOT NULL, name TEXT NOT NULL, external_workspace_id TEXT NULL, internal_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL, external_channel_id TEXT NULL, bot_token_ciphertext TEXT NULL, webhook_url TEXT NULL, sync_direction TEXT NOT NULL, user_mapping JSONB NULL, enabled BOOLEAN NOT NULL, installed_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, last_sync_at TIMESTAMPTZ NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_im_integration_workspace_id ON im_integration (workspace_id); CREATE INDEX IF NOT EXISTS idx_im_integration_internal_channel_id ON im_integration (internal_channel_id); CREATE INDEX IF NOT EXISTS idx_im_integration_provider ON im_integration (provider); -- ============================================================ -- 7. Message Attachments -- ============================================================ -- models/channels/message_attachments.rs → message_attachment CREATE TABLE IF NOT EXISTS message_attachment ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, filename TEXT NOT NULL, url TEXT NOT NULL, proxy_url TEXT NULL, size_bytes BIGINT NOT NULL, mime_type TEXT NOT NULL, width INTEGER NULL, height INTEGER NULL, duration_ms BIGINT NULL, thumbnail_url TEXT NULL, blurhash TEXT NULL, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_message_attachment_message_id ON message_attachment (message_id); CREATE INDEX IF NOT EXISTS idx_message_attachment_channel_id ON message_attachment (channel_id); -- ============================================================ -- 8. Message Embeds (Rich Text / Link Previews) -- ============================================================ -- models/channels/message_embeds.rs → message_embed CREATE TABLE IF NOT EXISTS message_embed ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, embed_type TEXT NOT NULL, title TEXT NULL, description TEXT NULL, url TEXT NULL, author_name TEXT NULL, author_url TEXT NULL, author_icon_url TEXT NULL, thumbnail_url TEXT NULL, thumbnail_width INTEGER NULL, thumbnail_height INTEGER NULL, image_url TEXT NULL, image_width INTEGER NULL, image_height INTEGER NULL, video_url TEXT NULL, video_width INTEGER NULL, video_height INTEGER NULL, color INTEGER NULL, fields JSONB NULL, footer_text TEXT NULL, footer_icon_url TEXT NULL, provider_name TEXT NULL, provider_url TEXT NULL, "timestamp" TIMESTAMPTZ NULL, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_message_embed_message_id ON message_embed (message_id); -- ============================================================ -- 9. Message Drafts -- ============================================================ -- models/channels/message_drafts.rs → message_draft CREATE TABLE IF NOT EXISTS message_draft ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, thread_id UUID NULL, reply_to_message_id UUID NULL, content TEXT NOT NULL, attachments JSONB NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_message_draft_user_channel UNIQUE (user_id, channel_id) ); CREATE INDEX IF NOT EXISTS idx_message_draft_user_id ON message_draft (user_id); CREATE INDEX IF NOT EXISTS idx_message_draft_channel_id ON message_draft (channel_id); -- ============================================================ -- 10. Custom Emojis -- ============================================================ -- models/channels/custom_emojis.rs → custom_emoji CREATE TABLE IF NOT EXISTS custom_emoji ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, name TEXT NOT NULL, url TEXT NOT NULL, animated BOOLEAN NOT NULL, managed BOOLEAN NOT NULL, created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_custom_emoji_workspace_name UNIQUE (workspace_id, name) ); CREATE INDEX IF NOT EXISTS idx_custom_emoji_workspace_id ON custom_emoji (workspace_id); -- ============================================================ -- 11. Message Pins -- ============================================================ -- models/channels/message_pins.rs → message_pin CREATE TABLE IF NOT EXISTS message_pin ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, pinned_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, pinned_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_message_pin_message_id UNIQUE (message_id) ); CREATE INDEX IF NOT EXISTS idx_message_pin_channel_id ON message_pin (channel_id); CREATE INDEX IF NOT EXISTS idx_message_pin_message_id ON message_pin (message_id); -- ============================================================ -- 12. Message Edit History -- ============================================================ -- models/channels/message_edit_history.rs → message_edit_history CREATE TABLE IF NOT EXISTS message_edit_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, previous_body TEXT NOT NULL, edited_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, edited_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_message_edit_history_message_id ON message_edit_history (message_id); CREATE INDEX IF NOT EXISTS idx_message_edit_history_channel_id ON message_edit_history (channel_id); -- ============================================================ -- 13. Saved Messages -- ============================================================ -- models/channels/saved_messages.rs → saved_message CREATE TABLE IF NOT EXISTS saved_message ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, note TEXT NULL, created_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_saved_message_user_message UNIQUE (user_id, message_id) ); CREATE INDEX IF NOT EXISTS idx_saved_message_user_id ON saved_message (user_id); CREATE INDEX IF NOT EXISTS idx_saved_message_message_id ON saved_message (message_id); -- ============================================================ -- 14. Thread Read States -- ============================================================ -- models/channels/thread_read_states.rs → thread_read_state CREATE TABLE IF NOT EXISTS thread_read_state ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, thread_id UUID NOT NULL, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, last_read_message_id UUID NULL, last_read_at TIMESTAMPTZ NULL, updated_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_thread_read_state_user_thread UNIQUE (user_id, thread_id) ); CREATE INDEX IF NOT EXISTS idx_thread_read_state_user_id ON thread_read_state (user_id); CREATE INDEX IF NOT EXISTS idx_thread_read_state_thread_id ON thread_read_state (thread_id); CREATE INDEX IF NOT EXISTS idx_thread_read_state_channel_id ON thread_read_state (channel_id); -- ============================================================ -- 15. Triggers — auto-refresh updated_at -- ============================================================ DROP TRIGGER IF EXISTS trg_user_presence_updated_at ON user_presence; CREATE TRIGGER trg_user_presence_updated_at BEFORE UPDATE ON user_presence FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_user_activity_updated_at ON user_activity; CREATE TRIGGER trg_user_activity_updated_at BEFORE UPDATE ON user_activity FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_category_updated_at ON channel_category; CREATE TRIGGER trg_channel_category_updated_at BEFORE UPDATE ON channel_category FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_permission_overwrite_updated_at ON channel_permission_overwrite; CREATE TRIGGER trg_channel_permission_overwrite_updated_at BEFORE UPDATE ON channel_permission_overwrite FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_im_integration_updated_at ON im_integration; CREATE TRIGGER trg_im_integration_updated_at BEFORE UPDATE ON im_integration FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_message_draft_updated_at ON message_draft; CREATE TRIGGER trg_message_draft_updated_at BEFORE UPDATE ON message_draft FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_custom_emoji_updated_at ON custom_emoji; CREATE TRIGGER trg_custom_emoji_updated_at BEFORE UPDATE ON custom_emoji FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_thread_read_state_updated_at ON thread_read_state; CREATE TRIGGER trg_thread_read_state_updated_at BEFORE UPDATE ON thread_read_state FOR EACH ROW EXECUTE FUNCTION set_updated_at();