diff --git a/bot.py b/bot.py
index cc48f98..c0f47ea 100644
--- a/bot.py
+++ b/bot.py
@@ -1,125 +1,297 @@
-import os
-import httpx
-from dotenv import load_dotenv
+import logging
+from typing import Optional
-load_dotenv()
+from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
+from telegram.ext import (
+ Application,
+ CommandHandler,
+ MessageHandler,
+ CallbackQueryHandler,
+ ConversationHandler,
+ ContextTypes,
+ filters,
+)
+
+from vndb_client import VndbClient
+from config import Config
+from utils import Formatter, ErrorHandler, QueryBuilder
+from detailed_handlers import get_detail_handlers
-class VndbClient:
- """
- Minimal safe VNDB client for Telegram bots
- """
+# ======================
+# LOGGING
+# ======================
+logging.basicConfig(
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ level=Config.LOG_LEVEL
+)
+logger = logging.getLogger(__name__)
- def __init__(self):
- self.base_url = os.getenv("VNDB_API_URL", "https://api.vndb.org/kana")
- self.token = os.getenv("VNDB_TOKEN")
- self.headers = {
- "Content-Type": "application/json"
- }
+# ======================
+# STATES
+# ======================
+SEARCH_VN, SELECT_VN, VN_DETAILS = range(3)
+SEARCH_CHARACTER, SELECT_CHARACTER = range(2)
+SEARCH_RELEASE, SELECT_RELEASE = range(2)
+SEARCH_STAFF, SELECT_STAFF = range(2)
- if self.token:
- self.headers["Authorization"] = f"Token {self.token}"
- # =========================
- # CORE REQUEST
- # =========================
- async def _request(self, endpoint: str, payload: dict):
- url = f"{self.base_url}{endpoint}"
+# ======================
+# CLIENT
+# ======================
+vndb_client = VndbClient(use_sandbox=Config.USE_SANDBOX)
- async with httpx.AsyncClient(timeout=20) as client:
- r = await client.post(url, json=payload, headers=self.headers)
- # debug-friendly
- if r.status_code >= 400:
- print("VNDB ERROR:", r.status_code, r.text)
- print("PAYLOAD:", payload)
+# ======================
+# HELPERS
+# ======================
+HTML_HELP_HEADER = "VNDB Telegram Bot\n\n"
- r.raise_for_status()
- return r.json()
- # =========================
- # SAFE SEARCH WRAPPER
- # =========================
- def _safe_search(self, query: str):
- if not query:
- return None
- return ["search", "=", query.strip()]
+# ======================
+# HANDLERS
+# ======================
+class BotHandlers:
- # =========================
- # VN SEARCH
- # =========================
- async def search_vn(self, query: str, limit: int = 10):
- filters = self._safe_search(query)
- if not filters:
- return {"results": []}
+ @staticmethod
+ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ text = """
+Добро пожаловать в VNDB Telegram Бот!
- return await self._request("/vn", {
- "filters": filters,
- "fields": "id,title,original,released,rating,votecount",
- "results": limit
- })
+Этот бот позволяет искать визуальные новеллы, персонажей и релизы из VNDB.
- # =========================
- # CHARACTER SEARCH
- # =========================
- async def search_character(self, query: str, limit: int = 10):
- filters = self._safe_search(query)
- if not filters:
- return {"results": []}
+Команды:
+/search - поиск VN
+/char - персонажи
+/release - релизы
+/staff - сотрудники
+/producer - продюсеры
+/tag - теги
+/trait - черты
+/quote - цитаты
+/help - помощь
+ """
- return await self._request("/character", {
- "filters": filters,
- "fields": "id,name,original",
- "results": limit
- })
+ await update.message.reply_text(text, parse_mode="HTML")
- # =========================
- # RELEASE SEARCH
- # =========================
- async def search_release(self, query: str, limit: int = 10):
- filters = self._safe_search(query)
- if not filters:
- return {"results": []}
+ # ======================
+ # HELP
+ # ======================
+ @staticmethod
+ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ text = """
+Справка по командам
- return await self._request("/release", {
- "filters": filters,
- "fields": "id,title,original,released",
- "results": limit
- })
+Поиск:
+/search <название>
+/char <имя>
+/release <название>
+/staff <имя>
+/producer <имя>
- # =========================
- # DETAIL VN
- # =========================
- async def vn_detail(self, vn_id: str):
- return await self._request("/vn", {
- "filters": ["id", "=", vn_id],
- "fields": "id,title,original,released,rating,votecount,description,length,developer",
- "results": 1
- })
+Информация:
+/tag - популярные теги
+/trait - черты
+/quote <число> - цитаты
+/stats - статистика
+/schema - API
- # =========================
- # DETAIL CHARACTER
- # =========================
- async def character_detail(self, char_id: str):
- return await self._request("/character", {
- "filters": ["id", "=", char_id],
- "fields": "id,name,original,gender,bloodtype",
- "results": 1
- })
+Детальный просмотр:
+/vn_detail <id>
+/char_detail <id>
+/release_detail <id>
- # =========================
- # DETAIL RELEASE
- # =========================
- async def release_detail(self, rel_id: str):
- return await self._request("/release", {
- "filters": ["id", "=", rel_id],
- "fields": "id,title,original,released,platform,type,language,description",
- "results": 1
- })
+Пример:
+/search Steins Gate
+/char Okabe
+/vn_detail v17
- # =========================
+Ссылка:
+
+Примеры команд
+
+ """
+
+ await update.message.reply_text(text, parse_mode="HTML")
+
+ # ======================
# STATS
- # =========================
- async def stats(self):
- return await self._request("/stats", {})
\ No newline at end of file
+ # ======================
+ @staticmethod
+ async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ try:
+ stats = await vndb_client.get_stats()
+
+ text = f"""
+Статистика VNDB
+
+VN: {stats.get('vn', 0)}
+Characters: {stats.get('chars', 0)}
+Releases: {stats.get('releases', 0)}
+Producers: {stats.get('producers', 0)}
+Staff: {stats.get('staff', 0)}
+"""
+
+ await update.message.reply_text(text, parse_mode="HTML")
+
+ except Exception as e:
+ logger.error(e)
+ await update.message.reply_text(f"❌ {ErrorHandler.format_error(e)}")
+
+ # ======================
+ # SCHEMA
+ # ======================
+ @staticmethod
+ async def schema(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ try:
+ await update.message.reply_text("⏳ Загружаю схему...", parse_mode="HTML")
+
+ schema = await vndb_client.get_schema()
+
+ text = "API Schema VNDB\n\n"
+
+ if "db_types" in schema:
+ text += "Типы:\n"
+ for k in list(schema["db_types"].keys())[:5]:
+ text += f"• {k}\n"
+
+ text += "\nДокументация API"
+
+ await update.message.reply_text(text, parse_mode="HTML")
+
+ except Exception as e:
+ logger.error(e)
+ await update.message.reply_text(f"❌ {ErrorHandler.format_error(e)}")
+
+ # ======================
+ # SEARCH VN
+ # ======================
+ @staticmethod
+ async def search_vn(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ try:
+ query = " ".join(context.args)
+
+ if not query:
+ await update.message.reply_text("Введите название")
+ return ConversationHandler.END
+
+ await update.message.reply_text(
+ f"Поиск: {query}",
+ parse_mode="HTML"
+ )
+
+ results = await vndb_client.query_vn(
+ filters=["search", "=", query.strip()],
+ fields=["id", "title"],
+ results=10
+ )
+
+ if not results.get("results"):
+ await update.message.reply_text("Ничего не найдено")
+ return ConversationHandler.END
+
+ text = "Результаты:\n\n"
+
+ for vn in results["results"]:
+ text += f"{vn.get('id')} - {vn.get('title')}\n"
+
+ await update.message.reply_text(text, parse_mode="HTML")
+
+ # images
+ for vn in results["results"]:
+ text += f"{vn.get('id')} - {vn.get('title')}\n"
+
+ # 👇 ДОБАВЬ ССЫЛКУ НА VNDB
+ text += f"https://vndb.org/{vn.get('id')}\n\n"
+
+ except Exception as e:
+ logger.error(e)
+ await update.message.reply_text(f"❌ {ErrorHandler.format_error(e)}")
+
+ return ConversationHandler.END
+
+ # ======================
+ # CHARACTER
+ # ======================
+ @staticmethod
+ async def search_character(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ try:
+ query = " ".join(context.args)
+
+ if not query:
+ await update.message.reply_text("Введите имя")
+ return ConversationHandler.END
+
+ results = await vndb_client.query_character(
+ filters=["search", "=", query.strip()],
+ fields=["id", "name", "original"],
+ results=10
+ )
+
+ text = "Персонажи:\n\n"
+
+ for c in results.get("results", []):
+ text += f"{c.get('id')} - {c.get('name')} ({c.get('original', '')})\n"
+
+ await update.message.reply_text(text, parse_mode="HTML")
+
+ except Exception as e:
+ logger.error(e)
+ await update.message.reply_text(f"❌ {ErrorHandler.format_error(e)}")
+
+ return ConversationHandler.END
+
+ # ======================
+ # RELEASE
+ # ======================
+ @staticmethod
+ async def search_release(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ try:
+ query = " ".join(context.args)
+
+ results = await vndb_client.query_release(
+ filters=["search", "=", query.strip()], # ✔️ важно
+ fields=["id", "title"],
+ results=10
+ )
+
+ text = "Релизы:\n\n"
+
+ for r in results.get("results", []):
+ text += f"{r.get('id')} - {r.get('title')}\n"
+
+ await update.message.reply_text(text, parse_mode="HTML")
+
+ except Exception as e:
+ logger.error(e)
+ await update.message.reply_text(f"❌ {ErrorHandler.format_error(e)}")
+
+ return ConversationHandler.END
+
+
+# ======================
+# MAIN
+# ======================
+def main():
+ Config.validate()
+
+ app = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
+
+ app.add_handler(CommandHandler("start", BotHandlers.start))
+ app.add_handler(CommandHandler("help", BotHandlers.help_command))
+ app.add_handler(CommandHandler("stats", BotHandlers.stats))
+ app.add_handler(CommandHandler("schema", BotHandlers.schema))
+ app.add_handler(CommandHandler("search", BotHandlers.search_vn))
+ app.add_handler(CommandHandler("char", BotHandlers.search_character))
+ app.add_handler(CommandHandler("release", BotHandlers.search_release))
+
+ for h in get_detail_handlers():
+ app.add_handler(h)
+
+ logger.info("Bot started")
+ app.run_polling()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file