diff --git a/config.example.yaml b/config.example.yaml index 373bb66..97e552c 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -27,6 +27,9 @@ arcane: api_key: "arc_..." env_id: 0 +security: + reboot_password: "CHANGE_ME" + docker: # If true, discover containers by name/label autodiscovery: true diff --git a/handlers/system.py b/handlers/system.py index 0ab6cc5..51f42cf 100644 --- a/handlers/system.py +++ b/handlers/system.py @@ -1,16 +1,15 @@ +import asyncio from aiogram import F -from aiogram.types import Message, CallbackQuery -from app import dp +from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton +from app import dp, cfg from auth import is_admin_msg from keyboards import system_kb from system_checks import security, disks -from app import cfg from services.http_checks import get_url_checks, check_url -import asyncio from services.queue import enqueue from services.updates import list_updates, apply_updates -from state import UPDATES_CACHE -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from services.runner import run_cmd +from state import UPDATES_CACHE, REBOOT_PENDING @dp.message(F.text == "πŸ’½ Disks") @@ -42,7 +41,7 @@ async def urls(msg: Message): results = await asyncio.gather(*tasks) lines = ["🌐 URLs\n"] - for (alias, url), (ok, status, ms, err) in zip(checks, results): + for (alias, _url), (ok, status, ms, err) in zip(checks, results): if ok: lines.append(f"🟒 {alias}: {status} ({ms}ms)") elif status is not None: @@ -79,12 +78,27 @@ async def updates_apply(msg: Message): if not is_admin_msg(msg): return - async def job(): - text = await apply_updates() - await msg.answer(text, reply_markup=system_kb, parse_mode="Markdown") + kb = InlineKeyboardMarkup( + inline_keyboard=[[ + InlineKeyboardButton(text="βœ… Confirm", callback_data="upgrade:confirm"), + InlineKeyboardButton(text="βœ– Cancel", callback_data="upgrade:cancel"), + ]] + ) + await msg.answer("⚠️ Confirm package upgrade?", reply_markup=kb) - pos = await enqueue("pkg-upgrade", job) - await msg.answer(f"πŸ•“ Upgrade queued (#{pos})", reply_markup=system_kb) + +@dp.message(F.text == "πŸ”„ Reboot") +async def reboot_request(msg: Message): + if not is_admin_msg(msg): + return + + kb = InlineKeyboardMarkup( + inline_keyboard=[[ + InlineKeyboardButton(text="βœ… Confirm", callback_data="reboot:confirm"), + InlineKeyboardButton(text="βœ– Cancel", callback_data="reboot:cancel"), + ]] + ) + await msg.answer("⚠️ Confirm reboot?", reply_markup=kb) def _updates_kb(page: int, total_pages: int) -> InlineKeyboardMarkup: @@ -133,3 +147,59 @@ async def updates_page(cb: CallbackQuery): return await cb.answer() await send_updates_page(cb.message, cb.from_user.id, page, edit=True) + + +@dp.callback_query(F.data == "upgrade:confirm") +async def upgrade_confirm(cb: CallbackQuery): + if cb.from_user.id != cfg["telegram"]["admin_id"]: + return + await cb.answer() + + async def job(): + text = await apply_updates() + await cb.message.answer(text, reply_markup=system_kb, parse_mode="Markdown") + + pos = await enqueue("pkg-upgrade", job) + await cb.message.answer(f"πŸ•“ Upgrade queued (#{pos})", reply_markup=system_kb) + + +@dp.callback_query(F.data == "upgrade:cancel") +async def upgrade_cancel(cb: CallbackQuery): + await cb.answer("Cancelled") + + +@dp.callback_query(F.data == "reboot:confirm") +async def reboot_confirm(cb: CallbackQuery): + if cb.from_user.id != cfg["telegram"]["admin_id"]: + return + await cb.answer() + REBOOT_PENDING[cb.from_user.id] = {} + await cb.message.answer("πŸ” Send reboot password", reply_markup=system_kb) + + +@dp.callback_query(F.data == "reboot:cancel") +async def reboot_cancel(cb: CallbackQuery): + await cb.answer("Cancelled") + + +@dp.message(F.text, F.func(lambda msg: msg.from_user and msg.from_user.id in REBOOT_PENDING)) +async def reboot_password(msg: Message): + if not is_admin_msg(msg): + return + REBOOT_PENDING.pop(msg.from_user.id, None) + + expected = cfg.get("security", {}).get("reboot_password") + if not expected: + await msg.answer("⚠️ Reboot password not configured", reply_markup=system_kb) + return + + if (msg.text or "").strip() != expected: + await msg.answer("❌ Wrong password", reply_markup=system_kb) + return + + async def job(): + await msg.answer("πŸ”„ Rebooting…", reply_markup=system_kb) + await run_cmd(["sudo", "reboot"], timeout=10) + + pos = await enqueue("reboot", job) + await msg.answer(f"πŸ•“ Reboot queued (#{pos})", reply_markup=system_kb) diff --git a/state.py b/state.py index 4685e12..bd5f3ea 100644 --- a/state.py +++ b/state.py @@ -4,3 +4,4 @@ DOCKER_MAP: Dict[str, str] = {} LOG_FILTER_PENDING: Dict[int, dict] = {} UPDATES_CACHE: Dict[int, dict] = {} ARCANE_CACHE: Dict[int, dict] = {} +REBOOT_PENDING: Dict[int, dict] = {}