From 8bc55325040a49f0c59d60e8bbe8b6cc743ed9fd Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Mon, 13 Jan 2025 23:34:05 +0500 Subject: [PATCH] feat: added a new quickview handler --- .../handlers/bot_handlers/inline/update.py | 2 +- pytmbot/handlers/handler_manager.py | 7 + pytmbot/handlers/server_handlers/quickview.py | 182 ++++++++++++++++++ pytmbot/settings.py | 1 + .../base_templates/b_how_update.jinja2 | 73 +++---- .../base_templates/b_quick_view.jinja2 | 30 +++ pytmbot/utils/utilities.py | 0 7 files changed, 253 insertions(+), 42 deletions(-) create mode 100644 pytmbot/handlers/server_handlers/quickview.py create mode 100644 pytmbot/templates/base_templates/b_quick_view.jinja2 delete mode 100644 pytmbot/utils/utilities.py diff --git a/pytmbot/handlers/bot_handlers/inline/update.py b/pytmbot/handlers/bot_handlers/inline/update.py index 6584872e..86b5021c 100644 --- a/pytmbot/handlers/bot_handlers/inline/update.py +++ b/pytmbot/handlers/bot_handlers/inline/update.py @@ -39,7 +39,7 @@ def handle_update_info(call: CallbackQuery, bot: TeleBot): chat_id=call.message.chat.id, message_id=call.message.message_id, text=bot_answer, - parse_mode="Markdown", + parse_mode="HTML", ) except Exception as error: bot.edit_message_text( diff --git a/pytmbot/handlers/handler_manager.py b/pytmbot/handlers/handler_manager.py index de6c5e94..d59e29c9 100644 --- a/pytmbot/handlers/handler_manager.py +++ b/pytmbot/handlers/handler_manager.py @@ -42,6 +42,7 @@ from pytmbot.handlers.server_handlers.memory import handle_memory from pytmbot.handlers.server_handlers.network import handle_network from pytmbot.handlers.server_handlers.process import handle_process +from pytmbot.handlers.server_handlers.quickview import handle_quick_view from pytmbot.handlers.server_handlers.sensors import handle_sensors from pytmbot.handlers.server_handlers.server import handle_server from pytmbot.handlers.server_handlers.uptime import handle_uptime @@ -92,6 +93,12 @@ def handler_factory() -> HandlerType: regexp="Enter 2FA code" ) ], + "quick_view": [ + HandlerConfig( + callback=handle_quick_view, + regexp="Quick view" + ) + ], "code_verification": [ HandlerConfig( callback=handle_totp_code_verification, diff --git a/pytmbot/handlers/server_handlers/quickview.py b/pytmbot/handlers/server_handlers/quickview.py new file mode 100644 index 00000000..38af3254 --- /dev/null +++ b/pytmbot/handlers/server_handlers/quickview.py @@ -0,0 +1,182 @@ +#!/venv/bin/python3 +""" +(c) Copyright 2024, Denis Rozhnovskiy +pyTMBot - A simple Telegram bot to handle Docker containers and images, +also providing basic information about the status of local servers. +""" +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import Dict, Any, Optional + +from telebot import TeleBot +from telebot.types import Message + +from pytmbot import exceptions +from pytmbot.adapters.docker.containers_info import fetch_docker_counters +from pytmbot.exceptions import ErrorContext +from pytmbot.globals import psutil_adapter, em +from pytmbot.logs import Logger +from pytmbot.parsers.compiler import Compiler + +logger = Logger() + + +def _get_uptime() -> Optional[str]: + """Get system uptime.""" + try: + return psutil_adapter.get_uptime() + except Exception as e: + logger.error(f"Failed to get uptime: {e}") + return None + + +def _get_load() -> Optional[tuple]: + """Get load average.""" + try: + return psutil_adapter.get_load_average() + except Exception as e: + logger.error(f"Failed to get load average: {e}") + return None + + +def _get_memory() -> Optional[Dict]: + """Get memory statistics.""" + try: + return psutil_adapter.get_memory() + except Exception as e: + logger.error(f"Failed to get memory stats: {e}") + return None + + +def _get_processes() -> Optional[Dict]: + """Get process counts.""" + try: + return psutil_adapter.get_process_counts() + except Exception as e: + logger.error(f"Failed to get process counts: {e}") + return None + + +def _get_cpu() -> Optional[Dict]: + """Get CPU usage.""" + try: + return psutil_adapter.get_cpu_usage() + except Exception as e: + logger.error(f"Failed to get CPU usage: {e}") + return None + + +def _get_docker() -> Optional[Dict]: + """Get Docker statistics.""" + try: + return fetch_docker_counters() + except Exception as e: + logger.error(f"Failed to get Docker stats: {e}") + return None + + +def _collect_metrics() -> Dict[str, Any]: + """ + Collect all metrics concurrently using ThreadPoolExecutor. + + Returns: + Dict containing all collected metrics. + """ + metrics = {} + + # Define tasks to run concurrently + tasks = { + 'uptime': _get_uptime, + 'load_average': _get_load, + 'memory': _get_memory, + 'processes': _get_processes, + 'cpu': _get_cpu, + 'docker': _get_docker + } + + with ThreadPoolExecutor(max_workers=len(tasks)) as executor: + # Start all tasks + future_to_task = { + executor.submit(func): name + for name, func in tasks.items() + } + + # Collect results as they complete + for future in as_completed(future_to_task): + task_name = future_to_task[future] + try: + result = future.result() + if result is not None: + metrics[task_name] = result + else: + logger.warning(f"Task {task_name} returned None") + except Exception as e: + logger.error(f"Task {task_name} generated an exception: {e}") + + return metrics + + +# regexp="Quick view|Quick status|qv" +@logger.session_decorator +def handle_quick_view(message: Message, bot: TeleBot): + """Handle quick view command to show system and Docker summary.""" + emojis = { + "computer": em.get_emoji("desktop_computer"), + "chart": em.get_emoji("bar_chart"), + "memory": em.get_emoji("brain"), + "cpu": em.get_emoji("electric_plug"), + "process": em.get_emoji("gear"), + "docker": em.get_emoji("whale"), + "warning": em.get_emoji("warning"), + } + + try: + bot.send_chat_action(message.chat.id, "typing") + + # Collect all metrics concurrently + metrics = _collect_metrics() + + if not metrics: + logger.error("Failed to collect any metrics for quick view") + return bot.send_message( + message.chat.id, + text="⚠️ Failed to get system metrics. Please try again later." + ) + + # Prepare context for template + context = { + "system": { + "uptime": metrics.get('uptime', 'N/A'), + "load_average": metrics.get('load_average', (0, 0, 0)), + "memory": metrics.get('memory', {}), + "processes": metrics.get('processes', {}), + "cpu": metrics.get('cpu', {}) + } + } + + # Add Docker metrics if available + if 'docker' in metrics: + context['docker'] = metrics['docker'] + + with Compiler( + template_name="b_quick_view.jinja2", + context=context, + **emojis + ) as compiler: + bot_answer = compiler.compile() + + bot.send_message( + message.chat.id, + text=bot_answer, + parse_mode="Markdown" + ) + + except Exception as error: + bot.send_message( + message.chat.id, + "⚠️ An error occurred while processing the command." + ) + raise exceptions.HandlingException(ErrorContext( + message="Failed handling quick view command", + error_code="HAND_QV1", + metadata={"exception": str(error)} + )) diff --git a/pytmbot/settings.py b/pytmbot/settings.py index a217a937..6342949c 100644 --- a/pytmbot/settings.py +++ b/pytmbot/settings.py @@ -64,6 +64,7 @@ def get_default_main_keyboard() -> Dict[str, str]: "rocket": "Server", "spouting_whale": "Docker", "lollipop": "Plugins", + "eyes": "Quick view", "mushroom": "About me" } diff --git a/pytmbot/templates/base_templates/b_how_update.jinja2 b/pytmbot/templates/base_templates/b_how_update.jinja2 index 53bcee3f..623d41d7 100644 --- a/pytmbot/templates/base_templates/b_how_update.jinja2 +++ b/pytmbot/templates/base_templates/b_how_update.jinja2 @@ -1,41 +1,32 @@ -{{ thought_balloon }} Here is: - -🏗 **Updating the image** - -To update the image to the latest version, please follow these steps: - -1. **Stop the running pyTMbot container:** -```bash -sudo docker stop pytmbot -``` - -2. **Delete the outdated container:** -```bash -sudo docker rm pytmbot -``` - -3. **Delete the outdated image:** -```bash -sudo docker rmi pytmbot -``` - -4. **Pull the updated image:** -```bash -sudo docker pull orenlab/pytmbot:latest -``` - -5. **Run the container as you would if you had just installed the bot:** - -```bash -sudo docker run -d \ --v /var/run/docker.sock:/var/run/docker.sock:ro \ --v /root/pytmbot.yaml:/opt/pytmbot/pytmbot.yaml:ro \ ---env TZ="Asia/Yekaterinburg" \ ---restart=always \ ---name=pytmbot \ ---pid=host \ ---security-opt=no-new-privileges \ -orenlab/pytmbot:latest -``` - -*Note: Please don't forget to adjust the time zone!* \ No newline at end of file +{{ thought_balloon }} Updating PyTMBot to Latest Version + +Choose your preferred update method: + +A. Using Docker Compose (Recommended) + +1) Update your docker-compose.yml: + +docker compose pull && docker compose up -d + +B. Using Docker CLI + +1) docker stop pytmbot && docker rm pytmbot + +2) docker pull orenlab/pytmbot:latest + +3) Run new container: +
docker run -d \
+  --name pytmbot \
+  --restart on-failure \
+  --env TZ="Asia/Yekaterinburg" \
+  -v /var/run/docker.sock:/var/run/docker.sock:ro \
+  -v /root/pytmbot.yaml:/opt/app/pytmbot.yaml:ro \
+  --security-opt no-new-privileges \
+  --read-only \
+  --cap-drop ALL \
+  --pid=host \
+  --log-opt max-size=10m \
+  --log-opt max-file=3 \
+  orenlab/pytmbot:latest --plugins SOME_PLUGINS
+ +⚠️ Note: Adjust timezone as needed! diff --git a/pytmbot/templates/base_templates/b_quick_view.jinja2 b/pytmbot/templates/base_templates/b_quick_view.jinja2 new file mode 100644 index 00000000..740bcd73 --- /dev/null +++ b/pytmbot/templates/base_templates/b_quick_view.jinja2 @@ -0,0 +1,30 @@ +{# templates/b_quick_view.jinja2 #} +🖥️ *System Quick View* + +```bash +{% if context.system %} +🖥️ +├─ ⏱️ Uptime: {{ "{:>10}".format(context.system.uptime if context.system.uptime else 'N/A') }} +├─ 📊 Load Average: +│ ├─ 1 min: {{ "{:>10.2f}".format(context.system.load_average[0] if context.system.load_average and context.system.load_average[0] is defined else 'N/A') }} +│ ├─ 5 min: {{ "{:>10.2f}".format(context.system.load_average[1] if context.system.load_average and context.system.load_average[1] is defined else 'N/A') }} +│ └─ 15 min: {{ "{:>10.2f}".format(context.system.load_average[2] if context.system.load_average and context.system.load_average[2] is defined else 'N/A') }} +├─ 🧠 Memory: +│ ├─ Used: {{ "{:>10}".format(context.system.memory.used if context.system.memory and context.system.memory.used else 'N/A') }} +│ ├─ Free: {{ "{:>10}".format(context.system.memory.free if context.system.memory and context.system.memory.free else 'N/A') }} +│ └─ Usage: {{ "{:>10}".format((context.system.memory.percent | string + '%') if context.system.memory and context.system.memory.percent is not none else 'N/A') }} +├─ ⚡ CPU Usage: +│ └─ {{ "{:>10}".format((context.system.cpu.cpu_percent | string + '%') if context.system.cpu and context.system.cpu.cpu_percent is not none else 'N/A') }} +└─ ⚙️ Processes: + ├─ Running: {{ "{:>10}".format(context.system.processes.running if context.system.processes and context.system.processes.running else 'N/A') }} + └─ Total: {{ "{:>10}".format(context.system.processes.total if context.system.processes and context.system.processes.total else 'N/A') }} +{% else %} +⚠️ System data is not available. +{% endif %} +{% if context.docker %} +🐳 +└─ 🐳 Docker Status + ├─ Containers: {{ "{:>10}".format(context.docker.containers_count if context.docker.containers_count else 'N/A') }} + └─ Images: {{ "{:>10}".format(context.docker.images_count if context.docker.images_count else 'N/A') }} +{% endif %} +``` \ No newline at end of file diff --git a/pytmbot/utils/utilities.py b/pytmbot/utils/utilities.py deleted file mode 100644 index e69de29b..00000000