from datetime import datetime, timedelta, timezone from aiogram import Bot from aiogram.exceptions import TelegramAPIError from loguru import logger from app.models.notification import Notification, NotificationType from app.repositories.notification import NotificationRepository from app.repositories.user import UserRepository from app.settings import settings _REMINDER_DAYS = (7, 3, 1) _DAYS_LABEL = {7: "7 days", 3: "3 days", 1: "1 day"} class NotificationService: def __init__( self, notification_repo: NotificationRepository, user_repo: UserRepository, bot: Bot | None = None, ): self.notification_repo = notification_repo self.user_repo = user_repo self._bot = bot def _get_bot(self) -> Bot | None: if self._bot is None and settings.bot_token: self._bot = Bot(token=settings.bot_token) return self._bot async def send( self, user_id: int, title: str, text: str, type: NotificationType = NotificationType.INFO, ) -> Notification: user = await self.user_repo.get(user_id) if user is None: raise ValueError(f"User not found: {user_id}") notification = await self.notification_repo.create( user_id=user_id, type=type, title=title, text=text, ) await self._deliver(user, text) logger.debug( "Notification sent: user={} type={} title={}", user_id, type.value, title, ) return notification async def send_expiry_reminder( self, user_id: int, remaining_days: int, tariff_name: str ) -> Notification | None: if remaining_days not in _REMINDER_DAYS: return None user = await self.user_repo.get(user_id) if user is None: raise ValueError(f"User not found: {user_id}") label = _DAYS_LABEL.get(remaining_days, f"{remaining_days} days") notification = await self.notification_repo.create( user_id=user_id, type=NotificationType.WARNING, title="Subscription Expiring Soon", text=( f"Your subscription ({tariff_name}) expires in {label}.\n" "Please renew to avoid service interruption." ), ) await self._deliver( user, f"⚠️ Your {tariff_name} subscription expires in {label}.\n" "Please renew to keep using the service.", ) logger.info( "Expiry reminder sent: user={} tariff={} expires_in={}d", user_id, tariff_name, remaining_days, ) return notification async def send_subscription_expired( self, user_id: int, tariff_name: str | None = None ) -> Notification: user = await self.user_repo.get(user_id) if user is None: raise ValueError(f"User not found: {user_id}") plan = tariff_name or "VPN" notification = await self.notification_repo.create( user_id=user_id, type=NotificationType.WARNING, title="Subscription Expired", text=( f"Your {plan} subscription has expired.\n" "Please renew to restore access." ), ) await self._deliver( user, f"❌ Your {plan} subscription has expired.\n" "Please renew to restore access.", ) logger.info("Expired notification sent: user={} tariff={}", user_id, plan) return notification async def _deliver(self, user, text: str) -> None: bot = self._get_bot() if bot is None: logger.debug("No bot configured, notification not delivered") return try: await bot.send_message(chat_id=user.telegram_id, text=text) except TelegramAPIError: logger.warning( "Failed to deliver to user {} (tg={}), fallback to admin group", user.id, user.telegram_id, ) await self._deliver_to_admin(text, user) async def _deliver_to_admin(self, text: str, user=None) -> None: bot = self._get_bot() if bot is None: return chat_id = settings.admin_group_id if not chat_id: logger.debug("No admin group configured, notification dropped") return prefix = ( f"👤 User #{user.id} (@{user.username or '—'})\n\n" if user else "" ) message_id = settings.admin_group_thread_id try: await bot.send_message( chat_id=chat_id, text=prefix + text, message_thread_id=message_id, ) except TelegramAPIError as e: logger.error("Failed to deliver to admin group: {}", e) async def send_info(self, user_id: int, title: str, text: str) -> Notification: return await self.send(user_id, title, text, NotificationType.INFO) async def send_warning(self, user_id: int, title: str, text: str) -> Notification: return await self.send(user_id, title, text, NotificationType.WARNING) async def send_success(self, user_id: int, title: str, text: str) -> Notification: return await self.send(user_id, title, text, NotificationType.SUCCESS) async def send_payment( self, user_id: int, title: str, text: str ) -> Notification: return await self.send(user_id, title, text, NotificationType.PAYMENT) async def get_user_notifications( self, user_id: int ) -> list[Notification]: return await self.notification_repo.get_by_user_id(user_id) async def get_unread(self, user_id: int) -> list[Notification]: return await self.notification_repo.get_unread(user_id) async def get_unread_count(self, user_id: int) -> int: return await self.notification_repo.count_unread(user_id) async def mark_read(self, notification_id: int) -> bool: result = await self.notification_repo.mark_as_read(notification_id) if result: logger.debug("Notification marked read: id={}", notification_id) return result async def mark_all_read(self, user_id: int) -> int: count = await self.notification_repo.mark_all_as_read(user_id) if count: logger.debug("Marked {} notifications as read: user={}", count, user_id) return count