Add BOINC Telegram bot, CI, and deploy compose
All checks were successful
publish-images / build (push) Successful in 1m3s

This commit is contained in:
dan
2026-01-06 09:44:48 +03:00
commit 55cfd73c24
15 changed files with 617 additions and 0 deletions

106
bot/main.py Normal file
View File

@@ -0,0 +1,106 @@
from __future__ import annotations
import asyncio
import logging
from functools import partial
from typing import Literal
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ParseMode
from telegram.ext import (
ApplicationBuilder,
CallbackQueryHandler,
CommandHandler,
ContextTypes,
)
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:
snapshot = await client.fetch_snapshot()
if view == "dashboard":
return format_dashboard(snapshot)
if view == "active":
return format_tasks_section("Активные задачи", snapshot.active)
if view == "queued":
return format_tasks_section("Ожидают запуска", snapshot.queued)
return format_tasks_section("Готовы к отправке", snapshot.completed)
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 = 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
)
async 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,
)
application = (
ApplicationBuilder()
.token(settings.telegram_token)
.parse_mode(ParseMode.HTML)
.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...")
await application.run_polling(close_loop=False)
if __name__ == "__main__":
asyncio.run(main())