""" WebSocket consumers для чата. """ import json from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async from django.utils import timezone from django.contrib.auth import get_user_model import logging logger = logging.getLogger(__name__) User = get_user_model() class ChatConsumer(AsyncWebsocketConsumer): """ WebSocket consumer для чата. URL: ws://domain/ws/chat/{chat_uuid}/ """ async def connect(self): """Подключение к WebSocket.""" try: self.chat_uuid = self.scope['url_route']['kwargs']['chat_uuid'] self.room_group_name = f'chat_{self.chat_uuid}' self.user = self.scope['user'] logger.info(f"WebSocket connection attempt: user={self.user}, chat_uuid={self.chat_uuid}, authenticated={self.user.is_authenticated if self.user else False}") # Проверяем авторизацию if not self.user or not self.user.is_authenticated: logger.warning(f"WebSocket connection rejected: user not authenticated") await self.close(code=4001) # Unauthorized return # Проверяем доступ к чату has_access = await self.check_chat_access() if not has_access: logger.warning(f"WebSocket connection rejected: user {self.user.id} has no access to chat {self.chat_uuid}") await self.close(code=4003) # Forbidden return # Проверяем что channel_layer доступен if not self.channel_layer: logger.error("Channel layer not available") await self.close(code=1011) return # Присоединяемся к группе чата try: await self.channel_layer.group_add( self.room_group_name, self.channel_name ) except Exception as e: logger.error(f"Error adding to channel group: {e}", exc_info=True) await self.close(code=1011) return await self.accept() # Отправляем подтверждение подключения await self.send(text_data=json.dumps({ 'type': 'connection_established', 'message': 'Подключено к чату', 'chat_uuid': self.chat_uuid })) # Уведомляем других участников await self.channel_layer.group_send( self.room_group_name, { 'type': 'user_joined', 'user_id': self.user.id, 'username': self.user.get_full_name() } ) logger.info(f"User {self.user.id} connected to chat {self.chat_uuid}") except Exception as e: logger.error(f"Error in WebSocket connect: {e}", exc_info=True) await self.close(code=1011) # Internal Error async def disconnect(self, close_code): """Отключение от WebSocket.""" # Уведомляем других участников await self.channel_layer.group_send( self.room_group_name, { 'type': 'user_left', 'user_id': self.user.id, 'username': self.user.get_full_name() } ) # Покидаем группу await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) logger.info(f"User {self.user.id} disconnected from chat {self.chat_uuid}") async def receive(self, text_data): """Получение сообщения от клиента.""" try: data = json.loads(text_data) message_type = data.get('type') if message_type == 'chat_message': # Отправляем сообщение в группу await self.channel_layer.group_send( self.room_group_name, { 'type': 'chat_message', 'message': data.get('content'), 'reply_to': data.get('reply_to'), 'user_id': self.user.id, 'username': self.user.get_full_name() } ) elif message_type == 'typing': # Отправляем статус печати в группу await self.channel_layer.group_send( self.room_group_name, { 'type': 'user_typing', 'user_id': self.user.id, 'username': self.user.get_full_name(), 'is_typing': data.get('is_typing', False) } ) elif message_type == 'read_messages': # Отправляем информацию о прочитанных сообщениях await self.channel_layer.group_send( self.room_group_name, { 'type': 'message_read', 'user_id': self.user.id, 'message_uuids': data.get('message_uuids', []) } ) except json.JSONDecodeError: logger.error("Invalid JSON received") except Exception as e: logger.error(f"Error processing message: {e}", exc_info=True) async def chat_message(self, event): """Отправка сообщения клиенту.""" # Это сообщение уже обработано в модели Message, просто уведомляем клиента await self.send(text_data=json.dumps({ 'type': 'chat_message', 'message': event.get('message') })) async def user_typing(self, event): """Отправка статуса печати клиенту.""" await self.send(text_data=json.dumps({ 'type': 'user_typing', 'user_id': event.get('user_id'), 'username': event.get('username'), 'is_typing': event.get('is_typing', False) })) async def message_read(self, event): """Отправка информации о прочитанных сообщениях.""" await self.send(text_data=json.dumps({ 'type': 'message_read', 'user_id': event.get('user_id'), 'message_uuids': event.get('message_uuids', []) })) async def user_joined(self, event): """Уведомление о присоединении пользователя.""" await self.send(text_data=json.dumps({ 'type': 'user_joined', 'user_id': event.get('user_id'), 'username': event.get('username') })) async def user_left(self, event): """Уведомление об уходе пользователя.""" await self.send(text_data=json.dumps({ 'type': 'user_left', 'user_id': event.get('user_id'), 'username': event.get('username') })) @database_sync_to_async def check_chat_access(self): """Проверка доступа пользователя к чату.""" from .models import Chat, ChatParticipant try: chat = Chat.objects.get(uuid=self.chat_uuid) return ChatParticipant.objects.filter(chat=chat, user=self.user).exists() except Chat.DoesNotExist: return False class UserPresenceConsumer(AsyncWebsocketConsumer): """ WebSocket consumer для отслеживания статусов пользователей (онлайн/оффлайн). URL: ws://domain/ws/presence/ """ async def connect(self): """Подключение к WebSocket.""" try: self.user = self.scope['user'] self.presence_group_name = 'user_presence' logger.info(f"Presence WebSocket connection attempt: user={self.user}, authenticated={self.user.is_authenticated if self.user else False}") # Проверяем авторизацию if not self.user or not self.user.is_authenticated: logger.warning(f"Presence WebSocket connection rejected: user not authenticated") await self.close(code=4001) # Unauthorized return # Проверяем что channel_layer доступен if not self.channel_layer: logger.error("Channel layer not available") await self.close(code=1011) return # Присоединяемся к группе присутствия try: await self.channel_layer.group_add( self.presence_group_name, self.channel_name ) except Exception as e: logger.error(f"Error adding to presence channel group: {e}", exc_info=True) await self.close(code=1011) return await self.accept() # Обновляем статус пользователя на онлайн await self.update_user_presence(True) # Отправляем подтверждение подключения await self.send(text_data=json.dumps({ 'type': 'connection_established', 'message': 'Подключено к отслеживанию статусов', 'user_id': self.user.id })) logger.info(f"User {self.user.id} connected to presence WebSocket") except Exception as e: logger.error(f"Error in Presence WebSocket connect: {e}", exc_info=True) await self.close(code=1011) # Internal Error async def disconnect(self, close_code): """Отключение от WebSocket.""" # Обновляем статус пользователя на оффлайн await self.update_user_presence(False) # Покидаем группу if self.channel_layer: await self.channel_layer.group_discard( self.presence_group_name, self.channel_name ) logger.info(f"User {self.user.id} disconnected from presence WebSocket") async def receive(self, text_data): """Получение сообщения от клиента.""" try: data = json.loads(text_data) message_type = data.get('type') if message_type == 'ping': # Отвечаем на ping для поддержания соединения await self.send(text_data=json.dumps({ 'type': 'pong' })) except json.JSONDecodeError: logger.error("Invalid JSON received in presence WebSocket") except Exception as e: logger.error(f"Error processing presence message: {e}", exc_info=True) async def user_status_update(self, event): """Отправка обновления статуса пользователя клиенту.""" await self.send(text_data=json.dumps({ 'type': 'user_status_update', 'user_id': event.get('user_id'), 'is_online': event.get('is_online'), 'last_activity': event.get('last_activity') })) @database_sync_to_async def update_user_presence(self, is_online): """Обновление статуса присутствия пользователя.""" try: now = timezone.now() # Обновляем last_activity при подключении/отключении User.objects.filter(id=self.user.id).update( last_activity=now ) # Отправляем обновление статуса всем подписчикам if self.channel_layer: from django.utils.dateparse import parse_datetime from django.utils import timezone as tz self.channel_layer.group_send( self.presence_group_name, { 'type': 'user_status_update', 'user_id': self.user.id, 'is_online': is_online, 'last_activity': now.isoformat() if now else None } ) except Exception as e: logger.error(f"Error updating user presence: {e}", exc_info=True)