add tg bot and readme
This commit is contained in:
42
README.md
Normal file
42
README.md
Normal file
@@ -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` — настройки
|
||||||
@@ -13,5 +13,14 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
|
bot:
|
||||||
|
build: ./dota-random-builds-bot
|
||||||
|
environment:
|
||||||
|
- BOT_TOKEN=${BOT_TOKEN}
|
||||||
|
- API_URL=http://backend:8000
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
backend-data:
|
backend-data:
|
||||||
|
|||||||
4
dota-random-builds-bot/.dockerignore
Normal file
4
dota-random-builds-bot/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.git
|
||||||
|
.env
|
||||||
10
dota-random-builds-bot/Dockerfile
Normal file
10
dota-random-builds-bot/Dockerfile
Normal file
@@ -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"]
|
||||||
288
dota-random-builds-bot/bot.py
Normal file
288
dota-random-builds-bot/bot.py
Normal file
@@ -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"📅 <b>Build of the Day</b> ({data.get('date', 'N/A')})")
|
||||||
|
else:
|
||||||
|
lines.append("🎲 <b>Random Build</b>")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Hero
|
||||||
|
hero = data["hero"]
|
||||||
|
attr_emoji = {"strength": "💪", "agility": "🏃", "intelligence": "🧠"}
|
||||||
|
emoji = attr_emoji.get(hero["primary"], "")
|
||||||
|
lines.append(f"🦸 <b>Hero:</b> {hero['name']} {emoji}")
|
||||||
|
|
||||||
|
# Items
|
||||||
|
items = [item["name"] for item in data["items"]]
|
||||||
|
lines.append(f"\n🎒 <b>Items:</b>")
|
||||||
|
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📊 <b>Skill Build:</b>")
|
||||||
|
lines.append(f"<code>{skills_str}</code>")
|
||||||
|
lines.append("<i>Levels: 1-16, 18, 20, 25</i>")
|
||||||
|
|
||||||
|
# Aspect
|
||||||
|
if "aspect" in data and data["aspect"]:
|
||||||
|
lines.append(f"\n✨ <b>Aspect:</b> {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(
|
||||||
|
"🎮 <b>Dota 2 Random Build Generator</b>\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(
|
||||||
|
"⚙️ <b>Settings</b>\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(
|
||||||
|
"⚙️ <b>Settings</b>\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())
|
||||||
2
dota-random-builds-bot/requirements.txt
Normal file
2
dota-random-builds-bot/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
aiogram>=3.4,<4.0
|
||||||
|
aiohttp>=3.9,<4.0
|
||||||
Reference in New Issue
Block a user