Рефакторинг клиента VNDB: добавление новых обработчиков команд, улучшение структуры кода и обработка ошибок

This commit is contained in:
2026-05-01 17:56:44 +03:00
parent e157c0fa62
commit fd0a403f37

378
bot.py
View File

@@ -1,125 +1,297 @@
import os import logging
import httpx from typing import Optional
from dotenv import load_dotenv
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: # ======================
""" # LOGGING
Minimal safe VNDB client for Telegram bots # ======================
""" 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 # CLIENT
# ========================= # ======================
async def _request(self, endpoint: str, payload: dict): vndb_client = VndbClient(use_sandbox=Config.USE_SANDBOX)
url = f"{self.base_url}{endpoint}"
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: # HELPERS
print("VNDB ERROR:", r.status_code, r.text) # ======================
print("PAYLOAD:", payload) HTML_HELP_HEADER = "<b>VNDB Telegram Bot</b>\n\n"
r.raise_for_status()
return r.json()
# ========================= # ======================
# SAFE SEARCH WRAPPER # HANDLERS
# ========================= # ======================
def _safe_search(self, query: str): class BotHandlers:
if not query:
return None
return ["search", "=", query.strip()]
# ========================= @staticmethod
# VN SEARCH async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# ========================= text = """
async def search_vn(self, query: str, limit: int = 10): <b>Добро пожаловать в VNDB Telegram Бот!</b>
filters = self._safe_search(query)
if not filters:
return {"results": []}
return await self._request("/vn", { Этот бот позволяет искать визуальные новеллы, персонажей и релизы из VNDB.
"filters": filters,
"fields": "id,title,original,released,rating,votecount",
"results": limit
})
# ========================= <b>Команды:</b>
# CHARACTER SEARCH /search - поиск VN
# ========================= /char - персонажи
async def search_character(self, query: str, limit: int = 10): /release - релизы
filters = self._safe_search(query) /staff - сотрудники
if not filters: /producer - продюсеры
return {"results": []} /tag - теги
/trait - черты
/quote - цитаты
/help - помощь
"""
return await self._request("/character", { await update.message.reply_text(text, parse_mode="HTML")
"filters": filters,
"fields": "id,name,original",
"results": limit
})
# ========================= # ======================
# RELEASE SEARCH # HELP
# ========================= # ======================
async def search_release(self, query: str, limit: int = 10): @staticmethod
filters = self._safe_search(query) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if not filters: text = """
return {"results": []} <b>Справка по командам</b>
return await self._request("/release", { <b>Поиск:</b>
"filters": filters, /search &lt;название&gt;
"fields": "id,title,original,released", /char &lt;имя&gt;
"results": limit /release &lt;название&gt;
}) /staff &lt;имя&gt;
/producer &lt;имя&gt;
# ========================= <b>Информация:</b>
# DETAIL VN /tag - популярные теги
# ========================= /trait - черты
async def vn_detail(self, vn_id: str): /quote &lt;число&gt; - цитаты
return await self._request("/vn", { /stats - статистика
"filters": ["id", "=", vn_id], /schema - API
"fields": "id,title,original,released,rating,votecount,description,length,developer",
"results": 1
})
# ========================= <b>Детальный просмотр:</b>
# DETAIL CHARACTER /vn_detail &lt;id&gt;
# ========================= /char_detail &lt;id&gt;
async def character_detail(self, char_id: str): /release_detail &lt;id&gt;
return await self._request("/character", {
"filters": ["id", "=", char_id],
"fields": "id,name,original,gender,bloodtype",
"results": 1
})
# ========================= <b>Пример:</b>
# DETAIL RELEASE /search Steins Gate
# ========================= /char Okabe
async def release_detail(self, rel_id: str): /vn_detail v17
return await self._request("/release", {
"filters": ["id", "=", rel_id],
"fields": "id,title,original,released,platform,type,language,description",
"results": 1
})
# ========================= <b>Ссылка:</b>
<a href="https://git.kotac.ru/King-of-the-all-Cookies/ayako/src/branch/main/EXAMPLES.md">
Примеры команд
</a>
"""
await update.message.reply_text(text, parse_mode="HTML")
# ======================
# STATS # STATS
# ========================= # ======================
async def stats(self): @staticmethod
return await self._request("/stats", {}) async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
try:
stats = await vndb_client.get_stats()
text = f"""
<b>Статистика VNDB</b>
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 = "<b>API Schema VNDB</b>\n\n"
if "db_types" in schema:
text += "<b>Типы:</b>\n"
for k in list(schema["db_types"].keys())[:5]:
text += f"{k}\n"
text += "\n<a href='https://api.vndb.org/kana'>Документация API</a>"
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"<b>Поиск:</b> {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 = "<b>Результаты:</b>\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 = "<b>Персонажи:</b>\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 = "<b>Релизы:</b>\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()