lizerk 80cdc36d92 fix: исправлена система прав доступа к чатам и сообщениям
Backend:
- Исправлен user_can_access_chat() для поддержки context-based чатов
- Обновлен is_chat_member() для использования get_participants()
- Добавлена проверка has_send_messages_permission (блокирует гостей)
- Исправлен MessageViewSet.get_queryset() для комментариев
- Добавлена поддержка Feed Post resolver в resolve_chat_participants()
- Чаты типа 'comments' исключены из общего списка
- Добавлены permissions для mark_read, messages, messages_around
- Добавлена команда manage_chat_permissions для управления ролями

Frontend:
- Скрыто поле ввода сообщений для гостей (role='guest')
- Добавлена проверка can_send_messages из membership
- Показывается информативная заглушка для пользователей без прав

Tests & Docs:
- Добавлены тесты для guest роли (send_message + reactions)
- Документация по ролям и правам в чатах
- Troubleshooting guide для отладки permissions

Fixes:
- 403 при доступе к чатам-комментариям
- 404 при удалении/редактировании комментариев
- Комментарии в общем списке чатов
- Гости могли отправлять сообщения
2026-03-12 14:30:25 +03:00

129 lines
5.4 KiB
Python

"""
Web Push отправитель для browser push notifications
"""
import json
from push_notifications.models import WebPushDevice
from .base import BaseNotificationSender
from ..config import get
class PushNotificationSender(BaseNotificationSender):
"""
Отправитель Web Push уведомлений через django-push-notifications.
Доставляет уведомления в браузер даже когда вкладка закрыта.
"""
def can_send(self, notification, user_preferences) -> bool:
"""Проверяет, включены ли push уведомления"""
if not user_preferences.push_enabled:
self.log_skip(notification, "push_enabled=False")
return False
return True
def send(self, notification, **kwargs) -> bool:
"""
Отправляет Web Push уведомление.
Args:
notification: Объект Notification
**kwargs: Дополнительные параметры
Returns:
True если отправлено успешно, False иначе
"""
try:
user = notification.recipient
# Получаем активные устройства через django-push-notifications
devices = WebPushDevice.objects.filter(
user=user,
active=True
)
if not devices.exists():
self.log_skip(notification, "no active push devices")
return False
# Формируем данные для push
actor_str = str(notification.actor) if notification.actor else 'Система'
title = f'{actor_str} {notification.verb}'
# Ограничиваем длину body (Web Push имеет лимит ~4KB на весь payload)
# Оставляем 300 символов для body, чтобы гарантировать что весь message поместится
body = notification.description or ''
if len(body) > 300:
body = body[:297] + '...'
# Получаем иконки из конфигурации (None = browser default)
default_icon = get('PUSH_DEFAULT_ICON')
default_badge = get('PUSH_DEFAULT_BADGE')
# django-push-notifications использует другой формат
# Формируем данные для Web Push API
message_data = {
'head': title,
'body': body,
'url': notification.action_url or '/',
'tag': f'notification-{notification.id}',
'requireInteraction': False,
'data': {
'notification_id': notification.id,
'verb': notification.verb,
}
}
# Добавляем иконки только если они настроены
if default_icon:
message_data['icon'] = default_icon
if default_badge:
message_data['badge'] = default_badge
# Преобразуем в JSON-строку для send_message()
message = json.dumps(message_data)
sent_count = 0
failed_count = 0
# Отправляем через все устройства пользователя
for device in devices:
try:
device.send_message(message)
sent_count += 1
self.logger.debug(
f"Push отправлен на device {device.id} ({device.browser})"
)
except Exception as e:
failed_count += 1
# Вычисляем размер payload для диагностики
message_size = len(message.encode('utf-8'))
self.logger.error(
f"Ошибка отправки push на device {device.id}: {e} "
f"(payload size: {message_size} bytes)"
)
# Деактивируем устройство если ошибка
if "expired" in str(e).lower() or "unregistered" in str(e).lower():
device.active = False
device.save()
self.logger.info(f"Device {device.id} деактивирован")
if sent_count > 0:
self.log_success(notification, f"{sent_count} devices")
if failed_count > 0:
self.logger.warning(
f"Push отправлен частично: {sent_count} успешно, "
f"{failed_count} ошибок"
)
return True
else:
self.log_skip(notification, f"no successful sends ({failed_count} failures)")
return False
except ImportError:
self.log_skip(notification, "django-push-notifications not installed")
return False
except Exception as e:
self.log_error(notification, e, f"user_{notification.recipient.id}")
return False