diff --git a/bot.py b/bot.py index c0f47ea..cc48f98 100644 --- a/bot.py +++ b/bot.py @@ -1,297 +1,125 @@ -import logging -from typing import Optional +import os +import httpx +from dotenv import 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 +load_dotenv() -# ====================== -# LOGGING -# ====================== -logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - level=Config.LOG_LEVEL -) -logger = logging.getLogger(__name__) +class VndbClient: + """ + Minimal safe VNDB client for Telegram bots + """ + def __init__(self): + self.base_url = os.getenv("VNDB_API_URL", "https://api.vndb.org/kana") + self.token = os.getenv("VNDB_TOKEN") -# ====================== -# 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) + self.headers = { + "Content-Type": "application/json" + } + if self.token: + self.headers["Authorization"] = f"Token {self.token}" -# ====================== -# CLIENT -# ====================== -vndb_client = VndbClient(use_sandbox=Config.USE_SANDBOX) + # ========================= + # CORE REQUEST + # ========================= + async def _request(self, endpoint: str, payload: dict): + url = f"{self.base_url}{endpoint}" + async with httpx.AsyncClient(timeout=20) as client: + r = await client.post(url, json=payload, headers=self.headers) -# ====================== -# HELPERS -# ====================== -HTML_HELP_HEADER = "VNDB Telegram Bot\n\n" + # debug-friendly + if r.status_code >= 400: + print("VNDB ERROR:", r.status_code, r.text) + print("PAYLOAD:", payload) + r.raise_for_status() + return r.json() -# ====================== -# HANDLERS -# ====================== -class BotHandlers: + # ========================= + # SAFE SEARCH WRAPPER + # ========================= + def _safe_search(self, query: str): + if not query: + return None + return ["search", "=", query.strip()] - @staticmethod - async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - text = """ -Добро пожаловать в VNDB Telegram Бот! + # ========================= + # VN SEARCH + # ========================= + async def search_vn(self, query: str, limit: int = 10): + filters = self._safe_search(query) + if not filters: + return {"results": []} -Этот бот позволяет искать визуальные новеллы, персонажей и релизы из VNDB. + return await self._request("/vn", { + "filters": filters, + "fields": "id,title,original,released,rating,votecount", + "results": limit + }) -Команды: -/search - поиск VN -/char - персонажи -/release - релизы -/staff - сотрудники -/producer - продюсеры -/tag - теги -/trait - черты -/quote - цитаты -/help - помощь - """ + # ========================= + # CHARACTER SEARCH + # ========================= + async def search_character(self, query: str, limit: int = 10): + filters = self._safe_search(query) + if not filters: + return {"results": []} - await update.message.reply_text(text, parse_mode="HTML") + return await self._request("/character", { + "filters": filters, + "fields": "id,name,original", + "results": limit + }) - # ====================== - # HELP - # ====================== - @staticmethod - async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - text = """ -Справка по командам + # ========================= + # RELEASE SEARCH + # ========================= + async def search_release(self, query: str, limit: int = 10): + filters = self._safe_search(query) + if not filters: + return {"results": []} -Поиск: -/search <название> -/char <имя> -/release <название> -/staff <имя> -/producer <имя> + return await self._request("/release", { + "filters": filters, + "fields": "id,title,original,released", + "results": limit + }) -Информация: -/tag - популярные теги -/trait - черты -/quote <число> - цитаты -/stats - статистика -/schema - API + # ========================= + # 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 + }) -Детальный просмотр: -/vn_detail <id> -/char_detail <id> -/release_detail <id> + # ========================= + # 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 + }) -Пример: -/search Steins Gate -/char Okabe -/vn_detail v17 + # ========================= + # 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 + }) -Ссылка: - -Примеры команд - - """ - - await update.message.reply_text(text, parse_mode="HTML") - - # ====================== + # ========================= # STATS - # ====================== - @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 + # ========================= + async def stats(self): + return await self._request("/stats", {}) \ No newline at end of file