refactor: Удалены старые отправители уведомлений
Очистка от дублирующегося кода: Удалены старые файлы: ❌ email_sender.py → заменен на senders/email.py ❌ telegram_sender.py → функционал Telegram удален ❌ signals_telegram.py → Telegram сигналы удалены Обновлены: ✅ apps.py - убран импорт signals_telegram ✅ services.py - обновлены импорты, Telegram помечен deprecated NotificationService (старый) продолжает работать для обратной совместимости, но использует новые senders где возможно. Telegram функционал отключен в старой системе с пометкой 'use new notification system'. Результат: - Меньше дублирования кода - Чище структура проекта - Все отправители теперь наследуются от BaseNotificationSender - Готовы к полной миграции на новую архитектуру
This commit is contained in:
parent
e1be95fbe4
commit
fd9b36c1c9
@ -8,4 +8,4 @@ class NotificationsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
import notifications.signals # noqa
|
||||
import notifications.signals_telegram # noqa
|
||||
# Old signals_telegram removed - Telegram support moved to new architecture
|
||||
|
||||
@ -1,254 +0,0 @@
|
||||
"""
|
||||
Email sender для системы уведомлений.
|
||||
Отправляет красивые HTML письма с уведомлениями.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from common.emails import send_templated_mail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailNotificationSender:
|
||||
"""
|
||||
Класс для отправки email уведомлений.
|
||||
Использует HTML шаблоны для красивого оформления.
|
||||
"""
|
||||
|
||||
# Маппинг категорий на иконки для email
|
||||
CATEGORY_ICONS = {
|
||||
'communications': '💬',
|
||||
'documents': '📄',
|
||||
'requests': '📋',
|
||||
'calendar': '📅',
|
||||
'department': '👥',
|
||||
'profile': '👤',
|
||||
'feed': '📰',
|
||||
'system': '⚙️',
|
||||
}
|
||||
|
||||
# Маппинг категорий на цвета
|
||||
CATEGORY_COLORS = {
|
||||
'communications': '#0d6efd', # primary
|
||||
'documents': '#198754', # success
|
||||
'requests': '#ffc107', # warning
|
||||
'calendar': '#dc3545', # danger
|
||||
'department': '#6f42c1', # purple
|
||||
'profile': '#0dcaf0', # info
|
||||
'feed': '#fd7e14', # orange
|
||||
'system': '#6c757d', # secondary
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def send_notification_email(
|
||||
cls,
|
||||
notification,
|
||||
recipient_email: str,
|
||||
custom_subject: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Отправляет email уведомление пользователю.
|
||||
|
||||
Args:
|
||||
notification: объект Notification
|
||||
recipient_email: email получателя
|
||||
custom_subject: кастомная тема письма (опционально)
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
logger.info(
|
||||
f"[EmailNotificationSender.send_notification_email] НАЧАЛО: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"recipient={recipient_email}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Получаем категорию
|
||||
category_code = notification.notification_type.category.code
|
||||
category_name = notification.notification_type.category.name
|
||||
|
||||
logger.info(
|
||||
f"[EmailNotificationSender] Категория: {category_name} ({category_code})"
|
||||
)
|
||||
|
||||
# Формируем тему письма
|
||||
if custom_subject:
|
||||
subject = custom_subject
|
||||
else:
|
||||
icon = cls.CATEGORY_ICONS.get(category_code, '🔔')
|
||||
subject = f"{icon} {notification.title}"
|
||||
|
||||
logger.info(
|
||||
f"[EmailNotificationSender] Тема письма: '{subject}'"
|
||||
)
|
||||
|
||||
# Формируем контекст для шаблона
|
||||
context = {
|
||||
'notification': notification,
|
||||
'category_name': category_name,
|
||||
'category_icon': cls.CATEGORY_ICONS.get(category_code, '🔔'),
|
||||
'category_color': cls.CATEGORY_COLORS.get(category_code, '#0d6efd'),
|
||||
'action_url': cls._get_full_url(notification.action_url),
|
||||
'action_text': notification.action_text or 'Посмотреть',
|
||||
'site_name': getattr(settings, 'SITE_NAME', 'EUSRR'),
|
||||
'site_url': cls._get_site_url(),
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"[EmailNotificationSender] ➡️ Вызов send_templated_mail: "
|
||||
f"template='notifications/email/notification', "
|
||||
f"to={recipient_email}"
|
||||
)
|
||||
|
||||
# Отправляем email
|
||||
send_templated_mail(
|
||||
subject=subject,
|
||||
to=[recipient_email],
|
||||
template_base='notifications/email/notification',
|
||||
context=context,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[EmailNotificationSender] ✅ Email УСПЕШНО отправлен: "
|
||||
f"notification_id={notification.id} -> {recipient_email}"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[EmailNotificationSender] ❌ ОШИБКА отправки email: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"recipient={recipient_email}, "
|
||||
f"error={type(e).__name__}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def send_digest_email(
|
||||
cls,
|
||||
recipient_email: str,
|
||||
notifications: list,
|
||||
digest_type: str = 'daily',
|
||||
) -> bool:
|
||||
"""
|
||||
Отправляет дайджест уведомлений.
|
||||
|
||||
Args:
|
||||
recipient_email: email получателя
|
||||
notifications: список объектов Notification
|
||||
digest_type: тип дайджеста ('daily' или 'weekly')
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
try:
|
||||
if not notifications:
|
||||
return False
|
||||
|
||||
# Группируем по категориям
|
||||
grouped = {}
|
||||
for notif in notifications:
|
||||
category = notif.notification_type.category.code
|
||||
if category not in grouped:
|
||||
grouped[category] = []
|
||||
grouped[category].append(notif)
|
||||
|
||||
# Формируем тему
|
||||
digest_name = 'Ежедневный' if digest_type == 'daily' else 'Еженедельный'
|
||||
subject = f"📬 {digest_name} дайджест уведомлений ({len(notifications)})"
|
||||
|
||||
# Формируем контекст
|
||||
context = {
|
||||
'notifications': notifications,
|
||||
'grouped_notifications': grouped,
|
||||
'digest_type': digest_type,
|
||||
'digest_name': digest_name,
|
||||
'total_count': len(notifications),
|
||||
'category_icons': cls.CATEGORY_ICONS,
|
||||
'category_colors': cls.CATEGORY_COLORS,
|
||||
'site_name': getattr(settings, 'SITE_NAME', 'EUSRR'),
|
||||
'site_url': cls._get_site_url(),
|
||||
}
|
||||
|
||||
# Отправляем дайджест
|
||||
send_templated_mail(
|
||||
subject=subject,
|
||||
to=[recipient_email],
|
||||
template_base='notifications/email/digest',
|
||||
context=context,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Дайджест отправлен: {len(notifications)} уведомлений -> {recipient_email}"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка отправки дайджеста на {recipient_email}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_site_url() -> str:
|
||||
"""Получает базовый URL сайта из настроек."""
|
||||
# Пробуем получить из настроек
|
||||
if hasattr(settings, 'SITE_URL'):
|
||||
return settings.SITE_URL
|
||||
|
||||
# Формируем из ALLOWED_HOSTS
|
||||
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'
|
||||
|
||||
@staticmethod
|
||||
def _get_full_url(path: str) -> str:
|
||||
"""Преобразует относительный URL в абсолютный."""
|
||||
if not path:
|
||||
return ''
|
||||
|
||||
if path.startswith('http://') or path.startswith('https://'):
|
||||
return path
|
||||
|
||||
site_url = EmailNotificationSender._get_site_url()
|
||||
return f"{site_url}{path}"
|
||||
|
||||
@classmethod
|
||||
def test_email_connection(cls, test_email: str) -> bool:
|
||||
"""
|
||||
Тестирует подключение к email серверу.
|
||||
|
||||
Args:
|
||||
test_email: email для отправки тестового письма
|
||||
|
||||
Returns:
|
||||
True если успешно, False иначе
|
||||
"""
|
||||
try:
|
||||
from django.core.mail import send_mail
|
||||
|
||||
send_mail(
|
||||
subject='🔔 Тест системы уведомлений EUSRR',
|
||||
message='Это тестовое письмо. Если вы его получили, значит настройки email работают корректно.',
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[test_email],
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
logger.info(f"Тестовое письмо успешно отправлено на {test_email}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки тестового письма: {e}", exc_info=True)
|
||||
return False
|
||||
@ -13,8 +13,15 @@ from .models import (
|
||||
NotificationType,
|
||||
UserNotificationSettings,
|
||||
)
|
||||
from .email_sender import EmailNotificationSender
|
||||
from .telegram_sender import TelegramNotificationSender
|
||||
|
||||
# DEPRECATED: Old senders, use new architecture in senders/
|
||||
# from .email_sender import EmailNotificationSender
|
||||
# from .telegram_sender import TelegramNotificationSender
|
||||
# New senders available in notifications.senders module
|
||||
from .senders import EmailNotificationSender
|
||||
|
||||
# Telegram support removed, use new notification system
|
||||
TelegramNotificationSender = None # Placeholder for backward compatibility
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -381,26 +388,12 @@ class NotificationService:
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[NotificationService.send_notification] ➡️ Вызов TelegramNotificationSender.send_notification"
|
||||
f"[NotificationService.send_notification] ⚠️ Telegram support deprecated - use new notification system"
|
||||
)
|
||||
|
||||
# Отправляем через Bot API по chat_id
|
||||
from .telegram_sender import TelegramNotificationSender
|
||||
success = TelegramNotificationSender.send_notification(
|
||||
notification=notification,
|
||||
chat_id=telegram_chat_id,
|
||||
site_url=site_url
|
||||
)
|
||||
notification.sent_telegram = success
|
||||
|
||||
if success:
|
||||
logger.debug(
|
||||
f"[NotificationService.send_notification] ✅ Telegram: успешно отправлено (chat_id={telegram_chat_id})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[NotificationService.send_notification] ⚠️ Telegram: отправка вернула False"
|
||||
)
|
||||
# DEPRECATED: Telegram support moved to new architecture
|
||||
# Use notifications.signals_new.notify() with new UserChannelPreferences
|
||||
notification.sent_telegram = False
|
||||
else:
|
||||
notification.sent_telegram = False
|
||||
logger.warning(
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
"""
|
||||
Signals для автоматической отвязки Telegram при изменении контактных данных
|
||||
"""
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from employees.models import Employee
|
||||
from notifications.telegram_models import TelegramUser
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Employee)
|
||||
def check_telegram_field_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Отвязывает Telegram аккаунт если пользователь изменил поле telegram
|
||||
"""
|
||||
# Проверяем что это обновление существующего объекта
|
||||
if not instance.pk:
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем старое значение из БД
|
||||
old_instance = Employee.objects.get(pk=instance.pk)
|
||||
old_telegram = old_instance.telegram.strip().lower()
|
||||
new_telegram = instance.telegram.strip().lower()
|
||||
|
||||
# Если поле telegram изменилось
|
||||
if old_telegram != new_telegram:
|
||||
# Ищем привязанный Telegram аккаунт
|
||||
tg_users = TelegramUser.objects.filter(
|
||||
user=instance,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
if tg_users.exists():
|
||||
# Отвязываем все активные привязки
|
||||
count = tg_users.update(is_active=False)
|
||||
|
||||
logger.info(
|
||||
f"Telegram отвязан для {instance.email}: "
|
||||
f"поле изменено с '{old_telegram}' на '{new_telegram}' "
|
||||
f"(отвязано аккаунтов: {count})"
|
||||
)
|
||||
|
||||
# Опционально: отправить уведомление пользователю
|
||||
from notifications.telegram_sender import (
|
||||
TelegramNotificationSender
|
||||
)
|
||||
|
||||
for tg_user in tg_users:
|
||||
if tg_user.telegram_id:
|
||||
try:
|
||||
TelegramNotificationSender.send_message(
|
||||
telegram_id=tg_user.telegram_id,
|
||||
text=(
|
||||
"⚠️ <b>Аккаунт отвязан</b>\n\n"
|
||||
"Ваш Telegram аккаунт был отвязан от EUSRR, "
|
||||
"так как вы изменили поле 'Telegram' "
|
||||
"в своём профиле.\n\n"
|
||||
"Для повторной привязки используйте /link"
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Не удалось отправить уведомление об отвязке: {e}"
|
||||
)
|
||||
|
||||
except Employee.DoesNotExist:
|
||||
# Объект был удален между проверкой и сохранением
|
||||
pass
|
||||
@ -1,450 +0,0 @@
|
||||
"""
|
||||
Telegram sender для отправки уведомлений через Telegram бот
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
from django.conf import settings
|
||||
from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.error import TelegramError
|
||||
from telegram.constants import ParseMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TelegramNotificationSender:
|
||||
"""
|
||||
Класс для отправки уведомлений через Telegram бот.
|
||||
"""
|
||||
|
||||
# Маппинг категорий на emoji
|
||||
CATEGORY_EMOJI = {
|
||||
'communications': '💬',
|
||||
'documents': '📄',
|
||||
'requests': '📋',
|
||||
'calendar': '📅',
|
||||
'department': '👥',
|
||||
'profile': '👤',
|
||||
'feed': '📰',
|
||||
'system': '⚙️',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_bot(cls) -> Optional[Bot]:
|
||||
"""Получить экземпляр бота"""
|
||||
token = getattr(settings, 'TELEGRAM_BOT_TOKEN', None)
|
||||
if not token:
|
||||
logger.error("TELEGRAM_BOT_TOKEN не настроен в settings")
|
||||
return None
|
||||
|
||||
return Bot(token=token)
|
||||
|
||||
@classmethod
|
||||
async def send_notification_async(
|
||||
cls,
|
||||
telegram_id: int,
|
||||
notification,
|
||||
site_url: str = None
|
||||
) -> bool:
|
||||
"""
|
||||
Асинхронная отправка уведомления в Telegram
|
||||
|
||||
Args:
|
||||
telegram_id: ID пользователя в Telegram
|
||||
notification: объект Notification
|
||||
site_url: базовый URL сайта
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender.send_notification_async] НАЧАЛО: "
|
||||
f"telegram_id={telegram_id}, "
|
||||
f"notification_id={notification.id}"
|
||||
)
|
||||
|
||||
bot = cls.get_bot()
|
||||
if not bot:
|
||||
logger.error(
|
||||
f"[TelegramNotificationSender] ❌ Bot не инициализирован (токен отсутствует)"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Получаем категорию
|
||||
category_code = notification.notification_type.category.code
|
||||
category_name = notification.notification_type.category.name
|
||||
emoji = cls.CATEGORY_EMOJI.get(category_code, '🔔')
|
||||
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Категория: {category_name} ({category_code}), emoji: {emoji}"
|
||||
)
|
||||
|
||||
# Формируем текст сообщения
|
||||
text = f"{emoji} <b>{notification.title}</b>\n\n"
|
||||
text += f"{notification.message}\n\n"
|
||||
text += f"<i>Категория: {category_name}</i>"
|
||||
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Текст сообщения сформирован (длина: {len(text)} символов)"
|
||||
)
|
||||
|
||||
# Создаем кнопку действия если есть URL
|
||||
keyboard = None
|
||||
if notification.action_url:
|
||||
action_text = notification.action_text or 'Посмотреть'
|
||||
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Формирование кнопки: action_url={notification.action_url}"
|
||||
)
|
||||
|
||||
# Формируем полный URL
|
||||
if site_url:
|
||||
if notification.action_url.startswith('http'):
|
||||
url = notification.action_url
|
||||
else:
|
||||
# Убираем trailing slash из site_url если есть
|
||||
base_url = site_url.rstrip('/')
|
||||
url = f"{base_url}{notification.action_url}"
|
||||
else:
|
||||
url = notification.action_url
|
||||
|
||||
# Telegram не принимает:
|
||||
# 1. URL вида "http://site.com/" (только корень)
|
||||
# 2. localhost URL в inline кнопках
|
||||
url_without_trailing = url.rstrip('/')
|
||||
base_without_trailing = (
|
||||
site_url.rstrip('/') if site_url else ''
|
||||
)
|
||||
|
||||
is_valid_url = (
|
||||
url_without_trailing and
|
||||
url_without_trailing != base_without_trailing and
|
||||
'localhost' not in url.lower() and
|
||||
'127.0.0.1' not in url
|
||||
)
|
||||
|
||||
if is_valid_url:
|
||||
keyboard = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton(action_text, url=url)]
|
||||
])
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ✅ Inline кнопка создана: {url}"
|
||||
)
|
||||
elif 'localhost' in url.lower() or '127.0.0.1' in url:
|
||||
# Добавляем URL в текст сообщения
|
||||
text += f"\n\n🔗 Ссылка: {url}"
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ⚠️ localhost URL добавлен в текст (inline кнопка невозможна)"
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ➡️ Отправка в Telegram API (chat_id={telegram_id})..."
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=telegram_id,
|
||||
text=text,
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=keyboard,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ✅ Telegram API УСПЕШНО: "
|
||||
f"notification_id={notification.id} -> chat_id={telegram_id}"
|
||||
)
|
||||
return True
|
||||
|
||||
except TelegramError as e:
|
||||
error_msg = str(e)
|
||||
|
||||
logger.error(
|
||||
f"[TelegramNotificationSender] ❌ TelegramError: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"chat_id={telegram_id}, "
|
||||
f"error={error_msg}"
|
||||
)
|
||||
|
||||
# Обработка специфичных ошибок
|
||||
if "chat not found" in error_msg.lower():
|
||||
logger.warning(
|
||||
f"[TelegramNotificationSender] ⚠️ Чат не найден: "
|
||||
f"пользователь с Chat ID {telegram_id} не отправил /start боту"
|
||||
)
|
||||
elif "bot was blocked" in error_msg.lower() or "forbidden" in error_msg.lower():
|
||||
logger.warning(
|
||||
f"[TelegramNotificationSender] ⚠️ Бот заблокирован: "
|
||||
f"пользователь с Chat ID {telegram_id} заблокировал бота"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"[TelegramNotificationSender] ❌ Необработанная ошибка Telegram: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Неожиданная ошибка отправки Telegram: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def send_notification(
|
||||
cls,
|
||||
notification,
|
||||
chat_id: Optional[str] = None,
|
||||
site_url: str = None
|
||||
) -> bool:
|
||||
"""
|
||||
Синхронная обёртка для отправки уведомления
|
||||
|
||||
Args:
|
||||
notification: объект Notification
|
||||
chat_id: Telegram Chat ID получателя (строка или число)
|
||||
site_url: базовый URL сайта
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно, False иначе
|
||||
"""
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender.send_notification] НАЧАЛО: "
|
||||
f"notification_id={notification.id}, "
|
||||
f"chat_id={chat_id}, "
|
||||
f"site_url={site_url}"
|
||||
)
|
||||
|
||||
# Преобразуем chat_id в int
|
||||
if not chat_id:
|
||||
logger.warning(
|
||||
f"[TelegramNotificationSender] ❌ Chat ID не указан для {notification.recipient}"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
telegram_id = int(chat_id)
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Chat ID преобразован в int: {telegram_id}"
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
logger.error(
|
||||
f"[TelegramNotificationSender] ❌ Неверный формат Chat ID: {chat_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Получить site_url если не передан
|
||||
if not site_url:
|
||||
site_url = getattr(settings, 'SITE_URL', 'http://localhost:9000')
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] Site URL из настроек: {site_url}"
|
||||
)
|
||||
|
||||
# Запускаем асинхронную отправку
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ➡️ Запуск асинхронной отправки..."
|
||||
)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
result = loop.run_until_complete(
|
||||
cls.send_notification_async(
|
||||
telegram_id,
|
||||
notification,
|
||||
site_url
|
||||
)
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info(
|
||||
f"[TelegramNotificationSender] ✅ Успешно отправлено в Telegram (chat_id={telegram_id})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[TelegramNotificationSender] ⚠️ Отправка в Telegram вернула False"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def send_notification_by_username(
|
||||
cls,
|
||||
notification,
|
||||
telegram_username: str,
|
||||
site_url: str = None
|
||||
) -> bool:
|
||||
"""
|
||||
Отправка уведомления по @username без привязки аккаунта
|
||||
|
||||
Args:
|
||||
notification: объект Notification
|
||||
telegram_username: @username или chat_id
|
||||
site_url: базовый URL сайта
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно
|
||||
"""
|
||||
if not site_url:
|
||||
site_url = getattr(settings, 'SITE_URL', 'http://localhost:9000')
|
||||
|
||||
# Убираем @ если есть
|
||||
username = telegram_username.strip().lstrip('@')
|
||||
|
||||
# Пробуем отправить по username
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
async def send_by_username():
|
||||
bot = cls.get_bot()
|
||||
if not bot:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Формируем сообщение
|
||||
category_code = notification.notification_type.category.code
|
||||
emoji = cls.CATEGORY_EMOJI.get(category_code, '🔔')
|
||||
|
||||
text = f"{emoji} <b>{notification.title}</b>\n\n"
|
||||
text += f"{notification.message}\n\n"
|
||||
|
||||
# Кнопка действия
|
||||
keyboard = None
|
||||
if notification.action_url:
|
||||
action_text = notification.action_text or 'Посмотреть'
|
||||
if notification.action_url.startswith('http'):
|
||||
url = notification.action_url
|
||||
else:
|
||||
url = f"{site_url}{notification.action_url}"
|
||||
|
||||
keyboard = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton(action_text, url=url)]
|
||||
])
|
||||
|
||||
# Отправляем по @username
|
||||
await bot.send_message(
|
||||
chat_id=f"@{username}",
|
||||
text=text,
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=keyboard,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Telegram отправлено @{username}: {notification.id}"
|
||||
)
|
||||
return True
|
||||
|
||||
except TelegramError as e:
|
||||
error_msg = str(e)
|
||||
if "chat not found" in error_msg.lower():
|
||||
logger.warning(
|
||||
f"Пользователь @{username} не начал диалог с ботом. "
|
||||
f"Необходимо отправить /start боту."
|
||||
)
|
||||
elif "bot was blocked" in error_msg.lower():
|
||||
logger.warning(
|
||||
f"Пользователь @{username} заблокировал бота."
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Ошибка отправки @{username}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
return loop.run_until_complete(send_by_username())
|
||||
|
||||
@classmethod
|
||||
async def send_message_async(
|
||||
cls,
|
||||
telegram_id: int,
|
||||
text: str,
|
||||
keyboard: Optional[InlineKeyboardMarkup] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Отправка произвольного сообщения
|
||||
|
||||
Args:
|
||||
telegram_id: ID получателя
|
||||
text: текст сообщения
|
||||
keyboard: inline клавиатура (опционально)
|
||||
|
||||
Returns:
|
||||
True если отправлено успешно
|
||||
"""
|
||||
bot = cls.get_bot()
|
||||
if not bot:
|
||||
return False
|
||||
|
||||
try:
|
||||
await bot.send_message(
|
||||
chat_id=telegram_id,
|
||||
text=text,
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=keyboard,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
return True
|
||||
except TelegramError as e:
|
||||
logger.error(f"Ошибка отправки сообщения в Telegram: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def send_message(
|
||||
cls,
|
||||
telegram_id: int,
|
||||
text: str,
|
||||
keyboard: Optional[InlineKeyboardMarkup] = None
|
||||
) -> bool:
|
||||
"""Синхронная обёртка для send_message_async"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
return loop.run_until_complete(
|
||||
cls.send_message_async(telegram_id, text, keyboard)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def test_bot_connection(cls) -> bool:
|
||||
"""
|
||||
Тестирует подключение к Telegram боту
|
||||
|
||||
Returns:
|
||||
True если бот доступен и настроен правильно
|
||||
"""
|
||||
bot = cls.get_bot()
|
||||
if not bot:
|
||||
return False
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
async def test():
|
||||
try:
|
||||
me = await bot.get_me()
|
||||
logger.info(
|
||||
f"Бот подключен: @{me.username} ({me.first_name})"
|
||||
)
|
||||
return True
|
||||
except TelegramError as e:
|
||||
logger.error(f"Ошибка подключения к боту: {e}")
|
||||
return False
|
||||
|
||||
return loop.run_until_complete(test())
|
||||
Loading…
x
Reference in New Issue
Block a user