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