import logging from datetime import datetime, timezone from typing import Any, Callable, Dict, Optional from aiogram import Bot from aiogram.exceptions import TelegramBadRequest from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo from aiogram.utils.text_decorations import html_decoration as hd from bot.middlewares.i18n import JsonI18n from bot.services.email_auth_service import EmailAuthService from bot.services.email_templates import ( render_support_admin_reply_user, render_support_new_ticket_admin, render_support_ticket_closed_user, render_support_user_reply_admin, ) from bot.utils import MessageContent, send_message_via_queue from bot.utils.message_queue import get_queue_manager from bot.utils.telegram_markup import ( is_profile_link_error, remove_profile_link_buttons, ) from bot.utils.text_sanitizer import ( display_name_or_fallback, username_for_display, ) from config.settings import Settings from db.dal import app_settings_dal, user_dal SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_KEY = "SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED" class NotificationService: """Enhanced notification service for sending messages to admins and log channels""" def __init__( self, bot: Bot, settings: Settings, i18n: Optional[JsonI18n] = None, *, session_factory=None, email_auth_service: Optional[EmailAuthService] = None, bot_username: Optional[str] = None, ): self.bot = bot self.settings = settings self.i18n = i18n self.session_factory = session_factory self.email_auth_service = email_auth_service self.bot_username = bot_username or "" @staticmethod def _format_user_display( user_id: int, username: Optional[str] = None, first_name: Optional[str] = None, email: Optional[str] = None, ) -> str: base_display = display_name_or_fallback(first_name, f"ID {user_id}") if username: base_display = f"{base_display} ({username_for_display(username)})" safe_display = hd.quote(base_display) clean_email = str(email or "").strip() if clean_email: safe_display = f"{safe_display} · {hd.quote(clean_email)}" return safe_display @staticmethod def _build_profile_keyboard( translate: Callable[..., str], user_id: int, referrer_id: Optional[int] = None, ) -> InlineKeyboardMarkup: """Create inline keyboard with links to user (and referrer) profiles. Email-only users have a synthetic negative ``user_id`` with no Telegram profile, so we skip the tg:// button for them. """ buttons = [] if user_id and user_id > 0: buttons.append( [ InlineKeyboardButton( text=translate("log_open_profile_link"), url=f"tg://user?id={user_id}", ) ] ) if referrer_id and referrer_id > 0: buttons.append( [ InlineKeyboardButton( text=translate("log_open_referrer_profile_button"), url=f"tg://user?id={referrer_id}", ) ] ) return InlineKeyboardMarkup(inline_keyboard=buttons) if buttons else None async def _send_to_log_channel( self, message: str, thread_id: Optional[int] = None, reply_markup: Optional[InlineKeyboardMarkup] = None, ): """Send message to configured log channel/group using message queue""" if not self.settings.LOG_CHAT_ID: return queue_manager = get_queue_manager() if not queue_manager: logging.warning("Message queue manager not available, falling back to direct send") final_thread_id = thread_id or self.settings.LOG_THREAD_ID def _build_kwargs(markup: Optional[InlineKeyboardMarkup]) -> Dict[str, Any]: kwargs: Dict[str, Any] = { "chat_id": self.settings.LOG_CHAT_ID, "text": message, "parse_mode": "HTML", "disable_web_page_preview": True, } if markup: kwargs["reply_markup"] = markup if final_thread_id: kwargs["message_thread_id"] = final_thread_id return kwargs try: await self.bot.send_message(**_build_kwargs(reply_markup)) except TelegramBadRequest as exc: if is_profile_link_error(exc): fallback_markup = remove_profile_link_buttons(reply_markup) logging.warning( "Telegram rejected profile buttons for log chat %s: %s. " "Retrying without tg:// links.", self.settings.LOG_CHAT_ID, getattr(exc, "message", "") or str(exc), ) try: await self.bot.send_message(**_build_kwargs(fallback_markup)) except Exception as retry_exc: logging.error( "Failed to send notification without profile buttons to log " f"channel {self.settings.LOG_CHAT_ID}: {retry_exc}" ) return logging.error( f"Failed to send notification to log channel {self.settings.LOG_CHAT_ID}: {exc}" ) except Exception: logging.exception( "Failed to send notification to log channel %s.", self.settings.LOG_CHAT_ID ) return try: # Use thread_id if provided, otherwise use from settings final_thread_id = thread_id or self.settings.LOG_THREAD_ID kwargs = {"text": message, "parse_mode": "HTML", "disable_web_page_preview": True} if reply_markup: kwargs["reply_markup"] = reply_markup # Add thread ID for supergroups if specified if final_thread_id: kwargs["message_thread_id"] = final_thread_id # Queue message for sending (groups are rate limited to 15/minute) await queue_manager.send_message(self.settings.LOG_CHAT_ID, **kwargs) except Exception: logging.exception( "Failed to queue notification to log channel %s.", self.settings.LOG_CHAT_ID ) async def _send_to_admins( self, message: str, reply_markup: Optional[InlineKeyboardMarkup] = None, ): """Send message to all admin users using message queue""" if not self.settings.ADMIN_IDS: return queue_manager = get_queue_manager() if not queue_manager: logging.warning("Message queue manager not available, falling back to direct send") for admin_id in self.settings.ADMIN_IDS: try: await self.bot.send_message( chat_id=admin_id, text=message, parse_mode="HTML", disable_web_page_preview=True, reply_markup=reply_markup, ) except Exception: logging.exception("Failed to send notification to admin %s.", admin_id) return for admin_id in self.settings.ADMIN_IDS: try: await queue_manager.send_message( chat_id=admin_id, text=message, parse_mode="HTML", disable_web_page_preview=True, reply_markup=reply_markup, ) except Exception: logging.exception("Failed to queue notification to admin %s.", admin_id) def _support_webapp_url(self, path: str) -> Optional[str]: base_url = str(getattr(self.settings, "SUBSCRIPTION_MINI_APP_URL", "") or "").strip() if not base_url: return None normalized_path = f"/{str(path or '').lstrip('/')}" return f"{base_url.rstrip('/')}{normalized_path}" def _support_ticket_url(self, ticket_id: int, *, admin: bool = True) -> str: path = f"/admin/support/{ticket_id}" if admin else f"/support/{ticket_id}" webapp_url = self._support_webapp_url(path) if webapp_url: return webapp_url bot_username = self.bot_username.strip().lstrip("@") if bot_username: return f"https://t.me/{bot_username}?startapp=ticket_{ticket_id}" return "https://t.me/" def _support_mini_app_button( self, *, text: str, path: str, fallback_url: str, ) -> InlineKeyboardButton: webapp_url = self._support_webapp_url(path) if webapp_url: return InlineKeyboardButton(text=text, web_app=WebAppInfo(url=webapp_url)) return InlineKeyboardButton(text=text, url=fallback_url) def _support_text(self, language: Optional[str], key: str, fallback: str) -> str: if not self.i18n: return fallback return self.i18n.gettext(language or self.settings.DEFAULT_LANGUAGE, key) or fallback @staticmethod def _support_preview(body: str, limit: int = 700) -> str: text = (body or "").strip() return text if len(text) <= limit else f"{text[: limit - 1]}…" @staticmethod def _support_user_display(user) -> str: name = " ".join( part for part in [user.first_name, getattr(user, "last_name", None)] if part ) if user.username: display = f"{name or user.username} (@{user.username})" else: display = name or f"ID {user.user_id}" email = str(getattr(user, "email", None) or "").strip() if email: return f"{display} · {email}" if display and display != email else email return display @staticmethod def _support_snapshot_rows(snapshot: Optional[dict]) -> list[tuple[str, str]]: if not snapshot: return [] rows = [] for key, label in ( ("tariff", "email_support_row_tariff"), ("end_date", "email_support_row_end_date"), ("remaining", "email_support_row_remaining"), ("panel_status", "email_support_row_panel_status"), ): value = snapshot.get(key) if value: rows.append((label, str(value))) return rows @staticmethod def _coerce_bool_setting(value: Any, default: bool) -> bool: if value is None: return default if isinstance(value, bool): return value if isinstance(value, (int, float)): return bool(value) if isinstance(value, str): normalized = value.strip().lower() if normalized in {"1", "true", "yes", "on"}: return True if normalized in {"0", "false", "no", "off"}: return False return bool(value) async def support_admin_email_notifications_enabled(self) -> bool: enabled = bool(getattr(self.settings, SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_KEY, False)) if not self.session_factory: return enabled try: async with self.session_factory() as session: found, raw_value = await app_settings_dal.get_override_value( session, SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_KEY, ) except Exception: logging.exception("Failed to read support admin email notification override.") return enabled if not found: return enabled return self._coerce_bool_setting(raw_value, enabled) def _support_keyboard(self, ticket, user, *, admin: bool = True) -> InlineKeyboardMarkup: ticket_path = ( f"/admin/support/{ticket.ticket_id}" if admin else f"/support/{ticket.ticket_id}" ) rows = [ [ self._support_mini_app_button( text="Открыть тикет", path=ticket_path, fallback_url=self._support_ticket_url(ticket.ticket_id, admin=admin), ) ] ] if admin: profile_row = [] if getattr(user, "user_id", 0) and int(user.user_id) > 0: profile_row.append( InlineKeyboardButton(text="Профиль", url=f"tg://user?id={user.user_id}") ) user_card_path = f"/admin/users/{user.user_id}" if self._support_webapp_url(user_card_path): profile_row.append( self._support_mini_app_button( text="Карточка пользователя", path=user_card_path, fallback_url=self._support_ticket_url(ticket.ticket_id, admin=True), ) ) if profile_row: rows.append(profile_row) return InlineKeyboardMarkup(inline_keyboard=rows) def _support_user_keyboard(self, ticket, user) -> InlineKeyboardMarkup: button_text = self._support_text( getattr(user, "language_code", None), "wa_support_open_ticket", "Открыть тикет", ) webapp_url = self._support_webapp_url(f"/support/{ticket.ticket_id}") if webapp_url: button = InlineKeyboardButton(text=button_text, web_app=WebAppInfo(url=webapp_url)) else: button = InlineKeyboardButton( text=button_text, url=self._support_ticket_url(ticket.ticket_id, admin=False), ) return InlineKeyboardMarkup(inline_keyboard=[[button]]) async def _admin_email_users(self): if not self.session_factory: return [] async with self.session_factory() as session: users = [] for admin_id in self.settings.ADMIN_IDS: user = await user_dal.get_user_by_id(session, int(admin_id)) if user and user.email: users.append(user) return users async def _send_admin_support_email(self, renderer, **kwargs) -> None: if not await self.support_admin_email_notifications_enabled(): return if not self.email_auth_service: return for admin in await self._admin_email_users(): try: content = renderer( self.settings, self.i18n, getattr(admin, "language_code", None) or self.settings.DEFAULT_LANGUAGE, **kwargs, ) await self.email_auth_service.send_rendered_email( email=admin.email, content=content, ) except Exception: logging.exception("Failed to send support email to admin %s.", admin.user_id) async def notify_new_support_ticket(self, ticket, user, first_message: str, snapshot: dict): if not getattr(self.settings, "LOG_SUPPORT", True): return priority_emoji = {"low": "🟢", "normal": "🟡", "high": "🟠", "urgent": "🔴"}.get( ticket.priority, "🟡", ) preview = self._support_preview(first_message) user_display = self._support_user_display(user) message = ( f"🆘 Новый тикет #{ticket.ticket_id}\n" f"{priority_emoji} {hd.quote(ticket.priority)} · {hd.quote(ticket.category)}\n\n" f"Пользователь\n{hd.quote(user_display)}\nID: {user.user_id}\n\n" f"Подписка\n" f"{hd.quote(str(snapshot.get('tariff') or '—'))}, " f"до {hd.quote(str(snapshot.get('end_date') or '—'))}, " f"осталось {hd.quote(str(snapshot.get('remaining') or '—'))}\n" f"статус: {hd.quote(str(snapshot.get('panel_status') or '—'))}\n\n" f"Текст обращения\n{hd.quote(preview)}" ) keyboard = self._support_keyboard(ticket, user, admin=True) await self._send_to_admins(message, reply_markup=keyboard) await self._send_to_log_channel( message, thread_id=getattr(self.settings, "LOG_SUPPORT_THREAD_ID", None), reply_markup=keyboard, ) await self._send_admin_support_email( render_support_new_ticket_admin, ticket_id=ticket.ticket_id, user_display=user_display, subject=ticket.subject, body_preview=preview, snapshot_rows=self._support_snapshot_rows(snapshot), ticket_url=self._support_ticket_url(ticket.ticket_id, admin=True), ) async def notify_support_user_reply( self, ticket, message, user, snapshot: dict, *, unread_count: Optional[int] = None, send_telegram: bool = True, send_email: bool = True, ): if not getattr(self.settings, "LOG_SUPPORT", True): return preview = self._support_preview(message.body) user_display = self._support_user_display(user) unread_line = ( f"\nUnread: {int(unread_count)}" if unread_count is not None and int(unread_count or 0) > 1 else "" ) text = ( f"💬 Ответ пользователя в тикете #{ticket.ticket_id}\n" f"{hd.quote(user_display)}{unread_line}\n\n{hd.quote(preview)}" ) keyboard = self._support_keyboard(ticket, user, admin=True) if send_telegram and getattr(self.settings, "LOG_SUPPORT", True): await self._send_to_admins(text, reply_markup=keyboard) await self._send_to_log_channel( text, thread_id=getattr(self.settings, "LOG_SUPPORT_THREAD_ID", None), reply_markup=keyboard, ) if send_email: await self._send_admin_support_email( render_support_user_reply_admin, ticket_id=ticket.ticket_id, user_display=user_display, subject=ticket.subject, body_preview=preview, snapshot_rows=self._support_snapshot_rows(snapshot), ticket_url=self._support_ticket_url(ticket.ticket_id, admin=True), ) async def notify_support_admin_reply(self, ticket, message, user): preview = self._support_preview(message.body, limit=500) url = self._support_ticket_url(ticket.ticket_id, admin=False) text = f"💬 Новый ответ по тикету #{ticket.ticket_id}\n\n{hd.quote(preview)}" keyboard = self._support_user_keyboard(ticket, user) if int(user.user_id) > 0: queue_manager = get_queue_manager() if queue_manager: await send_message_via_queue( queue_manager, int(user.user_id), MessageContent(content_type="text", text=text), parse_mode="HTML", disable_web_page_preview=True, reply_markup=keyboard, ) else: await self.bot.send_message( chat_id=int(user.user_id), text=text, parse_mode="HTML", disable_web_page_preview=True, reply_markup=keyboard, ) if self.email_auth_service and getattr(user, "email", None): content = render_support_admin_reply_user( self.settings, self.i18n, getattr(user, "language_code", None), ticket_id=ticket.ticket_id, subject=ticket.subject, body_preview=preview, ticket_url=url, ) await self.email_auth_service.send_rendered_email(email=user.email, content=content) async def notify_support_ticket_closed(self, ticket, user, closing_admin): url = self._support_ticket_url(ticket.ticket_id, admin=False) text = f"✅ Тикет #{ticket.ticket_id} закрыт\n\n{hd.quote(ticket.subject)}" keyboard = self._support_user_keyboard(ticket, user) if int(user.user_id) > 0: queue_manager = get_queue_manager() if queue_manager: await send_message_via_queue( queue_manager, int(user.user_id), MessageContent(content_type="text", text=text), parse_mode="HTML", disable_web_page_preview=True, reply_markup=keyboard, ) else: await self.bot.send_message( chat_id=int(user.user_id), text=text, parse_mode="HTML", disable_web_page_preview=True, reply_markup=keyboard, ) if self.email_auth_service and getattr(user, "email", None): content = render_support_ticket_closed_user( self.settings, self.i18n, getattr(user, "language_code", None), ticket_id=ticket.ticket_id, subject=ticket.subject, ticket_url=url, ) await self.email_auth_service.send_rendered_email(email=user.email, content=content) async def notify_new_user_registration( self, user_id: int, username: Optional[str] = None, first_name: Optional[str] = None, email: Optional[str] = None, referred_by_id: Optional[int] = None, ): """Send notification about new user registration""" if not self.settings.LOG_NEW_USERS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k user_display = self._format_user_display( user_id=user_id, username=username, first_name=first_name, email=email, ) referral_text = "" if referred_by_id: referrer_link = hd.link(str(referred_by_id), f"tg://user?id={referred_by_id}") referral_text = _( "log_referral_suffix", referrer_link=referrer_link, ) message = _( "log_new_user_registration", user_id=user_id, user_display=user_display, referral_text=referral_text, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) # Send to log channel profile_keyboard = self._build_profile_keyboard(_, user_id, referred_by_id) await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_new_email_user_registration( self, user_id: int, email: str, referred_by_id: Optional[int] = None, ): """Send notification about new user registration via email (Web App).""" if not self.settings.LOG_NEW_USERS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k referral_text = "" if referred_by_id: referrer_link = hd.link(str(referred_by_id), f"tg://user?id={referred_by_id}") referral_text = _( "log_referral_suffix", referrer_link=referrer_link, ) message = _( "log_new_email_user_registration", user_id=user_id, email=hd.quote(email), referral_text=referral_text, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) # Email users have a synthetic (negative) user_id with no Telegram profile, # so we only attach the referrer button when a real referrer is present. reply_markup: Optional[InlineKeyboardMarkup] = None if referred_by_id and referred_by_id > 0: reply_markup = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text=_("log_open_referrer_profile_button"), url=f"tg://user?id={referred_by_id}", ) ] ] ) await self._send_to_log_channel(message, reply_markup=reply_markup) async def notify_account_email_linked( self, user_id: int, email: str, telegram_id: Optional[int] = None, username: Optional[str] = None, first_name: Optional[str] = None, ): """Send notification when an email is linked to a Telegram-created account.""" if not self.settings.LOG_NEW_USERS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k user_display = self._format_user_display( user_id=telegram_id or user_id, username=username, first_name=first_name, email=email, ) message = _( "log_account_email_linked", user_id=user_id, telegram_id=telegram_id or user_id, user_display=user_display, email=hd.quote(email), timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) reply_markup: Optional[InlineKeyboardMarkup] = None if telegram_id and telegram_id > 0: reply_markup = self._build_profile_keyboard(_, telegram_id) await self._send_to_log_channel(message, reply_markup=reply_markup) async def notify_account_telegram_linked( self, user_id: int, email: Optional[str], telegram_id: int, username: Optional[str] = None, first_name: Optional[str] = None, ): """Send notification when Telegram is linked to an email-created account.""" if not self.settings.LOG_NEW_USERS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k user_display = self._format_user_display( user_id=telegram_id, username=username, first_name=first_name, email=email, ) message = _( "log_account_telegram_linked", user_id=user_id, telegram_id=telegram_id, user_display=user_display, email=hd.quote(email or ""), timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) profile_keyboard = self._build_profile_keyboard(_, telegram_id) await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_account_merged( self, *, primary_user_id: int, removed_user_id: int, email: Optional[str], telegram_id: Optional[int], username: Optional[str] = None, first_name: Optional[str] = None, final_end_date_text: Optional[str] = None, primary_panel_user_uuid: Optional[str] = None, removed_panel_user_uuid: Optional[str] = None, ): """Send notification when duplicate email/Telegram accounts are merged.""" if not self.settings.LOG_NEW_USERS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k display_user_id = int(telegram_id or primary_user_id) user_display = self._format_user_display( user_id=display_user_id, username=username, first_name=first_name, email=email, ) message = _( "log_account_merged", primary_user_id=primary_user_id, removed_user_id=removed_user_id, telegram_id=telegram_id or "", user_display=user_display, email=hd.quote(email or ""), final_end_date=hd.quote(final_end_date_text or ""), primary_panel_user_uuid=hd.quote(primary_panel_user_uuid or ""), removed_panel_user_uuid=hd.quote(removed_panel_user_uuid or ""), timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) profile_keyboard = ( self._build_profile_keyboard(_, int(telegram_id)) if telegram_id else None ) await self._send_to_log_channel(message, reply_markup=profile_keyboard) def _format_traffic_gb_admin(self, traffic_gb: float) -> str: value = float(traffic_gb) if value.is_integer(): return str(int(value)) return f"{value:g}" def _tariff_display_for_log(self, tariff_key: Optional[str]) -> str: if not tariff_key: return "" cfg = getattr(self.settings, "tariffs_config", None) if not cfg: return str(tariff_key) try: tariff = cfg.require(str(tariff_key)) return str(tariff.name(self.settings.DEFAULT_LANGUAGE)) except Exception: return str(tariff_key) async def notify_payment_received( self, user_id: int, amount: float, currency: str, months: int, payment_provider: str, username: Optional[str] = None, email: Optional[str] = None, traffic_gb: Optional[float] = None, *, traffic_is_premium: bool = False, tariff_key: Optional[str] = None, ): """Send notification about successful payment""" if not self.settings.LOG_PAYMENTS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k user_display = self._format_user_display( user_id=user_id, username=username, email=email, ) try: from bot.payment_providers import provider_emoji_map provider_emoji = provider_emoji_map(self.settings).get(payment_provider.lower(), "💰") except Exception: provider_emoji = "💰" if traffic_gb is not None: traffic_label = self._format_traffic_gb_admin(float(traffic_gb)) traffic_kind = _( "log_payment_traffic_kind_premium" if traffic_is_premium else "log_payment_traffic_kind_regular", ) traffic_summary = _( "log_payment_traffic_purchase_line", gb=traffic_label, kind=traffic_kind ) tariff_name = self._tariff_display_for_log(tariff_key) tariff_line = ( _("log_payment_tariff_line", name=hd.quote(tariff_name)) if tariff_name else "" ) message = _( "log_payment_received_traffic", provider_emoji=provider_emoji, user_display=user_display, amount=amount, currency=currency, traffic_summary=traffic_summary, tariff_line=tariff_line, payment_provider=payment_provider, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) else: message = _( "log_payment_received", provider_emoji=provider_emoji, user_display=user_display, amount=amount, currency=currency, months=months, payment_provider=payment_provider, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) # Send to log channel profile_keyboard = self._build_profile_keyboard(_, user_id) await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_promo_activation( self, user_id: int, promo_code: str, bonus_days: int, username: Optional[str] = None, email: Optional[str] = None, ): """Send notification about promo code activation""" if not self.settings.LOG_PROMO_ACTIVATIONS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k user_display = self._format_user_display( user_id=user_id, username=username, email=email, ) message = _( "log_promo_activation", user_display=user_display, promo_code=promo_code, bonus_days=bonus_days, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) # Send to log channel profile_keyboard = self._build_profile_keyboard(_, user_id) await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_trial_activation( self, user_id: int, end_date: datetime, username: Optional[str] = None, email: Optional[str] = None, ): """Send notification about trial activation""" if not self.settings.LOG_TRIAL_ACTIVATIONS: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k user_display = self._format_user_display( user_id=user_id, username=username, email=email, ) message = _( "log_trial_activation", user_display=user_display, end_date=end_date.strftime("%Y-%m-%d %H:%M"), timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) # Send to log channel profile_keyboard = self._build_profile_keyboard(_, user_id) await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_panel_sync( self, status: str, details: str, users_processed: int, subs_synced: int, username: Optional[str] = None, ): """Send notification about panel synchronization""" if not getattr(self.settings, "LOG_PANEL_SYNC", True): return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k # Status emoji based on sync result status_emoji = {"completed": "✅", "completed_with_errors": "⚠️", "failed": "❌"}.get( status, "🔄" ) message = _( "log_panel_sync", status_emoji=status_emoji, status=status, users_processed=users_processed, subs_synced=subs_synced, timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"), details=details, ) # Send to log channel await self._send_to_log_channel(message) async def notify_suspicious_promo_attempt( self, user_id: int, suspicious_input: str, username: Optional[str] = None, first_name: Optional[str] = None, email: Optional[str] = None, ): """Send notification about a suspicious promo code attempt.""" if not self.settings.LOG_SUSPICIOUS_ACTIVITY: return admin_lang = self.settings.DEFAULT_LANGUAGE _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k user_display = self._format_user_display( user_id=user_id, username=username, first_name=first_name, email=email, ) message = _( "log_suspicious_promo", user_display=user_display, user_id=user_id, suspicious_input=hd.quote(suspicious_input), timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"), ) # Send to log channel profile_keyboard = self._build_profile_keyboard(_, user_id) await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def send_custom_notification( self, message: str, to_admins: bool = False, to_log_channel: bool = True, thread_id: Optional[int] = None, ): """Send custom notification message""" if to_log_channel: await self._send_to_log_channel(message, thread_id) if to_admins: await self._send_to_admins(message) # Removed legacy helper functions that duplicated NotificationService API