95 lines
2.9 KiB
Python
95 lines
2.9 KiB
Python
"""Conversation and participant models for DM chat."""
|
|
|
|
from sqlalchemy import (
|
|
Column,
|
|
Integer,
|
|
String,
|
|
DateTime,
|
|
ForeignKey,
|
|
UniqueConstraint,
|
|
Index,
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class Conversation(Base):
|
|
"""A DM conversation between two users.
|
|
|
|
Each conversation is uniquely identified by the pair of participant
|
|
user IDs (regardless of order). A conversation is soft-deletable
|
|
via ``left_at`` on the participant row.
|
|
"""
|
|
|
|
__tablename__ = "conversations"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
# The most recent message's ID in this conversation (denormalised
|
|
# for fast listing; updated by triggers or app logic).
|
|
latest_message_id = Column(
|
|
Integer, ForeignKey("messages.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
# Relationships
|
|
latest_message = relationship(
|
|
"Message",
|
|
foreign_keys=[latest_message_id],
|
|
lazy="select",
|
|
)
|
|
participants = relationship(
|
|
"ConversationParticipant",
|
|
back_populates="conversation",
|
|
cascade="all, delete-orphan",
|
|
order_by="ConversationParticipant.id",
|
|
)
|
|
|
|
__table_args__ = (
|
|
# Ensure exactly one conversation per unique pair of users
|
|
UniqueConstraint("participant_1_id", "participant_2_id", name="uq_conversation_participants"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Conversation id={self.id}>"
|
|
|
|
|
|
class ConversationParticipant(Base):
|
|
"""Link table between conversations and users.
|
|
|
|
Stores per-participant metadata such as when they left the
|
|
conversation (for soft-delete / archival).
|
|
"""
|
|
|
|
__tablename__ = "conversation_participants"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
conversation_id = Column(
|
|
Integer,
|
|
ForeignKey("conversations.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
user_id = Column(
|
|
Integer,
|
|
ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
# When the participant left (NULL = still active).
|
|
left_at = Column(DateTime(timezone=True), nullable=True)
|
|
# Timestamp of the last message read by this participant.
|
|
last_read_at = Column(DateTime(timezone=True), nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
conversation = relationship("Conversation", back_populates="participants")
|
|
user = relationship("User")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("conversation_id", "user_id", name="uq_conversation_user"),
|
|
Index("ix_conversation_participants_user_id", "user_id"),
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<ConversationParticipant conversation_id={self.conversation_id} user_id={self.user_id}>"
|