BREAKING CHANGES: - Удалена старая система (6 моделей → 2 модели) - models_new.py → models.py (Notification, UserChannelPreferences) - signals_new.py → signals.py (notify.send API) - admin_new.py → admin.py Новая архитектура: - Все отправки через Celery (полностью асинхронно) - Модульная структура tasks/ (base, email, push, websocket) - Каждый канал - отдельный класс задачи наследующий BaseNotificationTask - Rate limiting, retry, приоритеты из коробки Обратная совместимость: - Создан services.py facade для старого API - NotificationService.create_notification() работает через notify.send() - Старый код продолжает работать без изменений Удалено: - Старые models.py, signals.py, admin.py, tasks.py (→ .old) - telegram_bot.py, telegram_models.py (Telegram отключен) - test_views.py, views.py, urls.py (устаревшие) Создано: - tasks/ модуль с классовой архитектурой (как senders/) - tasks/base.py - BaseNotificationTask - tasks/email.py - EmailNotificationTask + DigestEmailTask - tasks/push.py - PushNotificationTask - tasks/websocket.py - WebSocketNotificationTask Обновлено: - apps.py - загружает signals, channels, tasks - channels.py - все отправки через Celery задачи - Все импорты models_new → models
128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
"""
|
||
Обработчики каналов доставки уведомлений
|
||
|
||
Слушают post_save сигнал от Notification и асинхронно отправляют через Celery:
|
||
- WebSocket (realtime) - мгновенно
|
||
- Email - с retry и rate limiting
|
||
- Web Push - с автоматическим удалением неактивных устройств
|
||
"""
|
||
|
||
from django.db.models.signals import post_save
|
||
from django.dispatch import receiver
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@receiver(post_save, sender='notifications.Notification')
|
||
def route_notification_to_channels(sender, instance, created, **kwargs):
|
||
"""
|
||
Главный роутер уведомлений по каналам.
|
||
Вызывается при создании нового уведомления.
|
||
|
||
Асинхронно отправляет в Celery очередь для каждого канала.
|
||
"""
|
||
if not created:
|
||
return # Обрабатываем только новые уведомления
|
||
|
||
notification = instance
|
||
user = notification.recipient
|
||
|
||
# Получаем настройки пользователя
|
||
try:
|
||
prefs = user.channel_preferences
|
||
except Exception:
|
||
# Если настроек нет - создаем с дефолтными значениями
|
||
from .models import UserChannelPreferences
|
||
prefs = UserChannelPreferences.objects.create(user=user)
|
||
|
||
# Проверяем, не отключен ли этот тип уведомлений
|
||
if not prefs.is_verb_enabled(notification.verb):
|
||
logger.debug(f'Notification verb "{notification.verb}" disabled for user {user.id}')
|
||
return
|
||
|
||
# Импортируем задачи
|
||
from .tasks import (
|
||
send_websocket_notification,
|
||
send_email_notification,
|
||
send_push_notification,
|
||
)
|
||
|
||
# Проверяем режим "Не беспокоить"
|
||
is_dnd = prefs.is_in_dnd_period()
|
||
|
||
if is_dnd:
|
||
logger.debug(f'User {user.id} is in DND period, only web notifications (silent)')
|
||
# В DND режиме только WebSocket без звука
|
||
if prefs.web_enabled:
|
||
send_websocket_notification.delay(notification.id, silent=True)
|
||
return
|
||
|
||
# Отправляем по каналам асинхронно через Celery
|
||
|
||
if prefs.web_enabled:
|
||
send_websocket_notification.delay(notification.id, silent=False)
|
||
|
||
if prefs.email_enabled and prefs.email_frequency == 'instant':
|
||
send_email_notification.delay(notification.id)
|
||
|
||
if prefs.push_enabled:
|
||
send_push_notification.delay(notification.id)
|
||
|
||
Args:
|
||
notification: Объект Notification
|
||
|
||
DEPRECATED: Используйте PushNotificationSender().send()
|
||
"""
|
||
sender = PushNotificationSender()
|
||
return sender.send(notification)
|
||
|
||
|
||
# === Digest Email (для email_frequency = daily/weekly) ===
|
||
|
||
def send_email_digest(user, frequency='daily'):
|
||
"""
|
||
Отправка дайджеста уведомлений
|
||
|
||
Args:
|
||
user: User объект
|
||
frequency: 'daily' или 'weekly'
|
||
"""
|
||
try:
|
||
from datetime import timedelta
|
||
from django.utils import timezone
|
||
from .models import Notification
|
||
|
||
# Определяем период
|
||
if frequency == 'daily':
|
||
since = timezone.now() - timedelta(days=1)
|
||
elif frequency == 'weekly':
|
||
since = timezone.now() - timedelta(weeks=1)
|
||
else:
|
||
raise ValueError(f'Invalid frequency: {frequency}')
|
||
|
||
# Получаем неотправленные уведомления
|
||
notifications = Notification.objects.filter(
|
||
recipient=user,
|
||
timestamp__gte=since,
|
||
emailed=False,
|
||
deleted=False,
|
||
).order_by('-timestamp')
|
||
|
||
if not notifications.exists():
|
||
logger.debug(f'No notifications to digest for user {user.id}')
|
||
return
|
||
|
||
# Используем EmailNotificationSender для отправки дайджеста
|
||
sender = EmailNotificationSender()
|
||
success = sender.send_digest(user, notifications, frequency)
|
||
|
||
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)
|