from rest_framework import serializers from Auth.models import Profile from GestionMessagerie.models import Messagerie, Conversation, ConversationParticipant, Message, MessageRead, UserPresence from channels.layers import get_channel_layer from asgiref.sync import async_to_sync class ProfileSimpleSerializer(serializers.ModelSerializer): """Sérialiseur simple pour les profils utilisateur""" class Meta: model = Profile fields = ['id', 'first_name', 'last_name', 'email'] class UserPresenceSerializer(serializers.ModelSerializer): """Sérialiseur pour la présence utilisateur""" user = ProfileSimpleSerializer(read_only=True) class Meta: model = UserPresence fields = ['user', 'status', 'last_seen', 'is_typing_in'] class MessageReadSerializer(serializers.ModelSerializer): """Sérialiseur pour les messages lus""" participant = ProfileSimpleSerializer(read_only=True) class Meta: model = MessageRead fields = ['participant', 'read_at'] class MessageSerializer(serializers.ModelSerializer): """Sérialiseur pour les messages instantanés""" sender = ProfileSimpleSerializer(read_only=True) read_by = MessageReadSerializer(many=True, read_only=True) attachment = serializers.SerializerMethodField() is_read = serializers.SerializerMethodField() class Meta: model = Message fields = ['id', 'conversation', 'sender', 'content', 'message_type', 'file_url', 'file_name', 'file_size', 'file_type', 'attachment', 'created_at', 'updated_at', 'is_edited', 'is_deleted', 'read_by', 'is_read'] read_only_fields = ['id', 'created_at', 'updated_at'] def get_attachment(self, obj): """Retourne les informations du fichier attaché sous forme d'objet""" if obj.file_url: return { 'fileName': obj.file_name, 'fileSize': obj.file_size, 'fileType': obj.file_type, 'fileUrl': obj.file_url, } return None def get_is_read(self, obj): """Détermine si le message est lu par l'utilisateur actuel""" user = self.context.get('user') if not user or not user.is_authenticated: return False # Si c'est le message de l'utilisateur lui-même, vérifier si quelqu'un d'autre l'a lu if obj.sender == user: # Pour les messages envoyés par l'utilisateur, vérifier si au moins un autre participant l'a explicitement lu # Utiliser le modèle MessageRead pour une vérification précise from .models import MessageRead other_participants = obj.conversation.participants.exclude(participant=user).filter(is_active=True) for participant in other_participants: # Vérifier si ce participant a explicitement lu ce message if MessageRead.objects.filter(message=obj, participant=participant.participant).exists(): return True # Fallback: vérifier last_read_at seulement si l'utilisateur était en ligne récemment # ou si last_read_at est postérieur à created_at (lecture explicite après réception) if (participant.last_read_at and participant.last_read_at > obj.created_at): # Vérifier la présence de l'utilisateur pour s'assurer qu'il était en ligne try: from .models import UserPresence user_presence = UserPresence.objects.filter(user=participant.participant).first() # Si l'utilisateur était en ligne récemment (dans les 5 minutes suivant le message) # ou si last_read_at est bien après created_at (lecture délibérée) time_diff = participant.last_read_at - obj.created_at if (user_presence and user_presence.last_seen and user_presence.last_seen >= obj.created_at) or time_diff.total_seconds() > 10: return True except: # En cas d'erreur, continuer avec la logique conservative pass return False else: # Pour les messages reçus, vérifier si l'utilisateur actuel l'a lu # D'abord vérifier dans MessageRead pour une lecture explicite from .models import MessageRead if MessageRead.objects.filter(message=obj, participant=user).exists(): return True # Fallback: vérifier last_read_at du participant participant = obj.conversation.participants.filter( participant=user, is_active=True ).first() if participant and participant.last_read_at: # Seulement considérer comme lu si last_read_at est postérieur à created_at return participant.last_read_at > obj.created_at return False class ConversationParticipantSerializer(serializers.ModelSerializer): """Sérialiseur pour les participants d'une conversation""" participant = ProfileSimpleSerializer(read_only=True) class Meta: model = ConversationParticipant fields = ['participant', 'joined_at', 'last_read_at', 'is_active'] class ConversationSerializer(serializers.ModelSerializer): """Sérialiseur pour les conversations""" participants = ConversationParticipantSerializer(many=True, read_only=True) last_message = serializers.SerializerMethodField() unread_count = serializers.SerializerMethodField() interlocuteur = serializers.SerializerMethodField() class Meta: model = Conversation fields = ['id', 'name', 'conversation_type', 'created_at', 'updated_at', 'last_activity', 'is_active', 'participants', 'last_message', 'unread_count', 'interlocuteur'] read_only_fields = ['id', 'created_at', 'updated_at'] def get_last_message(self, obj): last_message = obj.messages.filter(is_deleted=False).last() if last_message: return MessageSerializer(last_message).data return None def get_unread_count(self, obj): user = self.context.get('user') if not user or not user.is_authenticated: return 0 participant = obj.participants.filter(participant=user).first() if not participant: return 0 # Nouvelle logique : compter les messages qui ne sont PAS dans MessageRead # et qui ont été créés après last_read_at (ou tous si last_read_at est None) # Base query : messages de la conversation, excluant les propres messages et les supprimés # ET ne comptant que les messages textuels base_query = obj.messages.filter( is_deleted=False, message_type='text' # Ne compter que les messages textuels ).exclude(sender=user) # Si l'utilisateur n'a pas de last_read_at, tous les messages sont non lus if not participant.last_read_at: unread_from_timestamp = base_query else: # Messages créés après le dernier moment de lecture unread_from_timestamp = base_query.filter( created_at__gt=participant.last_read_at ) # Soustraire les messages explicitement marqués comme lus dans MessageRead from .models import MessageRead read_message_ids = MessageRead.objects.filter( participant=user, message__conversation=obj ).values_list('message_id', flat=True) # Compter les messages non lus = messages après last_read_at MOINS ceux explicitement lus unread_count = unread_from_timestamp.exclude( id__in=read_message_ids ).count() return unread_count def get_interlocuteur(self, obj): """Pour les conversations privées, retourne l'autre participant""" user = self.context.get('user') if not user or not user.is_authenticated or obj.conversation_type != 'private': return None # Trouver l'autre participant (pas l'utilisateur actuel) other_participant = obj.participants.filter(is_active=True).exclude(participant=user).first() if other_participant: return ProfileSimpleSerializer(other_participant.participant).data return None class ConversationCreateSerializer(serializers.ModelSerializer): """Sérialiseur pour créer une conversation""" participant_ids = serializers.ListField( child=serializers.IntegerField(), write_only=True ) class Meta: model = Conversation fields = ['name', 'conversation_type', 'participant_ids'] def create(self, validated_data): participant_ids = validated_data.pop('participant_ids') conversation_type = validated_data.get('conversation_type', 'private') # Pour les conversations privées, ne pas utiliser de nom spécifique # Le nom sera géré côté frontend en affichant le nom de l'interlocuteur if conversation_type == 'private': validated_data['name'] = None conversation = super().create(validated_data) # Ajouter les participants participants = [] for participant_id in participant_ids: try: participant = Profile.objects.get(id=participant_id) ConversationParticipant.objects.create( conversation=conversation, participant=participant ) participants.append(participant) except Profile.DoesNotExist: continue # Notifier les participants via WebSocket de la nouvelle conversation try: from channels.layers import get_channel_layer from asgiref.sync import async_to_sync channel_layer = get_channel_layer() if channel_layer: # Envoyer à chaque participant avec le bon contexte for participant in participants: # Sérialiser la conversation avec le contexte de ce participant conversation_data = ConversationSerializer(conversation, context={'user': participant}).data async_to_sync(channel_layer.group_send)( f'user_{participant.id}', { 'type': 'new_conversation_notification', 'conversation': conversation_data } ) except Exception as e: # Log l'erreur mais ne pas interrompre la création de la conversation import logging logger = logging.getLogger(__name__) logger.error(f"Erreur lors de la notification WebSocket de nouvelle conversation: {str(e)}") return conversation # Ancien sérialiseur conservé pour compatibilité class MessageLegacySerializer(serializers.ModelSerializer): destinataire_profil = serializers.SerializerMethodField() emetteur_profil = serializers.SerializerMethodField() class Meta: model = Messagerie fields = '__all__' read_only_fields = ['date_envoi'] def get_destinataire_profil(self, obj): return obj.destinataire.email def get_emetteur_profil(self, obj): return obj.emetteur.email