from __future__ import annotations import logging from typing import Literal from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.constants import ParseMode from telegram.ext import ( ApplicationBuilder, CallbackQueryHandler, CommandHandler, ContextTypes, Defaults, ) from .boinc import BoincClient from .config import Settings from .ui import format_dashboard, format_tasks_section logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) logger = logging.getLogger(__name__) View = Literal["dashboard", "active", "queued", "completed"] def build_keyboard() -> InlineKeyboardMarkup: return InlineKeyboardMarkup( [ [ InlineKeyboardButton("Обновить", callback_data="dashboard"), InlineKeyboardButton("Активные", callback_data="active"), ], [ InlineKeyboardButton("Очередь", callback_data="queued"), InlineKeyboardButton("Готовы к отправке", callback_data="completed"), ], ] ) async def render_view(view: View, client: BoincClient) -> str: MAX_ITEMS = 20 snapshot = await client.fetch_snapshot() if view == "dashboard": return format_dashboard(snapshot) if view == "active": extra = len(snapshot.active) - MAX_ITEMS text = format_tasks_section("Активные задачи", snapshot.active[:MAX_ITEMS]) if extra > 0: text += f"\n... и еще {extra} задач(и)" return text if view == "queued": extra = len(snapshot.queued) - MAX_ITEMS text = format_tasks_section("Ожидают запуска", snapshot.queued[:MAX_ITEMS]) if extra > 0: text += f"\n... и еще {extra} задач(и)" return text extra = len(snapshot.completed) - MAX_ITEMS text = format_tasks_section("Готовы к отправке", snapshot.completed[:MAX_ITEMS]) if extra > 0: text += f"\n... и еще {extra} задач(и)" return text def clamp_message(text: str, limit: int = 3900) -> str: if len(text) <= limit: return text return text[: limit - 80] + "\n...сообщение укорочено, отфильтруйте список" async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: client: BoincClient = context.bot_data["boinc_client"] text = await render_view("dashboard", client) await update.message.reply_text(text, reply_markup=build_keyboard(), parse_mode=ParseMode.HTML) async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query await query.answer() client: BoincClient = context.bot_data["boinc_client"] view: View = query.data if query.data in {"dashboard", "active", "queued", "completed"} else "dashboard" try: text = clamp_message(await render_view(view, client)) except Exception as exc: # broad catch to show meaningful message in chat logger.exception("Error rendering view %s", view) text = f"Не удалось получить данные: {exc}" await query.edit_message_text(text=text, reply_markup=build_keyboard(), parse_mode=ParseMode.HTML) def main() -> None: settings = Settings.from_env() client = BoincClient( host=settings.boinc_host, port=settings.boinc_port, password=settings.boinc_password, boinccmd_path=settings.boinccmd_path, sample_output=settings.sample_output, ) defaults = Defaults(parse_mode=ParseMode.HTML) application = ApplicationBuilder().token(settings.telegram_token).defaults(defaults).build() application.bot_data["boinc_client"] = client application.add_handler(CommandHandler("start", start_handler)) application.add_handler(CommandHandler("status", start_handler)) application.add_handler(CallbackQueryHandler(callback_handler)) logger.info("Starting Telegram bot...") application.run_polling() if __name__ == "__main__": main()