Files
microdao-daarion/migrations/001_create_messenger_schema.sql

242 lines
9.6 KiB
PL/PgSQL

-- Migration: 001_create_messenger_schema
-- Description: Create Messenger module tables (Matrix-aware)
-- Date: 2025-11-24
-- ============================================================================
-- CHANNELS (Matrix rooms wrapper)
-- ============================================================================
CREATE TABLE channels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
-- MicroDAO context
microdao_id TEXT NOT NULL, -- microdao:7
team_id UUID NULL, -- FK to teams table (if exists)
-- Matrix integration
matrix_room_id TEXT NOT NULL UNIQUE, -- !roomid:daarion.city
matrix_version TEXT DEFAULT '10', -- Matrix room version
-- Visibility and access
visibility TEXT NOT NULL DEFAULT 'microdao', -- public | private | microdao
is_direct BOOLEAN NOT NULL DEFAULT false, -- DM channel
is_encrypted BOOLEAN NOT NULL DEFAULT false, -- E2EE enabled
-- Metadata
topic TEXT,
avatar_url TEXT, -- mxc:// URL
-- Audit
created_by TEXT NOT NULL, -- user:... | agent:...
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
archived_at TIMESTAMPTZ NULL,
-- Constraints
CONSTRAINT channels_slug_microdao_unique UNIQUE (slug, microdao_id),
CONSTRAINT channels_visibility_check CHECK (visibility IN ('public', 'private', 'microdao'))
);
-- Indexes
CREATE INDEX channels_microdao_idx ON channels(microdao_id);
CREATE INDEX channels_matrix_room_idx ON channels(matrix_room_id);
CREATE INDEX channels_created_at_idx ON channels(created_at DESC);
CREATE INDEX channels_visibility_idx ON channels(visibility) WHERE archived_at IS NULL;
COMMENT ON TABLE channels IS 'DAARION channels mapped to Matrix rooms';
COMMENT ON COLUMN channels.matrix_room_id IS 'Matrix room ID (!roomid:server)';
COMMENT ON COLUMN channels.visibility IS 'public = city-wide, microdao = microDAO members only, private = invited only';
-- ============================================================================
-- MESSAGES (Index of Matrix events, not primary storage)
-- ============================================================================
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
-- Matrix event
matrix_event_id TEXT NOT NULL UNIQUE, -- $event:server
matrix_type TEXT NOT NULL, -- m.room.message, m.reaction, etc
-- Sender
sender_id TEXT NOT NULL, -- user:... | agent:...
sender_type TEXT NOT NULL, -- human | agent
sender_matrix_id TEXT NOT NULL, -- @user:server
-- Content (indexed copy, full content in Matrix)
content_preview TEXT NOT NULL, -- truncated plaintext
content_type TEXT NOT NULL DEFAULT 'text', -- text | image | file | audio | video
-- Threading
thread_id UUID NULL REFERENCES messages(id), -- reply to message
-- Metadata
edited_at TIMESTAMPTZ NULL,
deleted_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Constraints
CONSTRAINT messages_sender_type_check CHECK (sender_type IN ('human', 'agent'))
);
-- Indexes
CREATE INDEX messages_channel_created_idx ON messages(channel_id, created_at DESC);
CREATE INDEX messages_sender_idx ON messages(sender_id);
CREATE INDEX messages_thread_idx ON messages(thread_id) WHERE thread_id IS NOT NULL;
CREATE INDEX messages_matrix_event_idx ON messages(matrix_event_id);
COMMENT ON TABLE messages IS 'Index of Matrix events, full content stored in Matrix';
COMMENT ON COLUMN messages.content_preview IS 'Truncated/plaintext summary for quick access';
COMMENT ON COLUMN messages.matrix_event_id IS 'Matrix event ID ($eventid:server)';
-- ============================================================================
-- CHANNEL_MEMBERS (Permissions and roles)
-- ============================================================================
CREATE TABLE channel_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
-- Member identity
member_id TEXT NOT NULL, -- user:... | agent:...
member_type TEXT NOT NULL, -- human | agent
matrix_user_id TEXT NOT NULL, -- @user:server | @agent:server
-- Role
role TEXT NOT NULL DEFAULT 'member', -- owner | admin | member | guest | agent
-- Capabilities (DAARION-specific)
can_read BOOLEAN NOT NULL DEFAULT true,
can_write BOOLEAN NOT NULL DEFAULT true,
can_invite BOOLEAN NOT NULL DEFAULT false,
can_kick BOOLEAN NOT NULL DEFAULT false,
can_create_tasks BOOLEAN NOT NULL DEFAULT false,
-- Matrix power level
matrix_power_level INTEGER NOT NULL DEFAULT 0, -- 0-100
-- Audit
invited_by TEXT NULL, -- who invited
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
left_at TIMESTAMPTZ NULL,
-- Constraints
CONSTRAINT channel_members_unique UNIQUE (channel_id, member_id),
CONSTRAINT channel_members_type_check CHECK (member_type IN ('human', 'agent')),
CONSTRAINT channel_members_role_check CHECK (role IN ('owner', 'admin', 'member', 'guest', 'agent'))
);
-- Indexes
CREATE INDEX channel_members_channel_idx ON channel_members(channel_id) WHERE left_at IS NULL;
CREATE INDEX channel_members_member_idx ON channel_members(member_id) WHERE left_at IS NULL;
COMMENT ON TABLE channel_members IS 'Channel membership and permissions';
COMMENT ON COLUMN channel_members.matrix_power_level IS 'Matrix room power level (0=user, 50=moderator, 100=admin)';
-- ============================================================================
-- MESSAGE_REACTIONS (Emoji reactions, mapped to Matrix reactions)
-- ============================================================================
CREATE TABLE message_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
-- Reactor
reactor_id TEXT NOT NULL, -- user:... | agent:...
reactor_type TEXT NOT NULL, -- human | agent
-- Reaction
emoji TEXT NOT NULL, -- 👍, ❤️, etc
matrix_event_id TEXT NOT NULL, -- $reaction_event:server
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
removed_at TIMESTAMPTZ NULL,
-- Constraints
CONSTRAINT message_reactions_unique UNIQUE (message_id, reactor_id, emoji),
CONSTRAINT message_reactions_type_check CHECK (reactor_type IN ('human', 'agent'))
);
-- Indexes
CREATE INDEX message_reactions_message_idx ON message_reactions(message_id) WHERE removed_at IS NULL;
COMMENT ON TABLE message_reactions IS 'Message reactions (mapped to m.reaction events)';
-- ============================================================================
-- CHANNEL_EVENTS (Audit log of channel actions)
-- ============================================================================
CREATE TABLE channel_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
-- Event
event_type TEXT NOT NULL, -- channel.created, member.joined, member.left, etc
actor_id TEXT NOT NULL, -- who did it
target_id TEXT NULL, -- who/what was affected
-- Payload
metadata JSONB NULL,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Constraints
CONSTRAINT channel_events_type_check CHECK (
event_type IN (
'channel.created', 'channel.updated', 'channel.archived',
'member.joined', 'member.left', 'member.invited', 'member.kicked',
'member.role_changed', 'message.pinned', 'message.unpinned'
)
)
);
-- Indexes
CREATE INDEX channel_events_channel_idx ON channel_events(channel_id, created_at DESC);
CREATE INDEX channel_events_type_idx ON channel_events(event_type);
COMMENT ON TABLE channel_events IS 'Audit log of channel-level actions';
-- ============================================================================
-- Functions and Triggers
-- ============================================================================
-- Update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER channels_updated_at
BEFORE UPDATE ON channels
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
-- ============================================================================
-- Seed Data (Example channels for DAARION.city)
-- ============================================================================
-- DAARION.city MicroDAO (assuming it exists as microdao:daarion)
INSERT INTO channels (slug, name, description, microdao_id, matrix_room_id, visibility, created_by)
VALUES
('general', 'General', 'Main DAARION.city channel', 'microdao:daarion', '!general:daarion.city', 'public', 'system:daarion'),
('announcements', 'Announcements', 'Official announcements', 'microdao:daarion', '!announcements:daarion.city', 'public', 'system:daarion'),
('agent-hub', 'Agent Hub', 'Chat with Team Assistant', 'microdao:daarion', '!agent-hub:daarion.city', 'microdao', 'system:daarion')
ON CONFLICT DO NOTHING;
COMMENT ON SCHEMA public IS 'Messenger schema v1 - Matrix-aware implementation';