refactor: Базовый класс для всех отправителей уведомлений
Создана объектно-ориентированная архитектура для отправителей: Новая структура: - BaseNotificationSender - абстрактный базовый класс - EmailNotificationSender(BaseNotificationSender) - WebSocketNotificationSender(BaseNotificationSender) - PushNotificationSender(BaseNotificationSender) Преимущества: ✅ Единый интерфейс send() для всех каналов ✅ can_send() - проверка возможности отправки ✅ Унифицированное логирование (log_success, log_error, log_skip) ✅ Легко расширять новыми каналами ✅ Переиспользуемость кода Изменения: - notifications/senders/base.py - базовый класс - notifications/senders/email.py - email отправитель (упрощен) - notifications/senders/websocket.py - WebSocket отправитель - notifications/senders/push.py - Web Push отправитель - notifications/channels.py - обновлен для использования классов Старые функции send_* сохранены для обратной совместимости (deprecated)
This commit is contained in:
parent
f4fa00d9bd
commit
e1be95fbe4
@ -9,11 +9,14 @@
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
import logging
|
||||
|
||||
from .senders import (
|
||||
EmailNotificationSender,
|
||||
WebSocketNotificationSender,
|
||||
PushNotificationSender,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -53,13 +56,16 @@ def handle_notification_channels(sender, instance, created, **kwargs):
|
||||
# Отправляем по каналам согласно настройкам
|
||||
|
||||
if prefs.web_enabled:
|
||||
send_websocket_notification(notification)
|
||||
sender = WebSocketNotificationSender()
|
||||
sender.send(notification, silent=prefs.is_in_dnd_period())
|
||||
|
||||
if prefs.email_enabled and prefs.email_frequency == 'instant':
|
||||
send_email_notification(notification)
|
||||
sender = EmailNotificationSender()
|
||||
sender.send(notification)
|
||||
|
||||
if prefs.push_enabled:
|
||||
send_push_notification(notification)
|
||||
sender = PushNotificationSender()
|
||||
sender.send(notification)
|
||||
|
||||
|
||||
def send_websocket_notification(notification, silent=False):
|
||||
@ -69,53 +75,11 @@ def send_websocket_notification(notification, silent=False):
|
||||
Args:
|
||||
notification: Объект Notification
|
||||
silent: Если True, уведомление без звука/визуальных эффектов
|
||||
|
||||
DEPRECATED: Используйте WebSocketNotificationSender().send()
|
||||
"""
|
||||
try:
|
||||
channel_layer = get_channel_layer()
|
||||
user_channel = f"user_{notification.recipient.id}"
|
||||
|
||||
# Формируем данные для фронтенда
|
||||
message_data = {
|
||||
'type': 'notification',
|
||||
'id': notification.id,
|
||||
'verb': notification.verb,
|
||||
'description': notification.description,
|
||||
'action_url': notification.action_url,
|
||||
'timestamp': notification.timestamp.isoformat(),
|
||||
'unread': notification.unread,
|
||||
'silent': silent,
|
||||
'data': notification.data,
|
||||
}
|
||||
|
||||
# Добавляем actor если есть
|
||||
if notification.actor:
|
||||
message_data['actor'] = {
|
||||
'type': notification.actor_content_type.model,
|
||||
'id': notification.actor_object_id,
|
||||
'str': str(notification.actor),
|
||||
}
|
||||
|
||||
# Добавляем target если есть
|
||||
if notification.target:
|
||||
message_data['target'] = {
|
||||
'type': notification.target_content_type.model,
|
||||
'id': notification.target_object_id,
|
||||
'str': str(notification.target),
|
||||
}
|
||||
|
||||
# Отправляем через Channels
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
user_channel,
|
||||
{
|
||||
'type': 'notification_message',
|
||||
'message': message_data
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f'WebSocket notification sent to user {notification.recipient.id}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send WebSocket notification: {e}', exc_info=True)
|
||||
sender = WebSocketNotificationSender()
|
||||
return sender.send(notification, silent=silent)
|
||||
|
||||
|
||||
def send_email_notification(notification):
|
||||
@ -124,42 +88,11 @@ def send_email_notification(notification):
|
||||
|
||||
Args:
|
||||
notification: Объект Notification
|
||||
|
||||
DEPRECATED: Используйте EmailNotificationSender().send()
|
||||
"""
|
||||
try:
|
||||
from common.emails import send_email
|
||||
|
||||
user = notification.recipient
|
||||
|
||||
# Формируем email
|
||||
subject = f'Уведомление: {notification.verb}'
|
||||
|
||||
context = {
|
||||
'notification': notification,
|
||||
'user': user,
|
||||
'action_url': notification.action_url,
|
||||
'site_url': settings.SITE_URL if hasattr(settings, 'SITE_URL') else 'http://localhost:8000',
|
||||
}
|
||||
|
||||
# Используем общий шаблон или специфичный для verb
|
||||
template_name = f'notifications/email/{notification.verb}.html'
|
||||
fallback_template = 'notifications/email/default.html'
|
||||
|
||||
send_email(
|
||||
subject=subject,
|
||||
template_name=template_name,
|
||||
fallback_template=fallback_template,
|
||||
context=context,
|
||||
recipient_list=[user.email],
|
||||
)
|
||||
|
||||
# Отмечаем что отправлено
|
||||
notification.emailed = True
|
||||
notification.save(update_fields=['emailed'])
|
||||
|
||||
logger.info(f'Email notification sent to {user.email}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send email notification: {e}', exc_info=True)
|
||||
sender = EmailNotificationSender()
|
||||
return sender.send(notification)
|
||||
|
||||
|
||||
def send_push_notification(notification):
|
||||
@ -168,60 +101,11 @@ def send_push_notification(notification):
|
||||
|
||||
Args:
|
||||
notification: Объект Notification
|
||||
|
||||
DEPRECATED: Используйте PushNotificationSender().send()
|
||||
"""
|
||||
try:
|
||||
from push_notifications.models import WebPushDevice
|
||||
from .web_push_models import WebPushSubscription
|
||||
|
||||
user = notification.recipient
|
||||
|
||||
# Получаем активные подписки
|
||||
subscriptions = WebPushSubscription.objects.filter(
|
||||
user=user,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
if not subscriptions.exists():
|
||||
logger.debug(f'User {user.id} has no active push subscriptions')
|
||||
return
|
||||
|
||||
# Формируем данные для push
|
||||
actor_str = str(notification.actor) if notification.actor else 'Система'
|
||||
title = f'{actor_str} {notification.verb}'
|
||||
body = notification.description or ''
|
||||
|
||||
push_data = {
|
||||
'head': title,
|
||||
'body': body,
|
||||
'icon': '/static/img/logo.png',
|
||||
'url': notification.action_url or '/',
|
||||
'tag': f'notification-{notification.id}',
|
||||
'requireInteraction': False,
|
||||
}
|
||||
|
||||
# Отправляем через django-push-notifications
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
# Используем push_notifications для отправки
|
||||
# (предполагается что WebPushSubscription связана с WebPushDevice)
|
||||
device = WebPushDevice.objects.filter(
|
||||
registration_id=sub.endpoint
|
||||
).first()
|
||||
|
||||
if device:
|
||||
device.send_message(
|
||||
message=push_data,
|
||||
extra={
|
||||
'notification_id': notification.id
|
||||
}
|
||||
)
|
||||
logger.info(f'Push notification sent to device {device.id}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send push to subscription {sub.id}: {e}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send push notification: {e}', exc_info=True)
|
||||
sender = PushNotificationSender()
|
||||
return sender.send(notification)
|
||||
|
||||
|
||||
# === Digest Email (для email_frequency = daily/weekly) ===
|
||||
@ -237,7 +121,6 @@ def send_email_digest(user, frequency='daily'):
|
||||
try:
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from common.emails import send_email
|
||||
from .models_new import Notification
|
||||
|
||||
# Определяем период
|
||||
@ -260,27 +143,15 @@ def send_email_digest(user, frequency='daily'):
|
||||
logger.debug(f'No notifications to digest for user {user.id}')
|
||||
return
|
||||
|
||||
# Формируем email
|
||||
subject = f'Дайджест уведомлений за {"день" if frequency == "daily" else "неделю"}'
|
||||
# Используем EmailNotificationSender для отправки дайджеста
|
||||
sender = EmailNotificationSender()
|
||||
success = sender.send_digest(user, notifications, frequency)
|
||||
|
||||
context = {
|
||||
'user': user,
|
||||
'notifications': notifications,
|
||||
'count': notifications.count(),
|
||||
'site_url': settings.SITE_URL if hasattr(settings, 'SITE_URL') else 'http://localhost:8000',
|
||||
}
|
||||
|
||||
send_email(
|
||||
subject=subject,
|
||||
template_name='notifications/email/digest.html',
|
||||
context=context,
|
||||
recipient_list=[user.email],
|
||||
)
|
||||
|
||||
# Отмечаем как отправленные
|
||||
notifications.update(emailed=True)
|
||||
|
||||
logger.info(f'Email digest ({frequency}) sent to {user.email} with {notifications.count()} notifications')
|
||||
if success:
|
||||
logger.info(
|
||||
f'Email digest ({frequency}) sent to {user.email} '
|
||||
f'with {notifications.count()} notifications'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to send email digest: {e}', exc_info=True)
|
||||
|
||||
14
backend/notifications/senders/__init__.py
Normal file
14
backend/notifications/senders/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""
|
||||
Отправители уведомлений по различным каналам
|
||||
"""
|
||||
from .base import BaseNotificationSender
|
||||
from .email import EmailNotificationSender
|
||||
from .websocket import WebSocketNotificationSender
|
||||
from .push import PushNotificationSender
|
||||
|
||||
__all__ = [
|
||||
'BaseNotificationSender',
|
||||
'EmailNotificationSender',
|
||||
'WebSocketNotificationSender',
|
||||
'PushNotificationSender',
|
||||
]
|
||||
75
backend/notifications/senders/base.py
Normal file
75
backend/notifications/senders/base.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""
|
||||
Базовый класс для всех отправителей уведомлений
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseNotificationSender(ABC):
|
||||
"""
|
||||
Абстрактный базовый класс для отправителей уведомлений.
|
||||
|
||||
Все отправители должны наследоваться от этого класса и реализовать метод send().
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
@abstractmethod
|
||||
def send(self, notification, **kwargs) -> bool:
|
||||
"""
|
||||
Отправляет уведомление через конкретный канал.
|
||||
|
||||
Args:
|
||||
notification: Объект Notification
|
||||
**kwargs: Дополнительные параметры для канала
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
pass
|
||||
|
||||
def can_send(self, notification, user_preferences) -> bool:
|
||||
"""
|
||||
Проверяет, можно ли отправить уведомление через этот канал.
|
||||
|
||||
Args:
|
||||
notification: Объект Notification
|
||||
user_preferences: UserChannelPreferences
|
||||
|
||||
Returns:
|
||||
True если можно отправлять, False иначе
|
||||
"""
|
||||
return True
|
||||
|
||||
def log_success(self, notification, recipient: str):
|
||||
"""Логирует успешную отправку"""
|
||||
self.logger.info(
|
||||
f"✅ Уведомление отправлено: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"recipient={recipient}, "
|
||||
f"channel={self.__class__.__name__}"
|
||||
)
|
||||
|
||||
def log_error(self, notification, error: Exception, recipient: Optional[str] = None):
|
||||
"""Логирует ошибку отправки"""
|
||||
self.logger.error(
|
||||
f"❌ Ошибка отправки: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"recipient={recipient}, "
|
||||
f"channel={self.__class__.__name__}, "
|
||||
f"error={type(error).__name__}: {error}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def log_skip(self, notification, reason: str):
|
||||
"""Логирует пропуск отправки"""
|
||||
self.logger.debug(
|
||||
f"⏭️ Отправка пропущена: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"channel={self.__class__.__name__}, "
|
||||
f"reason={reason}"
|
||||
)
|
||||
226
backend/notifications/senders/email.py
Normal file
226
backend/notifications/senders/email.py
Normal file
@ -0,0 +1,226 @@
|
||||
"""
|
||||
Email отправитель для системы уведомлений
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import send_mail
|
||||
|
||||
from .base import BaseNotificationSender
|
||||
|
||||
|
||||
class EmailNotificationSender(BaseNotificationSender):
|
||||
"""
|
||||
Отправитель email уведомлений.
|
||||
Использует HTML шаблоны для красивого оформления.
|
||||
"""
|
||||
|
||||
# Маппинг verb на эмодзи для email
|
||||
VERB_ICONS = {
|
||||
'liked': '❤️',
|
||||
'commented': '💬',
|
||||
'mentioned': '@',
|
||||
'approved': '✅',
|
||||
'rejected': '❌',
|
||||
'created': '📝',
|
||||
'updated': '🔄',
|
||||
'deleted': '🗑️',
|
||||
'shared': '🔗',
|
||||
'followed': '👥',
|
||||
}
|
||||
|
||||
def can_send(self, notification, user_preferences) -> bool:
|
||||
"""Проверяет, включена ли отправка email"""
|
||||
if not user_preferences.email_enabled:
|
||||
self.log_skip(notification, "email_enabled=False")
|
||||
return False
|
||||
|
||||
if user_preferences.email_frequency != 'instant':
|
||||
self.log_skip(notification, f"email_frequency={user_preferences.email_frequency}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def send(self, notification, **kwargs) -> bool:
|
||||
"""
|
||||
Отправляет email уведомление.
|
||||
|
||||
Args:
|
||||
notification: Объект Notification
|
||||
**kwargs:
|
||||
custom_subject: кастомная тема письма (опционально)
|
||||
recipient_email: явный email получателя (опционально)
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
try:
|
||||
user = notification.recipient
|
||||
recipient_email = kwargs.get('recipient_email', user.email)
|
||||
|
||||
if not recipient_email:
|
||||
self.log_skip(notification, "no email address")
|
||||
return False
|
||||
|
||||
# Формируем тему письма
|
||||
custom_subject = kwargs.get('custom_subject')
|
||||
if custom_subject:
|
||||
subject = custom_subject
|
||||
else:
|
||||
icon = self.VERB_ICONS.get(notification.verb, '🔔')
|
||||
actor_str = str(notification.actor) if notification.actor else 'Система'
|
||||
subject = f"{icon} {actor_str} {notification.verb}"
|
||||
|
||||
# Формируем контекст для шаблона
|
||||
context = {
|
||||
'notification': notification,
|
||||
'actor': notification.actor,
|
||||
'verb': notification.verb,
|
||||
'description': notification.description,
|
||||
'action_url': self._get_full_url(notification.action_url),
|
||||
'action_text': 'Посмотреть',
|
||||
'site_name': getattr(settings, 'SITE_NAME', 'EUSRR'),
|
||||
'site_url': self._get_site_url(),
|
||||
'verb_icon': self.VERB_ICONS.get(notification.verb, '🔔'),
|
||||
}
|
||||
|
||||
# Рендерим HTML и текстовую версию
|
||||
html_message = self._render_template('notifications/email/notification.html', context)
|
||||
text_message = self._render_template('notifications/email/notification.txt', context)
|
||||
|
||||
# Отправляем email
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[recipient_email],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
# Отмечаем что отправлено
|
||||
notification.emailed = True
|
||||
notification.save(update_fields=['emailed'])
|
||||
|
||||
self.log_success(notification, recipient_email)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log_error(notification, e, recipient_email)
|
||||
return False
|
||||
|
||||
def send_digest(self, user, notifications: list, frequency: str = 'daily') -> bool:
|
||||
"""
|
||||
Отправляет email дайджест уведомлений.
|
||||
|
||||
Args:
|
||||
user: User объект
|
||||
notifications: Список объектов Notification
|
||||
frequency: 'daily' или 'weekly'
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
try:
|
||||
if not notifications:
|
||||
return False
|
||||
|
||||
recipient_email = user.email
|
||||
if not recipient_email:
|
||||
return False
|
||||
|
||||
# Формируем тему
|
||||
digest_name = 'Ежедневный' if frequency == 'daily' else 'Еженедельный'
|
||||
subject = f"📬 {digest_name} дайджест уведомлений ({len(notifications)})"
|
||||
|
||||
# Группируем по verb
|
||||
grouped = {}
|
||||
for notif in notifications:
|
||||
verb = notif.verb
|
||||
if verb not in grouped:
|
||||
grouped[verb] = []
|
||||
grouped[verb].append(notif)
|
||||
|
||||
# Формируем контекст
|
||||
context = {
|
||||
'user': user,
|
||||
'notifications': notifications,
|
||||
'grouped_notifications': grouped,
|
||||
'frequency': frequency,
|
||||
'digest_name': digest_name,
|
||||
'total_count': len(notifications),
|
||||
'verb_icons': self.VERB_ICONS,
|
||||
'site_name': getattr(settings, 'SITE_NAME', 'EUSRR'),
|
||||
'site_url': self._get_site_url(),
|
||||
}
|
||||
|
||||
# Рендерим шаблоны
|
||||
html_message = self._render_template('notifications/email/digest.html', context)
|
||||
text_message = self._render_template('notifications/email/digest.txt', context)
|
||||
|
||||
# Отправляем дайджест
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=text_message,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[recipient_email],
|
||||
html_message=html_message,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
# Отмечаем как отправленные
|
||||
notifications.update(emailed=True)
|
||||
|
||||
self.logger.info(
|
||||
f"✅ Email дайджест отправлен: "
|
||||
f"user={user.id}, "
|
||||
f"count={len(notifications)}, "
|
||||
f"frequency={frequency}, "
|
||||
f"recipient={recipient_email}"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"❌ Ошибка отправки дайджеста: "
|
||||
f"user={user.id}, "
|
||||
f"frequency={frequency}, "
|
||||
f"error={type(e).__name__}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
def _render_template(self, template_name: str, context: dict) -> str:
|
||||
"""Рендерит шаблон с контекстом"""
|
||||
try:
|
||||
return render_to_string(template_name, context)
|
||||
except Exception:
|
||||
# Если шаблон не найден, возвращаем простой текст
|
||||
notif = context.get('notification')
|
||||
if notif:
|
||||
return f"{notif.description}\n\n{context.get('action_url', '')}"
|
||||
return ""
|
||||
|
||||
def _get_site_url(self) -> str:
|
||||
"""Получает базовый URL сайта из настроек."""
|
||||
if hasattr(settings, 'SITE_URL'):
|
||||
return settings.SITE_URL
|
||||
|
||||
if settings.ALLOWED_HOSTS and settings.ALLOWED_HOSTS[0] != '*':
|
||||
host = settings.ALLOWED_HOSTS[0]
|
||||
protocol = 'https' if getattr(settings, 'SECURE_SSL_REDIRECT', False) else 'http'
|
||||
return f"{protocol}://{host}"
|
||||
|
||||
return 'http://localhost:9000'
|
||||
|
||||
def _get_full_url(self, path: str) -> str:
|
||||
"""Преобразует относительный URL в абсолютный."""
|
||||
if not path:
|
||||
return ''
|
||||
|
||||
if path.startswith('http://') or path.startswith('https://'):
|
||||
return path
|
||||
|
||||
site_url = self._get_site_url()
|
||||
return f"{site_url}{path}"
|
||||
100
backend/notifications/senders/push.py
Normal file
100
backend/notifications/senders/push.py
Normal file
@ -0,0 +1,100 @@
|
||||
"""
|
||||
Web Push отправитель для browser push notifications
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from .base import BaseNotificationSender
|
||||
|
||||
|
||||
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:
|
||||
from push_notifications.models import WebPushDevice
|
||||
from ..web_push_models import WebPushSubscription
|
||||
|
||||
user = notification.recipient
|
||||
|
||||
# Получаем активные подписки
|
||||
subscriptions = WebPushSubscription.objects.filter(
|
||||
user=user,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
if not subscriptions.exists():
|
||||
self.log_skip(notification, "no active push subscriptions")
|
||||
return False
|
||||
|
||||
# Формируем данные для push
|
||||
actor_str = str(notification.actor) if notification.actor else 'Система'
|
||||
title = f'{actor_str} {notification.verb}'
|
||||
body = notification.description or ''
|
||||
|
||||
push_data = {
|
||||
'head': title,
|
||||
'body': body,
|
||||
'icon': '/static/img/logo.png',
|
||||
'url': notification.action_url or '/',
|
||||
'tag': f'notification-{notification.id}',
|
||||
'requireInteraction': False,
|
||||
}
|
||||
|
||||
sent_count = 0
|
||||
# Отправляем через django-push-notifications
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
# Используем push_notifications для отправки
|
||||
device = WebPushDevice.objects.filter(
|
||||
registration_id=sub.endpoint
|
||||
).first()
|
||||
|
||||
if device:
|
||||
device.send_message(
|
||||
message=push_data,
|
||||
extra={
|
||||
'notification_id': notification.id
|
||||
}
|
||||
)
|
||||
sent_count += 1
|
||||
self.logger.debug(f"Push отправлен на device {device.id}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Ошибка отправки push на subscription {sub.id}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
if sent_count > 0:
|
||||
self.log_success(notification, f"{sent_count} devices")
|
||||
return True
|
||||
else:
|
||||
self.log_skip(notification, "no successful sends")
|
||||
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
|
||||
87
backend/notifications/senders/websocket.py
Normal file
87
backend/notifications/senders/websocket.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""
|
||||
WebSocket отправитель для realtime уведомлений
|
||||
"""
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
from .base import BaseNotificationSender
|
||||
|
||||
|
||||
class WebSocketNotificationSender(BaseNotificationSender):
|
||||
"""
|
||||
Отправитель WebSocket уведомлений через Django Channels.
|
||||
Обеспечивает realtime доставку в браузер.
|
||||
"""
|
||||
|
||||
def can_send(self, notification, user_preferences) -> bool:
|
||||
"""Проверяет, включены ли веб уведомления"""
|
||||
if not user_preferences.web_enabled:
|
||||
self.log_skip(notification, "web_enabled=False")
|
||||
return False
|
||||
return True
|
||||
|
||||
def send(self, notification, silent: bool = False, **kwargs) -> bool:
|
||||
"""
|
||||
Отправляет уведомление через WebSocket.
|
||||
|
||||
Args:
|
||||
notification: Объект Notification
|
||||
silent: Если True, уведомление без звука/визуальных эффектов
|
||||
**kwargs: Дополнительные параметры
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
try:
|
||||
channel_layer = get_channel_layer()
|
||||
if not channel_layer:
|
||||
self.log_skip(notification, "channel_layer not configured")
|
||||
return False
|
||||
|
||||
user_id = notification.recipient.id
|
||||
user_channel = f"user_{user_id}"
|
||||
|
||||
# Формируем данные для фронтенда
|
||||
message_data = {
|
||||
'type': 'notification',
|
||||
'id': notification.id,
|
||||
'verb': notification.verb,
|
||||
'description': notification.description,
|
||||
'action_url': notification.action_url,
|
||||
'timestamp': notification.timestamp.isoformat(),
|
||||
'unread': notification.unread,
|
||||
'silent': silent,
|
||||
'data': notification.data,
|
||||
}
|
||||
|
||||
# Добавляем actor если есть
|
||||
if notification.actor:
|
||||
message_data['actor'] = {
|
||||
'type': notification.actor_content_type.model,
|
||||
'id': notification.actor_object_id,
|
||||
'str': str(notification.actor),
|
||||
}
|
||||
|
||||
# Добавляем target если есть
|
||||
if notification.target:
|
||||
message_data['target'] = {
|
||||
'type': notification.target_content_type.model,
|
||||
'id': notification.target_object_id,
|
||||
'str': str(notification.target),
|
||||
}
|
||||
|
||||
# Отправляем через Channels
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
user_channel,
|
||||
{
|
||||
'type': 'notification_message',
|
||||
'message': message_data
|
||||
}
|
||||
)
|
||||
|
||||
self.log_success(notification, f"user_{user_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log_error(notification, e, f"user_{notification.recipient.id}")
|
||||
return False
|
||||
Loading…
x
Reference in New Issue
Block a user