Рефакторинг клиента VNDB: удаление неиспользуемых импортов и обработчиков, добавление авторизации в заголовки запросов
This commit is contained in:
394
bot.py
394
bot.py
@@ -1,297 +1,125 @@
|
|||||||
import logging
|
import os
|
||||||
from typing import Optional
|
import httpx
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
load_dotenv()
|
||||||
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__)
|
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
|
||||||
# CLIENT
|
|
||||||
# ======================
|
|
||||||
vndb_client = VndbClient(use_sandbox=Config.USE_SANDBOX)
|
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
|
||||||
# HELPERS
|
|
||||||
# ======================
|
|
||||||
HTML_HELP_HEADER = "<b>VNDB Telegram Bot</b>\n\n"
|
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
|
||||||
# HANDLERS
|
|
||||||
# ======================
|
|
||||||
class BotHandlers:
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
||||||
text = """
|
|
||||||
<b>Добро пожаловать в VNDB Telegram Бот!</b>
|
|
||||||
|
|
||||||
Этот бот позволяет искать визуальные новеллы, персонажей и релизы из VNDB.
|
|
||||||
|
|
||||||
<b>Команды:</b>
|
|
||||||
/search - поиск VN
|
|
||||||
/char - персонажи
|
|
||||||
/release - релизы
|
|
||||||
/staff - сотрудники
|
|
||||||
/producer - продюсеры
|
|
||||||
/tag - теги
|
|
||||||
/trait - черты
|
|
||||||
/quote - цитаты
|
|
||||||
/help - помощь
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await update.message.reply_text(text, parse_mode="HTML")
|
def __init__(self):
|
||||||
|
self.base_url = os.getenv("VNDB_API_URL", "https://api.vndb.org/kana")
|
||||||
|
self.token = os.getenv("VNDB_TOKEN")
|
||||||
|
|
||||||
# ======================
|
self.headers = {
|
||||||
# HELP
|
"Content-Type": "application/json"
|
||||||
# ======================
|
}
|
||||||
@staticmethod
|
|
||||||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
||||||
text = """
|
|
||||||
<b>Справка по командам</b>
|
|
||||||
|
|
||||||
<b>Поиск:</b>
|
if self.token:
|
||||||
/search <название>
|
self.headers["Authorization"] = f"Token {self.token}"
|
||||||
/char <имя>
|
|
||||||
/release <название>
|
|
||||||
/staff <имя>
|
|
||||||
/producer <имя>
|
|
||||||
|
|
||||||
<b>Информация:</b>
|
# =========================
|
||||||
/tag - популярные теги
|
# CORE REQUEST
|
||||||
/trait - черты
|
# =========================
|
||||||
/quote <число> - цитаты
|
async def _request(self, endpoint: str, payload: dict):
|
||||||
/stats - статистика
|
url = f"{self.base_url}{endpoint}"
|
||||||
/schema - API
|
|
||||||
|
|
||||||
<b>Детальный просмотр:</b>
|
async with httpx.AsyncClient(timeout=20) as client:
|
||||||
/vn_detail <id>
|
r = await client.post(url, json=payload, headers=self.headers)
|
||||||
/char_detail <id>
|
|
||||||
/release_detail <id>
|
|
||||||
|
|
||||||
<b>Пример:</b>
|
# debug-friendly
|
||||||
/search Steins Gate
|
if r.status_code >= 400:
|
||||||
/char Okabe
|
print("VNDB ERROR:", r.status_code, r.text)
|
||||||
/vn_detail v17
|
print("PAYLOAD:", payload)
|
||||||
|
|
||||||
<b>Ссылка:</b>
|
r.raise_for_status()
|
||||||
<a href="https://git.kotac.ru/King-of-the-all-Cookies/ayako/src/branch/main/EXAMPLES.md">
|
return r.json()
|
||||||
Примеры команд
|
|
||||||
</a>
|
|
||||||
"""
|
|
||||||
|
|
||||||
await update.message.reply_text(text, parse_mode="HTML")
|
# =========================
|
||||||
|
# SAFE SEARCH WRAPPER
|
||||||
|
# =========================
|
||||||
|
def _safe_search(self, query: str):
|
||||||
|
if not query:
|
||||||
|
return None
|
||||||
|
return ["search", "=", query.strip()]
|
||||||
|
|
||||||
# ======================
|
# =========================
|
||||||
|
# VN SEARCH
|
||||||
|
# =========================
|
||||||
|
async def search_vn(self, query: str, limit: int = 10):
|
||||||
|
filters = self._safe_search(query)
|
||||||
|
if not filters:
|
||||||
|
return {"results": []}
|
||||||
|
|
||||||
|
return await self._request("/vn", {
|
||||||
|
"filters": filters,
|
||||||
|
"fields": "id,title,original,released,rating,votecount",
|
||||||
|
"results": limit
|
||||||
|
})
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# CHARACTER SEARCH
|
||||||
|
# =========================
|
||||||
|
async def search_character(self, query: str, limit: int = 10):
|
||||||
|
filters = self._safe_search(query)
|
||||||
|
if not filters:
|
||||||
|
return {"results": []}
|
||||||
|
|
||||||
|
return await self._request("/character", {
|
||||||
|
"filters": filters,
|
||||||
|
"fields": "id,name,original",
|
||||||
|
"results": limit
|
||||||
|
})
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# RELEASE SEARCH
|
||||||
|
# =========================
|
||||||
|
async def search_release(self, query: str, limit: int = 10):
|
||||||
|
filters = self._safe_search(query)
|
||||||
|
if not filters:
|
||||||
|
return {"results": []}
|
||||||
|
|
||||||
|
return await self._request("/release", {
|
||||||
|
"filters": filters,
|
||||||
|
"fields": "id,title,original,released",
|
||||||
|
"results": limit
|
||||||
|
})
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 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
|
||||||
|
})
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 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
|
||||||
|
})
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 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
|
||||||
|
})
|
||||||
|
|
||||||
|
# =========================
|
||||||
# STATS
|
# STATS
|
||||||
# ======================
|
# =========================
|
||||||
@staticmethod
|
async def stats(self):
|
||||||
async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
return await self._request("/stats", {})
|
||||||
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()
|
|
||||||
Reference in New Issue
Block a user