Files
game-marathon/backend/app/services/telegram_notifier.py
2025-12-18 23:47:11 +07:00

318 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"🌟 <b>Начался Golden Hour</b> в «{marathon_title}»!\n\nВсе очки x1.5 в течение часа!",
"jackpot": f"🎰 <b>JACKPOT</b> в «{marathon_title}»!\n\nОчки x3 за следующий сложный челлендж!",
"double_risk": f"⚡ <b>Double Risk</b> в «{marathon_title}»!\n\nПоловина очков, но дропы бесплатны!",
"common_enemy": f"👥 <b>Common Enemy</b> в «{marathon_title}»!\n\nВсе получают одинаковый челлендж. Первые 3 — бонус!",
"swap": f"🔄 <b>Swap</b> в «{marathon_title}»!\n\nМожно поменяться заданием с другим участником!",
"game_choice": f"🎲 <b>Выбор игры</b> в «{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"⏰ <b>{event_name}</b> в «{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"🚀 <b>Марафон «{marathon_title}» начался!</b>\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"🏆 <b>Марафон «{marathon_title}» завершён!</b>\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"⚠️ <b>На твоё задание подан спор</b>\n\n"
f"Марафон: {marathon_title}\n"
f"Задание: {challenge_title}"
)
reply_markup = {
"inline_keyboard": [[
{"text": "Открыть спор", "url": dispute_url}
]]
}
else:
message = (
f"⚠️ <b>На твоё задание подан спор</b>\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"❌ <b>Спор признан обоснованным</b>\n\n"
f"Марафон: {marathon_title}\n"
f"Задание: {challenge_title}\n\n"
f"Задание возвращено. Выполни его заново."
)
else:
message = (
f"✅ <b>Спор отклонён</b>\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"✅ <b>Твоя игра одобрена!</b>\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"❌ <b>Твоя игра отклонена</b>\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"✅ <b>Твой челлендж одобрен!</b>\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"❌ <b>Твой челлендж отклонён</b>\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()