import logging from typing import List import httpx from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.models import User, Participant, Marathon logger = logging.getLogger(__name__) class TelegramNotifier: """Service for sending Telegram notifications.""" def __init__(self): self.bot_token = settings.TELEGRAM_BOT_TOKEN self.api_url = f"https://api.telegram.org/bot{self.bot_token}" async def send_message( self, chat_id: int, text: str, parse_mode: str = "HTML", reply_markup: dict | None = None ) -> bool: """Send a message to a Telegram chat.""" if not self.bot_token: logger.warning("Telegram bot token not configured") return False try: async with httpx.AsyncClient() as client: payload = { "chat_id": chat_id, "text": text, "parse_mode": parse_mode } if reply_markup: payload["reply_markup"] = reply_markup response = await client.post( f"{self.api_url}/sendMessage", json=payload, timeout=10.0 ) if response.status_code == 200: return True else: logger.error(f"Failed to send message: {response.text}") return False except Exception as e: logger.error(f"Error sending Telegram message: {e}") return False async def notify_user( self, db: AsyncSession, user_id: int, message: str, reply_markup: dict | None = None ) -> bool: """Send notification to a user by user_id.""" result = await db.execute( select(User).where(User.id == user_id) ) user = result.scalar_one_or_none() if not user: logger.warning(f"[Notify] User {user_id} not found") return False if not user.telegram_id: logger.warning(f"[Notify] User {user_id} ({user.nickname}) has no telegram_id") return False logger.info(f"[Notify] Sending to user {user.nickname} (telegram_id={user.telegram_id})") return await self.send_message(user.telegram_id, message, reply_markup=reply_markup) async def notify_marathon_participants( self, db: AsyncSession, marathon_id: int, message: str, exclude_user_id: int | None = None ) -> int: """Send notification to all marathon participants with linked Telegram.""" result = await db.execute( select(User) .join(Participant, Participant.user_id == User.id) .where( Participant.marathon_id == marathon_id, User.telegram_id.isnot(None) ) ) users = result.scalars().all() sent_count = 0 for user in users: if exclude_user_id and user.id == exclude_user_id: continue if await self.send_message(user.telegram_id, message): sent_count += 1 return sent_count # Notification templates async def notify_event_start( self, db: AsyncSession, marathon_id: int, event_type: str, marathon_title: str ) -> int: """Notify participants about event start.""" event_messages = { "golden_hour": f"🌟 Начался Golden Hour в «{marathon_title}»!\n\nВсе очки x1.5 в течение часа!", "jackpot": f"🎰 JACKPOT в «{marathon_title}»!\n\nОчки x3 за следующий сложный челлендж!", "double_risk": f"⚡ Double Risk в «{marathon_title}»!\n\nПоловина очков, но дропы бесплатны!", "common_enemy": f"👥 Common Enemy в «{marathon_title}»!\n\nВсе получают одинаковый челлендж. Первые 3 — бонус!", "swap": f"🔄 Swap в «{marathon_title}»!\n\nМожно поменяться заданием с другим участником!", "game_choice": f"🎲 Выбор игры в «{marathon_title}»!\n\nВыбери игру и один из 3 челленджей!" } message = event_messages.get( event_type, f"📌 Новое событие в «{marathon_title}»!" ) return await self.notify_marathon_participants(db, marathon_id, message) async def notify_event_end( self, db: AsyncSession, marathon_id: int, event_type: str, marathon_title: str ) -> int: """Notify participants about event end.""" event_names = { "golden_hour": "Golden Hour", "jackpot": "Jackpot", "double_risk": "Double Risk", "common_enemy": "Common Enemy", "swap": "Swap", "game_choice": "Выбор игры" } event_name = event_names.get(event_type, "Событие") message = f"⏰ {event_name} в «{marathon_title}» завершён" return await self.notify_marathon_participants(db, marathon_id, message) async def notify_marathon_start( self, db: AsyncSession, marathon_id: int, marathon_title: str ) -> int: """Notify participants about marathon start.""" message = ( f"🚀 Марафон «{marathon_title}» начался!\n\n" f"Время крутить колесо и получить первое задание!" ) return await self.notify_marathon_participants(db, marathon_id, message) async def notify_marathon_finish( self, db: AsyncSession, marathon_id: int, marathon_title: str ) -> int: """Notify participants about marathon finish.""" message = ( f"🏆 Марафон «{marathon_title}» завершён!\n\n" f"Зайди на сайт, чтобы увидеть итоговую таблицу!" ) return await self.notify_marathon_participants(db, marathon_id, message) async def notify_dispute_raised( self, db: AsyncSession, user_id: int, marathon_title: str, challenge_title: str, assignment_id: int ) -> bool: """Notify user about dispute raised on their assignment.""" logger.info(f"[Dispute] Sending notification to user_id={user_id} for assignment_id={assignment_id}") dispute_url = f"{settings.FRONTEND_URL}/assignments/{assignment_id}" logger.info(f"[Dispute] URL: {dispute_url}") # Telegram requires HTTPS for inline keyboard URLs use_inline_button = dispute_url.startswith("https://") if use_inline_button: message = ( f"⚠️ На твоё задание подан спор\n\n" f"Марафон: {marathon_title}\n" f"Задание: {challenge_title}" ) reply_markup = { "inline_keyboard": [[ {"text": "Открыть спор", "url": dispute_url} ]] } else: message = ( f"⚠️ На твоё задание подан спор\n\n" f"Марафон: {marathon_title}\n" f"Задание: {challenge_title}\n\n" f"🔗 {dispute_url}" ) reply_markup = None result = await self.notify_user(db, user_id, message, reply_markup=reply_markup) logger.info(f"[Dispute] Notification result: {result}") return result async def notify_dispute_resolved( self, db: AsyncSession, user_id: int, marathon_title: str, challenge_title: str, is_valid: bool ) -> bool: """Notify user about dispute resolution.""" if is_valid: message = ( f"❌ Спор признан обоснованным\n\n" f"Марафон: {marathon_title}\n" f"Задание: {challenge_title}\n\n" f"Задание возвращено. Выполни его заново." ) else: message = ( f"✅ Спор отклонён\n\n" f"Марафон: {marathon_title}\n" f"Задание: {challenge_title}\n\n" f"Твоё выполнение засчитано!" ) return await self.notify_user(db, user_id, message) async def notify_game_approved( self, db: AsyncSession, user_id: int, marathon_title: str, game_title: str ) -> bool: """Notify user that their proposed game was approved.""" message = ( f"✅ Твоя игра одобрена!\n\n" f"Марафон: {marathon_title}\n" f"Игра: {game_title}\n\n" f"Теперь она доступна для всех участников." ) return await self.notify_user(db, user_id, message) async def notify_game_rejected( self, db: AsyncSession, user_id: int, marathon_title: str, game_title: str ) -> bool: """Notify user that their proposed game was rejected.""" message = ( f"❌ Твоя игра отклонена\n\n" f"Марафон: {marathon_title}\n" f"Игра: {game_title}\n\n" f"Ты можешь предложить другую игру." ) return await self.notify_user(db, user_id, message) async def notify_challenge_approved( self, db: AsyncSession, user_id: int, marathon_title: str, game_title: str, challenge_title: str ) -> bool: """Notify user that their proposed challenge was approved.""" message = ( f"✅ Твой челлендж одобрен!\n\n" f"Марафон: {marathon_title}\n" f"Игра: {game_title}\n" f"Задание: {challenge_title}\n\n" f"Теперь оно доступно для всех участников." ) return await self.notify_user(db, user_id, message) async def notify_challenge_rejected( self, db: AsyncSession, user_id: int, marathon_title: str, game_title: str, challenge_title: str ) -> bool: """Notify user that their proposed challenge was rejected.""" message = ( f"❌ Твой челлендж отклонён\n\n" f"Марафон: {marathon_title}\n" f"Игра: {game_title}\n" f"Задание: {challenge_title}\n\n" f"Ты можешь предложить другой челлендж." ) return await self.notify_user(db, user_id, message) # Global instance telegram_notifier = TelegramNotifier()