"""SQLAlchemy models for the kommunikation plugin.""" from __future__ import annotations import uuid from datetime import datetime from typing import Any from sqlalchemy import ( Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin class CommConversation(Base, TenantMixin): """Conversation / Room — tenant-scoped, supports pinning, locking, archiving.""" __tablename__ = "comm_conversations" __table_args__ = ( Index("ix_comm_conversations_tenant", "tenant_id"), Index("ix_comm_conversations_tenant_pinned", "tenant_id", "is_pinned"), Index("ix_comm_conversations_last_msg", "tenant_id", "last_msg_at"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) title: Mapped[str | None] = mapped_column(String(255), nullable=True) title_set_by: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) is_pinned: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_locked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) locked_by: Mapped[str | None] = mapped_column(String(100), nullable=True) is_direct: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) created_by: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) created_by_type: Mapped[str] = mapped_column(String(20), nullable=False, default="user") last_msg_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) last_msg_preview: Mapped[str | None] = mapped_column(Text, nullable=True) last_msg_sender_type: Mapped[str | None] = mapped_column(String(50), nullable=True) metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False) class CommParticipant(Base, TenantMixin): """Participant in a conversation — user, ai, system, gateway, etc.""" __tablename__ = "comm_participants" __table_args__ = ( UniqueConstraint( "conversation_id", "participant_id", "participant_type", name="uq_comm_participants_conv_part_type", ), Index("ix_comm_participants_tenant_conv", "tenant_id", "conversation_id"), Index("ix_comm_participants_tenant_user", "tenant_id", "participant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) conversation_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_conversations.id", ondelete="CASCADE"), nullable=False, index=True, ) participant_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) participant_type: Mapped[str] = mapped_column(String(50), nullable=False) display_name: Mapped[str | None] = mapped_column(String(255), nullable=True) role: Mapped[str] = mapped_column(String(20), nullable=False, default="member") joined_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) left_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class CommMessage(Base, TenantMixin): """Message in a conversation — text content plus rich content blocks.""" __tablename__ = "comm_messages" __table_args__ = ( Index("ix_comm_messages_tenant_conv", "tenant_id", "conversation_id", "created_at"), Index("ix_comm_messages_tenant_sender", "tenant_id", "sender_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) conversation_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_conversations.id", ondelete="CASCADE"), nullable=False, index=True, ) sender_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) sender_type: Mapped[str] = mapped_column(String(50), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False, default="") content_format: Mapped[str] = mapped_column(String(20), nullable=False, default="text") metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False) reply_to_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_messages.id", ondelete="SET NULL"), nullable=True, ) is_pinned: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) read_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) edited_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class CommMessageBlock(Base, TenantMixin): """Rich content block attached to a message.""" __tablename__ = "comm_message_blocks" __table_args__ = ( Index("ix_comm_blocks_tenant_msg", "tenant_id", "message_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) message_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_messages.id", ondelete="CASCADE"), nullable=False, index=True, ) block_type: Mapped[str] = mapped_column(String(50), nullable=False) block_data: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) class CommMessageAttachment(Base, TenantMixin): """File attachment on a message — DMS reference or comm-internal upload.""" __tablename__ = "comm_message_attachments" __table_args__ = ( Index("ix_comm_attachments_tenant_msg", "tenant_id", "message_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) message_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_messages.id", ondelete="CASCADE"), nullable=False, index=True, ) file_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) file_source: Mapped[str] = mapped_column(String(10), nullable=False, default="comm") file_name: Mapped[str] = mapped_column(String(255), nullable=False) file_type: Mapped[str] = mapped_column(String(255), nullable=False) file_size: Mapped[int | None] = mapped_column(Integer, nullable=True) thumbnail_path: Mapped[str | None] = mapped_column(String(1024), nullable=True) metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False) class CommMessageReaction(Base, TenantMixin): """Emoji reaction on a message.""" __tablename__ = "comm_message_reactions" __table_args__ = ( UniqueConstraint("message_id", "user_id", "emoji", name="uq_comm_reactions_msg_user_emoji"), Index("ix_comm_reactions_tenant_msg", "tenant_id", "message_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) message_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_messages.id", ondelete="CASCADE"), nullable=False, index=True, ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) emoji: Mapped[str] = mapped_column(String(50), nullable=False) class CommMessageRead(Base, TenantMixin): """Read state per user per conversation.""" __tablename__ = "comm_message_reads" __table_args__ = ( UniqueConstraint("conversation_id", "user_id", name="uq_comm_reads_conv_user"), Index("ix_comm_reads_tenant_user", "tenant_id", "user_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) conversation_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_conversations.id", ondelete="CASCADE"), nullable=False, ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) last_read_msg_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_messages.id", ondelete="SET NULL"), nullable=True, ) last_read_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class CommConversationPin(Base, TenantMixin): """User-specific conversation pinning.""" __tablename__ = "comm_conversation_pins" __table_args__ = ( UniqueConstraint("conversation_id", "user_id", name="uq_comm_pins_conv_user"), Index("ix_comm_pins_tenant_user", "tenant_id", "user_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) conversation_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_conversations.id", ondelete="CASCADE"), nullable=False, ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) pinned_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class CommConversationMute(Base, TenantMixin): """User-specific conversation muting.""" __tablename__ = "comm_conversation_mutes" __table_args__ = ( UniqueConstraint("conversation_id", "user_id", name="uq_comm_mutes_conv_user"), Index("ix_comm_mutes_tenant_user", "tenant_id", "user_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) conversation_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_conversations.id", ondelete="CASCADE"), nullable=False, ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) muted_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class CommMessageEdit(Base, TenantMixin): """Edit history for messages — stores old content before each edit.""" __tablename__ = "comm_message_edits" __table_args__ = ( Index("ix_comm_edits_tenant_msg", "tenant_id", "message_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) message_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("comm_messages.id", ondelete="CASCADE"), nullable=False, index=True, ) old_content: Mapped[str] = mapped_column(Text, nullable=False) old_blocks: Mapped[list[Any]] = mapped_column(JSONB, default=list, nullable=False) edited_by: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) edited_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() )