Add telegram bot
This commit is contained in:
10
bot/Dockerfile
Normal file
10
bot/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
14
bot/config.py
Normal file
14
bot/config.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
TELEGRAM_BOT_TOKEN: str
|
||||
API_URL: str = "http://backend:8000"
|
||||
BOT_USERNAME: str = "" # Will be set dynamically on startup
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
1
bot/handlers/__init__.py
Normal file
1
bot/handlers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Bot handlers
|
||||
60
bot/handlers/link.py
Normal file
60
bot/handlers/link.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from keyboards.main_menu import get_main_menu
|
||||
from services.api_client import api_client
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(Command("unlink"))
|
||||
async def cmd_unlink(message: Message):
|
||||
"""Handle /unlink command to disconnect Telegram account."""
|
||||
user = await api_client.get_user_by_telegram_id(message.from_user.id)
|
||||
|
||||
if not user:
|
||||
await message.answer(
|
||||
"Твой аккаунт не привязан к Game Marathon.\n"
|
||||
"Привяжи его через настройки профиля на сайте.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
result = await api_client.unlink_telegram(message.from_user.id)
|
||||
|
||||
if result.get("success"):
|
||||
await message.answer(
|
||||
"<b>Аккаунт отвязан</b>\n\n"
|
||||
"Ты больше не будешь получать уведомления.\n"
|
||||
"Чтобы привязать аккаунт снова, используй кнопку в настройках профиля на сайте.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"Произошла ошибка при отвязке аккаунта.\n"
|
||||
"Попробуй позже или обратись к администратору.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("status"))
|
||||
async def cmd_status(message: Message):
|
||||
"""Check account link status."""
|
||||
user = await api_client.get_user_by_telegram_id(message.from_user.id)
|
||||
|
||||
if user:
|
||||
await message.answer(
|
||||
f"<b>Статус аккаунта</b>\n\n"
|
||||
f"✅ Аккаунт привязан\n"
|
||||
f"👤 Никнейм: <b>{user.get('nickname', 'N/A')}</b>\n"
|
||||
f"🆔 ID: {user.get('id', 'N/A')}",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"<b>Статус аккаунта</b>\n\n"
|
||||
"❌ Аккаунт не привязан\n\n"
|
||||
"Привяжи его через настройки профиля на сайте.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
211
bot/handlers/marathons.py
Normal file
211
bot/handlers/marathons.py
Normal file
@@ -0,0 +1,211 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from keyboards.main_menu import get_main_menu
|
||||
from keyboards.inline import get_marathons_keyboard, get_marathon_details_keyboard
|
||||
from services.api_client import api_client
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(Command("marathons"))
|
||||
@router.message(F.text == "📊 Мои марафоны")
|
||||
async def cmd_marathons(message: Message):
|
||||
"""Show user's marathons."""
|
||||
user = await api_client.get_user_by_telegram_id(message.from_user.id)
|
||||
|
||||
if not user:
|
||||
await message.answer(
|
||||
"Сначала привяжи аккаунт через настройки профиля на сайте.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
marathons = await api_client.get_user_marathons(message.from_user.id)
|
||||
|
||||
if not marathons:
|
||||
await message.answer(
|
||||
"<b>Мои марафоны</b>\n\n"
|
||||
"У тебя пока нет активных марафонов.\n"
|
||||
"Присоединись к марафону на сайте!",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
text = "<b>📊 Мои марафоны</b>\n\n"
|
||||
|
||||
for m in marathons:
|
||||
status_emoji = {
|
||||
"preparing": "⏳",
|
||||
"active": "🎮",
|
||||
"finished": "🏁"
|
||||
}.get(m.get("status"), "❓")
|
||||
|
||||
text += f"{status_emoji} <b>{m.get('title')}</b>\n"
|
||||
text += f" Очки: {m.get('total_points', 0)} | "
|
||||
text += f"Место: #{m.get('position', '?')}\n\n"
|
||||
|
||||
await message.answer(
|
||||
text,
|
||||
reply_markup=get_marathons_keyboard(marathons)
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("marathon:"))
|
||||
async def marathon_details(callback: CallbackQuery):
|
||||
"""Show marathon details."""
|
||||
marathon_id = int(callback.data.split(":")[1])
|
||||
|
||||
details = await api_client.get_marathon_details(
|
||||
marathon_id=marathon_id,
|
||||
telegram_id=callback.from_user.id
|
||||
)
|
||||
|
||||
if not details:
|
||||
await callback.answer("Не удалось загрузить данные марафона", show_alert=True)
|
||||
return
|
||||
|
||||
marathon = details.get("marathon", {})
|
||||
participant = details.get("participant", {})
|
||||
active_events = details.get("active_events", [])
|
||||
current_assignment = details.get("current_assignment")
|
||||
|
||||
status_text = {
|
||||
"preparing": "⏳ Подготовка",
|
||||
"active": "🎮 Активен",
|
||||
"finished": "🏁 Завершён"
|
||||
}.get(marathon.get("status"), "❓")
|
||||
|
||||
text = f"<b>{marathon.get('title')}</b>\n"
|
||||
text += f"Статус: {status_text}\n\n"
|
||||
|
||||
text += f"<b>📈 Твоя статистика:</b>\n"
|
||||
text += f"• Очки: <b>{participant.get('total_points', 0)}</b>\n"
|
||||
text += f"• Место: <b>#{details.get('position', '?')}</b>\n"
|
||||
text += f"• Стрик: <b>{participant.get('current_streak', 0)}</b> 🔥\n"
|
||||
text += f"• Дропов: <b>{participant.get('drop_count', 0)}</b>\n\n"
|
||||
|
||||
if active_events:
|
||||
text += "<b>⚡ Активные события:</b>\n"
|
||||
for event in active_events:
|
||||
event_emoji = {
|
||||
"golden_hour": "🌟",
|
||||
"jackpot": "🎰",
|
||||
"double_risk": "⚡",
|
||||
"common_enemy": "👥",
|
||||
"swap": "🔄",
|
||||
"game_choice": "🎲"
|
||||
}.get(event.get("type"), "📌")
|
||||
text += f"{event_emoji} {event.get('type', '').replace('_', ' ').title()}\n"
|
||||
text += "\n"
|
||||
|
||||
if current_assignment:
|
||||
challenge = current_assignment.get("challenge", {})
|
||||
game = challenge.get("game", {})
|
||||
text += f"<b>🎯 Текущее задание:</b>\n"
|
||||
text += f"Игра: {game.get('title', 'N/A')}\n"
|
||||
text += f"Задание: {challenge.get('title', 'N/A')}\n"
|
||||
text += f"Сложность: {challenge.get('difficulty', 'N/A')}\n"
|
||||
text += f"Очки: {challenge.get('points', 0)}\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=get_marathon_details_keyboard(marathon_id)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back_to_marathons")
|
||||
async def back_to_marathons(callback: CallbackQuery):
|
||||
"""Go back to marathons list."""
|
||||
marathons = await api_client.get_user_marathons(callback.from_user.id)
|
||||
|
||||
if not marathons:
|
||||
await callback.message.edit_text(
|
||||
"<b>Мои марафоны</b>\n\n"
|
||||
"У тебя пока нет активных марафонов."
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
text = "<b>📊 Мои марафоны</b>\n\n"
|
||||
|
||||
for m in marathons:
|
||||
status_emoji = {
|
||||
"preparing": "⏳",
|
||||
"active": "🎮",
|
||||
"finished": "🏁"
|
||||
}.get(m.get("status"), "❓")
|
||||
|
||||
text += f"{status_emoji} <b>{m.get('title')}</b>\n"
|
||||
text += f" Очки: {m.get('total_points', 0)} | "
|
||||
text += f"Место: #{m.get('position', '?')}\n\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=get_marathons_keyboard(marathons)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(Command("stats"))
|
||||
@router.message(F.text == "📈 Статистика")
|
||||
async def cmd_stats(message: Message):
|
||||
"""Show user's overall statistics."""
|
||||
user = await api_client.get_user_by_telegram_id(message.from_user.id)
|
||||
|
||||
if not user:
|
||||
await message.answer(
|
||||
"Сначала привяжи аккаунт через настройки профиля на сайте.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
stats = await api_client.get_user_stats(message.from_user.id)
|
||||
|
||||
if not stats:
|
||||
await message.answer(
|
||||
"<b>📈 Статистика</b>\n\n"
|
||||
"Пока нет данных для отображения.\n"
|
||||
"Начни участвовать в марафонах!",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
text = f"<b>📈 Общая статистика</b>\n\n"
|
||||
text += f"👤 <b>{user.get('nickname', 'Игрок')}</b>\n\n"
|
||||
text += f"🏆 Марафонов завершено: <b>{stats.get('marathons_completed', 0)}</b>\n"
|
||||
text += f"🎮 Марафонов активно: <b>{stats.get('marathons_active', 0)}</b>\n"
|
||||
text += f"✅ Заданий выполнено: <b>{stats.get('challenges_completed', 0)}</b>\n"
|
||||
text += f"💰 Всего очков: <b>{stats.get('total_points', 0)}</b>\n"
|
||||
text += f"🔥 Лучший стрик: <b>{stats.get('best_streak', 0)}</b>\n"
|
||||
|
||||
await message.answer(text, reply_markup=get_main_menu())
|
||||
|
||||
|
||||
@router.message(Command("settings"))
|
||||
@router.message(F.text == "⚙️ Настройки")
|
||||
async def cmd_settings(message: Message):
|
||||
"""Show notification settings."""
|
||||
user = await api_client.get_user_by_telegram_id(message.from_user.id)
|
||||
|
||||
if not user:
|
||||
await message.answer(
|
||||
"Сначала привяжи аккаунт через настройки профиля на сайте.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
"<b>⚙️ Настройки</b>\n\n"
|
||||
"Управление уведомлениями будет доступно в следующем обновлении.\n\n"
|
||||
"Сейчас ты получаешь все уведомления:\n"
|
||||
"• 🌟 События (Golden Hour, Jackpot и др.)\n"
|
||||
"• 🚀 Старт/финиш марафонов\n"
|
||||
"• ⚠️ Споры по заданиям\n\n"
|
||||
"Команды:\n"
|
||||
"/unlink - Отвязать аккаунт\n"
|
||||
"/status - Проверить привязку",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
120
bot/handlers/start.py
Normal file
120
bot/handlers/start.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import logging
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import CommandStart, Command, CommandObject
|
||||
from aiogram.types import Message
|
||||
|
||||
from keyboards.main_menu import get_main_menu
|
||||
from services.api_client import api_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def cmd_start(message: Message, command: CommandObject):
|
||||
"""Handle /start command with or without deep link."""
|
||||
logger.info(f"[START] ==================== START COMMAND ====================")
|
||||
logger.info(f"[START] Telegram user: id={message.from_user.id}, username=@{message.from_user.username}")
|
||||
logger.info(f"[START] Full message text: '{message.text}'")
|
||||
logger.info(f"[START] Deep link args (command.args): '{command.args}'")
|
||||
|
||||
# Check if there's a deep link token (for account linking)
|
||||
token = command.args
|
||||
if token:
|
||||
logger.info(f"[START] -------- TOKEN RECEIVED --------")
|
||||
logger.info(f"[START] Token: {token}")
|
||||
logger.info(f"[START] Token length: {len(token)} chars")
|
||||
|
||||
logger.info(f"[START] -------- CALLING API --------")
|
||||
logger.info(f"[START] Sending to /telegram/confirm-link:")
|
||||
logger.info(f"[START] - token: {token}")
|
||||
logger.info(f"[START] - telegram_id: {message.from_user.id}")
|
||||
logger.info(f"[START] - telegram_username: {message.from_user.username}")
|
||||
|
||||
result = await api_client.confirm_telegram_link(
|
||||
token=token,
|
||||
telegram_id=message.from_user.id,
|
||||
telegram_username=message.from_user.username
|
||||
)
|
||||
|
||||
logger.info(f"[START] -------- API RESPONSE --------")
|
||||
logger.info(f"[START] Response: {result}")
|
||||
logger.info(f"[START] Success: {result.get('success')}")
|
||||
|
||||
if result.get("success"):
|
||||
user_nickname = result.get("nickname", "пользователь")
|
||||
logger.info(f"[START] ✅ LINK SUCCESS! User '{user_nickname}' linked to telegram_id={message.from_user.id}")
|
||||
await message.answer(
|
||||
f"<b>Аккаунт успешно привязан!</b>\n\n"
|
||||
f"Привет, <b>{user_nickname}</b>!\n\n"
|
||||
f"Теперь ты будешь получать уведомления о:\n"
|
||||
f"• Начале и окончании событий (Golden Hour, Jackpot и др.)\n"
|
||||
f"• Старте и завершении марафонов\n"
|
||||
f"• Спорах по твоим заданиям\n\n"
|
||||
f"Используй меню ниже для навигации:",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
else:
|
||||
error = result.get("error", "Неизвестная ошибка")
|
||||
logger.error(f"[START] ❌ LINK FAILED!")
|
||||
logger.error(f"[START] Error: {error}")
|
||||
logger.error(f"[START] Token was: {token}")
|
||||
await message.answer(
|
||||
f"<b>Ошибка привязки аккаунта</b>\n\n"
|
||||
f"{error}\n\n"
|
||||
f"Попробуй получить новую ссылку на сайте.",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
return
|
||||
|
||||
# No token - regular start
|
||||
logger.info(f"[START] No token, checking if user is already linked...")
|
||||
user = await api_client.get_user_by_telegram_id(message.from_user.id)
|
||||
logger.info(f"[START] API response: {user}")
|
||||
|
||||
if user:
|
||||
await message.answer(
|
||||
f"<b>С возвращением, {user.get('nickname', 'игрок')}!</b>\n\n"
|
||||
f"Твой аккаунт привязан. Используй меню для навигации:",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"<b>Добро пожаловать в Game Marathon Bot!</b>\n\n"
|
||||
"Этот бот поможет тебе следить за марафонами и "
|
||||
"получать уведомления о важных событиях.\n\n"
|
||||
"<b>Для начала работы:</b>\n"
|
||||
"1. Зайди на сайт в настройки профиля\n"
|
||||
"2. Нажми кнопку «Привязать Telegram»\n"
|
||||
"3. Перейди по полученной ссылке\n\n"
|
||||
"После привязки ты сможешь:\n"
|
||||
"• Смотреть свои марафоны\n"
|
||||
"• Получать уведомления о событиях\n"
|
||||
"• Следить за статистикой",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("help"))
|
||||
@router.message(F.text == "❓ Помощь")
|
||||
async def cmd_help(message: Message):
|
||||
"""Handle /help command."""
|
||||
await message.answer(
|
||||
"<b>Справка по командам:</b>\n\n"
|
||||
"/start - Начать работу с ботом\n"
|
||||
"/marathons - Мои марафоны\n"
|
||||
"/stats - Моя статистика\n"
|
||||
"/settings - Настройки уведомлений\n"
|
||||
"/help - Эта справка\n\n"
|
||||
"<b>Уведомления:</b>\n"
|
||||
"Бот присылает уведомления о:\n"
|
||||
"• 🌟 Golden Hour - очки x1.5\n"
|
||||
"• 🎰 Jackpot - очки x3\n"
|
||||
"• ⚡ Double Risk - половина очков, дропы бесплатны\n"
|
||||
"• 👥 Common Enemy - общий челлендж\n"
|
||||
"• 🚀 Старт/финиш марафонов\n"
|
||||
"• ⚠️ Споры по заданиям",
|
||||
reply_markup=get_main_menu()
|
||||
)
|
||||
1
bot/keyboards/__init__.py
Normal file
1
bot/keyboards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Bot keyboards
|
||||
42
bot/keyboards/inline.py
Normal file
42
bot/keyboards/inline.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
|
||||
def get_marathons_keyboard(marathons: list) -> InlineKeyboardMarkup:
|
||||
"""Create keyboard with marathon buttons."""
|
||||
buttons = []
|
||||
|
||||
for marathon in marathons:
|
||||
status_emoji = {
|
||||
"preparing": "⏳",
|
||||
"active": "🎮",
|
||||
"finished": "🏁"
|
||||
}.get(marathon.get("status"), "❓")
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"{status_emoji} {marathon.get('title', 'Marathon')}",
|
||||
callback_data=f"marathon:{marathon.get('id')}"
|
||||
)
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
def get_marathon_details_keyboard(marathon_id: int) -> InlineKeyboardMarkup:
|
||||
"""Create keyboard for marathon details view."""
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔄 Обновить",
|
||||
callback_data=f"marathon:{marathon_id}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="◀️ Назад к списку",
|
||||
callback_data="back_to_marathons"
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
21
bot/keyboards/main_menu.py
Normal file
21
bot/keyboards/main_menu.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
|
||||
|
||||
|
||||
def get_main_menu() -> ReplyKeyboardMarkup:
|
||||
"""Create main menu keyboard."""
|
||||
keyboard = [
|
||||
[
|
||||
KeyboardButton(text="📊 Мои марафоны"),
|
||||
KeyboardButton(text="📈 Статистика")
|
||||
],
|
||||
[
|
||||
KeyboardButton(text="⚙️ Настройки"),
|
||||
KeyboardButton(text="❓ Помощь")
|
||||
]
|
||||
]
|
||||
|
||||
return ReplyKeyboardMarkup(
|
||||
keyboard=keyboard,
|
||||
resize_keyboard=True,
|
||||
input_field_placeholder="Выбери действие..."
|
||||
)
|
||||
65
bot/main.py
Normal file
65
bot/main.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
|
||||
from config import settings
|
||||
from handlers import start, marathons, link
|
||||
from middlewares.logging import LoggingMiddleware
|
||||
|
||||
# Configure logging to stdout with DEBUG level
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Set aiogram logging level
|
||||
logging.getLogger("aiogram").setLevel(logging.INFO)
|
||||
|
||||
|
||||
async def main():
|
||||
logger.info("="*50)
|
||||
logger.info("Starting Game Marathon Bot...")
|
||||
logger.info(f"API_URL: {settings.API_URL}")
|
||||
logger.info(f"BOT_TOKEN: {settings.TELEGRAM_BOT_TOKEN[:20]}...")
|
||||
logger.info("="*50)
|
||||
|
||||
bot = Bot(
|
||||
token=settings.TELEGRAM_BOT_TOKEN,
|
||||
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||
)
|
||||
|
||||
# Get bot username for deep links
|
||||
bot_info = await bot.get_me()
|
||||
settings.BOT_USERNAME = bot_info.username
|
||||
logger.info(f"Bot info: @{settings.BOT_USERNAME} (id={bot_info.id})")
|
||||
|
||||
dp = Dispatcher()
|
||||
|
||||
# Register middleware
|
||||
dp.message.middleware(LoggingMiddleware())
|
||||
logger.info("Logging middleware registered")
|
||||
|
||||
# Register routers
|
||||
logger.info("Registering routers...")
|
||||
dp.include_router(start.router)
|
||||
dp.include_router(link.router)
|
||||
dp.include_router(marathons.router)
|
||||
logger.info("Routers registered: start, link, marathons")
|
||||
|
||||
# Start polling
|
||||
logger.info("Deleting webhook and starting polling...")
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
logger.info("Polling started! Waiting for messages...")
|
||||
await dp.start_polling(bot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
1
bot/middlewares/__init__.py
Normal file
1
bot/middlewares/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Bot middlewares
|
||||
28
bot/middlewares/logging.py
Normal file
28
bot/middlewares/logging.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import logging
|
||||
from typing import Any, Awaitable, Callable, Dict
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Message, Update
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoggingMiddleware(BaseMiddleware):
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
|
||||
event: Message,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
logger.info("="*60)
|
||||
logger.info(f"[MIDDLEWARE] Incoming message from user {event.from_user.id}")
|
||||
logger.info(f"[MIDDLEWARE] Username: @{event.from_user.username}")
|
||||
logger.info(f"[MIDDLEWARE] Text: {event.text}")
|
||||
logger.info(f"[MIDDLEWARE] Message ID: {event.message_id}")
|
||||
logger.info(f"[MIDDLEWARE] Chat ID: {event.chat.id}")
|
||||
logger.info("="*60)
|
||||
|
||||
result = await handler(event, data)
|
||||
|
||||
logger.info(f"[MIDDLEWARE] Handler completed for message {event.message_id}")
|
||||
return result
|
||||
5
bot/requirements.txt
Normal file
5
bot/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
aiogram==3.23.0
|
||||
aiohttp==3.10.5
|
||||
pydantic==2.9.2
|
||||
pydantic-settings==2.5.2
|
||||
python-dotenv==1.0.1
|
||||
1
bot/services/__init__.py
Normal file
1
bot/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Bot services
|
||||
123
bot/services/api_client.py
Normal file
123
bot/services/api_client.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIClient:
|
||||
"""HTTP client for backend API communication."""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = settings.API_URL
|
||||
self._session: aiohttp.ClientSession | None = None
|
||||
logger.info(f"[APIClient] Initialized with base_url: {self.base_url}")
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
logger.info("[APIClient] Creating new aiohttp session")
|
||||
self._session = aiohttp.ClientSession()
|
||||
return self._session
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
**kwargs
|
||||
) -> dict[str, Any] | None:
|
||||
"""Make HTTP request to backend API."""
|
||||
session = await self._get_session()
|
||||
url = f"{self.base_url}/api/v1{endpoint}"
|
||||
|
||||
logger.info(f"[APIClient] {method} {url}")
|
||||
if 'json' in kwargs:
|
||||
logger.info(f"[APIClient] Request body: {kwargs['json']}")
|
||||
if 'params' in kwargs:
|
||||
logger.info(f"[APIClient] Request params: {kwargs['params']}")
|
||||
|
||||
try:
|
||||
async with session.request(method, url, **kwargs) as response:
|
||||
logger.info(f"[APIClient] Response status: {response.status}")
|
||||
response_text = await response.text()
|
||||
logger.info(f"[APIClient] Response body: {response_text[:500]}")
|
||||
|
||||
if response.status == 200:
|
||||
import json
|
||||
return json.loads(response_text)
|
||||
elif response.status == 404:
|
||||
logger.warning(f"[APIClient] 404 Not Found")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"[APIClient] API error {response.status}: {response_text}")
|
||||
return {"error": response_text}
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"[APIClient] Request failed: {e}")
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"[APIClient] Unexpected error: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
async def confirm_telegram_link(
|
||||
self,
|
||||
token: str,
|
||||
telegram_id: int,
|
||||
telegram_username: str | None
|
||||
) -> dict[str, Any]:
|
||||
"""Confirm Telegram account linking."""
|
||||
result = await self._request(
|
||||
"POST",
|
||||
"/telegram/confirm-link",
|
||||
json={
|
||||
"token": token,
|
||||
"telegram_id": telegram_id,
|
||||
"telegram_username": telegram_username
|
||||
}
|
||||
)
|
||||
return result or {"error": "Не удалось связаться с сервером"}
|
||||
|
||||
async def get_user_by_telegram_id(self, telegram_id: int) -> dict[str, Any] | None:
|
||||
"""Get user by Telegram ID."""
|
||||
return await self._request("GET", f"/telegram/user/{telegram_id}")
|
||||
|
||||
async def unlink_telegram(self, telegram_id: int) -> dict[str, Any]:
|
||||
"""Unlink Telegram account."""
|
||||
result = await self._request(
|
||||
"POST",
|
||||
f"/telegram/unlink/{telegram_id}"
|
||||
)
|
||||
return result or {"error": "Не удалось связаться с сервером"}
|
||||
|
||||
async def get_user_marathons(self, telegram_id: int) -> list[dict[str, Any]]:
|
||||
"""Get user's marathons."""
|
||||
result = await self._request("GET", f"/telegram/marathons/{telegram_id}")
|
||||
if isinstance(result, list):
|
||||
return result
|
||||
return result.get("marathons", []) if result else []
|
||||
|
||||
async def get_marathon_details(
|
||||
self,
|
||||
marathon_id: int,
|
||||
telegram_id: int
|
||||
) -> dict[str, Any] | None:
|
||||
"""Get marathon details for user."""
|
||||
return await self._request(
|
||||
"GET",
|
||||
f"/telegram/marathon/{marathon_id}",
|
||||
params={"telegram_id": telegram_id}
|
||||
)
|
||||
|
||||
async def get_user_stats(self, telegram_id: int) -> dict[str, Any] | None:
|
||||
"""Get user's overall statistics."""
|
||||
return await self._request("GET", f"/telegram/stats/{telegram_id}")
|
||||
|
||||
async def close(self):
|
||||
"""Close the HTTP session."""
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
|
||||
# Global API client instance
|
||||
api_client = APIClient()
|
||||
Reference in New Issue
Block a user