Files
gitks/migrate/009_im_features.sql
T
2026-06-07 11:30:56 +08:00

321 lines
14 KiB
SQL

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