Add travel border Telegram bot

This commit is contained in:
serhei_t
2026-06-17 10:53:15 +03:00
parent 1c3e3f0a28
commit ae6ce2d886
12 changed files with 572 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
from __future__ import annotations
import asyncio
import logging
from aiogram import Bot, Dispatcher, F, Router
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.filters import Command, CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup
from aiogram.utils.chat_action import ChatActionSender
from app.ai_client import AiAssistant
from app.border import fetch_border_snapshot, format_best_options, format_snapshot, snapshot_context
from app.config import get_settings
class DialogState(StatesGroup):
waiting_for_ai_question = State()
router = Router()
def main_keyboard() -> ReplyKeyboardMarkup:
return ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="Очереди Польша"), KeyboardButton(text="Лучший пункт")],
[KeyboardButton(text="Спросить AI"), KeyboardButton(text="Источник")],
],
resize_keyboard=True,
input_field_placeholder="Выбери действие или задай вопрос",
)
@router.message(CommandStart())
async def start(message: Message, state: FSMContext) -> None:
await state.clear()
await message.answer(
"Я помощник по поездке и пересечению границы Беларусь-Польша. "
"Показываю очереди с сайта ГПК и могу подсказать маршрутные решения через AI.",
reply_markup=main_keyboard(),
)
@router.message(Command("help"))
async def help_command(message: Message, state: FSMContext) -> None:
await state.clear()
await message.answer(
"Команды:\n"
"/poland - текущие очереди по Польше\n"
"/best - самые свободные пункты\n"
"/ai вопрос - спросить AI с учетом текущих очередей\n\n"
"Можно просто нажимать кнопки меню.",
reply_markup=main_keyboard(),
)
@router.message(Command("poland"))
@router.message(F.text.casefold() == "очереди польша")
async def poland_queues(message: Message, state: FSMContext) -> None:
await state.clear()
settings = get_settings()
try:
async with ChatActionSender.typing(bot=message.bot, chat_id=message.chat.id):
snapshot = await fetch_border_snapshot(settings.border_status_url, settings.request_timeout_seconds)
except Exception:
logging.exception("Failed to fetch border queues")
await message.answer("Не удалось получить данные ГПК. Попробуй еще раз через несколько минут.", reply_markup=main_keyboard())
return
await message.answer(format_snapshot(snapshot), reply_markup=main_keyboard())
@router.message(Command("best"))
@router.message(F.text.casefold() == "лучший пункт")
async def best_checkpoint(message: Message, state: FSMContext) -> None:
await state.clear()
settings = get_settings()
try:
async with ChatActionSender.typing(bot=message.bot, chat_id=message.chat.id):
snapshot = await fetch_border_snapshot(settings.border_status_url, settings.request_timeout_seconds)
except Exception:
logging.exception("Failed to fetch border queues")
await message.answer("Не удалось получить данные ГПК. Попробуй еще раз через несколько минут.", reply_markup=main_keyboard())
return
await message.answer(format_best_options(snapshot), reply_markup=main_keyboard())
@router.message(F.text.casefold() == "источник")
async def source(message: Message, state: FSMContext) -> None:
await state.clear()
settings = get_settings()
await message.answer(
f"Официальный источник данных: {settings.border_status_url}\n"
"Информация обновляется на стороне ГПК, обычно каждый четный час.",
reply_markup=main_keyboard(),
)
@router.message(F.text.casefold() == "спросить ai")
async def ask_ai_prompt(message: Message, state: FSMContext) -> None:
await state.set_state(DialogState.waiting_for_ai_question)
await message.answer("Напиши вопрос. Например: 'Через какой пункт лучше ехать сегодня вечером?'", reply_markup=main_keyboard())
@router.message(Command("ai"))
async def ask_ai_command(message: Message, state: FSMContext) -> None:
question = (message.text or "").partition(" ")[2].strip()
if not question:
await ask_ai_prompt(message, state)
return
await answer_ai(message, question, state)
@router.message(DialogState.waiting_for_ai_question)
async def ask_ai_state(message: Message, state: FSMContext) -> None:
question = (message.text or "").strip()
if not question:
await message.answer("Напиши вопрос текстом.")
return
await answer_ai(message, question, state)
@router.message(F.text)
async def fallback_question(message: Message, state: FSMContext) -> None:
question = (message.text or "").strip()
if len(question) < 4:
await message.answer("Выбери действие в меню или задай вопрос по поездке.", reply_markup=main_keyboard())
return
await answer_ai(message, question, state)
async def answer_ai(message: Message, question: str, state: FSMContext) -> None:
await state.clear()
settings = get_settings()
assistant = AiAssistant(
settings.ai_base_url,
settings.ai_api_key,
settings.ai_model,
settings.request_timeout_seconds,
)
try:
async with ChatActionSender.typing(bot=message.bot, chat_id=message.chat.id):
snapshot = await fetch_border_snapshot(settings.border_status_url, settings.request_timeout_seconds)
answer = await assistant.ask(question, snapshot_context(snapshot))
except Exception:
logging.exception("Failed to answer AI question")
await message.answer(
"Не удалось получить ответ сейчас: проблема с источником данных или AI. Попробуй позже.",
reply_markup=main_keyboard(),
)
return
await message.answer(answer, reply_markup=main_keyboard())
async def run() -> None:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
settings = get_settings()
bot = Bot(settings.telegram_bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dispatcher = Dispatcher()
dispatcher.include_router(router)
await dispatcher.start_polling(bot)
if __name__ == "__main__":
asyncio.run(run())