uchill/backend/apps/board/consumers.py

243 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
WebSocket consumer для реал-тайм синхронизации доски
"""
import json
import asyncio
import logging
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from redis.exceptions import BusyLoadingError, ConnectionError as RedisConnectionError
logger = logging.getLogger(__name__)
class BoardConsumer(AsyncWebsocketConsumer):
"""Consumer для синхронизации доски между пользователями"""
async def connect(self):
"""Подключение к WebSocket"""
self.board_id = self.scope['url_route']['kwargs']['board_id']
self.room_group_name = f'board_{self.board_id}'
self.user = self.scope.get('user', AnonymousUser())
# Логируем информацию о подключении для отладки
query_string = self.scope.get('query_string', b'').decode()
logger.info(f'[BoardConsumer] Попытка подключения: board_id={self.board_id}, user={self.user}, is_anonymous={self.user.is_anonymous}, query_string={query_string}')
# Проверяем авторизацию
if self.user.is_anonymous:
logger.warning(f'[BoardConsumer] Отклонено: пользователь не авторизован для доски {self.board_id}')
await self.close(code=4001) # Unauthorized
return
# Присоединяемся к группе доски с обработкой ошибок Redis
max_retries = 5
retry_delay = 1
for attempt in range(max_retries):
try:
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
break
except (BusyLoadingError, RedisConnectionError) as e:
if attempt < max_retries - 1:
logger.warning(f'Redis загружается, повторная попытка {attempt + 1}/{max_retries}: {e}')
await asyncio.sleep(retry_delay * (attempt + 1))
else:
logger.error(f'Не удалось подключиться к Redis после {max_retries} попыток: {e}')
await self.close()
return
except Exception as e:
logger.error(f'Ошибка при подключении к channel layer: {e}', exc_info=True)
await self.close()
return
await self.accept()
logger.info(f'[BoardConsumer] Пользователь {self.user.email} подключился к доске {self.board_id}')
# Отправляем всем что пользователь присоединился
try:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_joined',
'user': {
'id': self.user.id,
'name': f'{self.user.first_name} {self.user.last_name}'.strip() or self.user.email,
}
}
)
except (BusyLoadingError, RedisConnectionError) as e:
logger.warning(f'Redis недоступен при отправке сообщения user_joined: {e}')
async def disconnect(self, close_code):
"""Отключение от WebSocket"""
if hasattr(self, 'room_group_name'):
# Проверяем, что пользователь авторизован перед отправкой сообщений
user = getattr(self, 'user', None)
if user and not user.is_anonymous:
try:
# Отправляем всем что пользователь отключился
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'user_left',
'user': {
'id': user.id,
'name': f'{user.first_name} {user.last_name}'.strip() or user.email,
}
}
)
except (BusyLoadingError, RedisConnectionError) as e:
logger.warning(f'Redis недоступен при отправке сообщения user_left: {e}')
except Exception as e:
logger.warning(f'Ошибка при отправке сообщения user_left: {e}')
try:
# Покидаем группу
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
except (BusyLoadingError, RedisConnectionError) as e:
logger.warning(f'Redis недоступен при покидании группы: {e}')
except Exception as e:
logger.warning(f'Ошибка при покидании группы: {e}')
# Логируем отключение с проверкой пользователя
if user and not user.is_anonymous:
logger.info(f'[BoardConsumer] Пользователь {user.email} отключился от доски {self.board_id}')
else:
logger.info(f'[BoardConsumer] Анонимный пользователь отключился от доски {self.board_id}')
async def receive(self, text_data):
"""Получение сообщения от клиента"""
# Проверяем, что пользователь авторизован
if not hasattr(self, 'user') or not self.user or self.user.is_anonymous:
logger.warning('[BoardConsumer] Попытка отправить сообщение неавторизованным пользователем')
return
try:
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'draw':
board_data = data.get('data', {})
elements = board_data.get('elements', [])
files = board_data.get('files', {})
# Логируем размер данных
size_kb = len(text_data) / 1024
user_email = getattr(self.user, 'email', 'unknown')
print(f'[BoardConsumer] Получено от {user_email}: {len(elements)} элементов, {len(files)} файлов, размер: {size_kb:.2f} KB')
if len(files) > 0:
print(f'[BoardConsumer] Files: {list(files.keys())}')
# Отправляем изменения всем кроме отправителя
try:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'board_update',
'data': board_data,
'user_id': self.user.id,
}
)
except (BusyLoadingError, RedisConnectionError) as e:
logger.warning(f'Redis недоступен при отправке board_update: {e}')
elif message_type == 'clear':
# Очистка доски
try:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'board_clear',
'user_id': self.user.id,
}
)
except (BusyLoadingError, RedisConnectionError) as e:
logger.warning(f'Redis недоступен при отправке board_clear: {e}')
elif message_type == 'undo':
# Отменить действие
try:
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'board_undo',
'user_id': self.user.id,
}
)
except (BusyLoadingError, RedisConnectionError) as e:
logger.warning(f'Redis недоступен при отправке board_undo: {e}')
except json.JSONDecodeError:
print('[BoardConsumer] Ошибка декодирования JSON')
async def board_update(self, event):
"""Отправка обновления доски клиенту"""
# Проверяем, что пользователь авторизован
if not hasattr(self, 'user') or not self.user or self.user.is_anonymous:
return
# Не отправляем обратно отправителю
if event.get('user_id') != self.user.id:
board_data = event['data']
elements = board_data.get('elements', [])
files = board_data.get('files', {})
message = json.dumps({
'type': 'draw',
'data': board_data,
})
size_kb = len(message) / 1024
user_email = getattr(self.user, 'email', 'unknown')
print(f'[BoardConsumer] Отправка {user_email}: {len(elements)} элементов, {len(files)} файлов, размер: {size_kb:.2f} KB')
await self.send(text_data=message)
async def board_clear(self, event):
"""Очистка доски"""
if not hasattr(self, 'user') or not self.user or self.user.is_anonymous:
return
if event.get('user_id') != self.user.id:
await self.send(text_data=json.dumps({
'type': 'clear',
}))
async def board_undo(self, event):
"""Отменить действие"""
if not hasattr(self, 'user') or not self.user or self.user.is_anonymous:
return
if event.get('user_id') != self.user.id:
await self.send(text_data=json.dumps({
'type': 'undo',
}))
async def user_joined(self, event):
"""Уведомление о присоединении пользователя"""
if not hasattr(self, 'user') or not self.user or self.user.is_anonymous:
return
if event['user']['id'] != self.user.id:
await self.send(text_data=json.dumps({
'type': 'user_joined',
'user': event['user'],
}))
async def user_left(self, event):
"""Уведомление об отключении пользователя"""
if not hasattr(self, 'user') or not self.user or self.user.is_anonymous:
return
if event['user']['id'] != self.user.id:
await self.send(text_data=json.dumps({
'type': 'user_left',
'user': event['user'],
}))