Compare commits

...

14 Commits

Author SHA1 Message Date
37bbff67ce Улучшение обработки команд: изменение лимита символов в функции clean_text, обновление логики обработки данных и улучшение формата ответов 2026-05-01 18:56:56 +03:00
0e429f668b Улучшение обработки команд: удаление функции выбора случайной новеллы и функции вывода топ-10 новелл 2026-05-01 18:56:37 +03:00
d59b16043c Merge branch 'main' of https://git.kotac.ru/King-of-the-all-Cookies/ayako 2026-05-01 18:55:23 +03:00
82afe8c653 Улучшение обработки команд: изменение лимита символов в функции clean_text, рефакторинг функции safe_list_to_str, добавление логики поиска точного совпадения и обновление формата ответов 2026-05-01 18:54:40 +03:00
621e72423b Улучшение обработки команд: добавление функции для случайного выбора новеллы, рефакторинг функций для обработки данных и обновление формата ответов 2026-05-01 18:47:26 +03:00
1cc82a133a Улучшение обработки команд: рефакторинг функций для отправки результатов, обновление формата ответов и улучшение обработки данных 2026-05-01 18:40:53 +03:00
597be864af Улучшение обработки команд: рефакторинг функции safe_list_to_str, улучшение обработки данных и упрощение логики вывода 2026-05-01 18:37:27 +03:00
c3b62e61d3 Улучшение обработки команд: рефакторинг сообщений об ошибках, обновление формата ответов и улучшение структуры текста 2026-05-01 18:34:13 +03:00
c3afb260f0 Улучшение обработки команд: добавление новых полей в запросы, улучшение сообщений об ошибках и рефакторинг функций для обработки данных 2026-05-01 18:30:37 +03:00
602952ff8d Улучшение обработки команд: рефакторинг функций для поиска, улучшение сообщений об ошибках и упрощение логики обработки аргументов 2026-05-01 18:24:28 +03:00
f11fc602e7 Обновление обработчиков команд: замена метода извлечения аргументов на CommandObject и улучшение сообщений об ошибках 2026-05-01 18:22:24 +03:00
27d262ddb5 Рефакторинг бота VNDB: обновление инициализации aiogram, улучшение обработки ошибок, добавление новых команд и изменение зависимостей в requirements.txt 2026-05-01 18:20:28 +03:00
c1d26fb369 Обновление зависимостей и улучшение структуры кода: исправление токена Telegram, добавление логирования, рефакторинг обработчиков команд и настройка Docker. 2026-05-01 18:17:48 +03:00
5544af841d Исправление фильтрации в методе search_vn: удаление лишнего оператора "=" 2026-05-01 18:14:18 +03:00
6 changed files with 213 additions and 89 deletions

View File

@@ -1,2 +1,2 @@
TELEGRAM_TOKEN=your_token_here
TELEGRAM_TOKEN=your_bot_token_here
VNDB_API_URL=https://api.vndb.org/kana

View File

@@ -2,9 +2,12 @@ FROM python:3.12-slim
WORKDIR /app
# Установка зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Копирование кода
COPY bot.py .
# Запуск
CMD ["python", "bot.py"]

277
bot.py
View File

