243 lines
12 KiB
Python
243 lines
12 KiB
Python
"""
|
||
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'],
|
||
}))
|