Coverage for chats\consumers.py: 94.8%
79 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-30 20:58 +0200
1import json
2import logging
3from collections import defaultdict
5from channels.generic.websocket import AsyncWebsocketConsumer
6from channels.db import database_sync_to_async
7from django.core.exceptions import ValidationError
9from travels.models import Travel
10from chats.models import Chat, user_is_member, chat_display_name
11from chats.services.chat_service import ChatService
12from services.push.push_service import PushNotification
14logger = logging.getLogger(__name__)
17_chat_presence = defaultdict(set)
19# Consumer que gestiona el chat en tiempo real de un viaje.
21class ChatConsumer(AsyncWebsocketConsumer):
23 # Funcion para conectarse al webSocket
24 async def connect(self):
25 self.user = self.scope.get('user')
26 self.travel_id = self.scope['url_route']['kwargs']['travel_id']
28 # Si no hay usuario autenticado, se cierra la conexion
29 if self.user is None:
30 await self.close(code=4001)
31 return
33 # Se comprueba que el usuario pertenezca al chat del viaje
34 self.chat = await self.get_chat_if_member(self.travel_id, self.user)
35 if self.chat is None:
36 await self.close(code=4003)
37 return
39 # Todos los miembros del viaje comparten el mismo grupo del channel layer
40 self.group_name = f"chat_{self.chat.id}"
41 # Se asigna un nombre de grupo adicional para cada usuario, que permite enviarle mensajes de manera individual
42 self.user_group_name = f"chat_{self.chat.id}_user_{self.user.id}"
43 await self.channel_layer.group_add(self.group_name, self.channel_name)
44 await self.channel_layer.group_add(self.user_group_name, self.channel_name)
45 await self.accept()
47 # Se marca al usuario como presente en este chat
48 _chat_presence[str(self.chat.id)].add(str(self.user.id))
49 logger.info(f"User {self.user.username} connected to chat of travel {self.travel_id}")
51 # Funcion para desconectarse del webSocket
52 async def disconnect(self, code):
53 # Se retira al usuario de la presencia del chat
54 if getattr(self, 'chat', None) is not None: 54 ↛ 58line 54 didn't jump to line 58 because the condition on line 54 was always true
55 _chat_presence[str(self.chat.id)].discard(str(self.user.id))
57 # Se le elimina de los grupos del channel layer
58 if getattr(self, 'group_name', None): 58 ↛ 61line 58 didn't jump to line 61 because the condition on line 58 was always true
59 await self.channel_layer.group_discard(self.group_name, self.channel_name)
61 if getattr(self, 'user_group_name', None): 61 ↛ exitline 61 didn't return from function 'disconnect' because the condition on line 61 was always true
62 await self.channel_layer.group_discard(self.user_group_name, self.channel_name)
64 # Funcion para recibir mensajes, se guarda y se reenvia al resto del grupo
65 async def receive(self, text_data=None, bytes_data=None):
66 try:
67 # Se parsea el JSON recibido
68 data = json.loads(text_data)
69 except (TypeError, ValueError): # Si es invalido, se ignora el mensaje
70 return
72 content = (data.get('content') or '').strip()
73 if not content: # Si el mensaje esta vacio, se ignora
74 return
76 # Se guarda el mensaje en la base de datos y se reenvia al resto del grupo
77 message = await self.save_message(content)
78 # save_message devuelve None si el viaje ha finalizado
79 if message is None:
80 return # No se envian mensajes en viajes finalizsados
81 self.last_message_created_at = message.created_at.isoformat()
83 # Se envia el mensaje a todos los miembros del grupo
84 await self.channel_layer.group_send(self.group_name, {
85 'type': 'chat.message',
86 'message': {
87 'id': str(message.id),
88 'user_id': str(self.user.id),
89 'username': self.user.username,
90 'content': message.content,
91 'timestamp': self.last_message_created_at,
92 }
93 })
95 # Se avisa por push a los miembros que no tienen el chat abierto
96 await self.notify_absent_members(content)
98 # Handler que reenvia a cada miembro del chat el mensaje difundido
99 async def chat_message(self, event):
100 await self.send(text_data=json.dumps(event['message']))
102 # Handler dirigido a un usuario al que se le ha expulsado del viaje, se le avisa y se cierra su conexion
103 async def chat_removed(self, event):
104 await self.send(text_data=json.dumps({
105 'type': 'removed',
106 'message': event.get(
107 'message',
108 'El creador del viaje te ha eliminado, ya no tienes acceso a este chat.',
109 ),
110 }))
111 await self.close(code=4003)
113 # Funcion para recuperar el chat del viaje, solo si el usuario es miembro
114 @database_sync_to_async
115 def get_chat_if_member(self, travel_id, user):
116 try:
117 travel = Travel.objects.select_related('creation_user').get(pk=travel_id, is_deleted=False)
118 except (Travel.DoesNotExist, ValueError, ValidationError): # Si el viaje no existe o el id es invalido, devuelve None
119 return None
120 # Se comprueba que el usuario sea miembro del viaje
121 if not user_is_member(travel, user):
122 return None
124 # Se guardan datos del viaje para no volver a consultarlos al enviar los push
125 self.travel = travel
126 self.chat_name = chat_display_name(travel)
127 return Chat.get_or_create_for_travel(travel)
129 # Funcion para guardar un mensaje en la base de datos
130 @database_sync_to_async
131 def save_message(self, content):
132 return ChatService.save_message(self.travel_id, self.chat, self.user, content)
134 # Envia una notificacion push del mensaje a los miembros del chat que no lo tienen abierto
135 @database_sync_to_async
136 def notify_absent_members(self, content):
137 present = set(_chat_presence.get(str(self.chat.id), set()))
139 # El servicio decide a quien hay que avisar, aqui solo se envia
140 for member in ChatService.members_to_notify(self.chat, self.travel, self.user, present):
141 member_id = str(member.id)
142 try:
143 PushNotification.send_to_user(
144 user=member,
145 title=self.chat_name,
146 body=f"{self.user.username}: {content}",
147 notification_type="chat_message",
148 data={
149 'travel_id': str(self.travel_id),
150 'chat_name': self.chat_name,
151 'sender': self.user.username,
152 'content': content,
153 'timestamp': self.last_message_created_at,
154 },
155 )
156 except Exception as e:
157 logger.error(f"Error enviando push de chat a {member_id}: {str(e)}")