213 lines
7.4 KiB
Python
213 lines
7.4 KiB
Python
|
|
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"
|
|||
|
|
) -> 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:
|
|||
|
|
response = await client.post(
|
|||
|
|
f"{self.api_url}/sendMessage",
|
|||
|
|
json={
|
|||
|
|
"chat_id": chat_id,
|
|||
|
|
"text": text,
|
|||
|
|
"parse_mode": parse_mode
|
|||
|
|
},
|
|||
|
|
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
|
|||
|
|
) -> 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 or not user.telegram_id:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
return await self.send_message(user.telegram_id, message)
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
) -> bool:
|
|||
|
|
"""Notify user about dispute raised on their assignment."""
|
|||
|
|
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_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)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# Global instance
|
|||
|
|
telegram_notifier = TelegramNotifier()
|