From 37bbff67ce5415a7f9047f37e96266f3146f02a3 Mon Sep 17 00:00:00 2001 From: King-of-the-all-Cookies Date: Fri, 1 May 2026 18:56:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4:=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?clean=5Ftext,=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=20=D1=83=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 80 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/bot.py b/bot.py index 6f8ecce..899273c 100644 --- a/bot.py +++ b/bot.py @@ -8,6 +8,7 @@ from aiogram.filters import Command, CommandObject from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode +# Конфиг TOKEN = os.getenv("TELEGRAM_TOKEN") API_URL = os.getenv("VNDB_API_URL", "https://api.vndb.org/kana") @@ -17,33 +18,35 @@ logger = logging.getLogger(__name__) bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) dp = Dispatcher() -# --- UTILS --- +# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ --- -def clean_text(text: str, limit: int = 900) -> str: +def clean_text(text: str, limit: int = 800) -> str: + """Очистка текста от VNDB тегов и лимит символов.""" if not text: return "No description available." - # Удаляем все теги [b], [i], [url] и т.д. - text = re.sub(r'\[.*?\]', '', text) + text = re.sub(r'\[.*?\]', '', text) # Удаляем [b], [url] и т.д. text = text.replace('"', "'") if len(text) > limit: return text[:limit].rsplit(' ', 1)[0] + "..." return text def safe_list_to_str(data) -> str: + """Универсальный парсер списков для VNDB API.""" if not data: return "N/A" if isinstance(data, list): processed = [] for x in data: if isinstance(x, str): processed.append(x.upper()) elif isinstance(x, dict): - # Ищем любое человекочитаемое поле + # Извлекаем lang для релизов или name для разработчиков val = x.get('lang') or x.get('name') or x.get('title') or x.get('label') if val: processed.append(str(val).upper()) return ", ".join(processed) if processed else "N/A" return str(data).upper() async def fetch_vndb(endpoint: str, filters: list, fields: str, sort: str = "id", results: int = 10): + """Базовый POST запрос к VNDB API.""" payload = {"filters": filters, "fields": fields, "sort": sort, "results": results} - async with httpx.AsyncClient(timeout=15.0) as client: + async with httpx.AsyncClient(timeout=20.0) as client: try: response = await client.post(f"{API_URL}/{endpoint}", json=payload) if response.status_code == 200: @@ -51,47 +54,48 @@ async def fetch_vndb(endpoint: str, filters: list, fields: str, sort: str = "id" logger.error(f"VNDB Error {response.status_code}: {response.text}") return None except Exception as e: - logger.error(f"Error: {e}") + logger.error(f"Request failed: {e}") return None async def send_result(message: types.Message, text: str, image_url: str = None): + """Отправка сообщения: с фото (caption) или просто текст.""" try: if image_url: await message.answer_photo(photo=image_url, caption=text[:1024]) else: await message.answer(text) except Exception as e: + logger.warning(f"Photo send failed: {e}") await message.answer(text) def find_exact_match(results, query, attr): - """Ищет 100% совпадение по имени/названию в результатах.""" + """Проверка на 100% совпадение названия в результатах поиска.""" + if not results or not query: return None for item in results: if item.get(attr, "").lower() == query.lower(): return item return None -# --- HANDLERS --- +# --- ОБРАБОТЧИКИ КОМАНД --- @dp.message(Command("vn")) async def handle_vn(message: types.Message, command: CommandObject): - if not command.args: return await message.answer("Usage: /vn ") + if not command.args: return await message.answer("Example: /vn Steins Gate") is_id = command.args.startswith('v') and command.args[1:].isdigit() filt = ["id", "=", command.args] if is_id else ["search", "=", command.args] fields = "id, title, alttitle, released, rating, votecount, description, languages, platforms, developers{name}, image{url}, relations{title, id, relation}" res = await fetch_vndb("vn", filt, fields) - if not res: return await message.answer("❌ Not found.") + if not res: return await message.answer("❌ VN not found.") - # Логика умного поиска target = res[0] if not is_id and len(res) > 1: exact = find_exact_match(res, command.args, 'title') - if exact: - target = exact + if exact: target = exact else: out = [f"• {i['title']} — {i['id']}" for i in res[:10]] - return await message.answer("🔍 Select VN:\n" + "\n".join(out)) + return await message.answer("🔍 Select VN by ID:\n" + "\n".join(out)) img = target.get('image', {}).get('url') if target.get('image') else None rel_str = "\n".join([f"• {r['title']} ({r['id']}) - {r['relation']}" for r in target.get('relations', [])[:3]]) @@ -99,26 +103,27 @@ async def handle_vn(message: types.Message, command: CommandObject): text = ( f"TITLE: {target['title']}\n" f"ORIGINAL: {target.get('alttitle') or 'N/A'}\n" + f"RELEASED: {target.get('released', 'N/A')}\n" f"DEVELOPER: {safe_list_to_str(target.get('developers'))}\n" f"RATING: {target['rating']/10 if target.get('rating') else 'N/A'} ({target.get('votecount', 0)} votes)\n" f"LANGS: {safe_list_to_str(target.get('languages'))}\n" f"PLATFORMS: {safe_list_to_str(target.get('platforms'))}\n\n" f"RELATIONS:\n{rel_str or 'None'}\n\n" - f"DESC:\n{clean_text(target.get('description'), 450)}\n\n" + f"DESC:\n{clean_text(target.get('description'), 400)}\n\n" f"VNDB: https://vndb.org/{target['id']}" ) await send_result(message, text, img) @dp.message(Command("char")) async def handle_char(message: types.Message, command: CommandObject): - if not command.args: return await message.answer("Usage: /char ") + if not command.args: return await message.answer("Example: /char Kurisu") is_id = command.args.startswith('c') and command.args[1:].isdigit() filt = ["id", "=", command.args] if is_id else ["search", "=", command.args] fields = "id, name, original, description, gender, age, blood_type, image{url}" res = await fetch_vndb("character", filt, fields) - if not res: return await message.answer("❌ Not found.") + if not res: return await message.answer("❌ Character not found.") target = res[0] if not is_id and len(res) > 1: @@ -126,7 +131,7 @@ async def handle_char(message: types.Message, command: CommandObject): if exact: target = exact else: out = [f"• {i['name']} — {i['id']}" for i in res[:10]] - return await message.answer("👤 Select Character:\n" + "\n".join(out)) + return await message.answer("👤 Select Character by ID:\n" + "\n".join(out)) img = target.get('image', {}).get('url') if target.get('image') else None raw_gender = target.get('gender') @@ -144,14 +149,14 @@ async def handle_char(message: types.Message, command: CommandObject): @dp.message(Command("release")) async def handle_release(message: types.Message, command: CommandObject): - if not command.args: return await message.answer("Usage: /release ") + if not command.args: return await message.answer("Example: /release Steins Gate") is_id = command.args.startswith('r') and command.args[1:].isdigit() filt = ["id", "=", command.args] if is_id else ["search", "=", command.args] fields = "id, title, alttitle, released, languages{lang}, platforms, extlinks{url, label}, images{url}" res = await fetch_vndb("release", filt, fields) - if not res: return await message.answer("❌ Not found.") + if not res: return await message.answer("❌ Release not found.") target = res[0] if not is_id and len(res) > 1: @@ -159,7 +164,7 @@ async def handle_release(message: types.Message, command: CommandObject): if exact: target = exact else: out = [f"• {i['title']} — {i['id']}" for i in res[:10]] - return await message.answer("💿 Select Release:\n" + "\n".join(out)) + return await message.answer("💿 Select Release by ID:\n" + "\n".join(out)) img_list = target.get('images', []) img = img_list[0].get('url') if img_list else None @@ -171,30 +176,51 @@ async def handle_release(message: types.Message, command: CommandObject): f"DATE: {target.get('released', 'N/A')}\n" f"LANGS: {safe_list_to_str(target.get('languages'))}\n" f"PLATFORMS: {safe_list_to_str(target.get('platforms'))}\n\n" - f"LINKS / STORES:\n{links or 'N/A'}\n\n" + f"STORES:\n{links or 'N/A'}\n\n" f"VNDB: https://vndb.org/{target['id']}" ) await send_result(message, text, img) @dp.message(Command("random")) async def handle_random(message: types.Message): + """Выдает случайную популярную VN с высоким рейтингом.""" + # Получаем 100 популярных новелл с рейтингом > 8.0 res = await fetch_vndb("vn", ["rating", ">=", 80], "id, title, image{url}, rating, description", sort="votecount", results=100) if not res: return await message.answer("❌ API error") + v = random.choice(res) - text = f"🎲 RANDOM VN\n\n{v['title']}\nRating: {v['rating']/10} ⭐\n\n{clean_text(v.get('description'), 400)}\n\nhttps://vndb.org/{v['id']}" + text = ( + f"🎲 RANDOM VN PICK\n\n" + f"{v['title']}\n" + f"Rating: {v['rating']/10} ⭐\n\n" + f"{clean_text(v.get('description'), 400)}\n\n" + f"https://vndb.org/{v['id']}" + ) await send_result(message, text, v.get('image', {}).get('url')) @dp.message(Command("top")) async def handle_top(message: types.Message): - res = await fetch_vndb("vn", ["votecount", ">", 2000], "id, title, rating", sort="rating", results=10) - out = ["🏆 TOP 10 VISUAL NOVELS"] + """Выводит Топ-10 по рейтингу (среди популярных).""" + res = await fetch_vndb("vn", ["votecount", ">", 2500], "id, title, rating", sort="rating", results=10) + if not res: return await message.answer("❌ API error") + + out = ["🏆 TOP 10 VISUAL NOVELS\n"] for i, v in enumerate(res, 1): out.append(f"{i}. {v['title']} — {v['rating']/10}") + await message.answer("\n".join(out)) @dp.message(Command("start", "help", "search")) async def cmd_start(message: types.Message): - await message.answer("🤖 VNDB Professional\n/vn, /char, /release, /random, /top") + await message.answer( + "🤖 VNDB Professional Bot\n\n" + "Commands:\n" + "• /vn [name/id] - VN Details\n" + "• /char [name/id] - Characters\n" + "• /release [name/id] - Release Info\n" + "• /random - Random high-rated VN\n" + "• /top - Top 10 by Rating" + ) if __name__ == "__main__": dp.run_polling(bot) \ No newline at end of file