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

1import json 

2import logging 

3from collections import defaultdict 

4 

5from channels.generic.websocket import AsyncWebsocketConsumer 

6from channels.db import database_sync_to_async 

7from django.core.exceptions import ValidationError 

8 

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 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17_chat_presence = defaultdict(set) 

18 

19# Consumer que gestiona el chat en tiempo real de un viaje. 

20 

21class ChatConsumer(AsyncWebsocketConsumer): 

22 

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'] 

27 

28 # Si no hay usuario autenticado, se cierra la conexion 

29 if self.user is None: 

30 await self.close(code=4001) 

31 return 

32 

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 

38 

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() 

46 

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}") 

50 

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)) 

56 

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) 

60 

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) 

63 

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 

71 

72 content = (data.get('content') or '').strip() 

73 if not content: # Si el mensaje esta vacio, se ignora 

74 return 

75 

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() 

82 

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 }) 

94 

95 # Se avisa por push a los miembros que no tienen el chat abierto 

96 await self.notify_absent_members(content) 

97 

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'])) 

101 

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) 

112 

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 

123 

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) 

128 

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) 

133 

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())) 

138 

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)}")