nexus-5/core/models/messaging.py
2026-01-26 11:09:40 -05:00

255 lines
8.3 KiB
Python

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django_choices_field import TextChoicesField
from core.models.base import BaseModel
from core.models.enums import ConversationTypeChoices
class Conversation(BaseModel):
"""
Conversation thread that groups messages together.
Can be linked to specific entities (Project, Service, Account, etc.) for context.
"""
subject = models.CharField(
max_length=500,
help_text="Conversation subject/title"
)
conversation_type = TextChoicesField(
choices_enum=ConversationTypeChoices,
help_text="Type of conversation (DIRECT, GROUP, SUPPORT)"
)
last_message_at = models.DateTimeField(
null=True,
blank=True,
help_text="Timestamp of most recent message"
)
is_archived = models.BooleanField(
default=False,
help_text="Whether this conversation is archived (system-wide)"
)
# Generic foreign key for linking to any entity (Project, Service, Account, etc.)
entity_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='conversations',
help_text="Content type of the related entity"
)
entity_object_id = models.UUIDField(
null=True,
blank=True,
help_text="UUID of the related entity"
)
entity = GenericForeignKey('entity_content_type', 'entity_object_id')
# Generic foreign key for conversation creator
created_by_content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_conversations',
help_text="Content type of the creator (TeamProfile or CustomerProfile)"
)
created_by_object_id = models.UUIDField(
null=True,
blank=True,
help_text="UUID of the creator"
)
created_by = GenericForeignKey('created_by_content_type', 'created_by_object_id')
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional conversation metadata"
)
class Meta:
db_table = 'conversations'
ordering = ['-last_message_at', '-created_at']
indexes = [
models.Index(fields=['-last_message_at']),
models.Index(fields=['entity_content_type', 'entity_object_id']),
models.Index(fields=['conversation_type', '-last_message_at']),
models.Index(fields=['created_by_content_type', 'created_by_object_id']),
]
def __str__(self):
entity_info = f" ({self.entity_content_type.model}:{self.entity_object_id})" if self.entity else ""
return f"{self.subject}{entity_info}"
class ConversationParticipant(BaseModel):
"""
Links users (TeamProfile or CustomerProfile) to conversations.
Tracks per-user read status and preferences.
"""
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name='participants',
help_text="Conversation this participant belongs to"
)
# Generic foreign key to support both TeamProfile and CustomerProfile
participant_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name='conversation_participants',
help_text="Content type of the participant (TeamProfile or CustomerProfile)"
)
participant_object_id = models.UUIDField(
help_text="UUID of the participant"
)
participant = GenericForeignKey('participant_content_type', 'participant_object_id')
last_read_at = models.DateTimeField(
null=True,
blank=True,
help_text="Timestamp when participant last read messages in this conversation"
)
unread_count = models.PositiveIntegerField(
default=0,
help_text="Number of unread messages for this participant"
)
is_muted = models.BooleanField(
default=False,
help_text="Whether participant has muted notifications for this conversation"
)
is_archived = models.BooleanField(
default=False,
help_text="Whether participant has archived this conversation (user-specific)"
)
joined_at = models.DateTimeField(
auto_now_add=True,
help_text="When participant joined the conversation"
)
class Meta:
db_table = 'conversation_participants'
ordering = ['conversation', 'joined_at']
indexes = [
models.Index(fields=['participant_content_type', 'participant_object_id', 'is_archived']),
models.Index(fields=['conversation', 'participant_content_type', 'participant_object_id']),
models.Index(fields=['unread_count']),
]
unique_together = [['conversation', 'participant_content_type', 'participant_object_id']]
def __str__(self):
return f"{self.participant} in {self.conversation.subject}"
class Message(BaseModel):
"""
Individual message within a conversation.
"""
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name='messages',
help_text="Conversation this message belongs to"
)
# Generic foreign key for sender (TeamProfile or CustomerProfile)
sender_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name='sent_messages',
help_text="Content type of the sender"
)
sender_object_id = models.UUIDField(
help_text="UUID of the sender"
)
sender = GenericForeignKey('sender_content_type', 'sender_object_id')
body = models.TextField(
help_text="Message content"
)
# For message threading/replies
reply_to = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='replies',
help_text="Message this is replying to (for threading)"
)
# Attachments stored as array of file references
attachments = models.JSONField(
default=list,
blank=True,
help_text="Array of attachment metadata (file paths, names, sizes, types)"
)
is_system_message = models.BooleanField(
default=False,
help_text="Whether this is an automated system message"
)
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional message metadata (formatting, mentions, etc.)"
)
class Meta:
db_table = 'messages'
ordering = ['created_at']
indexes = [
models.Index(fields=['conversation', 'created_at']),
models.Index(fields=['sender_content_type', 'sender_object_id', 'created_at']),
models.Index(fields=['reply_to']),
]
def __str__(self):
preview = self.body[:50] + "..." if len(self.body) > 50 else self.body
return f"Message from {self.sender} in {self.conversation.subject}: {preview}"
class MessageReadReceipt(BaseModel):
"""
Tracks when individual messages are read by specific participants.
Allows for fine-grained read tracking beyond conversation-level.
"""
message = models.ForeignKey(
Message,
on_delete=models.CASCADE,
related_name='read_receipts',
help_text="Message that was read"
)
# Generic foreign key for reader (TeamProfile or CustomerProfile)
reader_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name='message_reads',
help_text="Content type of the reader"
)
reader_object_id = models.UUIDField(
help_text="UUID of the reader"
)
reader = GenericForeignKey('reader_content_type', 'reader_object_id')
read_at = models.DateTimeField(
auto_now_add=True,
help_text="When the message was read"
)
class Meta:
db_table = 'message_read_receipts'
ordering = ['read_at']
indexes = [
models.Index(fields=['message', 'reader_content_type', 'reader_object_id']),
models.Index(fields=['reader_content_type', 'reader_object_id', 'read_at']),
]
unique_together = [['message', 'reader_content_type', 'reader_object_id']]
def __str__(self):
return f"{self.reader} read message {self.message_id} at {self.read_at}"