diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec6893a --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Dota 2 Random Build Generator + +Генератор случайных билдов для Dota 2. Включает веб-интерфейс и Telegram бота. + +## Быстрый старт + +### 1. Получи токен для Telegram бота + +1. Открой [@BotFather](https://t.me/BotFather) в Telegram +2. Отправь `/newbot` +3. Придумай имя и username для бота +4. Скопируй токен, который пришлёт BotFather + +### 2. Создай файл .env + +```bash +cp .env.example .env +``` + +Открой `.env` и вставь свой токен: + +``` +BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz +``` + +### 3. Запусти + +```bash +docker compose up --build +``` + +## Доступ + +- **Веб-интерфейс:** http://localhost +- **Telegram бот:** найди своего бота по username в Telegram + +## Команды бота + +- `/start` — главное меню +- `/random` — случайный билд +- `/daily` — билд дня +- `/settings` — настройки diff --git a/docker-compose.yml b/docker-compose.yml index 26d7a83..4126b87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,5 +13,14 @@ services: depends_on: - backend + bot: + build: ./dota-random-builds-bot + environment: + - BOT_TOKEN=${BOT_TOKEN} + - API_URL=http://backend:8000 + depends_on: + - backend + restart: unless-stopped + volumes: backend-data: diff --git a/dota-random-builds-bot/.dockerignore b/dota-random-builds-bot/.dockerignore new file mode 100644 index 0000000..05c4252 --- /dev/null +++ b/dota-random-builds-bot/.dockerignore @@ -0,0 +1,4 @@ +__pycache__ +*.pyc +.git +.env diff --git a/dota-random-builds-bot/Dockerfile b/dota-random-builds-bot/Dockerfile new file mode 100644 index 0000000..bc440c9 --- /dev/null +++ b/dota-random-builds-bot/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "bot.py"] diff --git a/dota-random-builds-bot/bot.py b/dota-random-builds-bot/bot.py new file mode 100644 index 0000000..9e45c03 --- /dev/null +++ b/dota-random-builds-bot/bot.py @@ -0,0 +1,288 @@ +import asyncio +import os +from typing import Any + +import aiohttp +from aiogram import Bot, Dispatcher, F +from aiogram.filters import Command +from aiogram.types import ( + Message, + InlineKeyboardMarkup, + InlineKeyboardButton, + CallbackQuery, +) + +API_URL = os.getenv("API_URL", "http://backend:8000") +BOT_TOKEN = os.getenv("BOT_TOKEN") + +if not BOT_TOKEN: + raise ValueError("BOT_TOKEN environment variable is required") + +bot = Bot(token=BOT_TOKEN) +dp = Dispatcher() + +# User preferences storage (in-memory, resets on restart) +user_prefs: dict[int, dict[str, Any]] = {} + + +def get_user_prefs(user_id: int) -> dict[str, Any]: + if user_id not in user_prefs: + user_prefs[user_id] = { + "include_skills": True, + "include_aspect": True, + "items_count": 6, + } + return user_prefs[user_id] + + +def format_skill(skill: str) -> str: + skill_map = { + "q": "Q", + "w": "W", + "e": "E", + "r": "R", + "left_talent": "L", + "right_talent": "R", + } + return skill_map.get(skill, skill) + + +def format_build(data: dict, is_daily: bool = False) -> str: + lines = [] + + if is_daily: + lines.append(f"📅 Build of the Day ({data.get('date', 'N/A')})") + else: + lines.append("🎲 Random Build") + + lines.append("") + + # Hero + hero = data["hero"] + attr_emoji = {"strength": "💪", "agility": "🏃", "intelligence": "🧠"} + emoji = attr_emoji.get(hero["primary"], "") + lines.append(f"🦸 Hero: {hero['name']} {emoji}") + + # Items + items = [item["name"] for item in data["items"]] + lines.append(f"\n🎒 Items:") + for item in items: + lines.append(f" • {item}") + + # Skill build + if "skillBuild" in data and data["skillBuild"]: + skill_build = data["skillBuild"] + levels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 25] + skills_str = " ".join( + format_skill(skill_build.get(str(lvl), "-")) for lvl in levels + ) + lines.append(f"\n📊 Skill Build:") + lines.append(f"{skills_str}") + lines.append("Levels: 1-16, 18, 20, 25") + + # Aspect + if "aspect" in data and data["aspect"]: + lines.append(f"\n✨ Aspect: {data['aspect']}") + + return "\n".join(lines) + + +def get_settings_keyboard(user_id: int) -> InlineKeyboardMarkup: + prefs = get_user_prefs(user_id) + skills_text = "✅ Skills" if prefs["include_skills"] else "❌ Skills" + aspect_text = "✅ Aspect" if prefs["include_aspect"] else "❌ Aspect" + + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text=skills_text, callback_data="toggle_skills"), + InlineKeyboardButton(text=aspect_text, callback_data="toggle_aspect"), + ], + [ + InlineKeyboardButton( + text=f"Items: {prefs['items_count']}", callback_data="items_count" + ), + ], + [ + InlineKeyboardButton(text="🎲 Generate", callback_data="generate"), + ], + ] + ) + + +def get_main_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="🎲 Random Build", callback_data="random"), + InlineKeyboardButton(text="📅 Build of Day", callback_data="daily"), + ], + [ + InlineKeyboardButton(text="⚙️ Settings", callback_data="settings"), + ], + ] + ) + + +@dp.message(Command("start")) +async def cmd_start(message: Message): + await message.answer( + "🎮 Dota 2 Random Build Generator\n\n" + "Generate random builds for your Dota 2 challenges!\n\n" + "Commands:\n" + "/random - Generate random build\n" + "/daily - Get build of the day\n" + "/settings - Configure options", + parse_mode="HTML", + reply_markup=get_main_keyboard(), + ) + + +@dp.message(Command("random")) +async def cmd_random(message: Message): + await generate_random_build(message) + + +@dp.message(Command("daily")) +async def cmd_daily(message: Message): + await get_daily_build(message) + + +@dp.message(Command("settings")) +async def cmd_settings(message: Message): + await message.answer( + "⚙️ Settings\n\nConfigure your random build options:", + parse_mode="HTML", + reply_markup=get_settings_keyboard(message.from_user.id), + ) + + +@dp.callback_query(F.data == "random") +async def callback_random(callback: CallbackQuery): + await callback.answer() + await generate_random_build(callback.message, callback.from_user.id) + + +@dp.callback_query(F.data == "daily") +async def callback_daily(callback: CallbackQuery): + await callback.answer() + await get_daily_build(callback.message) + + +@dp.callback_query(F.data == "settings") +async def callback_settings(callback: CallbackQuery): + await callback.answer() + await callback.message.edit_text( + "⚙️ Settings\n\nConfigure your random build options:", + parse_mode="HTML", + reply_markup=get_settings_keyboard(callback.from_user.id), + ) + + +@dp.callback_query(F.data == "toggle_skills") +async def callback_toggle_skills(callback: CallbackQuery): + prefs = get_user_prefs(callback.from_user.id) + prefs["include_skills"] = not prefs["include_skills"] + await callback.answer( + f"Skills: {'enabled' if prefs['include_skills'] else 'disabled'}" + ) + await callback.message.edit_reply_markup( + reply_markup=get_settings_keyboard(callback.from_user.id) + ) + + +@dp.callback_query(F.data == "toggle_aspect") +async def callback_toggle_aspect(callback: CallbackQuery): + prefs = get_user_prefs(callback.from_user.id) + prefs["include_aspect"] = not prefs["include_aspect"] + await callback.answer( + f"Aspect: {'enabled' if prefs['include_aspect'] else 'disabled'}" + ) + await callback.message.edit_reply_markup( + reply_markup=get_settings_keyboard(callback.from_user.id) + ) + + +@dp.callback_query(F.data == "items_count") +async def callback_items_count(callback: CallbackQuery): + prefs = get_user_prefs(callback.from_user.id) + # Cycle through 3, 4, 5, 6 + prefs["items_count"] = (prefs["items_count"] % 4) + 3 + await callback.answer(f"Items count: {prefs['items_count']}") + await callback.message.edit_reply_markup( + reply_markup=get_settings_keyboard(callback.from_user.id) + ) + + +@dp.callback_query(F.data == "generate") +async def callback_generate(callback: CallbackQuery): + await callback.answer() + await generate_random_build(callback.message, callback.from_user.id) + + +async def generate_random_build(message: Message, user_id: int = None): + if user_id is None: + user_id = message.from_user.id + + prefs = get_user_prefs(user_id) + + payload = { + "includeSkills": prefs["include_skills"], + "includeAspect": prefs["include_aspect"], + "itemsCount": prefs["items_count"], + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + f"{API_URL}/api/randomize", json=payload + ) as response: + if response.status == 200: + data = await response.json() + await message.answer( + format_build(data), + parse_mode="HTML", + reply_markup=get_main_keyboard(), + ) + else: + await message.answer( + "❌ Failed to generate build. Try again later.", + reply_markup=get_main_keyboard(), + ) + except Exception as e: + await message.answer( + f"❌ Error connecting to server: {e}", + reply_markup=get_main_keyboard(), + ) + + +async def get_daily_build(message: Message): + try: + async with aiohttp.ClientSession() as session: + async with session.get(f"{API_URL}/api/build-of-day") as response: + if response.status == 200: + data = await response.json() + await message.answer( + format_build(data, is_daily=True), + parse_mode="HTML", + reply_markup=get_main_keyboard(), + ) + else: + await message.answer( + "❌ Failed to get daily build. Try again later.", + reply_markup=get_main_keyboard(), + ) + except Exception as e: + await message.answer( + f"❌ Error connecting to server: {e}", + reply_markup=get_main_keyboard(), + ) + + +async def main(): + print("Bot started!") + await dp.start_polling(bot) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/dota-random-builds-bot/requirements.txt b/dota-random-builds-bot/requirements.txt new file mode 100644 index 0000000..ba6e2b3 --- /dev/null +++ b/dota-random-builds-bot/requirements.txt @@ -0,0 +1,2 @@ +aiogram>=3.4,<4.0 +aiohttp>=3.9,<4.0