@@ -1,111 +1,226 @@
import os
import asyncio
import logging
import re
import httpx
import random
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from vndb import VNDBClient
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")
bot = Bot(token=TOKEN)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher()
vndb = VNDBClient(os.getenv("VNDB_API_URL"))
# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
def safe_text(x):
if not x:
return ""
return str(x)
def clean_text(text: str, limit: int = 800) -> str:
"""Очистка текста от VNDB тегов и лимит символов."""
if not text: return "No description available."
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()
@dp.message(Command("start"))
async def start(msg: types.Message):
await msg.answer(
"VNDB Bot ready.\n"
"Commands:\n"
"/search <vn>\n"
"/vn <id>\n"
"/char <id>\n"
"/release <id>"
)
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=20.0) as client:
try:
response = await client.post(f"{API_URL}/{endpoint}", json=payload)
if response.status_code == 200:
return response.json().get("results", [])
logger.error(f"VNDB Error {response.status_code}: {response.text}")
return None
except Exception as 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)
@dp.message(Command("help"))
async def help(msg: types.Message):
await start(msg)
@dp.message(Command("search"))
async def search(msg: types.Message):
query = msg.text.replace("/search", "").strip()
if not query:
return await msg.answer("Empty query")
result = await vndb.search_vn(query)
if not result:
return await msg.answer("Not found")
text = "\n\n".join(
f"{i['id']}{i.get('title') or i.get('name')}"
for i in result
)
await msg.answer(text[:4000])
def find_exact_match(results, query, attr):
"""Проверка на 100% совпадение названия в результатах поиска."""
if not results or not query: return None
for item in results:
if item.get(attr, "").lower() == query.lower():
return item
return None
# --- ОБРАБОТЧИКИ КОМАНД ---
@dp.message(Command("vn"))
async def vn(msg: types.Message):
vid = msg.text.replace("/vn", "").strip()
if not vid:
return await msg.answer("No ID")
async def handle_vn(message: types.Message, command: CommandObject):
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("❌ VN not found.")
data = await vndb.get_vn(vid)
if not data:
return await msg.answer("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
else:
out = [f"{i['title']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("🔍 <b>Select VN by ID:</b>\n" + "\n".join(out))
await msg.answer(
f"{safe_text(data.get('title'))}\n"
f"Original: {safe_text(data.get('original'))}\n"
f"Released: {safe_text(data.get('released'))}\n"
f"Rating: {safe_text(data.get('rating'))}\n"
f"Votes: {safe_text(data.get('votecount'))}"
img = target.get('image', {}).get('url') if target.get('image') else None
rel_str = "\n".join([f"{r['title']} (<code>{r['id']}</code>) - {r['relation']}" for r in target.get('relations', [])[:3]])
text = (
f"<b>TITLE:</b> {target['title']}\n"
f"<b>ORIGINAL:</b> {target.get('alttitle') or 'N/A'}\n"
f"<b>RELEASED:</b> {target.get('released', 'N/A')}\n"
f"<b>DEVELOPER:</b> {safe_list_to_str(target.get('developers'))}\n"
f"<b>RATING:</b> {target['rating']/10 if target.get('rating') else 'N/A'} ({target.get('votecount', 0)} votes)\n"
f"<b>LANGS:</b> {safe_list_to_str(target.get('languages'))}\n"
f"<b>PLATFORMS:</b> {safe_list_to_str(target.get('platforms'))}\n\n"
f"<b>RELATIONS:</b>\n{rel_str or 'None'}\n\n"
f"<b>DESC:</b>\n<i>{clean_text(target.get('description'), 400)}</i>\n\n"
f"<b>VNDB:</b> https://vndb.org/{target['id']}"
)
await send_result(message, text, img)
@dp.message(Command("char"))
async def char(msg: types.Message):
cid = msg.text.replace("/char", "").strip()
if not cid:
return await msg.answer("No ID")
async def handle_char(message: types.Message, command: CommandObject):
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("❌ Character not found.")
data = await vndb.get_char(cid)
if not data:
return await msg.answer("Not found")
target = res[0]
if not is_id and len(res) > 1:
exact = find_exact_match(res, command.args, 'name')
if exact: target = exact
else:
out = [f"{i['name']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("👤 <b>Select Character by ID:</b>\n" + "\n".join(out))
await msg.answer(
f"{safe_text(data.get('name'))}\n"
f"Original: {safe_text(data.get('original'))}"
img = target.get('image', {}).get('url') if target.get('image') else None
raw_gender = target.get('gender')
if isinstance(raw_gender, list): raw_gender = raw_gender[0] if raw_gender else None
text = (
f"<b>NAME:</b> {target['name']}\n"
f"<b>ORIGINAL:</b> {target.get('original') or 'N/A'}\n"
f"<b>GENDER:</b> {str(raw_gender).upper()}\n"
f"<b>AGE:</b> {target.get('age') or 'N/A'} | <b>BLOOD:</b> {target.get('blood_type') or 'N/A'}\n\n"
f"<b>DESCRIPTION:</b>\n<i>{clean_text(target.get('description'), 800)}</i>\n\n"
f"<b>VNDB:</b> https://vndb.org/{target['id']}"
)
await send_result(message, text, img)
@dp.message(Command("release"))
async def release(msg: types.Message):
rid = msg.text.replace("/release", "").strip()
if not rid:
return await msg.answer("No ID")
async def handle_release(message: types.Message, command: CommandObject):
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("❌ Release not found.")
data = await vndb.get_release(rid)
if not data:
return await msg.answer("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
else:
out = [f"{i['title']} — <code>{i['id']}</code>" for i in res[:10]]
return await message.answer("💿 <b>Select Release by ID:</b>\n" + "\n".join(out))
await msg.answer(
f"{safe_text(data.get('title'))}\n"
f"Released: {safe_text(data.get('released'))}"
img_list = target.get('images', [])
img = img_list[0].get('url') if img_list else None
links = "\n".join([f"• <a href='{l['url']}'>{l['label']}</a>" for l in target.get('extlinks', [])[:8]])
text = (
f"<b>RELEASE:</b> {target['title']}\n"
f"<b>ORIGINAL:</b> {target.get('alttitle') or 'N/A'}\n\n"
f"<b>DATE:</b> {target.get('released', 'N/A')}\n"
f"<b>LANGS:</b> {safe_list_to_str(target.get('languages'))}\n"
f"<b>PLATFORMS:</b> {safe_list_to_str(target.get('platforms'))}\n\n"
f"<b>STORES:</b>\n{links or 'N/A'}\n\n"
f"<b>VNDB:</b> 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"🎲 <b>RANDOM VN PICK</b>\n\n"
f"<b>{v['title']}</b>\n"
f"Rating: {v['rating']/10}\n\n"
f"<i>{clean_text(v.get('description'), 400)}</i>\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):
"""Выводит Топ-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 = ["🏆 <b>TOP 10 VISUAL NOVELS</b>\n"]
for i, v in enumerate(res, 1):
out.append(f"{i}. {v['title']} — <b>{v['rating']/10}</b>")
await message.answer("\n".join(out))
@dp.message(Command("start", "help", "search"))
async def cmd_start(message: types.Message):
await message.answer(
"🤖 <b>VNDB Professional Bot</b>\n\n"
"<b>Commands:</b>\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"
)
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
dp.run_polling(bot)

View File

@@ -1,8 +1,12 @@
version: "3.9"
services:
bot:
build: .
container_name: vndb_bot
restart: always
env_file:
- .env
- .env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

View File

@@ -1,2 +1,2 @@
aiogram==3.6.0
httpx==0.27.0
aiogram>=3.10.0
httpx>=0.27.0

View File

@@ -19,8 +19,10 @@ class VNDBClient:
return None
async def search_vn(self, query):
query = query.strip()
return await self._post("vn", {
"filters": ["search", "=", query],
"filters": ["search", query],
"fields": "id,title,original,released,rating,votecount",
"results": 5
})