""" Сервисы для уведомлений и Telegram интеграции. """ import secrets import string from django.core.cache import cache from django.utils import timezone from datetime import timedelta import logging from channels.layers import get_channel_layer from asgiref.sync import async_to_sync logger = logging.getLogger(__name__) class TelegramLinkService: """Сервис для связывания Telegram аккаунтов.""" @staticmethod def generate_link_code(user_id: int) -> str: """ Генерация кода для связывания Telegram аккаунта. Args: user_id: ID пользователя Returns: str: Код связывания (6 символов) """ # Генерируем случайный код из цифр и букв code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(6)) # Сохраняем в кэш на 15 минут cache_key = f'telegram_link_{code}' cache.set(cache_key, user_id, timeout=900) # 15 минут logger.info(f"Generated link code {code} for user {user_id}") return code @staticmethod def link_account(link_code: str, telegram_id: int, telegram_username: str = '') -> dict: """ Связывание Telegram аккаунта с аккаунтом на платформе. Args: link_code: Код связывания telegram_id: ID пользователя в Telegram telegram_username: Username в Telegram Returns: dict: Результат связывания """ from apps.users.models import User # Проверяем код в кэше cache_key = f'telegram_link_{link_code}' user_id = cache.get(cache_key) if not user_id: return { 'success': False, 'error': 'Неверный или истекший код связывания' } try: # Находим пользователя user = User.objects.get(id=user_id) # Проверяем не привязан ли уже этот Telegram ID к другому аккаунту existing_user = User.objects.filter(telegram_id=telegram_id).first() if existing_user and existing_user.id != user.id: return { 'success': False, 'error': 'Этот Telegram аккаунт уже привязан к другому пользователю' } # Связываем аккаунты user.telegram_id = telegram_id user.telegram_username = telegram_username user.save(update_fields=['telegram_id', 'telegram_username']) # Удаляем код из кэша cache.delete(cache_key) # Включаем Telegram уведомления try: preferences = user.notification_preferences if not preferences.telegram_enabled: preferences.telegram_enabled = True preferences.save(update_fields=['telegram_enabled']) except: # Если нет настроек, создаем create_notification_preferences(user) logger.info(f"Linked Telegram {telegram_id} to user {user.id}") return { 'success': True, 'user_name': user.get_full_name() or user.email, 'user_email': user.email } except User.DoesNotExist: return { 'success': False, 'error': 'Пользователь не найден' } except Exception as e: logger.error(f"Error linking Telegram account: {e}") return { 'success': False, 'error': 'Ошибка связывания аккаунта' } @staticmethod def unlink_account(telegram_id: int) -> dict: """ Отвязка Telegram аккаунта. Args: telegram_id: ID пользователя в Telegram Returns: dict: Результат отвязки """ from apps.users.models import User try: user = User.objects.get(telegram_id=telegram_id) user.telegram_id = None user.telegram_username = '' user.save(update_fields=['telegram_id', 'telegram_username']) # Выключаем Telegram уведомления try: preferences = user.notification_preferences preferences.telegram_enabled = False preferences.save(update_fields=['telegram_enabled']) except: pass logger.info(f"Unlinked Telegram {telegram_id} from user {user.id}") return { 'success': True, 'message': 'Аккаунт успешно отвязан' } except User.DoesNotExist: return { 'success': False, 'error': 'Аккаунт не найден' } except Exception as e: logger.error(f"Error unlinking Telegram account: {e}") return { 'success': False, 'error': 'Ошибка отвязки аккаунта' } class NotificationService: """Сервис для работы с уведомлениями.""" @staticmethod def create_notification(recipient, notification_type, title, message, **kwargs): """ Создание уведомления. Args: recipient: Получатель (User) notification_type: Тип уведомления title: Заголовок message: Сообщение **kwargs: Дополнительные параметры (channel, priority, action_url, content_object и т.д.) """ from .models import Notification from .tasks import send_notification_task from .serializers import NotificationSerializer # Получаем канал из kwargs или используем 'in_app' по умолчанию channel = kwargs.pop('channel', 'in_app') # Создаем уведомление notification = Notification.create_notification( recipient=recipient, notification_type=notification_type, title=title, message=message, channel=channel, **kwargs ) # Отправляем асинхронно (кроме in_app, они уже в БД) if channel != 'in_app': send_notification_task.delay(notification.id) elif channel == 'in_app': # Отправляем через WebSocket для real-time уведомлений WebSocketNotificationService.send_notification_via_websocket(notification) return notification @staticmethod def create_notification_with_telegram(recipient, notification_type, title, message, **kwargs): """ Создание уведомления с автоматической отправкой в Telegram и Email (если возможно). Создает уведомления только если они включены в настройках пользователя. Args: recipient: Получатель (User) notification_type: Тип уведомления title: Заголовок message: Сообщение **kwargs: Дополнительные параметры (priority, action_url, content_object, child_id и т.д.) """ from .models import Notification, ParentChildNotificationSettings from .tasks import send_notification_task # Проверяем настройки уведомлений родителя для конкретного ребенка if recipient.role == 'parent': child_id = kwargs.get('child_id') or kwargs.get('data', {}).get('child_id') if child_id: try: from apps.users.models import Client, Parent parent = recipient.parent_profile if not parent: logger.warning(f'Parent profile not found for user {recipient.id}') # Продолжаем без фильтрации, если профиль родителя не найден else: # Получаем ребенка по user_id try: child = Client.objects.get(user_id=child_id) except Client.DoesNotExist: logger.warning(f'Child with user_id {child_id} not found') # Продолжаем без фильтрации, если ребенок не найден child = None if child: # Проверяем, что ребенок связан с этим родителем if child in parent.children.all(): # Получаем настройки для этого ребенка try: child_settings = ParentChildNotificationSettings.objects.get( parent=parent, child=child ) # Проверяем, включены ли уведомления для этого ребенка if not child_settings.enabled: logger.info(f'Notifications disabled for parent {recipient.id} and child {child_id}, skipping notification {notification_type}') return None # Проверяем, включен ли конкретный тип уведомления для этого ребенка if not child_settings.is_type_enabled(notification_type): logger.info(f'Notification type {notification_type} disabled for parent {recipient.id} and child {child_id}') return None except ParentChildNotificationSettings.DoesNotExist: # Если настройки не созданы, используем дефолтные (включено) logger.debug(f'No notification settings found for parent {recipient.id} and child {child_id}, using defaults (enabled)') pass else: logger.warning(f'Child {child_id} is not associated with parent {recipient.id}') # Продолжаем без фильтрации, если ребенок не связан с родителем except Exception as e: logger.warning(f'Error checking parent-child notification settings: {e}', exc_info=True) # Проверяем настройки уведомлений try: preferences = recipient.notification_preferences except: preferences = None # Если уведомления полностью отключены, не создаем ничего if preferences and not preferences.enabled: logger.info(f'Notifications disabled for user {recipient.id}, skipping notification {notification_type}') return None # Создаем in_app уведомление, если включено in_app_notification = None in_app_enabled = True if preferences: in_app_enabled = preferences.is_type_enabled(notification_type, 'in_app') else: # Если нет настроек, создаем по умолчанию in_app_enabled = True if in_app_enabled: in_app_notification = Notification.create_notification( recipient=recipient, notification_type=notification_type, title=title, message=message, channel='in_app', **kwargs ) # Отправляем через WebSocket для real-time уведомлений WebSocketNotificationService.send_notification_via_websocket(in_app_notification) # Создаем email уведомление, если включено email_enabled = True if preferences: email_enabled = preferences.is_type_enabled(notification_type, 'email') else: # Если нет настроек, проверяем есть ли email у пользователя email_enabled = bool(recipient.email) if email_enabled and recipient.email: # Создаем email уведомление email_notification = Notification.create_notification( recipient=recipient, notification_type=notification_type, title=title, message=message, channel='email', **kwargs ) # Отправляем асинхронно send_notification_task.delay(email_notification.id) # Создаем telegram уведомление, если возможно if recipient.telegram_id: telegram_enabled = True if preferences: telegram_enabled = preferences.is_type_enabled(notification_type, 'telegram') else: # Если нет настроек, считаем что telegram включен по умолчанию telegram_enabled = True if telegram_enabled: # Создаем telegram уведомление telegram_notification = Notification.create_notification( recipient=recipient, notification_type=notification_type, title=title, message=message, channel='telegram', **kwargs ) # Отправляем асинхронно send_notification_task.delay(telegram_notification.id) return in_app_notification @staticmethod def send_lesson_created(lesson): """ Отправка уведомления о создании занятия. Args: lesson: Объект занятия """ from django.utils import timezone import pytz # Уведомление клиенту if lesson.client and lesson.client.user: # Используем часовой пояс пользователя from apps.users.utils import get_user_timezone user_timezone = get_user_timezone(lesson.client.user.timezone or 'UTC') if timezone.is_aware(lesson.start_time): local_time = lesson.start_time.astimezone(user_timezone) else: # Если время не aware, считаем что оно в UTC utc_time = timezone.make_aware(lesson.start_time, pytz.UTC) local_time = utc_time.astimezone(user_timezone) start_time = local_time.strftime('%d.%m.%Y в %H:%M') # Проверяем, является ли занятие постоянным is_recurring = getattr(lesson, 'is_recurring', False) recurring_text = ' (постоянное занятие)' if is_recurring else '' NotificationService.create_notification_with_telegram( recipient=lesson.client.user, notification_type='lesson_created', title='📅 Новое занятие', message=f'Запланировано занятие{recurring_text}: {lesson.title} на {start_time}', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) # Отправляем уведомление родителям ребенка from apps.users.models import Parent child_user = lesson.client.user child_id = child_user.id # Находим всех родителей этого ребенка try: child_client = lesson.client # Оптимизация: используем list() для кеширования запроса parents = list(child_client.parents.select_related('user').all()) for parent in parents: if parent.user: NotificationService.create_notification_with_telegram( recipient=parent.user, notification_type='lesson_created', title='📅 Новое занятие для вашего ребенка', message=f'Запланировано занятие{recurring_text} для {child_user.get_full_name() or child_user.email}: {lesson.title} на {start_time}', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson, child_id=child_id, data={'child_id': child_id} ) except Exception as e: logger.warning(f'Error sending notification to parents for lesson {lesson.id}: {e}') @staticmethod def send_lesson_cancelled(lesson): """ Отправка уведомления об отмене занятия. Args: lesson: Объект занятия """ # Уведомление клиенту if lesson.client and lesson.client.user: child_user = lesson.client.user child_id = child_user.id NotificationService.create_notification_with_telegram( recipient=child_user, notification_type='lesson_cancelled', title='❌ Занятие отменено', message=f'Занятие "{lesson.title}" отменено', priority='high', action_url='/schedule', content_object=lesson ) # Отправляем уведомление родителям ребенка try: child_client = lesson.client # Оптимизация: используем list() для кеширования запроса parents = list(child_client.parents.select_related('user').all()) for parent in parents: if parent.user: NotificationService.create_notification_with_telegram( recipient=parent.user, notification_type='lesson_cancelled', title='❌ Занятие отменено', message=f'Занятие "{lesson.title}" для {child_user.get_full_name() or child_user.email} отменено', priority='high', action_url='/schedule', content_object=lesson, child_id=child_id, data={'child_id': child_id} ) except Exception as e: logger.warning(f'Error sending notification to parents for cancelled lesson {lesson.id}: {e}') @staticmethod def send_lesson_rescheduled(lesson): """Уведомление о переносе занятия.""" from django.utils import timezone import pytz from apps.users.utils import get_user_timezone user_tz = get_user_timezone(lesson.mentor.timezone or 'UTC') if timezone.is_aware(lesson.start_time): local_time = lesson.start_time.astimezone(user_tz) else: local_time = timezone.make_aware(lesson.start_time, pytz.UTC).astimezone(user_tz) start_str = local_time.strftime('%d.%m.%Y в %H:%M') msg = f'Занятие "{lesson.title}" перенесено на {start_str}' NotificationService.create_notification_with_telegram( recipient=lesson.mentor, notification_type='lesson_rescheduled', title='📅 Занятие перенесено', message=msg, priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) if lesson.client and lesson.client.user: child_user = lesson.client.user child_tz = get_user_timezone(child_user.timezone or 'UTC') if timezone.is_aware(lesson.start_time): local_time = lesson.start_time.astimezone(child_tz) else: local_time = timezone.make_aware(lesson.start_time, pytz.UTC).astimezone(child_tz) start_str = local_time.strftime('%d.%m.%Y в %H:%M') NotificationService.create_notification_with_telegram( recipient=child_user, notification_type='lesson_rescheduled', title='📅 Занятие перенесено', message=f'Занятие "{lesson.title}" перенесено на {start_str}', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) try: parents = list(lesson.client.parents.select_related('user').all()) for parent in parents: if parent.user: NotificationService.create_notification_with_telegram( recipient=parent.user, notification_type='lesson_rescheduled', title='📅 Занятие перенесено', message=f'Занятие "{lesson.title}" для {child_user.get_full_name() or child_user.email} перенесено на {start_str}', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson, child_id=child_user.id, data={'child_id': child_user.id} ) except Exception as e: logger.warning(f'Error sending rescheduled notification to parents for lesson {lesson.id}: {e}') @staticmethod def send_lesson_completed(lesson): """Уведомление о завершении занятия.""" NotificationService.create_notification_with_telegram( recipient=lesson.mentor, notification_type='lesson_completed', title='✅ Занятие завершено', message=f'Занятие "{lesson.title}" завершено', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) if lesson.client and lesson.client.user: child_user = lesson.client.user NotificationService.create_notification_with_telegram( recipient=child_user, notification_type='lesson_completed', title='✅ Занятие завершено', message=f'Занятие "{lesson.title}" завершено', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) try: parents = list(lesson.client.parents.select_related('user').all()) for parent in parents: if parent.user: NotificationService.create_notification_with_telegram( recipient=parent.user, notification_type='lesson_completed', title='✅ Занятие завершено', message=f'Занятие "{lesson.title}" для {child_user.get_full_name() or child_user.email} завершено', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson, child_id=child_user.id, data={'child_id': child_user.id} ) except Exception as e: logger.warning(f'Error sending completed notification to parents for lesson {lesson.id}: {e}') @staticmethod def send_lesson_deleted(lesson, is_recurring_series=False): """ Отправка уведомления об удалении занятия. Args: lesson: Объект занятия is_recurring_series: Если True, отправляет уведомление о удалении цепочки постоянных занятий """ # Уведомление клиенту if lesson.client and lesson.client.user: if is_recurring_series: # Уведомление об удалении цепочки постоянных занятий NotificationService.create_notification_with_telegram( recipient=lesson.client.user, notification_type='lesson_cancelled', title='🗑️ Постоянные занятия удалены', message=f'Цепочка постоянных занятий "{lesson.title}" удалена', priority='high', action_url='/schedule', content_object=lesson ) else: # Уведомление об удалении одного занятия NotificationService.create_notification_with_telegram( recipient=lesson.client.user, notification_type='lesson_cancelled', title='🗑️ Занятие удалено', message=f'Занятие "{lesson.title}" удалено', priority='high', action_url='/schedule', content_object=lesson ) @staticmethod def send_lesson_reminder(lesson, time_before=None): """ Отправка напоминания о занятии. Args: lesson: Объект занятия time_before: Время до занятия (например, "24 часа", "1 час", "15 минут") """ from django.utils import timezone import pytz # Вычисляем время до занятия для сообщения now = timezone.now() if timezone.is_aware(lesson.start_time): time_until = lesson.start_time - now else: utc_time = timezone.make_aware(lesson.start_time, pytz.UTC) time_until = utc_time - now # Форматируем время до занятия if time_before: time_text = f"через {time_before}" else: # Автоматически определяем время до занятия total_seconds = int(time_until.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 if hours >= 24: days = hours // 24 time_text = f"через {days} {NotificationService._pluralize_days(days)}" elif hours > 0: time_text = f"через {hours} {NotificationService._pluralize_hours(hours)}" elif minutes > 0: time_text = f"через {minutes} {NotificationService._pluralize_minutes(minutes)}" else: time_text = "скоро" # Отправляем ментору from apps.users.utils import get_user_timezone mentor_timezone = get_user_timezone(lesson.mentor.timezone or 'UTC') if timezone.is_aware(lesson.start_time): mentor_local_time = lesson.start_time.astimezone(mentor_timezone) else: utc_time = timezone.make_aware(lesson.start_time, pytz.UTC) mentor_local_time = utc_time.astimezone(mentor_timezone) mentor_start_time = mentor_local_time.strftime('%d.%m.%Y в %H:%M') NotificationService.create_notification_with_telegram( recipient=lesson.mentor, notification_type='lesson_reminder', title='⏰ Напоминание о занятии', message=f'Занятие "{lesson.title}" начнется {time_text} ({mentor_start_time})', priority='high', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) # Отправляем клиенту if lesson.client and lesson.client.user: from apps.users.utils import get_user_timezone client_timezone = get_user_timezone(lesson.client.user.timezone or 'UTC') if timezone.is_aware(lesson.start_time): client_local_time = lesson.start_time.astimezone(client_timezone) else: utc_time = timezone.make_aware(lesson.start_time, pytz.UTC) client_local_time = utc_time.astimezone(client_timezone) client_start_time = client_local_time.strftime('%d.%m.%Y в %H:%M') child_user = lesson.client.user child_id = child_user.id NotificationService.create_notification_with_telegram( recipient=child_user, notification_type='lesson_reminder', title='⏰ Напоминание о занятии', message=f'Занятие "{lesson.title}" начнется {time_text} ({client_start_time})', priority='high', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) # Отправляем уведомление родителям ребенка try: child_client = lesson.client # Оптимизация: используем list() для кеширования запроса parents = list(child_client.parents.select_related('user').all()) for parent in parents: if parent.user: NotificationService.create_notification_with_telegram( recipient=parent.user, notification_type='lesson_reminder', title='⏰ Напоминание о занятии для вашего ребенка', message=f'{child_user.get_full_name() or child_user.email}: Занятие "{lesson.title}" начнется {time_text} ({client_start_time})', priority='high', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson, child_id=child_id, data={'child_id': child_id} ) except Exception as e: logger.warning(f'Error sending reminder notification to parents for lesson {lesson.id}: {e}') @staticmethod def _pluralize_days(count): """Склонение слова 'день'.""" if count % 10 == 1 and count % 100 != 11: return 'день' elif 2 <= count % 10 <= 4 and (count % 100 < 10 or count % 100 >= 20): return 'дня' else: return 'дней' @staticmethod def _pluralize_hours(count): """Склонение слова 'час'.""" if count % 10 == 1 and count % 100 != 11: return 'час' elif 2 <= count % 10 <= 4 and (count % 100 < 10 or count % 100 >= 20): return 'часа' else: return 'часов' @staticmethod def _pluralize_minutes(count): """Склонение слова 'минута'.""" if count % 10 == 1 and count % 100 != 11: return 'минуту' elif 2 <= count % 10 <= 4 and (count % 100 < 10 or count % 100 >= 20): return 'минуты' else: return 'минут' @staticmethod def send_homework_notification(homework, notification_type='homework_assigned', student=None): """ Отправка уведомления о домашнем задании. Args: homework: Объект домашнего задания notification_type: Тип уведомления student: Студент (для homework_submitted и homework_reviewed) """ if notification_type == 'homework_assigned': # Уведомление всем назначенным ученикам о новом ДЗ recipients = homework.assigned_to.all() title = '📝 Новое домашнее задание' lesson_title = homework.lesson.title if homework.lesson else homework.title message = f'Вам назначено домашнее задание по занятию "{lesson_title}"' # Отправляем уведомление каждому назначенному студенту for recipient in recipients: child_id = recipient.id NotificationService.create_notification_with_telegram( recipient=recipient, notification_type=notification_type, title=title, message=message, priority='normal', action_url=f'/homework/{homework.id}', content_object=homework ) # Отправляем уведомление родителям ребенка try: from apps.users.models import Client, Parent child_client = Client.objects.get(user=recipient) parents = child_client.parents.all() for parent in parents: if parent.user: NotificationService.create_notification_with_telegram( recipient=parent.user, notification_type=notification_type, title='📝 Новое домашнее задание для вашего ребенка', message=f'{recipient.get_full_name() or recipient.email}: {message}', priority='normal', action_url=f'/homework/{homework.id}', content_object=homework, child_id=child_id, data={'child_id': child_id} ) except Exception as e: logger.warning(f'Error sending notification to parents for homework {homework.id}: {e}') return elif notification_type == 'homework_submitted': # Уведомление ментору о сданном ДЗ if not homework.lesson: return recipient = homework.lesson.mentor student_name = student.get_full_name() if student else 'Ученик' title = '📤 Домашнее задание сдано' lesson_title = homework.lesson.title if homework.lesson else homework.title message = f'{student_name} сдал домашнее задание по занятию "{lesson_title}"' elif notification_type == 'homework_reviewed': # Уведомление ученику о проверенном ДЗ if not student: return recipient = student child_id = student.id title = '✅ Домашнее задание проверено' lesson_title = homework.lesson.title if homework.lesson else homework.title message = f'Ваше домашнее задание по занятию "{lesson_title}" проверено' else: return if recipient: NotificationService.create_notification_with_telegram( recipient=recipient, notification_type=notification_type, title=title, message=message, priority='normal', action_url=f'/homework/{homework.id}', content_object=homework ) # Отправляем уведомление родителям ребенка (только для homework_reviewed и homework_submitted) if notification_type in ['homework_reviewed', 'homework_submitted'] and recipient.role == 'client': try: from apps.users.models import Client, Parent child_client = Client.objects.get(user=recipient) parents = child_client.parents.all() for parent in parents: if parent.user: NotificationService.create_notification_with_telegram( recipient=parent.user, notification_type=notification_type, title=f'{title} для вашего ребенка', message=f'{recipient.get_full_name() or recipient.email}: {message}', priority='normal', action_url=f'/homework/{homework.id}', content_object=homework, child_id=child_id, data={'child_id': child_id} ) except Exception as e: logger.warning(f'Error sending notification to parents for homework {homework.id}: {e}') @staticmethod def send_attendance_confirmation_request(lesson): """ Отправка запроса о подтверждении присутствия студенту. Args: lesson: Объект занятия """ from django.utils import timezone import pytz if not lesson.client or not lesson.client.user: return student = lesson.client.user # Используем часовой пояс пользователя from apps.users.utils import get_user_timezone user_timezone = get_user_timezone(student.timezone or 'UTC') if timezone.is_aware(lesson.start_time): local_time = lesson.start_time.astimezone(user_timezone) else: utc_time = timezone.make_aware(lesson.start_time, pytz.UTC) local_time = utc_time.astimezone(user_timezone) start_time = local_time.strftime('%d.%m.%Y в %H:%M') # Создаем in-app уведомление и отправляем email from .models import Notification from .tasks import send_notification_task # Создаем in-app уведомление notification = Notification.create_notification( recipient=student, notification_type='lesson_reminder', # Используем существующий тип title='❓ Подтверждение присутствия', message=f'Будете ли вы присутствовать на занятии "{lesson.title}" {start_time}?', channel='in_app', priority='high', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson, data={ 'lesson_id': lesson.id, 'requires_attendance_confirmation': True, 'attendance_yes_url': f'/api/schedule/lessons/{lesson.id}/confirm-attendance/?response=yes', 'attendance_no_url': f'/api/schedule/lessons/{lesson.id}/confirm-attendance/?response=no', } ) # Отправляем email уведомление, если включено try: preferences = student.notification_preferences email_enabled = preferences.is_type_enabled('lesson_reminder', 'email') except: email_enabled = bool(student.email) if email_enabled and student.email: email_notification = Notification.create_notification( recipient=student, notification_type='lesson_reminder', title='❓ Подтверждение присутствия', message=f'Будете ли вы присутствовать на занятии "{lesson.title}" {start_time}?', channel='email', priority='high', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson, ) send_notification_task.delay(email_notification.id) # Отправляем также в Telegram с кнопками if student.telegram_id: try: preferences = student.notification_preferences telegram_enabled = preferences.is_type_enabled('lesson_reminder', 'telegram') except: telegram_enabled = True if telegram_enabled: from telegram import InlineKeyboardButton, InlineKeyboardMarkup from .telegram_bot import send_telegram_message_with_buttons import asyncio keyboard = [ [ InlineKeyboardButton("✅ Да, буду", callback_data=f"attendance_yes_{lesson.id}"), InlineKeyboardButton("❌ Нет, не смогу", callback_data=f"attendance_no_{lesson.id}") ] ] reply_markup = InlineKeyboardMarkup(keyboard) message_text = ( f"❓ Подтверждение присутствия\n\n" f"Будете ли вы присутствовать на занятии:\n" f"{lesson.title}\n" f"📅 {start_time}" ) # Отправляем сообщение с кнопками напрямую try: # Запускаем асинхронную отправку loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) success = loop.run_until_complete( send_telegram_message_with_buttons( student.telegram_id, message_text, reply_markup ) ) loop.close() except Exception as e: logger.error(f'Error sending Telegram message with buttons: {e}') success = False @staticmethod def send_attendance_response_to_mentor(lesson, response): """ Отправка уведомления ментору о ответе студента на запрос о присутствии. Args: lesson: Объект занятия response: True если будет присутствовать, False если нет """ if not lesson.mentor: return student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент' response_text = "будет присутствовать" if response else "не сможет присутствовать" NotificationService.create_notification_with_telegram( recipient=lesson.mentor, notification_type='lesson_reminder', title='📢 Ответ о присутствии', message=f'{student_name} {response_text} на занятии "{lesson.title}"', priority='normal', action_url=f'/schedule?lesson={lesson.id}', content_object=lesson ) @staticmethod def send_message_notification(message): """ Отправка уведомлений о новом сообщении всем участникам чата, кроме отправителя. Args: message: Экземпляр Message """ from .models import Notification chat = message.chat sender = message.sender # Получаем всех участников чата, кроме отправителя participants = chat.participants.exclude(user=sender).select_related('user') # Формируем имя отправителя sender_name = sender.get_full_name() if sender else 'Система' # Обрезаем сообщение для уведомления content_preview = '' if message.content: content_preview = message.content[:100] if len(message.content) > 100: content_preview += '...' # Определяем заголовок в зависимости от типа чата if chat.chat_type == 'direct': title = f'💬 Новое сообщение от {sender_name}' else: title = f'💬 {sender_name} в чате "{chat.name or "Групповой чат"}"' # Создаем уведомления для каждого участника for participant in participants: # Пропускаем если уведомления отключены if participant.is_muted: continue NotificationService.create_notification_with_telegram( recipient=participant.user, notification_type='message_received', title=title, message=content_preview or 'Новое сообщение', priority='normal', action_url=f'/chat?chat={chat.uuid}', content_object=message ) def create_notification_preferences(user): """ Создание настроек уведомлений для пользователя. Args: user: Пользователь """ from .models import NotificationPreference try: NotificationPreference.objects.get_or_create( user=user, defaults={ 'enabled': True, 'email_enabled': True, 'telegram_enabled': bool(user.telegram_id), 'in_app_enabled': True, } ) except Exception as e: logger.error(f"Error creating notification preferences: {e}") class WebSocketNotificationService: """Сервис для отправки уведомлений через WebSocket.""" @staticmethod def send_notification_via_websocket(notification): """ Отправка уведомления через WebSocket в реальном времени. Args: notification: Объект Notification """ try: channel_layer = get_channel_layer() if not channel_layer: logger.warning("Channel layer not available, skipping WebSocket notification") return from .serializers import NotificationSerializer from .models import Notification # Сериализуем уведомление serializer = NotificationSerializer(notification) notification_data = serializer.data # Группа пользователя для уведомлений user_group_name = f'notifications_user_{notification.recipient.id}' # Получаем количество непрочитанных уведомлений unread_count = Notification.objects.filter( recipient=notification.recipient, channel='in_app', is_read=False ).count() # Отправляем через channel_layer async_to_sync(channel_layer.group_send)( user_group_name, { 'type': 'notification_created', 'notification': notification_data, 'unread_count': unread_count } ) logger.info(f"WebSocket notification sent to user {notification.recipient.id} (unread: {unread_count})") except Exception as e: logger.error(f"Error sending WebSocket notification: {e}", exc_info=True) @staticmethod def send_nav_badges_updated(user_id): """ Уведомить пользователя об изменении бейджей нижнего меню (чат, расписание, ДЗ и т.д.). Клиент по событию nav_badges_updated перезапросит GET /api/nav-badges/. """ try: channel_layer = get_channel_layer() if not channel_layer: return user_group_name = f'notifications_user_{user_id}' async_to_sync(channel_layer.group_send)( user_group_name, {'type': 'nav_badges_updated'}, ) except Exception as e: logger.debug("send_nav_badges_updated: %s", e)