import logging from datetime import datetime, timezone from typing import Any, Optional from aiogram import Bot from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo from sqlalchemy.ext.asyncio import AsyncSession from bot.middlewares.i18n import JsonI18n from config.settings import Settings from db.dal import user_dal from db.models import User logger = logging.getLogger(__name__) TELEGRAM_NOTIFICATIONS_UNKNOWN = "unknown" TELEGRAM_NOTIFICATIONS_ENABLED = "enabled" TELEGRAM_NOTIFICATIONS_NEEDS_START = "needs_start" TELEGRAM_NOTIFICATIONS_BLOCKED = "blocked" TELEGRAM_NOTIFICATION_STATUSES = { TELEGRAM_NOTIFICATIONS_UNKNOWN, TELEGRAM_NOTIFICATIONS_ENABLED, TELEGRAM_NOTIFICATIONS_NEEDS_START, TELEGRAM_NOTIFICATIONS_BLOCKED, } def normalize_telegram_notification_status(value: Optional[str]) -> str: status = str(value or "").strip().lower() return status if status in TELEGRAM_NOTIFICATION_STATUSES else TELEGRAM_NOTIFICATIONS_UNKNOWN def telegram_notifications_enabled(user: Optional[User]) -> bool: return ( bool(getattr(user, "telegram_id", None)) and normalize_telegram_notification_status( getattr(user, "telegram_notifications_status", None) ) == TELEGRAM_NOTIFICATIONS_ENABLED ) def telegram_notifications_need_prompt(user: Optional[User]) -> bool: status = normalize_telegram_notification_status( getattr(user, "telegram_notifications_status", None) ) return bool(getattr(user, "telegram_id", None)) and status in { TELEGRAM_NOTIFICATIONS_NEEDS_START, TELEGRAM_NOTIFICATIONS_BLOCKED, } def telegram_notifications_start_link(bot_username: Optional[str]) -> Optional[str]: username = str(bot_username or "").strip().lstrip("@") if not username or username == "your_bot_username": return None return f"https://t.me/{username}?start=notifications" def telegram_notification_status_from_error(exc: Exception) -> Optional[str]: if isinstance(exc, TelegramForbiddenError): return TELEGRAM_NOTIFICATIONS_BLOCKED if not isinstance(exc, TelegramBadRequest): return None message = str(exc).lower() if any( token in message for token in ( "bot was blocked", "user is deactivated", "forbidden", ) ): return TELEGRAM_NOTIFICATIONS_BLOCKED if any( token in message for token in ( "chat not found", "bot can't initiate conversation", "bot can't initiate", "user not found", ) ): return TELEGRAM_NOTIFICATIONS_NEEDS_START return None async def mark_telegram_notifications_status( session: AsyncSession, user_id: int, status: str, *, telegram_id: Optional[int] = None, checked_at: Optional[datetime] = None, ) -> Optional[User]: normalized = normalize_telegram_notification_status(status) now = checked_at or datetime.now(timezone.utc) update_data: dict[str, Any] = { "telegram_notifications_status": normalized, "telegram_notifications_checked_at": now, } if telegram_id: update_data["telegram_id"] = int(telegram_id) if normalized == TELEGRAM_NOTIFICATIONS_ENABLED: update_data["telegram_notifications_enabled_at"] = now update_data["telegram_notifications_blocked_at"] = None elif normalized == TELEGRAM_NOTIFICATIONS_BLOCKED: update_data["telegram_notifications_blocked_at"] = now return await user_dal.update_user(session, user_id, update_data) async def mark_telegram_notifications_enabled_for_telegram_user( session: AsyncSession, telegram_id: int, ) -> Optional[User]: db_user = await user_dal.get_user_by_telegram_id(session, telegram_id) if not db_user: db_user = await user_dal.get_user_by_id(session, telegram_id) if not db_user: return None return await mark_telegram_notifications_status( session, int(db_user.user_id), TELEGRAM_NOTIFICATIONS_ENABLED, telegram_id=telegram_id, ) def _translate( i18n: Optional[JsonI18n], language: str, key: str, fallback: str, **kwargs: Any, ) -> str: if not i18n: return fallback.format(**kwargs) if kwargs else fallback return i18n.gettext(language, key, **kwargs) or fallback def _probe_keyboard( settings: Settings, i18n: Optional[JsonI18n], language: str, ) -> Optional[InlineKeyboardMarkup]: app_url = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", "") or "").strip() if not app_url: return None text = _translate( i18n, language, "telegram_notifications_open_app_button", "Open app", ) return InlineKeyboardMarkup( inline_keyboard=[[InlineKeyboardButton(text=text, web_app=WebAppInfo(url=app_url))]] ) async def probe_telegram_notifications( *, session: AsyncSession, bot: Bot, settings: Settings, i18n: Optional[JsonI18n], user: User, bot_username: Optional[str] = None, force: bool = False, ) -> dict[str, Any]: telegram_id = getattr(user, "telegram_id", None) if not telegram_id: return { "ok": False, "status": TELEGRAM_NOTIFICATIONS_UNKNOWN, "start_link": telegram_notifications_start_link(bot_username), } current_status = normalize_telegram_notification_status( getattr(user, "telegram_notifications_status", None) ) if current_status == TELEGRAM_NOTIFICATIONS_ENABLED and not force: return { "ok": True, "status": TELEGRAM_NOTIFICATIONS_ENABLED, "start_link": telegram_notifications_start_link(bot_username), } language = str(getattr(user, "language_code", "") or settings.DEFAULT_LANGUAGE) text = _translate( i18n, language, "telegram_notifications_enabled_message", "Telegram notifications are enabled.", ) try: await bot.send_message( int(telegram_id), text, reply_markup=_probe_keyboard(settings, i18n, language), disable_web_page_preview=True, ) except Exception as exc: status = telegram_notification_status_from_error(exc) if status: await mark_telegram_notifications_status(session, int(user.user_id), status) return { "ok": False, "status": status, "start_link": telegram_notifications_start_link(bot_username), } logger.warning( "Telegram notification probe failed for user %s / telegram %s: %s", user.user_id, telegram_id, exc, ) await mark_telegram_notifications_status( session, int(user.user_id), TELEGRAM_NOTIFICATIONS_UNKNOWN, ) return { "ok": False, "status": TELEGRAM_NOTIFICATIONS_UNKNOWN, "start_link": telegram_notifications_start_link(bot_username), } await mark_telegram_notifications_status( session, int(user.user_id), TELEGRAM_NOTIFICATIONS_ENABLED, telegram_id=int(telegram_id), ) return { "ok": True, "status": TELEGRAM_NOTIFICATIONS_ENABLED, "start_link": telegram_notifications_start_link(bot_username), }