Files
vpn-control-panel/app/services/notification.py
smolkik-code 7c7c88621d Initial commit: VPN Control Panel
- FastAPI REST API with JWT auth
- aiogram 3 Telegram bot with admin middleware
- APScheduler daily tasks (expiry, reminders, revoke, sync)
- SQLAlchemy 2 async ORM with Alembic migrations
- Jinja2 admin panel (Dashboard, Users, Payments, Servers, Tariffs)
- VPN provider abstraction with MockProvider
- Stats service with revenue/subscription analytics
- Docker Compose (PostgreSQL + Redis + app)
- Healthcheck endpoint
2026-07-05 17:50:11 +07:00

203 lines
6.5 KiB
Python

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"⚠️ <b>Your {tariff_name} subscription expires in {label}.</b>\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"❌ <b>Your {plan} subscription has expired.</b>\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