Добавлены основные файлы проекта: конфигурация, обработчики команд, мидлвары и утилиты для бота на Python.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
TOKEN=
|
||||
USER_ID=
|
||||
@@ -174,3 +174,4 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
uv.lock
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -0,0 +1,46 @@
|
||||
[project]
|
||||
name = "server-bot-python"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"aiogram>=3.27.0",
|
||||
"psutil>=7.2.2",
|
||||
"pydantic-settings>=2.14.0",
|
||||
"python-dotenv>=1.2.2",
|
||||
"types-psutil>=7.2.2.20260408",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"deptry>=0.25.1",
|
||||
"mypy>=1.20.1",
|
||||
"ruff>=0.15.11",
|
||||
]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 160
|
||||
target-version = "py313"
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
indent-style = "space"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B", "Q", "ERA", "D", "SIM", "ARG", "RUF", "C4", "RET", "ASYNC", "PERF", "PL"]
|
||||
ignore = ["D100", "RUF002", "D107", "RUF001", "D104", "D203", "D213", "ARG001"]
|
||||
|
||||
[tool.ruff.lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
multiline-quotes = "double"
|
||||
docstring-quotes = "double"
|
||||
avoid-escape = true
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.13"
|
||||
strict = true
|
||||
warn_unused_ignores = true
|
||||
warn_redundant_casts = true
|
||||
warn_unreachable = true
|
||||
@@ -0,0 +1,3 @@
|
||||
python-dotenv
|
||||
psutil
|
||||
aiogram
|
||||
@@ -0,0 +1,21 @@
|
||||
from pydantic import Field, ValidationError
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""App settings loaded from environment."""
|
||||
|
||||
TOKEN: str = Field(default='default_value', description='Токен Telegram-бота')
|
||||
USER_ID: int = Field(default=0, description='ID пользователя для доступа')
|
||||
|
||||
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
|
||||
|
||||
|
||||
try:
|
||||
settings = Settings()
|
||||
except ValidationError as exc:
|
||||
msg = f'Ошибка валидации настроек: {exc}'
|
||||
raise RuntimeError(msg) from exc
|
||||
|
||||
TOKEN: str = settings.TOKEN
|
||||
USER_ID: str = str(settings.USER_ID)
|
||||
@@ -0,0 +1,96 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import psutil
|
||||
from aiogram import Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.config import USER_ID
|
||||
from src.utils.system import execute_command, get_cpu_temperature
|
||||
|
||||
router = Router()
|
||||
|
||||
current_directory = os.getcwd()
|
||||
|
||||
|
||||
@router.message(Command('start'))
|
||||
async def cmd_start(message: Message) -> None:
|
||||
"""Команда /start - приветствие и краткая инструкция."""
|
||||
await message.answer('Привет! Я бот для мониторинга сервера. Используй /help для списка команд.')
|
||||
|
||||
|
||||
@router.message(Command('help'))
|
||||
async def cmd_help(message: Message) -> None:
|
||||
"""Команда /help - показать список доступных команд и их описание."""
|
||||
help_text = (
|
||||
'📋 <b>Список команд:</b>\n'
|
||||
'/start - Начать работу с ботом\n'
|
||||
'/help - Показать это сообщение\n'
|
||||
'/status - Показать текущее состояние сервера\n'
|
||||
'/c <команда> - Выполнить команду на сервере\n'
|
||||
'/sysinfo - Показать информацию о системе'
|
||||
)
|
||||
await message.answer(help_text, parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@router.message(Command('status'))
|
||||
async def cmd_status(message: Message) -> None:
|
||||
"""Выводит текущую загрузку CPU, RAM, диска и температуру CPU."""
|
||||
cpu_usage = psutil.cpu_percent(interval=1)
|
||||
ram_usage = psutil.virtual_memory().percent
|
||||
disk_usage = psutil.disk_usage('/').percent
|
||||
cpu_temp = get_cpu_temperature()
|
||||
|
||||
status_message = f'📊 <b>Состояние сервера:</b>\n• <b>CPU:</b> {cpu_usage}%\n• <b>RAM:</b> {ram_usage}%\n• <b>Диск:</b> {disk_usage}%\n'
|
||||
if cpu_temp is not None:
|
||||
status_message += f'• <b>Температура CPU:</b> {cpu_temp}°C\n'
|
||||
else:
|
||||
status_message += '• <b>Температура CPU:</b> N/A\n'
|
||||
await message.answer(status_message, parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@router.message(Command('c'))
|
||||
async def cmd_execute(message: Message) -> None:
|
||||
"""Команда /c <команда> - выполняет указанную команду на сервере и возвращает результат."""
|
||||
global current_directory
|
||||
user_id = message.from_user.id if message.from_user else 'Unknown'
|
||||
if str(user_id) != str(USER_ID):
|
||||
await message.answer('❌ У вас нет прав для использования этого бота.')
|
||||
return
|
||||
|
||||
if not message.text:
|
||||
await message.answer('❌ Укажите команду для выполнения.')
|
||||
return
|
||||
command = message.text.split(maxsplit=1)[1] if len(message.text.split()) > 1 else None
|
||||
if not command:
|
||||
await message.answer('❌ Укажите команду для выполнения.')
|
||||
return
|
||||
|
||||
logging.info(f'Пользователь {user_id} выполнил команду: {command}')
|
||||
result, current_directory = execute_command(command, current_directory)
|
||||
await message.answer(f'✅ Результат выполнения команды:\n<pre>{result}</pre>', parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@router.message(Command('sysinfo'))
|
||||
async def cmd_sysinfo(message: Message) -> None:
|
||||
"""Sysinfo - Показать информацию о системе, включая имя хоста, ОС и количество ядер CPU."""
|
||||
"""Команда /sysinfo - выводит информацию о системе, включая имя хоста, ОС и количество ядер CPU."""
|
||||
user_id = message.from_user.id if message.from_user else 'Unknown'
|
||||
if str(user_id) != str(USER_ID):
|
||||
await message.answer('❌ У вас нет прав для использования этого бота.')
|
||||
return
|
||||
logging.info(f'Пользователь {user_id} выполнил команду /sysinfo')
|
||||
try:
|
||||
hostname = os.uname().nodename if hasattr(os, 'uname') else os.getenv('COMPUTERNAME', 'N/A')
|
||||
os_info = os.uname().sysname + ' ' + os.uname().release if hasattr(os, 'uname') else os.name
|
||||
cpu_count = os.cpu_count()
|
||||
|
||||
sysinfo_message = (
|
||||
f'📊 <b>Информация о системе:</b>\n• <b>Имя хоста:</b> {hostname}\n• <b>ОС:</b> {os_info}\n• <b>Количество ядер CPU:</b> {cpu_count}\n'
|
||||
)
|
||||
|
||||
await message.answer(sysinfo_message, parse_mode=ParseMode.HTML)
|
||||
except Exception as e:
|
||||
await message.answer(f'❌ Ошибка:\n<pre>{e!s}</pre>', parse_mode=ParseMode.HTML)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import logging
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.enums import ParseMode
|
||||
|
||||
from src.config import settings
|
||||
from src.handlers import commands
|
||||
from src.middlewares.access import AccessMiddleware
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[logging.FileHandler('bot.log'), logging.StreamHandler()],
|
||||
)
|
||||
|
||||
|
||||
def setup_routers(dp: Dispatcher) -> None:
|
||||
"""Регистрирует роутеры и мидлвары в диспетчере."""
|
||||
dp.message.middleware(AccessMiddleware())
|
||||
dp.include_router(commands.router)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Создает экземпляр бота, регистрирует роутеры и запускает поллинг."""
|
||||
bot = Bot(token=settings.TOKEN, parse_mode=ParseMode.HTML)
|
||||
dp = Dispatcher()
|
||||
setup_routers(dp)
|
||||
logging.info('Бот запущен.')
|
||||
await dp.start_polling(bot)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import asyncio
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,28 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||
from aiogram.types import Message, TelegramObject
|
||||
|
||||
from src.config import USER_ID
|
||||
|
||||
|
||||
class AccessMiddleware(BaseMiddleware):
|
||||
"""Мидлвар для проверки доступа по USER_ID. Если ID пользователя не совпадает, отправляет сообщение об ошибке и не пропускает дальше."""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
"""Проверяет ID пользователя только для Message."""
|
||||
if isinstance(event, Message):
|
||||
if event.from_user is None:
|
||||
return None
|
||||
|
||||
if str(event.from_user.id) != USER_ID:
|
||||
await event.answer('❌ У вас нет прав для использования этого бота.')
|
||||
return None
|
||||
|
||||
return await handler(event, data)
|
||||
@@ -0,0 +1,20 @@
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
|
||||
class LoggingMiddleware(BaseMiddleware):
|
||||
"""Мидлвар для логирования всех входящих обновлений. Логирует тип и содержимое обновления. Логи сохраняются в файл bot.log и выводятся в консоль."""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: dict[str, Any],
|
||||
) -> Any:
|
||||
"""Выполняет логирование входящего обновления и передает его дальше по цепочке обработки."""
|
||||
logging.info('Update: %s', event)
|
||||
return await handler(event, data)
|
||||
@@ -0,0 +1,38 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
def get_cpu_temperature() -> Any:
|
||||
"""Выполняет попытку получения температуры CPU. Сначала через psutil, затем через чтение из системного файла."""
|
||||
try:
|
||||
if hasattr(psutil, 'sensors_temperatures'):
|
||||
temps = psutil.sensors_temperatures()
|
||||
if 'coretemp' in temps:
|
||||
return temps['coretemp'][0].current
|
||||
with open('/sys/class/thermal/thermal_zone0/temp') as f:
|
||||
return int(f.read()) / 1000
|
||||
except Exception as e:
|
||||
logging.error(f'Не удалось получить температуру CPU: {e}')
|
||||
return None
|
||||
|
||||
|
||||
def execute_command(command: str, current_directory: str) -> tuple[str, str]:
|
||||
"""Выполняет команду в текущей директории. Если команда начинается с "cd", меняет директорию. Возвращает результат выполнения и текущую директорию."""
|
||||
try:
|
||||
if command.startswith('cd '):
|
||||
new_dir = command.split(' ', 1)[1].strip()
|
||||
try:
|
||||
os.chdir(new_dir)
|
||||
return f'Текущая директория изменена на: {os.getcwd()}', os.getcwd()
|
||||
except Exception as e:
|
||||
return f'Ошибка при смене директории: {e}', current_directory
|
||||
else:
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, cwd=current_directory, check=True)
|
||||
return result.stdout or result.stderr, current_directory
|
||||
except Exception as e:
|
||||
logging.error(f'Ошибка при выполнении команды: {e}')
|
||||
return f'Ошибка: {e}', current_directory
|
||||
Reference in New Issue
Block a user