Add travel border Telegram bot
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user