Pre deploy commit.

This commit is contained in:
2025-08-30 12:38:20 +09:00
parent 49b3cea942
commit 3d189c415f
35 changed files with 20843 additions and 43 deletions

0
.env.example Normal file
View File

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Главный модуль запуска телеграм-бота для управления Synology NAS
"""
import os
import signal
import sys
import asyncio
import logging
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
MessageHandler,
filters
)
from src.config.config import TELEGRAM_TOKEN
from src.handlers.help_handlers import (
start_command,
help_command
)
from src.handlers.command_handlers import (
status_command,
power_command,
power_callback
)
from src.handlers.extended_handlers import (
storage_command,
shares_command,
system_command,
load_command,
security_command,
check_api_command
)
from src.handlers.advanced_handlers import (
processes_command,
network_command,
temperature_command,
schedule_command,
browse_command,
search_command,
updates_command,
backup_command,
quickreboot_command,
reboot_command,
sleep_command,
wakeup_command,
quota_command,
schedule_callback,
browse_callback,
advanced_power_callback
)
from src.utils.admin_utils import (
add_admin,
remove_admin,
list_admins
)
from src.utils.logger import setup_logging
async def shutdown(application: Application) -> None:
"""Корректное завершение работы бота"""
logger = logging.getLogger(__name__)
logger.info("Stopping Synology Power Control Bot...")
# Останавливаем прием обновлений
await application.stop()
logger.info("Bot stopped successfully")
def signal_handler(sig, frame, application=None):
"""Обработчик сигналов для корректного завершения"""
logger = logging.getLogger(__name__)
logger.info(f"Received signal {sig}, shutting down gracefully")
if application:
# Создаем и запускаем задачу завершения в event loop
loop = asyncio.get_event_loop()
if loop.is_running():
loop.create_task(shutdown(application))
else:
loop.run_until_complete(shutdown(application))
sys.exit(0)
def main() -> None:
"""Основная функция запуска бота"""
# Настройка логирования
setup_logging()
logger = logging.getLogger(__name__)
# Проверка наличия токена
if not TELEGRAM_TOKEN:
logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
return
# Создание и настройка приложения бота
logger.info("Starting Synology Power Control Bot")
application = Application.builder().token(TELEGRAM_TOKEN).build()
# Регистрация обработчиков команд
application.add_handler(CommandHandler("start", start_command))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("status", status_command))
application.add_handler(CommandHandler("power", power_command))
# Регистрация расширенных обработчиков команд
application.add_handler(CommandHandler("storage", storage_command))
application.add_handler(CommandHandler("shares", shares_command))
application.add_handler(CommandHandler("system", system_command))
application.add_handler(CommandHandler("load", load_command))
application.add_handler(CommandHandler("security", security_command))
application.add_handler(CommandHandler("checkapi", check_api_command))
# Регистрация продвинутых обработчиков команд
application.add_handler(CommandHandler("processes", processes_command))
application.add_handler(CommandHandler("network", network_command))
application.add_handler(CommandHandler("temperature", temperature_command))
application.add_handler(CommandHandler("schedule", schedule_command))
application.add_handler(CommandHandler("browse", browse_command))
application.add_handler(CommandHandler("search", search_command))
application.add_handler(CommandHandler("updates", updates_command))
application.add_handler(CommandHandler("backup", backup_command))
application.add_handler(CommandHandler("quickreboot", quickreboot_command))
application.add_handler(CommandHandler("reboot", reboot_command))
application.add_handler(CommandHandler("sleep", sleep_command))
application.add_handler(CommandHandler("wakeup", wakeup_command))
application.add_handler(CommandHandler("quota", quota_command))
# Регистрация обработчиков callback-запросов
# Сначала обрабатываем более специфичные паттерны
application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
# Затем более общие паттерны
application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
# Настройка обработчиков сигналов для корректного завершения
signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
# Запуск бота
logger.info("Bot started. Press Ctrl+C to stop.")
application.run_polling(allowed_updates=["message", "callback_query"])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Главный модуль запуска телеграм-бота для управления Synology NAS
"""
import os
import signal
import sys
import asyncio
import logging
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
MessageHandler,
filters
)
from src.config.config import TELEGRAM_TOKEN
from src.handlers.help_handlers import (
start_command,
help_command
)
from src.handlers.command_handlers import (
status_command,
power_command,
power_callback
)
from src.handlers.extended_handlers import (
storage_command,
shares_command,
system_command,
load_command,
security_command,
check_api_command
)
from src.handlers.advanced_handlers import (
processes_command,
network_command,
temperature_command,
schedule_command,
browse_command,
search_command,
updates_command,
backup_command,
quickreboot_command,
reboot_command,
sleep_command,
wakeup_command,
quota_command,
schedule_callback,
browse_callback,
advanced_power_callback
)
from src.utils.admin_utils import (
add_admin,
remove_admin,
list_admins
)
from src.utils.logger import setup_logging
async def shutdown(application: Application) -> None:
"""Корректное завершение работы бота"""
logger = logging.getLogger(__name__)
logger.info("Stopping Synology Power Control Bot...")
# Останавливаем прием обновлений
await application.stop()
logger.info("Bot stopped successfully")
def signal_handler(sig, frame, application=None):
"""Обработчик сигналов для корректного завершения"""
logger = logging.getLogger(__name__)
logger.info(f"Received signal {sig}, shutting down gracefully")
if application:
# Создаем и запускаем задачу завершения в event loop
loop = asyncio.get_event_loop()
if loop.is_running():
loop.create_task(shutdown(application))
else:
loop.run_until_complete(shutdown(application))
sys.exit(0)
def main() -> None:
"""Основная функция запуска бота"""
# Настройка логирования
setup_logging()
logger = logging.getLogger(__name__)
# Проверка наличия токена
if not TELEGRAM_TOKEN:
logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
return
# Создание и настройка приложения бота
logger.info("Starting Synology Power Control Bot")
application = Application.builder().token(TELEGRAM_TOKEN).build()
# Регистрация обработчиков команд
application.add_handler(CommandHandler("start", start_command))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("status", status_command))
application.add_handler(CommandHandler("power", power_command))
# Регистрация расширенных обработчиков команд
application.add_handler(CommandHandler("storage", storage_command))
application.add_handler(CommandHandler("shares", shares_command))
application.add_handler(CommandHandler("system", system_command))
application.add_handler(CommandHandler("load", load_command))
application.add_handler(CommandHandler("security", security_command))
application.add_handler(CommandHandler("checkapi", check_api_command))
# Регистрация продвинутых обработчиков команд
application.add_handler(CommandHandler("processes", processes_command))
application.add_handler(CommandHandler("network", network_command))
application.add_handler(CommandHandler("temperature", temperature_command))
application.add_handler(CommandHandler("schedule", schedule_command))
application.add_handler(CommandHandler("browse", browse_command))
application.add_handler(CommandHandler("search", search_command))
application.add_handler(CommandHandler("updates", updates_command))
application.add_handler(CommandHandler("backup", backup_command))
application.add_handler(CommandHandler("quickreboot", quickreboot_command))
application.add_handler(CommandHandler("reboot", reboot_command))
application.add_handler(CommandHandler("sleep", sleep_command))
application.add_handler(CommandHandler("wakeup", wakeup_command))
application.add_handler(CommandHandler("quota", quota_command))
# Регистрация обработчиков для управления администраторами
application.add_handler(CommandHandler("addadmin", add_admin))
application.add_handler(CommandHandler("removeadmin", remove_admin))
application.add_handler(CommandHandler("admins", list_admins))
# Регистрация обработчиков callback-запросов
# Сначала обрабатываем более специфичные паттерны
application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
# Затем более общие паттерны
application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
# Настройка обработчиков сигналов для корректного завершения
signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
# Запуск бота
logger.info("Bot started. Press Ctrl+C to stop.")
application.run_polling(allowed_updates=["message", "callback_query"])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Главный модуль запуска телеграм-бота для управления Synology NAS
"""
import os
import signal
import sys
import asyncio
import logging
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
MessageHandler,
filters
)
from src.config.config import TELEGRAM_TOKEN
from src.handlers.help_handlers import (
start_command,
help_command
)
from src.handlers.command_handlers import (
status_command,
power_command,
power_callback
)
from src.handlers.extended_handlers import (
storage_command,
shares_command,
system_command,
load_command,
security_command,
check_api_command
)
from src.handlers.advanced_handlers import (
processes_command,
network_command,
temperature_command,
schedule_command,
browse_command,
search_command,
updates_command,
backup_command,
quickreboot_command,
reboot_command,
sleep_command,
wakeup_command,
quota_command,
schedule_callback,
browse_callback,
advanced_power_callback
)
from src.utils.admin_utils import (
add_admin,
remove_admin,
list_admins
)
from src.utils.logger import setup_logging
async def shutdown(application: Application) -> None:
"""Корректное завершение работы бота"""
logger = logging.getLogger(__name__)
logger.info("Stopping Synology Power Control Bot...")
# Останавливаем прием обновлений
await application.stop()
logger.info("Bot stopped successfully")
def signal_handler(sig, frame, application=None):
"""Обработчик сигналов для корректного завершения"""
logger = logging.getLogger(__name__)
logger.info(f"Received signal {sig}, shutting down gracefully")
if application:
# Создаем и запускаем задачу завершения в event loop
loop = asyncio.get_event_loop()
if loop.is_running():
loop.create_task(shutdown(application))
else:
loop.run_until_complete(shutdown(application))
sys.exit(0)
def main() -> None:
"""Основная функция запуска бота"""
# Настройка логирования
setup_logging()
logger = logging.getLogger(__name__)
# Проверка наличия токена
if not TELEGRAM_TOKEN:
logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
return
# Создание и настройка приложения бота
logger.info("Starting Synology Power Control Bot")
application = Application.builder().token(TELEGRAM_TOKEN).build()
# Регистрация обработчиков команд
application.add_handler(CommandHandler("start", start_command))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("status", status_command))
application.add_handler(CommandHandler("power", power_command))
# Регистрация расширенных обработчиков команд
application.add_handler(CommandHandler("storage", storage_command))
application.add_handler(CommandHandler("shares", shares_command))
application.add_handler(CommandHandler("system", system_command))
application.add_handler(CommandHandler("load", load_command))
application.add_handler(CommandHandler("security", security_command))
application.add_handler(CommandHandler("checkapi", check_api_command))
# Регистрация продвинутых обработчиков команд
application.add_handler(CommandHandler("processes", processes_command))
application.add_handler(CommandHandler("network", network_command))
application.add_handler(CommandHandler("temperature", temperature_command))
application.add_handler(CommandHandler("schedule", schedule_command))
application.add_handler(CommandHandler("browse", browse_command))
application.add_handler(CommandHandler("search", search_command))
application.add_handler(CommandHandler("updates", updates_command))
application.add_handler(CommandHandler("backup", backup_command))
application.add_handler(CommandHandler("quickreboot", quickreboot_command))
application.add_handler(CommandHandler("reboot", reboot_command))
application.add_handler(CommandHandler("sleep", sleep_command))
application.add_handler(CommandHandler("wakeup", wakeup_command))
application.add_handler(CommandHandler("quota", quota_command))
# Регистрация обработчиков для управления администраторами
application.add_handler(CommandHandler("addadmin", add_admin))
application.add_handler(CommandHandler("removeadmin", remove_admin))
application.add_handler(CommandHandler("admins", list_admins))
# Регистрация обработчиков callback-запросов
# Сначала обрабатываем более специфичные паттерны
application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
# Затем более общие паттерны
application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
# Настройка обработчиков сигналов для корректного завершения
signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
# Запуск бота
logger.info("Bot started. Press Ctrl+C to stop.")
application.run_polling(allowed_updates=["message", "callback_query"])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,972 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Расширенные обработчики команд для управления Synology NAS
"""
import logging
from datetime import datetime
from typing import List, Dict, Any
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /processes для получения списка активных процессов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
return
try:
processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
if not processes:
await message.edit_text("❌ <b>Ошибка получения информации о процессах</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о процессах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о процессах
reply_text = f"⚙️ <b>Активные процессы Synology NAS</b>\n\n"
for process in processes:
name = process.get("name", "unknown")
pid = process.get("pid", "?")
cpu_usage = process.get("cpu_usage", 0)
memory_usage = process.get("memory_usage", 0)
reply_text += f"• <b>{name}</b> (PID: {pid})\n"
reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
reply_text += f"\n<i>Показано {len(processes)} наиболее активных процессов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /network для получения информации о сетевых подключениях"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
return
try:
network_status = synology_api.get_network_status()
if not network_status:
await message.edit_text("❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о сетевых интерфейсах
interfaces = network_status.get("interfaces", [])
reply_text = f"🌐 <b>Сетевые подключения Synology NAS</b>\n\n"
for interface in interfaces:
name = interface.get("id", "unknown")
ip = interface.get("ip", "Нет данных")
mac = interface.get("mac", "Нет данных")
status = "Активен" if interface.get("status") else "Неактивен"
# Информация о трафике
rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
reply_text += f"• <b>{name}</b> ({status})\n"
reply_text += f" └ IP: {ip}, MAC: {mac}\n"
if rx_bytes > 0 or tx_bytes > 0:
reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /temperature для мониторинга температуры"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о температуре...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
return
try:
temp_status = synology_api.get_temperature_status()
if not temp_status:
await message.edit_text("❌ <b>Ошибка получения информации о температуре</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о температуре</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о температуре
system_temp = temp_status.get("system_temperature")
disk_temps = temp_status.get("disk_temperatures", [])
is_warning = temp_status.get("warning", False)
# Выбор emoji в зависимости от температуры
temp_emoji = "🔥" if is_warning else "🌡️"
reply_text = f"{temp_emoji} <b>Температура Synology NAS</b>\n\n"
if system_temp is not None:
temp_status_text = "❗ <b>ПОВЫШЕННАЯ</b>" if is_warning else "✅ Нормальная"
reply_text += f"<b>Температура системы:</b> {system_temp}°C ({temp_status_text})\n\n"
if disk_temps:
reply_text += "<b>Температура дисков:</b>\n"
for disk in disk_temps:
name = disk.get("name", "unknown")
model = disk.get("model", "unknown")
temp = disk.get("temperature", 0)
disk_temp_emoji = "🔥" if temp > 45 else ""
reply_text += f"{disk_temp_emoji} <b>{name}</b> ({model}): {temp}°C\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /schedule для управления расписанием питания"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
return
try:
schedule = synology_api.get_power_schedule()
if not schedule:
await message.edit_text("❌ <b>Ошибка получения информации о расписании питания</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о расписании питания</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о расписании питания
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
reply_text = f"⏱️ <b>Расписание питания Synology NAS</b>\n\n"
if boot_tasks:
reply_text += "<b>Расписание включения:</b>\n"
for task in boot_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание включения:</b> Не настроено\n"
reply_text += "\n"
if shutdown_tasks:
reply_text += "<b>Расписание выключения:</b>\n"
for task in shutdown_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание выключения:</b> Не настроено\n"
# Добавляем кнопки для управления расписанием
keyboard = [
[
InlineKeyboardButton(" Добавить включение", callback_data="schedule_add_boot"),
InlineKeyboardButton(" Добавить выключение", callback_data="schedule_add_shutdown")
],
[
InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /browse для просмотра файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем путь из аргументов команды или используем корневую директорию
path = " ".join(context.args) if context.args else ""
message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /search для поиска файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем шаблон поиска из аргументов команды
if not context.args:
await update.message.reply_text("❌ Укажите шаблон для поиска: /search &lt;шаблон&gt;")
return
pattern = " ".join(context.args)
message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
return
try:
search_result = synology_api.search_files(pattern=pattern, limit=20)
if not search_result.get("success", False):
error = search_result.get("error", "unknown")
progress = search_result.get("progress", 0)
if error == "search_timeout":
await message.edit_text(f"❌ <b>Превышено время ожидания результатов поиска</b>\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
else:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
files = search_result.get("results", [])
total = search_result.get("total", len(files))
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение с результатами поиска
reply_text = f"🔍 <b>Результаты поиска по шаблону «{pattern}»</b>\n\n"
if not files:
reply_text += "📭 <i>Файлы не найдены</i>"
else:
# Сортируем: сначала папки, потом файлы
folders = []
found_files = []
for item in files:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path))
else:
# Для файлов получаем размер и путь к родительской папке
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
parent_path = "/".join(path.split("/")[:-1])
found_files.append((name, path, size_str, parent_path))
# Добавляем папки в сообщение
if folders:
reply_text += "<b>Найденные папки:</b>\n"
for name, path in folders[:5]: # Показываем первые 5 папок
reply_text += f"📁 <a href=\"tg://browse?path={path}\">{name}</a>\n"
if len(folders) > 5:
reply_text += f"<i>...и еще {len(folders) - 5} папок</i>\n"
reply_text += "\n"
# Добавляем файлы в сообщение
if found_files:
reply_text += "<b>Найденные файлы:</b>\n"
for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
reply_text += f" <i>Путь: .../{path.split('/')[-2]}/</i>\n"
if len(found_files) > 10:
reply_text += f"<i>...и еще {len(found_files) - 10} файлов</i>\n"
# Добавляем информацию о общем количестве результатов
reply_text += f"\n<i>Всего найдено: {total} элементов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /updates для проверки обновлений"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
return
try:
update_info = synology_api.check_for_updates()
if not update_info.get("success", False):
error = update_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {error}", parse_mode="HTML")
return
current_version = update_info.get("current_version", "unknown")
update_available = update_info.get("update_available", False)
auto_update = update_info.get("auto_update_enabled", False)
updates = update_info.get("updates", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение об обновлениях
if update_available:
reply_text = f"🔄 <b>Доступны обновления DSM</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
reply_text += "<b>Доступные обновления:</b>\n"
for update_item in updates:
update_name = update_item.get("name", "unknown")
update_version = update_item.get("version", "unknown")
update_size = update_item.get("size", 0)
update_size_str = format_size(update_size)
reply_text += f"• <b>{update_name}</b> v{update_version}\n"
reply_text += f" └ Размер: {update_size_str}\n"
else:
reply_text = f"✅ <b>Система в актуальном состоянии</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /backup для управления резервным копированием"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
return
try:
backup_status = synology_api.get_backup_status()
if not backup_status.get("success", False):
error = backup_status.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {error}", parse_mode="HTML")
return
backups = backup_status.get("backups", {})
api_status = backup_status.get("available_apis", {})
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о резервном копировании
reply_text = f"💾 <b>Резервное копирование Synology NAS</b>\n\n"
# Информация о Hyper Backup
hyper_backups = backups.get("hyper_backup", [])
hyper_api_available = api_status.get("hyper_backup", False)
if hyper_api_available:
reply_text += "<b>Hyper Backup:</b>\n"
if hyper_backups:
for backup in hyper_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
last_backup = backup.get("last_backup", "never")
status_emoji = "" if status.lower() == "success" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
reply_text += f" └ Последнее копирование: {last_backup}\n"
else:
reply_text += "<i>Задачи Hyper Backup не настроены</i>\n"
reply_text += "\n"
# Информация о Time Backup
time_backups = backups.get("time_backup", [])
time_api_available = api_status.get("time_backup", False)
if time_api_available:
reply_text += "<b>Time Backup:</b>\n"
if time_backups:
for backup in time_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
status_emoji = "" if status.lower() == "normal" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
else:
reply_text += "<i>Задачи Time Backup не настроены</i>\n"
reply_text += "\n"
# Информация о USB Copy
usb_copy = backups.get("usb_copy", {})
usb_api_available = api_status.get("usb_copy", False)
if usb_api_available:
usb_enabled = usb_copy.get("enabled", False)
usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
reply_text += f"<b>USB Copy:</b> {usb_status}\n\n"
# Если ни один из API не доступен
if not any(api_status.values()):
reply_text += "<i>API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.</i>\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /reboot для перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед перезагрузкой
keyboard = [
[
InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перезагрузить Synology NAS?</b>\n\n"
"Это действие может привести к прерыванию работы всех сервисов.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /sleep для перевода NAS в спящий режим"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед отправкой в спящий режим
keyboard = [
[
InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перевести Synology NAS в спящий режим?</b>\n\n"
"Это действие приведет к остановке всех сервисов и отключению NAS.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
# Выполняем перезагрузку
result = synology_api.reboot_system()
if result:
# Формируем сообщение об успешной перезагрузке
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /wakeup для включения NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
# Проверяем, не включен ли NAS уже
if synology_api.is_online(force_check=True):
await message.edit_text(" <b>Synology NAS уже включен</b>\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
return
try:
# Отправляем сигнал пробуждения
result = synology_api.power_on()
if result:
# Формируем сообщение об успешном включении
reply_text = "✅ <b>Synology NAS успешно включен</b>\n\n"
reply_text += "<i>NAS полностью готов к работе.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при включении NAS</b>\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при включении NAS</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quota для просмотра информации о квотах"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
return
try:
quota_info = synology_api.get_quota_info()
if not quota_info.get("success", False):
error = quota_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {error}", parse_mode="HTML")
return
user_quotas = quota_info.get("user_quotas", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о квотах
reply_text = f"📊 <b>Квоты пользователей Synology NAS</b>\n\n"
if not user_quotas:
reply_text += "<i>Квоты пользователей не настроены или недоступны</i>"
else:
for user_quota in user_quotas:
user = user_quota.get("user", "unknown")
quotas = user_quota.get("quotas", [])
if quotas:
reply_text += f"<b>Пользователь {user}:</b>\n"
for quota in quotas:
volume = quota.get("volume_name", "unknown")
limit = quota.get("limit", 0)
used = quota.get("used", 0)
# Переводим байты в ГБ
limit_gb = limit / (1024**3) if limit > 0 else 0
used_gb = used / (1024**3)
# Рассчитываем процент использования
if limit_gb > 0:
usage_percent = (used_gb / limit_gb) * 100
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
else:
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
reply_text += "\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления расписанием питания"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("schedule_"):
action_type = action.split("_")[1]
if action_type == "add_boot":
# Логика добавления расписания включения
# В реальном боте здесь будет диалог для настройки расписания
await query.edit_message_text("⚙️ <b>Добавление расписания включения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "add_shutdown":
# Логика добавления расписания выключения
await query.edit_message_text("⚙️ <b>Добавление расписания выключения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "delete":
# Логика удаления расписания
await query.edit_message_text("⚙️ <b>Удаление расписания</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для навигации по файловой системе"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("browse_"):
path = action[7:] # Убираем префикс "browse_"
# Используем команду browse с указанным путем
message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках (аналогично функции browse_command)
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления питанием"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action == "confirm_reboot":
# Выполняем перезагрузку
message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
result = synology_api.reboot_system()
if result:
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_reboot":
# Отменяем перезагрузку
await query.edit_message_text("✅ <b>Перезагрузка отменена</b>", parse_mode="HTML")
elif action == "confirm_sleep":
# Выполняем переход в спящий режим (выключение)
message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS уже оффлайн</b>\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
return
try:
result = synology_api.power_off()
if result:
reply_text = "💤 <b>Synology NAS переведен в спящий режим</b>\n\n"
reply_text += "<i>Для пробуждения используйте команду /wakeup</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при переходе в спящий режим</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при переходе в спящий режим</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_sleep":
# Отменяем переход в спящий режим
await query.edit_message_text("✅ <b>Переход в спящий режим отменен</b>", parse_mode="HTML")
# Вспомогательные функции
def format_size(size_bytes: int) -> str:
"""Преобразует размер в байтах в человекочитаемый формат"""
if size_bytes < 1024:
return f"{size_bytes} Б"
elif size_bytes < 1024**2:
return f"{size_bytes/1024:.1f} КБ"
elif size_bytes < 1024**3:
return f"{size_bytes/1024**2:.1f} МБ"
else:
return f"{size_bytes/1024**3:.1f} ГБ"
def get_file_icon(filename: str) -> str:
"""Возвращает эмодзи-иконку в зависимости от типа файла"""
extension = filename.lower().split('.')[-1] if '.' in filename else ''
if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
return "🖼️"
elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
return "🎬"
elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
return "🎵"
elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
return "📄"
elif extension in ['xls', 'xlsx', 'csv']:
return "📊"
elif extension in ['ppt', 'pptx']:
return "📑"
elif extension in ['pdf']:
return "📕"
elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
return "🗜️"
elif extension in ['exe', 'msi']:
return "⚙️"
else:
return "📄"

View File

@@ -0,0 +1,972 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Расширенные обработчики команд для управления Synology NAS
"""
import logging
from datetime import datetime
from typing import List, Dict, Any
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /processes для получения списка активных процессов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
return
try:
processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
if not processes:
await message.edit_text("❌ <b>Ошибка получения информации о процессах</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о процессах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о процессах
reply_text = f"⚙️ <b>Активные процессы Synology NAS</b>\n\n"
for process in processes:
name = process.get("name", "unknown")
pid = process.get("pid", "?")
cpu_usage = process.get("cpu_usage", 0)
memory_usage = process.get("memory_usage", 0)
reply_text += f"• <b>{name}</b> (PID: {pid})\n"
reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
reply_text += f"\n<i>Показано {len(processes)} наиболее активных процессов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /network для получения информации о сетевых подключениях"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
return
try:
network_status = synology_api.get_network_status()
if not network_status:
await message.edit_text("❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о сетевых интерфейсах
interfaces = network_status.get("interfaces", [])
reply_text = f"🌐 <b>Сетевые подключения Synology NAS</b>\n\n"
for interface in interfaces:
name = interface.get("id", "unknown")
ip = interface.get("ip", "Нет данных")
mac = interface.get("mac", "Нет данных")
status = "Активен" if interface.get("status") else "Неактивен"
# Информация о трафике
rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
reply_text += f"• <b>{name}</b> ({status})\n"
reply_text += f" └ IP: {ip}, MAC: {mac}\n"
if rx_bytes > 0 or tx_bytes > 0:
reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /temperature для мониторинга температуры"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о температуре...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
return
try:
temp_status = synology_api.get_temperature_status()
if not temp_status:
await message.edit_text("❌ <b>Ошибка получения информации о температуре</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о температуре</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о температуре
system_temp = temp_status.get("system_temperature")
disk_temps = temp_status.get("disk_temperatures", [])
is_warning = temp_status.get("warning", False)
# Выбор emoji в зависимости от температуры
temp_emoji = "🔥" if is_warning else "🌡️"
reply_text = f"{temp_emoji} <b>Температура Synology NAS</b>\n\n"
if system_temp is not None:
temp_status_text = "❗ <b>ПОВЫШЕННАЯ</b>" if is_warning else "✅ Нормальная"
reply_text += f"<b>Температура системы:</b> {system_temp}°C ({temp_status_text})\n\n"
if disk_temps:
reply_text += "<b>Температура дисков:</b>\n"
for disk in disk_temps:
name = disk.get("name", "unknown")
model = disk.get("model", "unknown")
temp = disk.get("temperature", 0)
disk_temp_emoji = "🔥" if temp > 45 else ""
reply_text += f"{disk_temp_emoji} <b>{name}</b> ({model}): {temp}°C\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /schedule для управления расписанием питания"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
return
try:
schedule = synology_api.get_power_schedule()
if not schedule:
await message.edit_text("❌ <b>Ошибка получения информации о расписании питания</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о расписании питания</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о расписании питания
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
reply_text = f"⏱️ <b>Расписание питания Synology NAS</b>\n\n"
if boot_tasks:
reply_text += "<b>Расписание включения:</b>\n"
for task in boot_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание включения:</b> Не настроено\n"
reply_text += "\n"
if shutdown_tasks:
reply_text += "<b>Расписание выключения:</b>\n"
for task in shutdown_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание выключения:</b> Не настроено\n"
# Добавляем кнопки для управления расписанием
keyboard = [
[
InlineKeyboardButton(" Добавить включение", callback_data="schedule_add_boot"),
InlineKeyboardButton(" Добавить выключение", callback_data="schedule_add_shutdown")
],
[
InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /browse для просмотра файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем путь из аргументов команды или используем корневую директорию
path = " ".join(context.args) if context.args else ""
message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /search для поиска файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем шаблон поиска из аргументов команды
if not context.args:
await update.message.reply_text("❌ Укажите шаблон для поиска: /search &lt;шаблон&gt;")
return
pattern = " ".join(context.args)
message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
return
try:
search_result = synology_api.search_files(pattern=pattern, limit=20)
if not search_result.get("success", False):
error = search_result.get("error", "unknown")
progress = search_result.get("progress", 0)
if error == "search_timeout":
await message.edit_text(f"❌ <b>Превышено время ожидания результатов поиска</b>\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
else:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
files = search_result.get("results", [])
total = search_result.get("total", len(files))
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение с результатами поиска
reply_text = f"🔍 <b>Результаты поиска по шаблону «{pattern}»</b>\n\n"
if not files:
reply_text += "📭 <i>Файлы не найдены</i>"
else:
# Сортируем: сначала папки, потом файлы
folders = []
found_files = []
for item in files:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path))
else:
# Для файлов получаем размер и путь к родительской папке
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
parent_path = "/".join(path.split("/")[:-1])
found_files.append((name, path, size_str, parent_path))
# Добавляем папки в сообщение
if folders:
reply_text += "<b>Найденные папки:</b>\n"
for name, path in folders[:5]: # Показываем первые 5 папок
reply_text += f"📁 <a href=\"tg://browse?path={path}\">{name}</a>\n"
if len(folders) > 5:
reply_text += f"<i>...и еще {len(folders) - 5} папок</i>\n"
reply_text += "\n"
# Добавляем файлы в сообщение
if found_files:
reply_text += "<b>Найденные файлы:</b>\n"
for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
reply_text += f" <i>Путь: .../{path.split('/')[-2]}/</i>\n"
if len(found_files) > 10:
reply_text += f"<i>...и еще {len(found_files) - 10} файлов</i>\n"
# Добавляем информацию о общем количестве результатов
reply_text += f"\n<i>Всего найдено: {total} элементов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /updates для проверки обновлений"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
return
try:
update_info = synology_api.check_for_updates()
if not update_info.get("success", False):
error = update_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {error}", parse_mode="HTML")
return
current_version = update_info.get("current_version", "unknown")
update_available = update_info.get("update_available", False)
auto_update = update_info.get("auto_update_enabled", False)
updates = update_info.get("updates", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение об обновлениях
if update_available:
reply_text = f"🔄 <b>Доступны обновления DSM</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
reply_text += "<b>Доступные обновления:</b>\n"
for update_item in updates:
update_name = update_item.get("name", "unknown")
update_version = update_item.get("version", "unknown")
update_size = update_item.get("size", 0)
update_size_str = format_size(update_size)
reply_text += f"• <b>{update_name}</b> v{update_version}\n"
reply_text += f" └ Размер: {update_size_str}\n"
else:
reply_text = f"✅ <b>Система в актуальном состоянии</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /backup для управления резервным копированием"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
return
try:
backup_status = synology_api.get_backup_status()
if not backup_status.get("success", False):
error = backup_status.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {error}", parse_mode="HTML")
return
backups = backup_status.get("backups", {})
api_status = backup_status.get("available_apis", {})
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о резервном копировании
reply_text = f"💾 <b>Резервное копирование Synology NAS</b>\n\n"
# Информация о Hyper Backup
hyper_backups = backups.get("hyper_backup", [])
hyper_api_available = api_status.get("hyper_backup", False)
if hyper_api_available:
reply_text += "<b>Hyper Backup:</b>\n"
if hyper_backups:
for backup in hyper_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
last_backup = backup.get("last_backup", "never")
status_emoji = "" if status.lower() == "success" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
reply_text += f" └ Последнее копирование: {last_backup}\n"
else:
reply_text += "<i>Задачи Hyper Backup не настроены</i>\n"
reply_text += "\n"
# Информация о Time Backup
time_backups = backups.get("time_backup", [])
time_api_available = api_status.get("time_backup", False)
if time_api_available:
reply_text += "<b>Time Backup:</b>\n"
if time_backups:
for backup in time_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
status_emoji = "" if status.lower() == "normal" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
else:
reply_text += "<i>Задачи Time Backup не настроены</i>\n"
reply_text += "\n"
# Информация о USB Copy
usb_copy = backups.get("usb_copy", {})
usb_api_available = api_status.get("usb_copy", False)
if usb_api_available:
usb_enabled = usb_copy.get("enabled", False)
usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
reply_text += f"<b>USB Copy:</b> {usb_status}\n\n"
# Если ни один из API не доступен
if not any(api_status.values()):
reply_text += "<i>API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.</i>\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /reboot для перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед перезагрузкой
keyboard = [
[
InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перезагрузить Synology NAS?</b>\n\n"
"Это действие может привести к прерыванию работы всех сервисов.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /sleep для перевода NAS в спящий режим"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед отправкой в спящий режим
keyboard = [
[
InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перевести Synology NAS в спящий режим?</b>\n\n"
"Это действие приведет к остановке всех сервисов и отключению NAS.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
# Выполняем перезагрузку
result = synology_api.reboot_system()
if result:
# Формируем сообщение об успешной перезагрузке
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /wakeup для включения NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
# Проверяем, не включен ли NAS уже
if synology_api.is_online(force_check=True):
await message.edit_text(" <b>Synology NAS уже включен</b>\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
return
try:
# Отправляем сигнал пробуждения
result = synology_api.power_on()
if result:
# Формируем сообщение об успешном включении
reply_text = "✅ <b>Synology NAS успешно включен</b>\n\n"
reply_text += "<i>NAS полностью готов к работе.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при включении NAS</b>\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при включении NAS</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quota для просмотра информации о квотах"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
return
try:
quota_info = synology_api.get_quota_info()
if not quota_info.get("success", False):
error = quota_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {error}", parse_mode="HTML")
return
user_quotas = quota_info.get("user_quotas", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о квотах
reply_text = f"📊 <b>Квоты пользователей Synology NAS</b>\n\n"
if not user_quotas:
reply_text += "<i>Квоты пользователей не настроены или недоступны</i>"
else:
for user_quota in user_quotas:
user = user_quota.get("user", "unknown")
quotas = user_quota.get("quotas", [])
if quotas:
reply_text += f"<b>Пользователь {user}:</b>\n"
for quota in quotas:
volume = quota.get("volume_name", "unknown")
limit = quota.get("limit", 0)
used = quota.get("used", 0)
# Переводим байты в ГБ
limit_gb = limit / (1024**3) if limit > 0 else 0
used_gb = used / (1024**3)
# Рассчитываем процент использования
if limit_gb > 0:
usage_percent = (used_gb / limit_gb) * 100
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
else:
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
reply_text += "\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления расписанием питания"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("schedule_"):
action_type = action.split("_")[1]
if action_type == "add_boot":
# Логика добавления расписания включения
# В реальном боте здесь будет диалог для настройки расписания
await query.edit_message_text("⚙️ <b>Добавление расписания включения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "add_shutdown":
# Логика добавления расписания выключения
await query.edit_message_text("⚙️ <b>Добавление расписания выключения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "delete":
# Логика удаления расписания
await query.edit_message_text("⚙️ <b>Удаление расписания</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для навигации по файловой системе"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("browse_"):
path = action[7:] # Убираем префикс "browse_"
# Используем команду browse с указанным путем
message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках (аналогично функции browse_command)
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления питанием"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action == "confirm_reboot":
# Выполняем перезагрузку
message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
result = synology_api.reboot_system()
if result:
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_reboot":
# Отменяем перезагрузку
await query.edit_message_text("✅ <b>Перезагрузка отменена</b>", parse_mode="HTML")
elif action == "confirm_sleep":
# Выполняем переход в спящий режим (выключение)
message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS уже оффлайн</b>\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
return
try:
result = synology_api.power_off()
if result:
reply_text = "💤 <b>Synology NAS переведен в спящий режим</b>\n\n"
reply_text += "<i>Для пробуждения используйте команду /wakeup</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при переходе в спящий режим</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при переходе в спящий режим</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_sleep":
# Отменяем переход в спящий режим
await query.edit_message_text("✅ <b>Переход в спящий режим отменен</b>", parse_mode="HTML")
# Вспомогательные функции
def format_size(size_bytes: int) -> str:
"""Преобразует размер в байтах в человекочитаемый формат"""
if size_bytes < 1024:
return f"{size_bytes} Б"
elif size_bytes < 1024**2:
return f"{size_bytes/1024:.1f} КБ"
elif size_bytes < 1024**3:
return f"{size_bytes/1024**2:.1f} МБ"
else:
return f"{size_bytes/1024**3:.1f} ГБ"
def get_file_icon(filename: str) -> str:
"""Возвращает эмодзи-иконку в зависимости от типа файла"""
extension = filename.lower().split('.')[-1] if '.' in filename else ''
if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
return "🖼️"
elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
return "🎬"
elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
return "🎵"
elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
return "📄"
elif extension in ['xls', 'xlsx', 'csv']:
return "📊"
elif extension in ['ppt', 'pptx']:
return "📑"
elif extension in ['pdf']:
return "📕"
elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
return "🗜️"
elif extension in ['exe', 'msi']:
return "⚙️"
else:
return "📄"

View File

@@ -0,0 +1,982 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Расширенные обработчики команд для управления Synology NAS
"""
import logging
from datetime import datetime
from typing import List, Dict, Any
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /processes для получения списка активных процессов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
return
try:
processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
if not processes:
await message.edit_text("❌ <b>Ошибка получения информации о процессах</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о процессах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о процессах
reply_text = f"⚙️ <b>Активные процессы Synology NAS</b>\n\n"
for process in processes:
name = process.get("name", "unknown")
pid = process.get("pid", "?")
cpu_usage = process.get("cpu_usage", 0)
memory_usage = process.get("memory_usage", 0)
reply_text += f"• <b>{name}</b> (PID: {pid})\n"
reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
reply_text += f"\n<i>Показано {len(processes)} наиболее активных процессов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /network для получения информации о сетевых подключениях"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
return
try:
network_status = synology_api.get_network_status()
if not network_status:
await message.edit_text("❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о сетевых интерфейсах
interfaces = network_status.get("interfaces", [])
reply_text = f"🌐 <b>Сетевые подключения Synology NAS</b>\n\n"
for interface in interfaces:
name = interface.get("id", "unknown")
ip = interface.get("ip", "Нет данных")
mac = interface.get("mac", "Нет данных")
status = "Активен" if interface.get("status") else "Неактивен"
# Информация о трафике
rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
reply_text += f"• <b>{name}</b> ({status})\n"
reply_text += f" └ IP: {ip}, MAC: {mac}\n"
if rx_bytes > 0 or tx_bytes > 0:
reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /temperature для мониторинга температуры"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о температуре...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
return
try:
temp_status = synology_api.get_temperature_status()
if not temp_status:
await message.edit_text("❌ <b>Ошибка получения информации о температуре</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о температуре</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о температуре
system_temp = temp_status.get("system_temperature")
disk_temps = temp_status.get("disk_temperatures", [])
is_warning = temp_status.get("warning", False)
# Выбор emoji в зависимости от температуры
temp_emoji = "🔥" if is_warning else "🌡️"
reply_text = f"{temp_emoji} <b>Температура Synology NAS</b>\n\n"
if system_temp is not None:
temp_status_text = "❗ <b>ПОВЫШЕННАЯ</b>" if is_warning else "✅ Нормальная"
reply_text += f"<b>Температура системы:</b> {system_temp}°C ({temp_status_text})\n\n"
if disk_temps:
reply_text += "<b>Температура дисков:</b>\n"
for disk in disk_temps:
name = disk.get("name", "unknown")
model = disk.get("model", "unknown")
temp = disk.get("temperature", 0)
disk_temp_emoji = "🔥" if temp > 45 else ""
reply_text += f"{disk_temp_emoji} <b>{name}</b> ({model}): {temp}°C\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /schedule для управления расписанием питания"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
return
try:
schedule = synology_api.get_power_schedule()
# Проверяем, пустая ли структура расписания
if not schedule:
await message.edit_text("❌ <b>Ошибка получения информации о расписании питания</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
# Проверяем, содержит ли расписание хотя бы одну задачу
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
if not boot_tasks and not shutdown_tasks:
await message.edit_text(" <b>Расписание питания не настроено</b>\n\nНа вашем устройстве отсутствует настроенное расписание включения и выключения, либо API не поддерживается.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о расписании питания</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о расписании питания
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
reply_text = f"⏱️ <b>Расписание питания Synology NAS</b>\n\n"
if boot_tasks:
reply_text += "<b>Расписание включения:</b>\n"
for task in boot_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание включения:</b> Не настроено\n"
reply_text += "\n"
if shutdown_tasks:
reply_text += "<b>Расписание выключения:</b>\n"
for task in shutdown_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание выключения:</b> Не настроено\n"
# Добавляем кнопки для управления расписанием
keyboard = [
[
InlineKeyboardButton(" Добавить включение", callback_data="schedule_add_boot"),
InlineKeyboardButton(" Добавить выключение", callback_data="schedule_add_shutdown")
],
[
InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /browse для просмотра файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем путь из аргументов команды или используем корневую директорию
path = " ".join(context.args) if context.args else ""
message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /search для поиска файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем шаблон поиска из аргументов команды
if not context.args:
await update.message.reply_text("❌ Укажите шаблон для поиска: /search &lt;шаблон&gt;")
return
pattern = " ".join(context.args)
message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
return
try:
search_result = synology_api.search_files(pattern=pattern, limit=20)
if not search_result.get("success", False):
error = search_result.get("error", "unknown")
progress = search_result.get("progress", 0)
if error == "search_timeout":
await message.edit_text(f"❌ <b>Превышено время ожидания результатов поиска</b>\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
else:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
files = search_result.get("results", [])
total = search_result.get("total", len(files))
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение с результатами поиска
reply_text = f"🔍 <b>Результаты поиска по шаблону «{pattern}»</b>\n\n"
if not files:
reply_text += "📭 <i>Файлы не найдены</i>"
else:
# Сортируем: сначала папки, потом файлы
folders = []
found_files = []
for item in files:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path))
else:
# Для файлов получаем размер и путь к родительской папке
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
parent_path = "/".join(path.split("/")[:-1])
found_files.append((name, path, size_str, parent_path))
# Добавляем папки в сообщение
if folders:
reply_text += "<b>Найденные папки:</b>\n"
for name, path in folders[:5]: # Показываем первые 5 папок
reply_text += f"📁 <a href=\"tg://browse?path={path}\">{name}</a>\n"
if len(folders) > 5:
reply_text += f"<i>...и еще {len(folders) - 5} папок</i>\n"
reply_text += "\n"
# Добавляем файлы в сообщение
if found_files:
reply_text += "<b>Найденные файлы:</b>\n"
for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
reply_text += f" <i>Путь: .../{path.split('/')[-2]}/</i>\n"
if len(found_files) > 10:
reply_text += f"<i>...и еще {len(found_files) - 10} файлов</i>\n"
# Добавляем информацию о общем количестве результатов
reply_text += f"\n<i>Всего найдено: {total} элементов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /updates для проверки обновлений"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
return
try:
update_info = synology_api.check_for_updates()
if not update_info.get("success", False):
error = update_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {error}", parse_mode="HTML")
return
current_version = update_info.get("current_version", "unknown")
update_available = update_info.get("update_available", False)
auto_update = update_info.get("auto_update_enabled", False)
updates = update_info.get("updates", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение об обновлениях
if update_available:
reply_text = f"🔄 <b>Доступны обновления DSM</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
reply_text += "<b>Доступные обновления:</b>\n"
for update_item in updates:
update_name = update_item.get("name", "unknown")
update_version = update_item.get("version", "unknown")
update_size = update_item.get("size", 0)
update_size_str = format_size(update_size)
reply_text += f"• <b>{update_name}</b> v{update_version}\n"
reply_text += f" └ Размер: {update_size_str}\n"
else:
reply_text = f"✅ <b>Система в актуальном состоянии</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /backup для управления резервным копированием"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
return
try:
backup_status = synology_api.get_backup_status()
if not backup_status.get("success", False):
error = backup_status.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {error}", parse_mode="HTML")
return
backups = backup_status.get("backups", {})
api_status = backup_status.get("available_apis", {})
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о резервном копировании
reply_text = f"💾 <b>Резервное копирование Synology NAS</b>\n\n"
# Информация о Hyper Backup
hyper_backups = backups.get("hyper_backup", [])
hyper_api_available = api_status.get("hyper_backup", False)
if hyper_api_available:
reply_text += "<b>Hyper Backup:</b>\n"
if hyper_backups:
for backup in hyper_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
last_backup = backup.get("last_backup", "never")
status_emoji = "" if status.lower() == "success" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
reply_text += f" └ Последнее копирование: {last_backup}\n"
else:
reply_text += "<i>Задачи Hyper Backup не настроены</i>\n"
reply_text += "\n"
# Информация о Time Backup
time_backups = backups.get("time_backup", [])
time_api_available = api_status.get("time_backup", False)
if time_api_available:
reply_text += "<b>Time Backup:</b>\n"
if time_backups:
for backup in time_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
status_emoji = "" if status.lower() == "normal" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
else:
reply_text += "<i>Задачи Time Backup не настроены</i>\n"
reply_text += "\n"
# Информация о USB Copy
usb_copy = backups.get("usb_copy", {})
usb_api_available = api_status.get("usb_copy", False)
if usb_api_available:
usb_enabled = usb_copy.get("enabled", False)
usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
reply_text += f"<b>USB Copy:</b> {usb_status}\n\n"
# Если ни один из API не доступен
if not any(api_status.values()):
reply_text += "<i>API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.</i>\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /reboot для перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед перезагрузкой
keyboard = [
[
InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перезагрузить Synology NAS?</b>\n\n"
"Это действие может привести к прерыванию работы всех сервисов.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /sleep для перевода NAS в спящий режим"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед отправкой в спящий режим
keyboard = [
[
InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перевести Synology NAS в спящий режим?</b>\n\n"
"Это действие приведет к остановке всех сервисов и отключению NAS.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
# Выполняем перезагрузку
result = synology_api.reboot_system()
if result:
# Формируем сообщение об успешной перезагрузке
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /wakeup для включения NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
# Проверяем, не включен ли NAS уже
if synology_api.is_online(force_check=True):
await message.edit_text(" <b>Synology NAS уже включен</b>\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
return
try:
# Отправляем сигнал пробуждения
result = synology_api.power_on()
if result:
# Формируем сообщение об успешном включении
reply_text = "✅ <b>Synology NAS успешно включен</b>\n\n"
reply_text += "<i>NAS полностью готов к работе.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при включении NAS</b>\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при включении NAS</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quota для просмотра информации о квотах"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
return
try:
quota_info = synology_api.get_quota_info()
if not quota_info.get("success", False):
error = quota_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {error}", parse_mode="HTML")
return
user_quotas = quota_info.get("user_quotas", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о квотах
reply_text = f"📊 <b>Квоты пользователей Synology NAS</b>\n\n"
if not user_quotas:
reply_text += "<i>Квоты пользователей не настроены или недоступны</i>"
else:
for user_quota in user_quotas:
user = user_quota.get("user", "unknown")
quotas = user_quota.get("quotas", [])
if quotas:
reply_text += f"<b>Пользователь {user}:</b>\n"
for quota in quotas:
volume = quota.get("volume_name", "unknown")
limit = quota.get("limit", 0)
used = quota.get("used", 0)
# Переводим байты в ГБ
limit_gb = limit / (1024**3) if limit > 0 else 0
used_gb = used / (1024**3)
# Рассчитываем процент использования
if limit_gb > 0:
usage_percent = (used_gb / limit_gb) * 100
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
else:
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
reply_text += "\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления расписанием питания"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("schedule_"):
action_type = action.split("_")[1]
if action_type == "add_boot":
# Логика добавления расписания включения
# В реальном боте здесь будет диалог для настройки расписания
await query.edit_message_text("⚙️ <b>Добавление расписания включения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "add_shutdown":
# Логика добавления расписания выключения
await query.edit_message_text("⚙️ <b>Добавление расписания выключения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "delete":
# Логика удаления расписания
await query.edit_message_text("⚙️ <b>Удаление расписания</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для навигации по файловой системе"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("browse_"):
path = action[7:] # Убираем префикс "browse_"
# Используем команду browse с указанным путем
message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках (аналогично функции browse_command)
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления питанием"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action == "confirm_reboot":
# Выполняем перезагрузку
message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
result = synology_api.reboot_system()
if result:
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_reboot":
# Отменяем перезагрузку
await query.edit_message_text("✅ <b>Перезагрузка отменена</b>", parse_mode="HTML")
elif action == "confirm_sleep":
# Выполняем переход в спящий режим (выключение)
message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS уже оффлайн</b>\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
return
try:
result = synology_api.power_off()
if result:
reply_text = "💤 <b>Synology NAS переведен в спящий режим</b>\n\n"
reply_text += "<i>Для пробуждения используйте команду /wakeup</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при переходе в спящий режим</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при переходе в спящий режим</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_sleep":
# Отменяем переход в спящий режим
await query.edit_message_text("✅ <b>Переход в спящий режим отменен</b>", parse_mode="HTML")
# Вспомогательные функции
def format_size(size_bytes: int) -> str:
"""Преобразует размер в байтах в человекочитаемый формат"""
if size_bytes < 1024:
return f"{size_bytes} Б"
elif size_bytes < 1024**2:
return f"{size_bytes/1024:.1f} КБ"
elif size_bytes < 1024**3:
return f"{size_bytes/1024**2:.1f} МБ"
else:
return f"{size_bytes/1024**3:.1f} ГБ"
def get_file_icon(filename: str) -> str:
"""Возвращает эмодзи-иконку в зависимости от типа файла"""
extension = filename.lower().split('.')[-1] if '.' in filename else ''
if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
return "🖼️"
elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
return "🎬"
elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
return "🎵"
elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
return "📄"
elif extension in ['xls', 'xlsx', 'csv']:
return "📊"
elif extension in ['ppt', 'pptx']:
return "📑"
elif extension in ['pdf']:
return "📕"
elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
return "🗜️"
elif extension in ['exe', 'msi']:
return "⚙️"
else:
return "📄"

View File

@@ -0,0 +1,980 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Расширенные обработчики команд для управления Synology NAS
"""
import logging
from datetime import datetime
from typing import List, Dict, Any
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /processes для получения списка активных процессов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
return
try:
processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
if not processes:
await message.edit_text("❌ <b>Ошибка получения информации о процессах</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о процессах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о процессах
reply_text = f"⚙️ <b>Активные процессы Synology NAS</b>\n\n"
for process in processes:
name = process.get("name", "unknown")
pid = process.get("pid", "?")
cpu_usage = process.get("cpu_usage", 0)
memory_usage = process.get("memory_usage", 0)
reply_text += f"• <b>{name}</b> (PID: {pid})\n"
reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
reply_text += f"\n<i>Показано {len(processes)} наиболее активных процессов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /network для получения информации о сетевых подключениях"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
return
try:
network_status = synology_api.get_network_status()
if not network_status:
await message.edit_text("❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о сетевых интерфейсах
interfaces = network_status.get("interfaces", [])
reply_text = f"🌐 <b>Сетевые подключения Synology NAS</b>\n\n"
for interface in interfaces:
name = interface.get("id", "unknown")
ip = interface.get("ip", "Нет данных")
mac = interface.get("mac", "Нет данных")
status = "Активен" if interface.get("status") else "Неактивен"
# Информация о трафике
rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
reply_text += f"• <b>{name}</b> ({status})\n"
reply_text += f" └ IP: {ip}, MAC: {mac}\n"
if rx_bytes > 0 or tx_bytes > 0:
reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /temperature для мониторинга температуры"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о температуре...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
return
try:
temp_status = synology_api.get_temperature_status()
if not temp_status:
await message.edit_text("❌ <b>Ошибка получения информации о температуре</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о температуре</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о температуре
system_temp = temp_status.get("system_temperature")
disk_temps = temp_status.get("disk_temperatures", [])
is_warning = temp_status.get("warning", False)
# Выбор emoji в зависимости от температуры
temp_emoji = "🔥" if is_warning else "🌡️"
reply_text = f"{temp_emoji} <b>Температура Synology NAS</b>\n\n"
if system_temp is not None:
temp_status_text = "❗ <b>ПОВЫШЕННАЯ</b>" if is_warning else "✅ Нормальная"
reply_text += f"<b>Температура системы:</b> {system_temp}°C ({temp_status_text})\n\n"
if disk_temps:
reply_text += "<b>Температура дисков:</b>\n"
for disk in disk_temps:
name = disk.get("name", "unknown")
model = disk.get("model", "unknown")
temp = disk.get("temperature", 0)
disk_temp_emoji = "🔥" if temp > 45 else ""
reply_text += f"{disk_temp_emoji} <b>{name}</b> ({model}): {temp}°C\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /schedule для управления расписанием питания"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
return
try:
schedule = synology_api.get_power_schedule()
# Проверяем, пустая ли структура расписания
if not schedule:
await message.edit_text("❌ <b>Ошибка получения информации о расписании питания</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
# Получаем задачи расписания
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
if not boot_tasks and not shutdown_tasks:
await message.edit_text(" <b>Расписание питания не настроено</b>\n\nНа вашем устройстве отсутствует настроенное расписание включения и выключения, либо API не поддерживается.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о расписании питания</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о расписании питания
reply_text = f"⏱️ <b>Расписание питания Synology NAS</b>\n\n"
if boot_tasks:
reply_text += "<b>Расписание включения:</b>\n"
for task in boot_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание включения:</b> Не настроено\n"
reply_text += "\n"
if shutdown_tasks:
reply_text += "<b>Расписание выключения:</b>\n"
for task in shutdown_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание выключения:</b> Не настроено\n"
# Добавляем кнопки для управления расписанием
keyboard = [
[
InlineKeyboardButton(" Добавить включение", callback_data="schedule_add_boot"),
InlineKeyboardButton(" Добавить выключение", callback_data="schedule_add_shutdown")
],
[
InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /browse для просмотра файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем путь из аргументов команды или используем корневую директорию
path = " ".join(context.args) if context.args else ""
message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /search для поиска файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем шаблон поиска из аргументов команды
if not context.args:
await update.message.reply_text("❌ Укажите шаблон для поиска: /search &lt;шаблон&gt;")
return
pattern = " ".join(context.args)
message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
return
try:
search_result = synology_api.search_files(pattern=pattern, limit=20)
if not search_result.get("success", False):
error = search_result.get("error", "unknown")
progress = search_result.get("progress", 0)
if error == "search_timeout":
await message.edit_text(f"❌ <b>Превышено время ожидания результатов поиска</b>\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
else:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
files = search_result.get("results", [])
total = search_result.get("total", len(files))
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение с результатами поиска
reply_text = f"🔍 <b>Результаты поиска по шаблону «{pattern}»</b>\n\n"
if not files:
reply_text += "📭 <i>Файлы не найдены</i>"
else:
# Сортируем: сначала папки, потом файлы
folders = []
found_files = []
for item in files:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path))
else:
# Для файлов получаем размер и путь к родительской папке
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
parent_path = "/".join(path.split("/")[:-1])
found_files.append((name, path, size_str, parent_path))
# Добавляем папки в сообщение
if folders:
reply_text += "<b>Найденные папки:</b>\n"
for name, path in folders[:5]: # Показываем первые 5 папок
reply_text += f"📁 <a href=\"tg://browse?path={path}\">{name}</a>\n"
if len(folders) > 5:
reply_text += f"<i>...и еще {len(folders) - 5} папок</i>\n"
reply_text += "\n"
# Добавляем файлы в сообщение
if found_files:
reply_text += "<b>Найденные файлы:</b>\n"
for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
reply_text += f" <i>Путь: .../{path.split('/')[-2]}/</i>\n"
if len(found_files) > 10:
reply_text += f"<i>...и еще {len(found_files) - 10} файлов</i>\n"
# Добавляем информацию о общем количестве результатов
reply_text += f"\n<i>Всего найдено: {total} элементов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /updates для проверки обновлений"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
return
try:
update_info = synology_api.check_for_updates()
if not update_info.get("success", False):
error = update_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {error}", parse_mode="HTML")
return
current_version = update_info.get("current_version", "unknown")
update_available = update_info.get("update_available", False)
auto_update = update_info.get("auto_update_enabled", False)
updates = update_info.get("updates", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение об обновлениях
if update_available:
reply_text = f"🔄 <b>Доступны обновления DSM</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
reply_text += "<b>Доступные обновления:</b>\n"
for update_item in updates:
update_name = update_item.get("name", "unknown")
update_version = update_item.get("version", "unknown")
update_size = update_item.get("size", 0)
update_size_str = format_size(update_size)
reply_text += f"• <b>{update_name}</b> v{update_version}\n"
reply_text += f" └ Размер: {update_size_str}\n"
else:
reply_text = f"✅ <b>Система в актуальном состоянии</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /backup для управления резервным копированием"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
return
try:
backup_status = synology_api.get_backup_status()
if not backup_status.get("success", False):
error = backup_status.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {error}", parse_mode="HTML")
return
backups = backup_status.get("backups", {})
api_status = backup_status.get("available_apis", {})
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о резервном копировании
reply_text = f"💾 <b>Резервное копирование Synology NAS</b>\n\n"
# Информация о Hyper Backup
hyper_backups = backups.get("hyper_backup", [])
hyper_api_available = api_status.get("hyper_backup", False)
if hyper_api_available:
reply_text += "<b>Hyper Backup:</b>\n"
if hyper_backups:
for backup in hyper_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
last_backup = backup.get("last_backup", "never")
status_emoji = "" if status.lower() == "success" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
reply_text += f" └ Последнее копирование: {last_backup}\n"
else:
reply_text += "<i>Задачи Hyper Backup не настроены</i>\n"
reply_text += "\n"
# Информация о Time Backup
time_backups = backups.get("time_backup", [])
time_api_available = api_status.get("time_backup", False)
if time_api_available:
reply_text += "<b>Time Backup:</b>\n"
if time_backups:
for backup in time_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
status_emoji = "" if status.lower() == "normal" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
else:
reply_text += "<i>Задачи Time Backup не настроены</i>\n"
reply_text += "\n"
# Информация о USB Copy
usb_copy = backups.get("usb_copy", {})
usb_api_available = api_status.get("usb_copy", False)
if usb_api_available:
usb_enabled = usb_copy.get("enabled", False)
usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
reply_text += f"<b>USB Copy:</b> {usb_status}\n\n"
# Если ни один из API не доступен
if not any(api_status.values()):
reply_text += "<i>API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.</i>\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /reboot для перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед перезагрузкой
keyboard = [
[
InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перезагрузить Synology NAS?</b>\n\n"
"Это действие может привести к прерыванию работы всех сервисов.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /sleep для перевода NAS в спящий режим"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед отправкой в спящий режим
keyboard = [
[
InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перевести Synology NAS в спящий режим?</b>\n\n"
"Это действие приведет к остановке всех сервисов и отключению NAS.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
# Выполняем перезагрузку
result = synology_api.reboot_system()
if result:
# Формируем сообщение об успешной перезагрузке
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /wakeup для включения NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
# Проверяем, не включен ли NAS уже
if synology_api.is_online(force_check=True):
await message.edit_text(" <b>Synology NAS уже включен</b>\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
return
try:
# Отправляем сигнал пробуждения
result = synology_api.power_on()
if result:
# Формируем сообщение об успешном включении
reply_text = "✅ <b>Synology NAS успешно включен</b>\n\n"
reply_text += "<i>NAS полностью готов к работе.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при включении NAS</b>\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при включении NAS</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quota для просмотра информации о квотах"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
return
try:
quota_info = synology_api.get_quota_info()
if not quota_info.get("success", False):
error = quota_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {error}", parse_mode="HTML")
return
user_quotas = quota_info.get("user_quotas", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о квотах
reply_text = f"📊 <b>Квоты пользователей Synology NAS</b>\n\n"
if not user_quotas:
reply_text += "<i>Квоты пользователей не настроены или недоступны</i>"
else:
for user_quota in user_quotas:
user = user_quota.get("user", "unknown")
quotas = user_quota.get("quotas", [])
if quotas:
reply_text += f"<b>Пользователь {user}:</b>\n"
for quota in quotas:
volume = quota.get("volume_name", "unknown")
limit = quota.get("limit", 0)
used = quota.get("used", 0)
# Переводим байты в ГБ
limit_gb = limit / (1024**3) if limit > 0 else 0
used_gb = used / (1024**3)
# Рассчитываем процент использования
if limit_gb > 0:
usage_percent = (used_gb / limit_gb) * 100
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
else:
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
reply_text += "\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления расписанием питания"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("schedule_"):
action_type = action.split("_")[1]
if action_type == "add_boot":
# Логика добавления расписания включения
# В реальном боте здесь будет диалог для настройки расписания
await query.edit_message_text("⚙️ <b>Добавление расписания включения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "add_shutdown":
# Логика добавления расписания выключения
await query.edit_message_text("⚙️ <b>Добавление расписания выключения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "delete":
# Логика удаления расписания
await query.edit_message_text("⚙️ <b>Удаление расписания</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для навигации по файловой системе"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("browse_"):
path = action[7:] # Убираем префикс "browse_"
# Используем команду browse с указанным путем
message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках (аналогично функции browse_command)
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления питанием"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action == "confirm_reboot":
# Выполняем перезагрузку
message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
result = synology_api.reboot_system()
if result:
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_reboot":
# Отменяем перезагрузку
await query.edit_message_text("✅ <b>Перезагрузка отменена</b>", parse_mode="HTML")
elif action == "confirm_sleep":
# Выполняем переход в спящий режим (выключение)
message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS уже оффлайн</b>\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
return
try:
result = synology_api.power_off()
if result:
reply_text = "💤 <b>Synology NAS переведен в спящий режим</b>\n\n"
reply_text += "<i>Для пробуждения используйте команду /wakeup</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при переходе в спящий режим</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при переходе в спящий режим</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_sleep":
# Отменяем переход в спящий режим
await query.edit_message_text("✅ <b>Переход в спящий режим отменен</b>", parse_mode="HTML")
# Вспомогательные функции
def format_size(size_bytes: int) -> str:
"""Преобразует размер в байтах в человекочитаемый формат"""
if size_bytes < 1024:
return f"{size_bytes} Б"
elif size_bytes < 1024**2:
return f"{size_bytes/1024:.1f} КБ"
elif size_bytes < 1024**3:
return f"{size_bytes/1024**2:.1f} МБ"
else:
return f"{size_bytes/1024**3:.1f} ГБ"
def get_file_icon(filename: str) -> str:
"""Возвращает эмодзи-иконку в зависимости от типа файла"""
extension = filename.lower().split('.')[-1] if '.' in filename else ''
if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
return "🖼️"
elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
return "🎬"
elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
return "🎵"
elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
return "📄"
elif extension in ['xls', 'xlsx', 'csv']:
return "📊"
elif extension in ['ppt', 'pptx']:
return "📑"
elif extension in ['pdf']:
return "📕"
elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
return "🗜️"
elif extension in ['exe', 'msi']:
return "⚙️"
else:
return "📄"

View File

@@ -0,0 +1,980 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Расширенные обработчики команд для управления Synology NAS
"""
import logging
from datetime import datetime
from typing import List, Dict, Any
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /processes для получения списка активных процессов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
return
try:
processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
if not processes:
await message.edit_text("❌ <b>Ошибка получения информации о процессах</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о процессах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о процессах
reply_text = f"⚙️ <b>Активные процессы Synology NAS</b>\n\n"
for process in processes:
name = process.get("name", "unknown")
pid = process.get("pid", "?")
cpu_usage = process.get("cpu_usage", 0)
memory_usage = process.get("memory_usage", 0)
reply_text += f"• <b>{name}</b> (PID: {pid})\n"
reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
reply_text += f"\n<i>Показано {len(processes)} наиболее активных процессов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /network для получения информации о сетевых подключениях"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
return
try:
network_status = synology_api.get_network_status()
if not network_status:
await message.edit_text("❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о сетевых подключениях</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о сетевых интерфейсах
interfaces = network_status.get("interfaces", [])
reply_text = f"🌐 <b>Сетевые подключения Synology NAS</b>\n\n"
for interface in interfaces:
name = interface.get("id", "unknown")
ip = interface.get("ip", "Нет данных")
mac = interface.get("mac", "Нет данных")
status = "Активен" if interface.get("status") else "Неактивен"
# Информация о трафике
rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
reply_text += f"• <b>{name}</b> ({status})\n"
reply_text += f" └ IP: {ip}, MAC: {mac}\n"
if rx_bytes > 0 or tx_bytes > 0:
reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /temperature для мониторинга температуры"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о температуре...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
return
try:
temp_status = synology_api.get_temperature_status()
if not temp_status:
await message.edit_text("❌ <b>Ошибка получения информации о температуре</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о температуре</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о температуре
system_temp = temp_status.get("system_temperature")
disk_temps = temp_status.get("disk_temperatures", [])
is_warning = temp_status.get("warning", False)
# Выбор emoji в зависимости от температуры
temp_emoji = "🔥" if is_warning else "🌡️"
reply_text = f"{temp_emoji} <b>Температура Synology NAS</b>\n\n"
if system_temp is not None:
temp_status_text = "❗ <b>ПОВЫШЕННАЯ</b>" if is_warning else "✅ Нормальная"
reply_text += f"<b>Температура системы:</b> {system_temp}°C ({temp_status_text})\n\n"
if disk_temps:
reply_text += "<b>Температура дисков:</b>\n"
for disk in disk_temps:
name = disk.get("name", "unknown")
model = disk.get("model", "unknown")
temp = disk.get("temperature", 0)
disk_temp_emoji = "🔥" if temp > 45 else ""
reply_text += f"{disk_temp_emoji} <b>{name}</b> ({model}): {temp}°C\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /schedule для управления расписанием питания"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
return
try:
schedule = synology_api.get_power_schedule()
# Проверяем, пустая ли структура расписания
if not schedule:
await message.edit_text("❌ <b>Ошибка получения информации о расписании питания</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
# Получаем задачи расписания
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
if not boot_tasks and not shutdown_tasks:
await message.edit_text(" <b>Расписание питания не настроено</b>\n\nНа вашем устройстве отсутствует настроенное расписание включения и выключения, либо API не поддерживается.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о расписании питания</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о расписании питания
reply_text = f"⏱️ <b>Расписание питания Synology NAS</b>\n\n"
if boot_tasks:
reply_text += "<b>Расписание включения:</b>\n"
for task in boot_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание включения:</b> Не настроено\n"
reply_text += "\n"
if shutdown_tasks:
reply_text += "<b>Расписание выключения:</b>\n"
for task in shutdown_tasks:
days = task.get("day", [])
time = task.get("time", "00:00")
enabled = task.get("enabled", False)
# Преобразуем номера дней в названия
day_names = []
for day in days:
if day == 0: day_names.append("Пн")
elif day == 1: day_names.append("Вт")
elif day == 2: day_names.append("Ср")
elif day == 3: day_names.append("Чт")
elif day == 4: day_names.append("Пт")
elif day == 5: day_names.append("Сб")
elif day == 6: day_names.append("Вс")
status = "✅ Активно" if enabled else "❌ Отключено"
day_str = ", ".join(day_names) if day_names else "Нет дней"
reply_text += f"{status}: {time} ({day_str})\n"
else:
reply_text += "<b>Расписание выключения:</b> Не настроено\n"
# Добавляем кнопки для управления расписанием
keyboard = [
[
InlineKeyboardButton(" Добавить включение", callback_data="schedule_add_boot"),
InlineKeyboardButton(" Добавить выключение", callback_data="schedule_add_shutdown")
],
[
InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /browse для просмотра файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем путь из аргументов команды или используем корневую директорию
path = " ".join(context.args) if context.args else ""
message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /search для поиска файлов"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Получаем шаблон поиска из аргументов команды
if not context.args:
await update.message.reply_text("❌ Укажите шаблон для поиска: /search &lt;шаблон&gt;")
return
pattern = " ".join(context.args)
message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
return
try:
search_result = synology_api.search_files(pattern=pattern, limit=20)
if not search_result.get("success", False):
error = search_result.get("error", "unknown")
progress = search_result.get("progress", 0)
if error == "search_timeout":
await message.edit_text(f"❌ <b>Превышено время ожидания результатов поиска</b>\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
else:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
files = search_result.get("results", [])
total = search_result.get("total", len(files))
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при поиске файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение с результатами поиска
reply_text = f"🔍 <b>Результаты поиска по шаблону «{pattern}»</b>\n\n"
if not files:
reply_text += "📭 <i>Файлы не найдены</i>"
else:
# Сортируем: сначала папки, потом файлы
folders = []
found_files = []
for item in files:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path))
else:
# Для файлов получаем размер и путь к родительской папке
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
parent_path = "/".join(path.split("/")[:-1])
found_files.append((name, path, size_str, parent_path))
# Добавляем папки в сообщение
if folders:
reply_text += "<b>Найденные папки:</b>\n"
for name, path in folders[:5]: # Показываем первые 5 папок
reply_text += f"📁 <a href=\"tg://browse?path={path}\">{name}</a>\n"
if len(folders) > 5:
reply_text += f"<i>...и еще {len(folders) - 5} папок</i>\n"
reply_text += "\n"
# Добавляем файлы в сообщение
if found_files:
reply_text += "<b>Найденные файлы:</b>\n"
for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
reply_text += f" <i>Путь: .../{path.split('/')[-2]}/</i>\n"
if len(found_files) > 10:
reply_text += f"<i>...и еще {len(found_files) - 10} файлов</i>\n"
# Добавляем информацию о общем количестве результатов
reply_text += f"\n<i>Всего найдено: {total} элементов</i>"
await message.edit_text(reply_text, parse_mode="HTML")
async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /updates для проверки обновлений"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
return
try:
update_info = synology_api.check_for_updates()
if not update_info.get("success", False):
error = update_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {error}", parse_mode="HTML")
return
current_version = update_info.get("current_version", "unknown")
update_available = update_info.get("update_available", False)
auto_update = update_info.get("auto_update_enabled", False)
updates = update_info.get("updates", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при проверке обновлений</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение об обновлениях
if update_available:
reply_text = f"🔄 <b>Доступны обновления DSM</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
reply_text += "<b>Доступные обновления:</b>\n"
for update_item in updates:
update_name = update_item.get("name", "unknown")
update_version = update_item.get("version", "unknown")
update_size = update_item.get("size", 0)
update_size_str = format_size(update_size)
reply_text += f"• <b>{update_name}</b> v{update_version}\n"
reply_text += f" └ Размер: {update_size_str}\n"
else:
reply_text = f"✅ <b>Система в актуальном состоянии</b>\n\n"
reply_text += f"<b>Текущая версия:</b> {current_version}\n"
reply_text += f"<b>Автообновление:</b> {'✅ Включено' if auto_update else '❌ Отключено'}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /backup для управления резервным копированием"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
return
try:
backup_status = synology_api.get_backup_status()
if not backup_status.get("success", False):
error = backup_status.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {error}", parse_mode="HTML")
return
backups = backup_status.get("backups", {})
api_status = backup_status.get("available_apis", {})
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о резервном копировании</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о резервном копировании
reply_text = f"💾 <b>Резервное копирование Synology NAS</b>\n\n"
# Информация о Hyper Backup
hyper_backups = backups.get("hyper_backup", [])
hyper_api_available = api_status.get("hyper_backup", False)
if hyper_api_available:
reply_text += "<b>Hyper Backup:</b>\n"
if hyper_backups:
for backup in hyper_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
last_backup = backup.get("last_backup", "never")
status_emoji = "" if status.lower() == "success" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
reply_text += f" └ Последнее копирование: {last_backup}\n"
else:
reply_text += "<i>Задачи Hyper Backup не настроены</i>\n"
reply_text += "\n"
# Информация о Time Backup
time_backups = backups.get("time_backup", [])
time_api_available = api_status.get("time_backup", False)
if time_api_available:
reply_text += "<b>Time Backup:</b>\n"
if time_backups:
for backup in time_backups:
name = backup.get("name", "unknown")
status = backup.get("status", "unknown")
status_emoji = "" if status.lower() == "normal" else "⚠️"
reply_text += f"{status_emoji} <b>{name}</b>\n"
else:
reply_text += "<i>Задачи Time Backup не настроены</i>\n"
reply_text += "\n"
# Информация о USB Copy
usb_copy = backups.get("usb_copy", {})
usb_api_available = api_status.get("usb_copy", False)
if usb_api_available:
usb_enabled = usb_copy.get("enabled", False)
usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
reply_text += f"<b>USB Copy:</b> {usb_status}\n\n"
# Если ни один из API не доступен
if not any(api_status.values()):
reply_text += "<i>API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.</i>\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /reboot для перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед перезагрузкой
keyboard = [
[
InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перезагрузить Synology NAS?</b>\n\n"
"Это действие может привести к прерыванию работы всех сервисов.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /sleep для перевода NAS в спящий режим"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
# Добавляем подтверждение перед отправкой в спящий режим
keyboard = [
[
InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"⚠️ <b>Вы уверены, что хотите перевести Synology NAS в спящий режим?</b>\n\n"
"Это действие приведет к остановке всех сервисов и отключению NAS.",
parse_mode="HTML",
reply_markup=reply_markup
)
async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
# Выполняем перезагрузку
result = synology_api.reboot_system()
if result:
# Формируем сообщение об успешной перезагрузке
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /wakeup для включения NAS"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
# Проверяем, не включен ли NAS уже
if synology_api.is_online(force_check=True):
await message.edit_text(" <b>Synology NAS уже включен</b>\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
return
try:
# Отправляем сигнал пробуждения
result = synology_api.power_on()
if result:
# Формируем сообщение об успешном включении
reply_text = "✅ <b>Synology NAS успешно включен</b>\n\n"
reply_text += "<i>NAS полностью готов к работе.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при включении NAS</b>\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при включении NAS</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /quota для просмотра информации о квотах"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
return
try:
quota_info = synology_api.get_quota_info()
if not quota_info.get("success", False):
error = quota_info.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {error}", parse_mode="HTML")
return
user_quotas = quota_info.get("user_quotas", [])
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о квотах</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о квотах
reply_text = f"📊 <b>Квоты пользователей Synology NAS</b>\n\n"
if not user_quotas:
reply_text += "<i>Квоты пользователей не настроены или недоступны</i>"
else:
for user_quota in user_quotas:
user = user_quota.get("user", "unknown")
quotas = user_quota.get("quotas", [])
if quotas:
reply_text += f"<b>Пользователь {user}:</b>\n"
for quota in quotas:
volume = quota.get("volume_name", "unknown")
limit = quota.get("limit", 0)
used = quota.get("used", 0)
# Переводим байты в ГБ
limit_gb = limit / (1024**3) if limit > 0 else 0
used_gb = used / (1024**3)
# Рассчитываем процент использования
if limit_gb > 0:
usage_percent = (used_gb / limit_gb) * 100
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
else:
reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
reply_text += "\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления расписанием питания"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("schedule_"):
action_type = action.split("_")[1]
if action_type == "add_boot":
# Логика добавления расписания включения
# В реальном боте здесь будет диалог для настройки расписания
await query.edit_message_text("⚙️ <b>Добавление расписания включения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "add_shutdown":
# Логика добавления расписания выключения
await query.edit_message_text("⚙️ <b>Добавление расписания выключения</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
elif action_type == "delete":
# Логика удаления расписания
await query.edit_message_text("⚙️ <b>Удаление расписания</b>\n\n<i>Эта функция находится в разработке.</i>", parse_mode="HTML")
async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для навигации по файловой системе"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action.startswith("browse_"):
path = action[7:] # Убираем префикс "browse_"
# Используем команду browse с указанным путем
message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить список файлов.", parse_mode="HTML")
return
try:
browse_result = synology_api.browse_files(folder_path=path)
if not browse_result.get("success", False):
error = browse_result.get("error", "unknown")
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {error}", parse_mode="HTML")
return
items = browse_result.get("items", [])
current_path = browse_result.get("path", "")
is_root = browse_result.get("is_root", True)
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения списка файлов</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о файлах и папках (аналогично функции browse_command)
if is_root:
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
else:
reply_text = f"📁 <b>Содержимое папки</b>\n<code>{current_path}</code>\n\n"
# Сортируем: сначала папки, потом файлы
folders = []
files = []
for item in items:
if is_root: # Для корневого уровня все элементы - это общие папки
name = item.get("name", "unknown")
path = item.get("path", "")
folders.append((name, path, True))
else:
name = item.get("name", "unknown")
path = item.get("path", "")
is_dir = item.get("isdir", False)
if is_dir:
folders.append((name, path, False))
else:
# Для файлов получаем размер
size = item.get("additional", {}).get("size", 0)
size_str = format_size(size)
files.append((name, path, size_str))
# Добавляем папки в сообщение
if folders:
for name, path, is_share in folders:
# Для общих папок добавляем иконку дома
icon = "🏠" if is_share else "📁"
reply_text += f"{icon} <a href=\"tg://browse?path={path}\">{name}</a>\n"
# Добавляем файлы в сообщение
if files:
for name, path, size in files:
# Выбираем иконку в зависимости от расширения
icon = get_file_icon(name)
reply_text += f"{icon} {name} ({size})\n"
# Если нет элементов для отображения
if not folders and not files:
reply_text += "📭 <i>Папка пуста</i>\n"
# Добавляем кнопку возврата наверх, если мы не в корне
if not is_root:
# Определяем родительскую директорию
parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
keyboard = [
[InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
else:
await message.edit_text(reply_text, parse_mode="HTML")
async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для управления питанием"""
query = update.callback_query
await query.answer()
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await query.edit_message_text("У вас нет доступа к этому боту.")
return
action = query.data
if action == "confirm_reboot":
# Выполняем перезагрузку
message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
return
try:
result = synology_api.reboot_system()
if result:
reply_text = "🔄 <b>Synology NAS перезагружается</b>\n\n"
reply_text += "<i>Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен.</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при выполнении перезагрузки</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_reboot":
# Отменяем перезагрузку
await query.edit_message_text("✅ <b>Перезагрузка отменена</b>", parse_mode="HTML")
elif action == "confirm_sleep":
# Выполняем переход в спящий режим (выключение)
message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS уже оффлайн</b>\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
return
try:
result = synology_api.power_off()
if result:
reply_text = "💤 <b>Synology NAS переведен в спящий режим</b>\n\n"
reply_text += "<i>Для пробуждения используйте команду /wakeup</i>"
await message.edit_text(reply_text, parse_mode="HTML")
else:
await message.edit_text("❌ <b>Ошибка при переходе в спящий режим</b>\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка при переходе в спящий режим</b>\n\nПричина: {str(e)}", parse_mode="HTML")
elif action == "cancel_sleep":
# Отменяем переход в спящий режим
await query.edit_message_text("✅ <b>Переход в спящий режим отменен</b>", parse_mode="HTML")
# Вспомогательные функции
def format_size(size_bytes: int) -> str:
"""Преобразует размер в байтах в человекочитаемый формат"""
if size_bytes < 1024:
return f"{size_bytes} Б"
elif size_bytes < 1024**2:
return f"{size_bytes/1024:.1f} КБ"
elif size_bytes < 1024**3:
return f"{size_bytes/1024**2:.1f} МБ"
else:
return f"{size_bytes/1024**3:.1f} ГБ"
def get_file_icon(filename: str) -> str:
"""Возвращает эмодзи-иконку в зависимости от типа файла"""
extension = filename.lower().split('.')[-1] if '.' in filename else ''
if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
return "🖼️"
elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
return "🎬"
elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
return "🎵"
elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
return "📄"
elif extension in ['xls', 'xlsx', 'csv']:
return "📊"
elif extension in ['ppt', 'pptx']:
return "📑"
elif extension in ['pdf']:
return "📕"
elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
return "🗜️"
elif extension in ['exe', 'msi']:
return "⚙️"
else:
return "📄"

View File

@@ -0,0 +1,328 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Обработчики команд для телеграм-бота
"""
import logging
import socket
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import (
ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
)
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
from src.utils.admin_utils import admin_required
@admin_required
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /status"""
message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
is_online = synology_api.is_online()
if is_online:
try:
# Если NAS включен, попробуем получить дополнительную информацию
system_info = synology_api.get_system_status()
if system_info and system_info.get("status") != "error":
model = system_info.get("model", "Неизвестная модель")
version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
# Преобразование времени работы в удобочитаемый формат
days, remainder = divmod(int(uptime_seconds), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Модель:</b> {model}\n"
f"<b>Версия DSM:</b> {version}\n"
f"<b>Время работы:</b> {uptime_str}",
parse_mode="HTML"
)
else:
# Обработка возможной ошибки API
error_info = ""
if system_info and system_info.get("status") == "error":
error_code = system_info.get("error_code", "неизвестно")
error_info = f"\n<i>Код ошибки API: {error_code}</i>"
# Проверяем порт и сеть
network_info = ""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
s.close()
if result == 0:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>открыт</b>"
else:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>закрыт</b> (код {result})"
except Exception as e:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"Устройство доступно по сети, но детальная информация через API недоступна. "
f"Возможно, необходимо проверить учетные данные или права доступа."
f"{error_info}"
f"{network_info}",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Ошибка при получении информации:</b> {str(e)[:100]}...\n\n"
f"<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
parse_mode="HTML"
)
else:
# Устройство не в сети, проверим соседние порты для диагностики
port_scan_info = ""
try:
for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, test_port))
s.close()
status = "открыт" if result == 0 else "закрыт"
port_scan_info += f"Порт {test_port}: <b>{status}</b>\n"
# Добавим информацию о MAC-адресе для WoL
mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Информация о сети:</b>\n"
f"IP: {SYNOLOGY_HOST}\n"
f"{port_scan_info}\n"
f"{mac_info}\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Ошибка при сканировании портов:</b> {str(e)[:100]}...\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /power"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
is_online = synology_api.is_online()
keyboard = []
# Кнопка включения
if not is_online:
keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
else:
keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
# Кнопка выключения
if is_online:
keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
else:
keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
# Кнопка перезагрузки
if is_online:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
else:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
# Кнопка отмены
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
reply_markup = InlineKeyboardMarkup(keyboard)
status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
await update.message.reply_text(
f"<b>Управление питанием Synology NAS</b>\n\n"
f"<b>Текущий статус:</b> {status_text}\n\n"
f"Выберите действие:",
reply_markup=reply_markup,
parse_mode="HTML"
)
async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для кнопок управления питанием"""
query = update.callback_query
await query.answer()
user_id = query.from_user.id
if user_id not in ADMIN_USER_IDS:
return
action = query.data
if action == "cancel":
await query.edit_message_text("❌ Действие отменено")
return
# Обработка неактивных кнопок
if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
if action == "power_on_no_op":
await query.edit_message_text(" Synology NAS уже включен")
elif action == "power_off_no_op":
await query.edit_message_text(" Synology NAS уже выключен")
else:
await query.edit_message_text(" Невозможно перезагрузить выключенное устройство")
return
# Обработка основных действий
if action == "power_on":
await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
if await context.application.create_task(
handle_power_on(query.message.chat_id, context)
):
# Функция вернула True, успешное включение
pass
else:
# Функция вернула False, ошибка включения
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
)
elif action == "power_off":
await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
try:
success = await handle_power_off(query.message.chat_id, context)
# Если handle_power_off уже отправил сообщение об успехе или ошибке,
# дополнительных сообщений не требуется
except Exception as e:
logger.error(f"Exception in power_off callback: {str(e)}")
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
)
elif action == "reboot":
await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
if await context.application.create_task(
handle_reboot(query.message.chat_id, context)
):
# Функция вернула True, успешная перезагрузка
pass
else:
# Функция вернула False, ошибка перезагрузки
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
)
async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для включения NAS"""
try:
# Отправка запроса на включение
success = synology_api.power_on()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно включен и доступен"
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during power on: {str(e)}")
return False
async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для выключения NAS"""
try:
# Проверка доступности NAS
if not synology_api.is_online():
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
)
return False
# Отправка запроса на выключение
success = synology_api.power_off()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
)
return True
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
)
return False
except Exception as e:
error_msg = str(e)
logger.error(f"Error during power off: {error_msg}")
await context.bot.send_message(
chat_id=chat_id,
text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
)
return False
async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для перезагрузки NAS"""
try:
# Отправка запроса на перезагрузку
success = synology_api.reboot_system()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
)
# Ждем некоторое время перед проверкой статуса
await context.bot.send_message(
chat_id=chat_id,
text="⏳ Ожидание перезагрузки системы..."
)
# Создаем задачу для ожидания загрузки
wait_successful = synology_api.wait_for_boot()
if wait_successful:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно перезагружен и снова онлайн"
)
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during reboot: {str(e)}")
return False

View File

@@ -0,0 +1,329 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Обработчики команд для телеграм-бота
"""
import logging
import socket
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import (
ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
)
from src.api.synology import SynologyAPI
from src.utils.admin_utils import admin_required
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
from src.utils.admin_utils import admin_required
@admin_required
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /status"""
message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
is_online = synology_api.is_online()
if is_online:
try:
# Если NAS включен, попробуем получить дополнительную информацию
system_info = synology_api.get_system_status()
if system_info and system_info.get("status") != "error":
model = system_info.get("model", "Неизвестная модель")
version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
# Преобразование времени работы в удобочитаемый формат
days, remainder = divmod(int(uptime_seconds), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Модель:</b> {model}\n"
f"<b>Версия DSM:</b> {version}\n"
f"<b>Время работы:</b> {uptime_str}",
parse_mode="HTML"
)
else:
# Обработка возможной ошибки API
error_info = ""
if system_info and system_info.get("status") == "error":
error_code = system_info.get("error_code", "неизвестно")
error_info = f"\n<i>Код ошибки API: {error_code}</i>"
# Проверяем порт и сеть
network_info = ""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
s.close()
if result == 0:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>открыт</b>"
else:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>закрыт</b> (код {result})"
except Exception as e:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"Устройство доступно по сети, но детальная информация через API недоступна. "
f"Возможно, необходимо проверить учетные данные или права доступа."
f"{error_info}"
f"{network_info}",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Ошибка при получении информации:</b> {str(e)[:100]}...\n\n"
f"<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
parse_mode="HTML"
)
else:
# Устройство не в сети, проверим соседние порты для диагностики
port_scan_info = ""
try:
for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, test_port))
s.close()
status = "открыт" if result == 0 else "закрыт"
port_scan_info += f"Порт {test_port}: <b>{status}</b>\n"
# Добавим информацию о MAC-адресе для WoL
mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Информация о сети:</b>\n"
f"IP: {SYNOLOGY_HOST}\n"
f"{port_scan_info}\n"
f"{mac_info}\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Ошибка при сканировании портов:</b> {str(e)[:100]}...\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /power"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
is_online = synology_api.is_online()
keyboard = []
# Кнопка включения
if not is_online:
keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
else:
keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
# Кнопка выключения
if is_online:
keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
else:
keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
# Кнопка перезагрузки
if is_online:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
else:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
# Кнопка отмены
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
reply_markup = InlineKeyboardMarkup(keyboard)
status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
await update.message.reply_text(
f"<b>Управление питанием Synology NAS</b>\n\n"
f"<b>Текущий статус:</b> {status_text}\n\n"
f"Выберите действие:",
reply_markup=reply_markup,
parse_mode="HTML"
)
async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для кнопок управления питанием"""
query = update.callback_query
await query.answer()
user_id = query.from_user.id
if user_id not in ADMIN_USER_IDS:
return
action = query.data
if action == "cancel":
await query.edit_message_text("❌ Действие отменено")
return
# Обработка неактивных кнопок
if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
if action == "power_on_no_op":
await query.edit_message_text(" Synology NAS уже включен")
elif action == "power_off_no_op":
await query.edit_message_text(" Synology NAS уже выключен")
else:
await query.edit_message_text(" Невозможно перезагрузить выключенное устройство")
return
# Обработка основных действий
if action == "power_on":
await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
if await context.application.create_task(
handle_power_on(query.message.chat_id, context)
):
# Функция вернула True, успешное включение
pass
else:
# Функция вернула False, ошибка включения
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
)
elif action == "power_off":
await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
try:
success = await handle_power_off(query.message.chat_id, context)
# Если handle_power_off уже отправил сообщение об успехе или ошибке,
# дополнительных сообщений не требуется
except Exception as e:
logger.error(f"Exception in power_off callback: {str(e)}")
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
)
elif action == "reboot":
await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
if await context.application.create_task(
handle_reboot(query.message.chat_id, context)
):
# Функция вернула True, успешная перезагрузка
pass
else:
# Функция вернула False, ошибка перезагрузки
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
)
async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для включения NAS"""
try:
# Отправка запроса на включение
success = synology_api.power_on()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно включен и доступен"
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during power on: {str(e)}")
return False
async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для выключения NAS"""
try:
# Проверка доступности NAS
if not synology_api.is_online():
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
)
return False
# Отправка запроса на выключение
success = synology_api.power_off()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
)
return True
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
)
return False
except Exception as e:
error_msg = str(e)
logger.error(f"Error during power off: {error_msg}")
await context.bot.send_message(
chat_id=chat_id,
text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
)
return False
async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для перезагрузки NAS"""
try:
# Отправка запроса на перезагрузку
success = synology_api.reboot_system()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
)
# Ждем некоторое время перед проверкой статуса
await context.bot.send_message(
chat_id=chat_id,
text="⏳ Ожидание перезагрузки системы..."
)
# Создаем задачу для ожидания загрузки
wait_successful = synology_api.wait_for_boot()
if wait_successful:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно перезагружен и снова онлайн"
)
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during reboot: {str(e)}")
return False

View File

@@ -0,0 +1,325 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Обработчики команд для телеграм-бота
"""
import logging
import socket
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import (
ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
)
from src.api.synology import SynologyAPI
from src.utils.admin_utils import admin_required
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
from src.utils.admin_utils import admin_required
@admin_required
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /status"""
message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
is_online = synology_api.is_online()
if is_online:
try:
# Если NAS включен, попробуем получить дополнительную информацию
system_info = synology_api.get_system_status()
if system_info and system_info.get("status") != "error":
model = system_info.get("model", "Неизвестная модель")
version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
# Преобразование времени работы в удобочитаемый формат
days, remainder = divmod(int(uptime_seconds), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Модель:</b> {model}\n"
f"<b>Версия DSM:</b> {version}\n"
f"<b>Время работы:</b> {uptime_str}",
parse_mode="HTML"
)
else:
# Обработка возможной ошибки API
error_info = ""
if system_info and system_info.get("status") == "error":
error_code = system_info.get("error_code", "неизвестно")
error_info = f"\n<i>Код ошибки API: {error_code}</i>"
# Проверяем порт и сеть
network_info = ""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
s.close()
if result == 0:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>открыт</b>"
else:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>закрыт</b> (код {result})"
except Exception as e:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"Устройство доступно по сети, но детальная информация через API недоступна. "
f"Возможно, необходимо проверить учетные данные или права доступа."
f"{error_info}"
f"{network_info}",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Ошибка при получении информации:</b> {str(e)[:100]}...\n\n"
f"<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
parse_mode="HTML"
)
else:
# Устройство не в сети, проверим соседние порты для диагностики
port_scan_info = ""
try:
for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, test_port))
s.close()
status = "открыт" if result == 0 else "закрыт"
port_scan_info += f"Порт {test_port}: <b>{status}</b>\n"
# Добавим информацию о MAC-адресе для WoL
mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Информация о сети:</b>\n"
f"IP: {SYNOLOGY_HOST}\n"
f"{port_scan_info}\n"
f"{mac_info}\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Ошибка при сканировании портов:</b> {str(e)[:100]}...\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
@admin_required
async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /power"""
is_online = synology_api.is_online()
keyboard = []
# Кнопка включения
if not is_online:
keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
else:
keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
# Кнопка выключения
if is_online:
keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
else:
keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
# Кнопка перезагрузки
if is_online:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
else:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
# Кнопка отмены
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
reply_markup = InlineKeyboardMarkup(keyboard)
status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
await update.message.reply_text(
f"<b>Управление питанием Synology NAS</b>\n\n"
f"<b>Текущий статус:</b> {status_text}\n\n"
f"Выберите действие:",
reply_markup=reply_markup,
parse_mode="HTML"
)
async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для кнопок управления питанием"""
query = update.callback_query
await query.answer()
user_id = query.from_user.id
if user_id not in ADMIN_USER_IDS:
return
action = query.data
if action == "cancel":
await query.edit_message_text("❌ Действие отменено")
return
# Обработка неактивных кнопок
if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
if action == "power_on_no_op":
await query.edit_message_text(" Synology NAS уже включен")
elif action == "power_off_no_op":
await query.edit_message_text(" Synology NAS уже выключен")
else:
await query.edit_message_text(" Невозможно перезагрузить выключенное устройство")
return
# Обработка основных действий
if action == "power_on":
await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
if await context.application.create_task(
handle_power_on(query.message.chat_id, context)
):
# Функция вернула True, успешное включение
pass
else:
# Функция вернула False, ошибка включения
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
)
elif action == "power_off":
await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
try:
success = await handle_power_off(query.message.chat_id, context)
# Если handle_power_off уже отправил сообщение об успехе или ошибке,
# дополнительных сообщений не требуется
except Exception as e:
logger.error(f"Exception in power_off callback: {str(e)}")
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
)
elif action == "reboot":
await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
if await context.application.create_task(
handle_reboot(query.message.chat_id, context)
):
# Функция вернула True, успешная перезагрузка
pass
else:
# Функция вернула False, ошибка перезагрузки
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
)
async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для включения NAS"""
try:
# Отправка запроса на включение
success = synology_api.power_on()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно включен и доступен"
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during power on: {str(e)}")
return False
async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для выключения NAS"""
try:
# Проверка доступности NAS
if not synology_api.is_online():
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
)
return False
# Отправка запроса на выключение
success = synology_api.power_off()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
)
return True
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
)
return False
except Exception as e:
error_msg = str(e)
logger.error(f"Error during power off: {error_msg}")
await context.bot.send_message(
chat_id=chat_id,
text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
)
return False
async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для перезагрузки NAS"""
try:
# Отправка запроса на перезагрузку
success = synology_api.reboot_system()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
)
# Ждем некоторое время перед проверкой статуса
await context.bot.send_message(
chat_id=chat_id,
text="⏳ Ожидание перезагрузки системы..."
)
# Создаем задачу для ожидания загрузки
wait_successful = synology_api.wait_for_boot()
if wait_successful:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно перезагружен и снова онлайн"
)
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during reboot: {str(e)}")
return False

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Обработчики команд для телеграм-бота
"""
import logging
import socket
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import (
ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
)
from src.api.synology import SynologyAPI
from src.utils.admin_utils import admin_required
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
from src.utils.admin_utils import admin_required
@admin_required
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /status"""
message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
is_online = synology_api.is_online()
if is_online:
try:
# Если NAS включен, попробуем получить дополнительную информацию
system_info = synology_api.get_system_status()
if system_info and system_info.get("status") != "error":
model = system_info.get("model", "Неизвестная модель")
version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
# Преобразование времени работы в удобочитаемый формат
days, remainder = divmod(int(uptime_seconds), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Модель:</b> {model}\n"
f"<b>Версия DSM:</b> {version}\n"
f"<b>Время работы:</b> {uptime_str}",
parse_mode="HTML"
)
else:
# Обработка возможной ошибки API
error_info = ""
if system_info and system_info.get("status") == "error":
error_code = system_info.get("error_code", "неизвестно")
error_info = f"\n<i>Код ошибки API: {error_code}</i>"
# Проверяем порт и сеть
network_info = ""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
s.close()
if result == 0:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>открыт</b>"
else:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>закрыт</b> (код {result})"
except Exception as e:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"Устройство доступно по сети, но детальная информация через API недоступна. "
f"Возможно, необходимо проверить учетные данные или права доступа."
f"{error_info}"
f"{network_info}",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Ошибка при получении информации:</b> {str(e)[:100]}...\n\n"
f"<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
parse_mode="HTML"
)
else:
# Устройство не в сети, проверим соседние порты для диагностики
port_scan_info = ""
try:
for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, test_port))
s.close()
status = "открыт" if result == 0 else "закрыт"
port_scan_info += f"Порт {test_port}: <b>{status}</b>\n"
# Добавим информацию о MAC-адресе для WoL
mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Информация о сети:</b>\n"
f"IP: {SYNOLOGY_HOST}\n"
f"{port_scan_info}\n"
f"{mac_info}\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Ошибка при сканировании портов:</b> {str(e)[:100]}...\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
@admin_required
async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /power"""
is_online = synology_api.is_online()
keyboard = []
# Кнопка включения
if not is_online:
keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
else:
keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
# Кнопка выключения
if is_online:
keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
else:
keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
# Кнопка перезагрузки
if is_online:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
else:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
# Кнопка отмены
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
reply_markup = InlineKeyboardMarkup(keyboard)
status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
await update.message.reply_text(
f"<b>Управление питанием Synology NAS</b>\n\n"
f"<b>Текущий статус:</b> {status_text}\n\n"
f"Выберите действие:",
reply_markup=reply_markup,
parse_mode="HTML"
)
@admin_required
async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для кнопок управления питанием"""
query = update.callback_query
await query.answer()
action = query.data
if action == "cancel":
await query.edit_message_text("❌ Действие отменено")
return
# Обработка неактивных кнопок
if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
if action == "power_on_no_op":
await query.edit_message_text(" Synology NAS уже включен")
elif action == "power_off_no_op":
await query.edit_message_text(" Synology NAS уже выключен")
else:
await query.edit_message_text(" Невозможно перезагрузить выключенное устройство")
return
# Обработка основных действий
if action == "power_on":
await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
if await context.application.create_task(
handle_power_on(query.message.chat_id, context)
):
# Функция вернула True, успешное включение
pass
else:
# Функция вернула False, ошибка включения
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
)
elif action == "power_off":
await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
try:
success = await handle_power_off(query.message.chat_id, context)
# Если handle_power_off уже отправил сообщение об успехе или ошибке,
# дополнительных сообщений не требуется
except Exception as e:
logger.error(f"Exception in power_off callback: {str(e)}")
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
)
elif action == "reboot":
await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
if await context.application.create_task(
handle_reboot(query.message.chat_id, context)
):
# Функция вернула True, успешная перезагрузка
pass
else:
# Функция вернула False, ошибка перезагрузки
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
)
async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для включения NAS"""
try:
# Отправка запроса на включение
success = synology_api.power_on()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно включен и доступен"
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during power on: {str(e)}")
return False
async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для выключения NAS"""
try:
# Проверка доступности NAS
if not synology_api.is_online():
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
)
return False
# Отправка запроса на выключение
success = synology_api.power_off()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
)
return True
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
)
return False
except Exception as e:
error_msg = str(e)
logger.error(f"Error during power off: {error_msg}")
await context.bot.send_message(
chat_id=chat_id,
text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
)
return False
async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для перезагрузки NAS"""
try:
# Отправка запроса на перезагрузку
success = synology_api.reboot_system()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
)
# Ждем некоторое время перед проверкой статуса
await context.bot.send_message(
chat_id=chat_id,
text="⏳ Ожидание перезагрузки системы..."
)
# Создаем задачу для ожидания загрузки
wait_successful = synology_api.wait_for_boot()
if wait_successful:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно перезагружен и снова онлайн"
)
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during reboot: {str(e)}")
return False

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Обработчики команд для телеграм-бота
"""
import logging
import socket
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import (
ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
)
from src.api.synology import SynologyAPI
from src.utils.admin_utils import admin_required
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
from src.utils.admin_utils import admin_required
@admin_required
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /status"""
message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
is_online = synology_api.is_online()
if is_online:
try:
# Если NAS включен, попробуем получить дополнительную информацию
system_info = synology_api.get_system_status()
if system_info and system_info.get("status") != "error":
model = system_info.get("model", "Неизвестная модель")
version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
# Преобразование времени работы в удобочитаемый формат
days, remainder = divmod(int(uptime_seconds), 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Модель:</b> {model}\n"
f"<b>Версия DSM:</b> {version}\n"
f"<b>Время работы:</b> {uptime_str}",
parse_mode="HTML"
)
else:
# Обработка возможной ошибки API
error_info = ""
if system_info and system_info.get("status") == "error":
error_code = system_info.get("error_code", "неизвестно")
error_info = f"\n<i>Код ошибки API: {error_code}</i>"
# Проверяем порт и сеть
network_info = ""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
s.close()
if result == 0:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>открыт</b>"
else:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: <b>закрыт</b> (код {result})"
except Exception as e:
network_info = f"\n\n<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"Устройство доступно по сети, но детальная информация через API недоступна. "
f"Возможно, необходимо проверить учетные данные или права доступа."
f"{error_info}"
f"{network_info}",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"✅ <b>Synology NAS онлайн</b>\n\n"
f"<b>Ошибка при получении информации:</b> {str(e)[:100]}...\n\n"
f"<b>Сетевая информация:</b>\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
parse_mode="HTML"
)
else:
# Устройство не в сети, проверим соседние порты для диагностики
port_scan_info = ""
try:
for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
result = s.connect_ex((SYNOLOGY_HOST, test_port))
s.close()
status = "открыт" if result == 0 else "закрыт"
port_scan_info += f"Порт {test_port}: <b>{status}</b>\n"
# Добавим информацию о MAC-адресе для WoL
mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Информация о сети:</b>\n"
f"IP: {SYNOLOGY_HOST}\n"
f"{port_scan_info}\n"
f"{mac_info}\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
except Exception as e:
await message.edit_text(
f"❌ <b>Synology NAS оффлайн</b>\n\n"
f"<b>Ошибка при сканировании портов:</b> {str(e)[:100]}...\n\n"
f"Используйте /power для отправки Wake-on-LAN пакета",
parse_mode="HTML"
)
@admin_required
async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /power"""
is_online = synology_api.is_online()
keyboard = []
# Кнопка включения
if not is_online:
keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
else:
keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
# Кнопка выключения
if is_online:
keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
else:
keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
# Кнопка перезагрузки
if is_online:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
else:
keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
# Кнопка отмены
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
reply_markup = InlineKeyboardMarkup(keyboard)
status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
await update.message.reply_text(
f"<b>Управление питанием Synology NAS</b>\n\n"
f"<b>Текущий статус:</b> {status_text}\n\n"
f"Выберите действие:",
reply_markup=reply_markup,
parse_mode="HTML"
)
@admin_required
async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для кнопок управления питанием"""
query = update.callback_query
await query.answer()
action = query.data
if action == "cancel":
await query.edit_message_text("❌ Действие отменено")
return
# Обработка неактивных кнопок
if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
if action == "power_on_no_op":
await query.edit_message_text(" Synology NAS уже включен")
elif action == "power_off_no_op":
await query.edit_message_text(" Synology NAS уже выключен")
else:
await query.edit_message_text(" Невозможно перезагрузить выключенное устройство")
return
# Обработка основных действий
if action == "power_on":
await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
if await context.application.create_task(
handle_power_on(query.message.chat_id, context)
):
# Функция вернула True, успешное включение
pass
else:
# Функция вернула False, ошибка включения
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
)
elif action == "power_off":
await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
try:
success = await handle_power_off(query.message.chat_id, context)
# Если handle_power_off уже отправил сообщение об успехе или ошибке,
# дополнительных сообщений не требуется
except Exception as e:
logger.error(f"Exception in power_off callback: {str(e)}")
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
)
elif action == "reboot":
await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
if await context.application.create_task(
handle_reboot(query.message.chat_id, context)
):
# Функция вернула True, успешная перезагрузка
pass
else:
# Функция вернула False, ошибка перезагрузки
await context.bot.send_message(
chat_id=query.message.chat_id,
text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
)
async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для включения NAS"""
try:
# Отправка запроса на включение
success = synology_api.power_on()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно включен и доступен"
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during power on: {str(e)}")
return False
async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для выключения NAS"""
try:
# Проверка доступности NAS
if not synology_api.is_online():
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
)
return False
# Отправка запроса на выключение
success = synology_api.power_off()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
)
return True
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
)
return False
except Exception as e:
error_msg = str(e)
logger.error(f"Error during power off: {error_msg}")
await context.bot.send_message(
chat_id=chat_id,
text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
)
return False
async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Асинхронная функция для перезагрузки NAS"""
try:
# Отправка запроса на перезагрузку
success = synology_api.reboot_system()
if success:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
)
# Ждем некоторое время перед проверкой статуса
await context.bot.send_message(
chat_id=chat_id,
text="⏳ Ожидание перезагрузки системы..."
)
# Создаем задачу для ожидания загрузки
wait_successful = synology_api.wait_for_boot()
if wait_successful:
await context.bot.send_message(
chat_id=chat_id,
text="✅ Synology NAS успешно перезагружен и снова онлайн"
)
else:
await context.bot.send_message(
chat_id=chat_id,
text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
)
return True
else:
return False
except Exception as e:
logger.error(f"Error during reboot: {str(e)}")
return False

View File

@@ -0,0 +1,378 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Дополнительные обработчики команд для телеграм-бота
"""
import logging
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /checkapi для диагностики проблем с API"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
from src.api.api_discovery import discover_available_apis
from src.config.config import (
SYNOLOGY_HOST,
SYNOLOGY_PORT,
SYNOLOGY_SECURE,
SYNOLOGY_POWER_API,
SYNOLOGY_INFO_API,
SYNOLOGY_API_VERSION
)
# Формируем базовый URL
protocol = "https" if SYNOLOGY_SECURE else "http"
base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
# Получаем список доступных API
apis = discover_available_apis(base_url)
if not apis:
await message.edit_text(
"❌ <b>Не удалось получить список доступных API</b>\n\n"
"Проверьте доступность NAS и сетевое подключение.",
parse_mode="HTML"
)
return
# Поиск API для управления питанием
power_apis = [name for name in apis.keys() if "power" in name.lower()]
system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
# Формируем рекомендации
recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
# Формируем текст отчета
api_report = (
f"✅ <b>Найдено {len(apis)} доступных API</b>\n\n"
f"<b>API для управления питанием:</b>\n"
f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
f"<b>API для информации о системе:</b>\n"
f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
f"<b>API для перезагрузки:</b>\n"
f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
f"<b>Рекомендуемые настройки:</b>\n"
f"Power API: {recommended_power_api}\n"
f"Info API: {recommended_info_api}\n\n"
f"<b>Текущие настройки в конфигурации:</b>\n"
f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
)
await message.edit_text(api_report, parse_mode="HTML")
async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /storage"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о хранилище...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
return
try:
storage_info = synology_api.get_storage_status()
if not storage_info:
await message.edit_text("❌ <b>Ошибка получения информации о хранилище</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о хранилище</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о состоянии хранилища
summary = storage_info.get("summary", {})
total_size_gb = summary.get("total_space_gb", 0)
total_used_gb = summary.get("used_space_gb", 0)
free_space_gb = summary.get("free_space_gb", 0)
usage_percent = summary.get("usage_percent", 0)
reply_text = f"📊 <b>Информация о хранилище Synology NAS</b>\n\n"
reply_text += f"<b>Общий размер:</b> {total_size_gb:.2f} ГБ\n"
reply_text += f"<b>Использовано:</b> {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
reply_text += f"<b>Свободно:</b> {free_space_gb:.2f} ГБ\n\n"
# Добавляем информацию о томах
volumes = storage_info.get("volumes", [])
if volumes:
reply_text += "<b>Тома:</b>\n"
for volume in volumes:
name = volume.get("name", "Неизвестно")
status = volume.get("status", "Неизвестно")
size = volume.get("size", 0)
used_size = volume.get("used_size", 0)
size_gb = size / (1024**3)
used_gb = used_size / (1024**3)
percent = round((used_size / size) * 100, 1) if size > 0 else 0
reply_text += f"• <b>{name}</b> ({status})\n"
reply_text += f"{used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
# Добавляем информацию о дисках
disks = storage_info.get("disks", [])
if disks:
reply_text += "\n<b>Диски:</b>\n"
for disk in disks:
name = disk.get("name", "Неизвестно")
model = disk.get("model", "Неизвестно")
status = disk.get("status", "Неизвестно")
temp = disk.get("temp", "?")
reply_text += f"• <b>{name}</b> - {model}\n"
reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /shares"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации об общих папках...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
return
try:
shares = synology_api.get_shared_folders()
if not shares:
await message.edit_text("❌ <b>Ошибка получения информации об общих папках</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации об общих папках</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение об общих папках
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
for share in shares:
name = share.get("name", "Неизвестно")
path = share.get("path", "Неизвестно")
desc = share.get("desc", "")
reply_text += f"• <b>{name}</b>\n"
reply_text += f" └ Путь: {path}\n"
if desc:
reply_text += f" └ Описание: {desc}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /system"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о системе...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
return
try:
system_status = synology_api.get_system_status()
if not system_status:
await message.edit_text("❌ <b>Ошибка получения информации о системе</b>", parse_mode="HTML")
return
# Если получен статус с ошибкой
if system_status.get("status") == "error":
error_code = system_status.get("error_code", "неизвестно")
await message.edit_text(f"❌ <b>Ошибка получения информации о системе</b>\n\nКод ошибки API: {error_code}", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о системе</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о состоянии системы
model = system_status.get("model", "Неизвестно")
version = system_status.get("version", "Неизвестно")
serial = system_status.get("serial", "Неизвестно")
uptime_seconds = system_status.get("uptime", 0)
temperature = system_status.get("temperature", "?")
# Преобразование времени работы в удобочитаемый формат
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
reply_text = f"🖥️ <b>Информация о системе Synology NAS</b>\n\n"
reply_text += f"<b>Модель:</b> {model}\n"
reply_text += f"<b>Серийный номер:</b> {serial}\n"
reply_text += f"<b>Версия DSM:</b> {version}\n"
reply_text += f"<b>Время работы:</b> {uptime_str}\n"
reply_text += f"<b>Температура:</b> {temperature}°C\n\n"
# Добавляем информацию о CPU и памяти
memory = system_status.get("memory", {})
total_memory_gb = memory.get("total_mb", 0) / 1024
available_memory_gb = memory.get("available_mb", 0) / 1024
memory_usage = memory.get("usage_percent", 0)
cpu_usage = system_status.get("cpu_usage", 0)
reply_text += f"<b>Загрузка CPU:</b> {cpu_usage}%\n"
reply_text += f"<b>Память:</b> {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
reply_text += f"<b>Доступно памяти:</b> {available_memory_gb:.1f} ГБ\n\n"
# Добавляем информацию о сетевых интерфейсах
network_info = system_status.get("network", [])
if network_info:
reply_text += "<b>Сетевые интерфейсы:</b>\n"
for interface in network_info:
device = interface.get("device", "Неизвестно")
ip = interface.get("ip", "Неизвестно")
mac = interface.get("mac", "Неизвестно")
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ IP: {ip}, MAC: {mac}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /load"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
return
try:
system_load = synology_api.get_system_load()
if not system_load:
await message.edit_text("❌ <b>Ошибка получения информации о нагрузке системы</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о нагрузке системы</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о нагрузке системы
cpu_load = system_load.get("cpu_load", 0)
memory = system_load.get("memory", {})
memory_usage = memory.get("usage_percent", 0)
reply_text = f"📈 <b>Текущая нагрузка Synology NAS</b>\n\n"
reply_text += f"<b>Загрузка CPU:</b> {cpu_load}%\n"
reply_text += f"<b>Загрузка памяти:</b> {memory_usage}%\n\n"
# Добавляем информацию о сетевой активности
network = system_load.get("network", [])
if network:
reply_text += "<b>Сетевая активность:</b>\n"
if isinstance(network, dict):
# Если это словарь (старое API)
for device, stats in network.items():
rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
elif isinstance(network, list):
# Если это список (новое API)
for interface in network:
device = interface.get("device", "неизвестно")
rx = int(interface.get("rx", 0)) / (1024**2) # МБ
tx = int(interface.get("tx", 0)) / (1024**2) # МБ
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /security"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о безопасности...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
return
try:
security_info = synology_api.get_security_status()
if not security_info.get("success", False):
await message.edit_text("❌ <b>Ошибка получения информации о безопасности</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о безопасности</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о безопасности
status = security_info.get("status", "unknown")
is_secure = security_info.get("is_secure", False)
last_check = security_info.get("last_check", "Неизвестно")
status_emoji = "" if is_secure else "⚠️"
status_text = "Безопасно" if is_secure else "Требуется внимание"
reply_text = f"🔐 <b>Статус безопасности Synology NAS</b>\n\n"
reply_text += f"<b>Статус:</b> {status_emoji} {status_text}\n"
reply_text += f"<b>Подробности:</b> {status}\n"
reply_text += f"<b>Последняя проверка:</b> {last_check}\n"
await message.edit_text(reply_text, parse_mode="HTML")

View File

@@ -0,0 +1,378 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Дополнительные обработчики команд для телеграм-бота
"""
import logging
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
from src.api.synology import SynologyAPI
logger = logging.getLogger(__name__)
# Инициализация API Synology
synology_api = SynologyAPI()
async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /checkapi для диагностики проблем с API"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
from src.api.api_discovery import discover_available_apis
from src.config.config import (
SYNOLOGY_HOST,
SYNOLOGY_PORT,
SYNOLOGY_SECURE,
SYNOLOGY_POWER_API,
SYNOLOGY_INFO_API,
SYNOLOGY_API_VERSION
)
# Формируем базовый URL
protocol = "https" if SYNOLOGY_SECURE else "http"
base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
# Получаем список доступных API
apis = discover_available_apis(base_url)
if not apis:
await message.edit_text(
"❌ <b>Не удалось получить список доступных API</b>\n\n"
"Проверьте доступность NAS и сетевое подключение.",
parse_mode="HTML"
)
return
# Поиск API для управления питанием
power_apis = [name for name in apis.keys() if "power" in name.lower()]
system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
# Формируем рекомендации
recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
# Формируем текст отчета
api_report = (
f"✅ <b>Найдено {len(apis)} доступных API</b>\n\n"
f"<b>API для управления питанием:</b>\n"
f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
f"<b>API для информации о системе:</b>\n"
f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
f"<b>API для перезагрузки:</b>\n"
f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
f"<b>Рекомендуемые настройки:</b>\n"
f"Power API: {recommended_power_api}\n"
f"Info API: {recommended_info_api}\n\n"
f"<b>Текущие настройки в конфигурации:</b>\n"
f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
)
await message.edit_text(api_report, parse_mode="HTML")
async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /storage"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о хранилище...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
return
try:
storage_info = synology_api.get_storage_status()
if not storage_info:
await message.edit_text("❌ <b>Ошибка получения информации о хранилище</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о хранилище</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о состоянии хранилища
summary = storage_info.get("summary", {})
total_size_gb = summary.get("total_space_gb", 0)
total_used_gb = summary.get("used_space_gb", 0)
free_space_gb = summary.get("free_space_gb", 0)
usage_percent = summary.get("usage_percent", 0)
reply_text = f"📊 <b>Информация о хранилище Synology NAS</b>\n\n"
reply_text += f"<b>Общий размер:</b> {total_size_gb:.2f} ГБ\n"
reply_text += f"<b>Использовано:</b> {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
reply_text += f"<b>Свободно:</b> {free_space_gb:.2f} ГБ\n\n"
# Добавляем информацию о томах
volumes = storage_info.get("volumes", [])
if volumes:
reply_text += "<b>Тома:</b>\n"
for volume in volumes:
name = volume.get("name", "Неизвестно")
status = volume.get("status", "Неизвестно")
size = volume.get("size", 0)
used_size = volume.get("used_size", 0)
size_gb = size / (1024**3)
used_gb = used_size / (1024**3)
percent = round((used_size / size) * 100, 1) if size > 0 else 0
reply_text += f"• <b>{name}</b> ({status})\n"
reply_text += f"{used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
# Добавляем информацию о дисках
disks = storage_info.get("disks", [])
if disks:
reply_text += "\n<b>Диски:</b>\n"
for disk in disks:
name = disk.get("name", "Неизвестно")
model = disk.get("model", "Неизвестно")
status = disk.get("status", "Неизвестно")
temp = disk.get("temp", "?")
reply_text += f"• <b>{name}</b> - {model}\n"
reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /shares"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации об общих папках...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
return
try:
shares = synology_api.get_shared_folders()
if not shares:
await message.edit_text("❌ <b>Ошибка получения информации об общих папках</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации об общих папках</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение об общих папках
reply_text = f"📁 <b>Общие папки Synology NAS</b>\n\n"
for share in shares:
name = share.get("name", "Неизвестно")
path = share.get("path", "Неизвестно")
desc = share.get("desc", "")
reply_text += f"• <b>{name}</b>\n"
reply_text += f" └ Путь: {path}\n"
if desc:
reply_text += f" └ Описание: {desc}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /system"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о системе...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
return
try:
system_status = synology_api.get_system_status()
if not system_status:
await message.edit_text("❌ <b>Ошибка получения информации о системе</b>", parse_mode="HTML")
return
# Если получен статус с ошибкой
if system_status.get("status") == "error":
error_code = system_status.get("error_code", "неизвестно")
await message.edit_text(f"❌ <b>Ошибка получения информации о системе</b>\n\nКод ошибки API: {error_code}", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о системе</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о состоянии системы
model = system_status.get("model", "Неизвестно")
version = system_status.get("version", "Неизвестно")
serial = system_status.get("serial", "Неизвестно")
uptime_seconds = system_status.get("uptime", 0)
temperature = system_status.get("temperature", "?")
# Преобразование времени работы в удобочитаемый формат
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
reply_text = f"🖥️ <b>Информация о системе Synology NAS</b>\n\n"
reply_text += f"<b>Модель:</b> {model}\n"
reply_text += f"<b>Серийный номер:</b> {serial}\n"
reply_text += f"<b>Версия DSM:</b> {version}\n"
reply_text += f"<b>Время работы:</b> {uptime_str}\n"
reply_text += f"<b>Температура:</b> {temperature}°C\n\n"
# Добавляем информацию о CPU и памяти
memory = system_status.get("memory", {})
total_memory_gb = memory.get("total_mb", 0) / 1024
available_memory_gb = memory.get("available_mb", 0) / 1024
memory_usage = memory.get("usage_percent", 0)
cpu_usage = system_status.get("cpu_usage", 0)
reply_text += f"<b>Загрузка CPU:</b> {cpu_usage}%\n"
reply_text += f"<b>Память:</b> {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
reply_text += f"<b>Доступно памяти:</b> {available_memory_gb:.1f} ГБ\n\n"
# Добавляем информацию о сетевых интерфейсах
network_info = system_status.get("network", [])
if network_info:
reply_text += "<b>Сетевые интерфейсы:</b>\n"
for interface in network_info:
device = interface.get("device", "Неизвестно")
ip = interface.get("ip", "Неизвестно")
mac = interface.get("mac", "Неизвестно")
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ IP: {ip}, MAC: {mac}\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /load"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
return
try:
system_load = synology_api.get_system_load()
if not system_load:
await message.edit_text("❌ <b>Ошибка получения информации о нагрузке системы</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о нагрузке системы</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о нагрузке системы
cpu_load = system_load.get("cpu_load", 0)
memory = system_load.get("memory", {})
memory_usage = memory.get("usage_percent", 0)
reply_text = f"📈 <b>Текущая нагрузка Synology NAS</b>\n\n"
reply_text += f"<b>Загрузка CPU:</b> {cpu_load}%\n"
reply_text += f"<b>Загрузка памяти:</b> {memory_usage}%\n\n"
# Добавляем информацию о сетевой активности
network = system_load.get("network", [])
if network:
reply_text += "<b>Сетевая активность:</b>\n"
if isinstance(network, dict):
# Если это словарь (старое API)
for device, stats in network.items():
rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
elif isinstance(network, list):
# Если это список (новое API)
for interface in network:
device = interface.get("device", "неизвестно")
rx = int(interface.get("rx", 0)) / (1024**2) # МБ
tx = int(interface.get("tx", 0)) / (1024**2) # МБ
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML")
async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /security"""
if not update.message or not update.effective_user:
return
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Получение информации о безопасности...")
if not synology_api.is_online():
await message.edit_text("❌ <b>Synology NAS оффлайн</b>\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
return
try:
security_info = synology_api.get_security_status()
if not security_info.get("success", False):
await message.edit_text("❌ <b>Ошибка получения информации о безопасности</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return
except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о безопасности</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return
# Формируем сообщение о безопасности
status = security_info.get("status", "unknown")
is_secure = security_info.get("is_secure", False)
last_check = security_info.get("last_check", "Неизвестно")
status_emoji = "" if is_secure else "⚠️"
status_text = "Безопасно" if is_secure else "Требуется внимание"
reply_text = f"🔐 <b>Статус безопасности Synology NAS</b>\n\n"
reply_text += f"<b>Статус:</b> {status_emoji} {status_text}\n"
reply_text += f"<b>Подробности:</b> {status}\n"
reply_text += f"<b>Последняя проверка:</b> {last_check}\n"
await message.edit_text(reply_text, parse_mode="HTML")

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Модуль с функциями для генерации справочных сообщений о командах бота
"""
import logging
from telegram import Update
from telegram.ext import ContextTypes
from telegram.constants import ParseMode
from src.config.config import ADMIN_USER_IDS
logger = logging.getLogger(__name__)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработчик команды /help - выводит справку по всем доступным командам
"""
user_id = update.effective_user.id if update.effective_user else "Unknown"
username = update.effective_user.username if update.effective_user else "Unknown"
logger.info(f"User {user_id} (@{username}) requested help")
# Проверка прав доступа
if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
if update.message:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
help_text = (
"🤖 <b>Synology Power Control Bot</b>\n\n"
"<b>БАЗОВЫЕ КОМАНДЫ:</b>\n"
"/status - Проверить состояние NAS\n"
"/power - Управление питанием NAS (меню)\n"
"/reboot - Перезагрузка NAS (с подтверждением)\n"
"/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
"<b>ИНФОРМАЦИОННЫЕ КОМАНДЫ:</b>\n"
"/system - Информация о системе\n"
"/storage - Состояние хранилища\n"
"/shares - Список общих папок\n"
"/load - Нагрузка на систему\n"
"/security - Информация о безопасности\n"
"/temperature - Температура устройства\n"
"/processes - Список активных процессов\n"
"/network - Сетевая информация\n\n"
"<b>РАСШИРЕННЫЕ КОМАНДЫ:</b>\n"
"/schedule - Расписание питания\n"
"/browse - Просмотр файлов\n"
"/search &lt;запрос&gt; - Поиск файлов\n"
"/updates - Проверка обновлений\n"
"/backup - Статус резервного копирования\n"
"/quota - Квоты пользователей\n\n"
"<b>БЫСТРЫЕ КОМАНДЫ:</b>\n"
"/quickreboot - Быстрая перезагрузка\n"
"/wakeup - Пробуждение NAS (WOL)\n\n"
"<b>СЛУЖЕБНЫЕ КОМАНДЫ:</b>\n"
"/checkapi - Проверка API\n"
"/help - Эта справка\n\n"
"<b>УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ:</b>\n"
"/admins - Список администраторов\n"
"/addadmin &lt;id&gt; - Добавить администратора\n"
"/removeadmin &lt;id&gt; - Удалить администратора\n"
)
if update.message:
await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
elif update.callback_query:
await update.callback_query.answer()
if update.callback_query.message:
try:
await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Failed to edit message: {e}")
# Отправляем новое сообщение в текущий чат
await context.bot.send_message(
chat_id=update.callback_query.message.chat_id,
text=help_text,
parse_mode=ParseMode.HTML
)
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработчик команды /start - приветствие и краткая информация
"""
user_id = update.effective_user.id if update.effective_user else "Unknown"
username = update.effective_user.username if update.effective_user else "Unknown"
logger.info(f"User {user_id} (@{username}) started the bot")
# Проверка прав доступа
if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
if update.message:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
welcome_text = (
"👋 <b>Добро пожаловать в Synology Power Control Bot!</b>\n\n"
"С помощью этого бота вы можете управлять питанием вашего Synology NAS "
"и получать различную информацию о его состоянии.\n\n"
"Для просмотра списка доступных команд используйте /help\n\n"
"Базовые команды:\n"
"• /status - Проверить состояние NAS\n"
"• /power - Управление питанием\n"
"• /system - Информация о системе\n"
"• /storage - Состояние хранилища"
)
if update.message:
await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Модуль с функциями для генерации справочных сообщений о командах бота
"""
import logging
from telegram import Update
from telegram.ext import ContextTypes
from telegram.constants import ParseMode
from src.config.config import ADMIN_USER_IDS
logger = logging.getLogger(__name__)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработчик команды /help - выводит справку по всем доступным командам
"""
user_id = update.effective_user.id if update.effective_user else "Unknown"
username = update.effective_user.username if update.effective_user else "Unknown"
logger.info(f"User {user_id} (@{username}) requested help")
# Проверка прав доступа
if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
if update.message:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
help_text = (
"🤖 <b>Synology Power Control Bot</b>\n\n"
"<b>БАЗОВЫЕ КОМАНДЫ:</b>\n"
"/status - Проверить состояние NAS\n"
"/power - Управление питанием NAS (меню)\n"
"/reboot - Перезагрузка NAS (с подтверждением)\n"
"/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
"<b>ИНФОРМАЦИОННЫЕ КОМАНДЫ:</b>\n"
"/system - Информация о системе\n"
"/storage - Состояние хранилища\n"
"/shares - Список общих папок\n"
"/load - Нагрузка на систему\n"
"/security - Информация о безопасности\n"
"/temperature - Температура устройства\n"
"/processes - Список активных процессов\n"
"/network - Сетевая информация\n\n"
"<b>РАСШИРЕННЫЕ КОМАНДЫ:</b>\n"
"/schedule - Расписание питания\n"
"/browse - Просмотр файлов\n"
"/search &lt;запрос&gt; - Поиск файлов\n"
"/updates - Проверка обновлений\n"
"/backup - Статус резервного копирования\n"
"/quota - Квоты пользователей\n\n"
"<b>БЫСТРЫЕ КОМАНДЫ:</b>\n"
"/quickreboot - Быстрая перезагрузка\n"
"/wakeup - Пробуждение NAS (WOL)\n\n"
"<b>СЛУЖЕБНЫЕ КОМАНДЫ:</b>\n"
"/checkapi - Проверка API\n"
"/help - Эта справка\n\n"
"<b>УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ:</b>\n"
"/admins - Список администраторов\n"
"/addadmin &lt;id&gt; - Добавить администратора\n"
"/removeadmin &lt;id&gt; - Удалить администратора\n"
)
if update.message:
await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
elif update.callback_query:
await update.callback_query.answer()
if update.callback_query.message:
try:
await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
except Exception as e:
logger.error(f"Failed to edit message: {e}")
# Отправляем новое сообщение в текущий чат
await context.bot.send_message(
chat_id=update.callback_query.message.chat_id,
text=help_text,
parse_mode=ParseMode.HTML
)
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
Обработчик команды /start - приветствие и краткая информация
"""
user_id = update.effective_user.id if update.effective_user else "Unknown"
username = update.effective_user.username if update.effective_user else "Unknown"
logger.info(f"User {user_id} (@{username}) started the bot")
# Проверка прав доступа
if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
if update.message:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
welcome_text = (
"👋 <b>Добро пожаловать в Synology Power Control Bot!</b>\n\n"
"С помощью этого бота вы можете управлять питанием вашего Synology NAS "
"и получать различную информацию о его состоянии.\n\n"
"Для просмотра списка доступных команд используйте /help\n\n"
"Базовые команды:\n"
"• /status - Проверить состояние NAS\n"
"• /power - Управление питанием\n"
"• /system - Информация о системе\n"
"• /storage - Состояние хранилища"
)
if update.message:
await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)

View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Утилиты для управления администраторами бота
"""
import os
import logging
from typing import List, Optional, Callable, Any, Union
from functools import wraps
from telegram import Update
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
# Настройка логирования
logger = logging.getLogger(__name__)
def is_admin(user_id: int) -> bool:
"""Проверяет, является ли пользователь администратором бота
Args:
user_id: ID пользователя Telegram
Returns:
True если пользователь администратор, иначе False
"""
return user_id in ADMIN_USER_IDS
def admin_required(func: Callable) -> Callable:
"""Декоратор для проверки, является ли пользователь администратором
Args:
func: Оригинальная функция обработчика
Returns:
Обернутая функция с проверкой прав администратора
"""
@wraps(func)
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
# Проверяем доступность объекта update и effective_user
if not update or not update.effective_user:
logger.warning("Update object is incomplete, unable to check admin status")
return
user_id = update.effective_user.id
username = update.effective_user.username or "Unknown"
if not is_admin(user_id):
logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
# Если это сообщение, отправляем уведомление
if update.message:
await update.message.reply_text(
"⛔️ У вас нет прав на использование этой команды.\n"
"Обратитесь к владельцу бота, чтобы получить доступ."
)
# Если это callback query, отвечаем на него
elif update.callback_query:
await update.callback_query.answer(
"⛔️ У вас нет прав на использование этой функции."
)
return
# Если пользователь админ, вызываем оригинальную функцию
return await func(update, context, *args, **kwargs)
return wrapper
async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Добавляет нового администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID нового администратора
new_admin_id = int(context.args[0])
# Проверяем, не является ли пользователь уже администратором
if new_admin_id in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {new_admin_id} уже является администратором.")
return
# Добавляем нового администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Обновляем или добавляем строку с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip()
new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
else:
lines.append(f"ADMIN_USER_IDS={new_admin_id}")
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.append(new_admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{new_admin_id}</code> добавлен в список администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error adding admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при добавлении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Удаляет администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID администратора для удаления
admin_id = int(context.args[0])
# Проверяем, не удаляет ли админ сам себя
if admin_id == update.effective_user.id:
await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
return
# Проверяем, является ли пользователь администратором
if admin_id not in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {admin_id} не является администратором.")
return
# Удаляем администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Удаляем ID из строки с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
new_ids = [id for id in current_ids if int(id) != admin_id]
if not new_ids:
# Если не осталось администраторов, добавляем текущего пользователя
# чтобы избежать ситуации, когда нет администраторов
new_ids = [str(update.effective_user.id)]
lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.remove(admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{admin_id}</code> удален из списка администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
else:
await update.message.reply_text("❌ <b>Ошибка:</b> Не удалось найти настройки администраторов.")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error removing admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при удалении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Показывает список администраторов бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
try:
if not ADMIN_USER_IDS:
await update.message.reply_text("⚠️ Список администраторов пуст.")
return
# Формируем сообщение со списком администраторов
admin_list = "\n".join([f"• <code>{admin_id}</code>" for admin_id in ADMIN_USER_IDS])
await update.message.reply_text(
f"👑 <b>Список администраторов бота:</b>\n\n"
f"{admin_list}\n\n"
f"Всего администраторов: {len(ADMIN_USER_IDS)}",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error listing admins: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при получении списка администраторов:</b>\n\n{str(e)}",
parse_mode="HTML"
)

View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Утилиты для управления администраторами бота
"""
import os
import logging
from typing import List, Optional, Callable, Any, Union
from functools import wraps
from telegram import Update
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
# Настройка логирования
logger = logging.getLogger(__name__)
def is_admin(user_id: int) -> bool:
"""Проверяет, является ли пользователь администратором бота
Args:
user_id: ID пользователя Telegram
Returns:
True если пользователь администратор, иначе False
"""
return user_id in ADMIN_USER_IDS
def admin_required(func: Callable) -> Callable:
"""Декоратор для проверки, является ли пользователь администратором
Args:
func: Оригинальная функция обработчика
Returns:
Обернутая функция с проверкой прав администратора
"""
@wraps(func)
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
# Проверяем доступность объекта update и effective_user
if not update or not update.effective_user:
logger.warning("Update object is incomplete, unable to check admin status")
return
user_id = update.effective_user.id
username = update.effective_user.username or "Unknown"
if not is_admin(user_id):
logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
# Если это сообщение, отправляем уведомление
if update.message:
await update.message.reply_text(
"⛔️ У вас нет прав на использование этой команды.\n"
"Обратитесь к владельцу бота, чтобы получить доступ."
)
# Если это callback query, отвечаем на него
elif update.callback_query:
await update.callback_query.answer(
"⛔️ У вас нет прав на использование этой функции."
)
return
# Если пользователь админ, вызываем оригинальную функцию
return await func(update, context, *args, **kwargs)
return wrapper
async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Добавляет нового администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID нового администратора
new_admin_id = int(context.args[0])
# Проверяем, не является ли пользователь уже администратором
if new_admin_id in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {new_admin_id} уже является администратором.")
return
# Добавляем нового администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Обновляем или добавляем строку с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip()
new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
else:
lines.append(f"ADMIN_USER_IDS={new_admin_id}")
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.append(new_admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{new_admin_id}</code> добавлен в список администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error adding admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при добавлении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Удаляет администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID администратора для удаления
admin_id = int(context.args[0])
# Проверяем, не удаляет ли админ сам себя
if admin_id == update.effective_user.id:
await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
return
# Проверяем, является ли пользователь администратором
if admin_id not in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {admin_id} не является администратором.")
return
# Удаляем администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Удаляем ID из строки с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
new_ids = [id for id in current_ids if int(id) != admin_id]
if not new_ids:
# Если не осталось администраторов, добавляем текущего пользователя
# чтобы избежать ситуации, когда нет администраторов
new_ids = [str(update.effective_user.id)]
lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.remove(admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{admin_id}</code> удален из списка администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
else:
await update.message.reply_text("❌ <b>Ошибка:</b> Не удалось найти настройки администраторов.")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error removing admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при удалении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Показывает список администраторов бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
try:
if not ADMIN_USER_IDS:
await update.message.reply_text("⚠️ Список администраторов пуст.")
return
# Формируем сообщение со списком администраторов
admin_list = "\n".join([f"• <code>{admin_id}</code>" for admin_id in ADMIN_USER_IDS])
await update.message.reply_text(
f"👑 <b>Список администраторов бота:</b>\n\n"
f"{admin_list}\n\n"
f"Всего администраторов: {len(ADMIN_USER_IDS)}",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error listing admins: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при получении списка администраторов:</b>\n\n{str(e)}",
parse_mode="HTML"
)

View File

@@ -0,0 +1,302 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Утилиты для управления администраторами бота
"""
import os
import logging
from typing import List, Optional, Callable, Any, Union
from functools import wraps
from telegram import Update
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
# Настройка логирования
logger = logging.getLogger(__name__)
def is_admin(user_id: int) -> bool:
"""Проверяет, является ли пользователь администратором бота
Args:
user_id: ID пользователя Telegram
Returns:
True если пользователь администратор, иначе False
"""
# Получаем актуальный список администраторов из .env файла
try:
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
if os.path.exists(env_path):
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Ищем строку с ADMIN_USER_IDS
for line in env_content.split('\n'):
if line.startswith('ADMIN_USER_IDS='):
admin_ids_str = line.split('=')[1].strip()
if admin_ids_str:
admin_ids = list(map(int, admin_ids_str.split(',')))
return user_id in admin_ids
except Exception as e:
logger.error(f"Error reading admin IDs from .env: {e}")
# Если не удалось прочитать из файла, используем загруженные при старте
return user_id in ADMIN_USER_IDS
def admin_required(func: Callable) -> Callable:
"""Декоратор для проверки, является ли пользователь администратором
Args:
func: Оригинальная функция обработчика
Returns:
Обернутая функция с проверкой прав администратора
"""
@wraps(func)
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
# Проверяем доступность объекта update и effective_user
if not update or not update.effective_user:
logger.warning("Update object is incomplete, unable to check admin status")
return
user_id = update.effective_user.id
username = update.effective_user.username or "Unknown"
if not is_admin(user_id):
logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
# Если это сообщение, отправляем уведомление
if update.message:
await update.message.reply_text(
"⛔️ У вас нет прав на использование этой команды.\n"
"Обратитесь к владельцу бота, чтобы получить доступ."
)
# Если это callback query, отвечаем на него
elif update.callback_query:
await update.callback_query.answer(
"⛔️ У вас нет прав на использование этой функции."
)
return
# Если пользователь админ, вызываем оригинальную функцию
return await func(update, context, *args, **kwargs)
return wrapper
async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Добавляет нового администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID нового администратора
new_admin_id = int(context.args[0])
# Проверяем, не является ли пользователь уже администратором
if new_admin_id in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {new_admin_id} уже является администратором.")
return
# Добавляем нового администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Обновляем или добавляем строку с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip()
new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
else:
lines.append(f"ADMIN_USER_IDS={new_admin_id}")
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.append(new_admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{new_admin_id}</code> добавлен в список администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error adding admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при добавлении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Удаляет администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID администратора для удаления
admin_id = int(context.args[0])
# Проверяем, не удаляет ли админ сам себя
if admin_id == update.effective_user.id:
await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
return
# Проверяем, является ли пользователь администратором
if admin_id not in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {admin_id} не является администратором.")
return
# Удаляем администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Удаляем ID из строки с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
new_ids = [id for id in current_ids if int(id) != admin_id]
if not new_ids:
# Если не осталось администраторов, добавляем текущего пользователя
# чтобы избежать ситуации, когда нет администраторов
new_ids = [str(update.effective_user.id)]
lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.remove(admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{admin_id}</code> удален из списка администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
else:
await update.message.reply_text("❌ <b>Ошибка:</b> Не удалось найти настройки администраторов.")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error removing admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при удалении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Показывает список администраторов бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
try:
if not ADMIN_USER_IDS:
await update.message.reply_text("⚠️ Список администраторов пуст.")
return
# Формируем сообщение со списком администраторов
admin_list = "\n".join([f"• <code>{admin_id}</code>" for admin_id in ADMIN_USER_IDS])
await update.message.reply_text(
f"👑 <b>Список администраторов бота:</b>\n\n"
f"{admin_list}\n\n"
f"Всего администраторов: {len(ADMIN_USER_IDS)}",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error listing admins: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при получении списка администраторов:</b>\n\n{str(e)}",
parse_mode="HTML"
)

View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Утилиты для управления администраторами бота
"""
import os
import logging
from typing import List, Optional, Callable, Any, Union
from functools import wraps
from telegram import Update
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
# Настройка логирования
logger = logging.getLogger(__name__)
def is_admin(user_id: int) -> bool:
"""Проверяет, является ли пользователь администратором бота
Args:
user_id: ID пользователя Telegram
Returns:
True если пользователь администратор, иначе False
"""
return user_id in ADMIN_USER_IDS
def admin_required(func: Callable) -> Callable:
"""Декоратор для проверки, является ли пользователь администратором
Args:
func: Оригинальная функция обработчика
Returns:
Обернутая функция с проверкой прав администратора
"""
@wraps(func)
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
# Проверяем доступность объекта update и effective_user
if not update or not update.effective_user:
logger.warning("Update object is incomplete, unable to check admin status")
return
user_id = update.effective_user.id
username = update.effective_user.username or "Unknown"
if not is_admin(user_id):
logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
# Если это сообщение, отправляем уведомление
if update.message:
await update.message.reply_text(
"⛔️ У вас нет прав на использование этой команды.\n"
"Обратитесь к владельцу бота, чтобы получить доступ."
)
# Если это callback query, отвечаем на него
elif update.callback_query:
await update.callback_query.answer(
"⛔️ У вас нет прав на использование этой функции."
)
return
# Если пользователь админ, вызываем оригинальную функцию
return await func(update, context, *args, **kwargs)
return wrapper
async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Добавляет нового администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID нового администратора
new_admin_id = int(context.args[0])
# Проверяем, не является ли пользователь уже администратором
if new_admin_id in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {new_admin_id} уже является администратором.")
return
# Добавляем нового администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Обновляем или добавляем строку с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip()
new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
else:
lines.append(f"ADMIN_USER_IDS={new_admin_id}")
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.append(new_admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{new_admin_id}</code> добавлен в список администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error adding admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при добавлении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Удаляет администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID администратора для удаления
admin_id = int(context.args[0])
# Проверяем, не удаляет ли админ сам себя
if admin_id == update.effective_user.id:
await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
return
# Проверяем, является ли пользователь администратором
if admin_id not in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {admin_id} не является администратором.")
return
# Удаляем администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Удаляем ID из строки с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
new_ids = [id for id in current_ids if int(id) != admin_id]
if not new_ids:
# Если не осталось администраторов, добавляем текущего пользователя
# чтобы избежать ситуации, когда нет администраторов
new_ids = [str(update.effective_user.id)]
lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.remove(admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{admin_id}</code> удален из списка администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
else:
await update.message.reply_text("❌ <b>Ошибка:</b> Не удалось найти настройки администраторов.")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error removing admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при удалении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Показывает список администраторов бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
try:
if not ADMIN_USER_IDS:
await update.message.reply_text("⚠️ Список администраторов пуст.")
return
# Формируем сообщение со списком администраторов
admin_list = "\n".join([f"• <code>{admin_id}</code>" for admin_id in ADMIN_USER_IDS])
await update.message.reply_text(
f"👑 <b>Список администраторов бота:</b>\n\n"
f"{admin_list}\n\n"
f"Всего администраторов: {len(ADMIN_USER_IDS)}",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error listing admins: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при получении списка администраторов:</b>\n\n{str(e)}",
parse_mode="HTML"
)

View File

@@ -1425,17 +1425,46 @@ class SynologyAPI:
return {} return {}
try: try:
# Получаем расписание через API # Список возможных API для получения расписания питания
result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) apis_to_try = [
{"api": "SYNO.Core.System.PowerSchedule", "method": "get", "version": 1},
{"api": "SYNO.Core.System", "method": "get_power_schedule", "version": 1},
{"api": "SYNO.Core.Power", "method": "schedule", "version": 1},
{"api": "SYNO.Core.Power.Schedule", "method": "get", "version": 1},
{"api": "SYNO.PowerScheduler", "method": "load", "version": 1},
{"api": "SYNO.PowerSchedule", "method": "get", "version": 1}
]
result = {}
# Пробуем все возможные API по очереди
for api_config in apis_to_try:
logger.debug(f"Trying API: {api_config['api']}.{api_config['method']} v{api_config['version']}")
temp_result = self._make_api_request(
api_config["api"],
api_config["method"],
version=api_config["version"]
)
if temp_result:
logger.info(f"Successfully retrieved power schedule using {api_config['api']}.{api_config['method']}")
result = temp_result
break
if not result: if not result:
return {} # Если нет результатов, вернем структуру, которую ожидает код
logger.warning("No PowerSchedule API available, returning empty schedule structure")
return {
"boot_tasks": [],
"shutdown_tasks": []
}
return result return result
except Exception as e: except Exception as e:
logger.error(f"Error getting power schedule: {str(e)}") logger.error(f"Error getting power schedule: {str(e)}")
return {} return {
"boot_tasks": [],
"shutdown_tasks": []
}
def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
"""Настройка расписания включения/выключения """Настройка расписания включения/выключения
@@ -1457,14 +1486,7 @@ class SynologyAPI:
return False return False
try: try:
# Получаем текущее расписание # Подготавливаем базовые параметры расписания
current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
if not current_schedule:
logger.error("Failed to get current power schedule")
return False
# Подготавливаем новое расписание
params = { params = {
"enabled": enabled, "enabled": enabled,
"type": schedule_type, "type": schedule_type,
@@ -1472,14 +1494,39 @@ class SynologyAPI:
"time": time "time": time
} }
# Устанавливаем новое расписание # Список возможных API для установки расписания питания
result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) apis_to_try = [
{"api": "SYNO.Core.System.PowerSchedule", "method": "set", "version": 1},
{"api": "SYNO.Core.System", "method": "set_power_schedule", "version": 1},
{"api": "SYNO.Core.Power", "method": "set_schedule", "version": 1},
{"api": "SYNO.Core.Power.Schedule", "method": "set", "version": 1},
{"api": "SYNO.PowerScheduler", "method": "save", "version": 1},
{"api": "SYNO.PowerSchedule", "method": "set", "version": 1}
]
if not result: success = False
logger.error("Failed to set power schedule") last_used_api = ""
# Пробуем все возможные API по очереди
for api_config in apis_to_try:
api_name = api_config["api"]
method = api_config["method"]
version = api_config["version"]
logger.debug(f"Trying to set power schedule with API: {api_name}.{method} v{version}")
result = self._make_api_request(api_name, method, version, params=params)
if result:
logger.info(f"Successfully set power schedule using {api_name}.{method}")
success = True
last_used_api = api_name
break
if not success:
logger.error("Failed to set power schedule with any available API")
return False return False
logger.info(f"Power schedule for {schedule_type} set successfully") logger.info(f"Power schedule for {schedule_type} set successfully with {last_used_api}")
return True return True
except Exception as e: except Exception as e:

View File

@@ -54,6 +54,11 @@ from src.handlers.advanced_handlers import (
browse_callback, browse_callback,
advanced_power_callback advanced_power_callback
) )
from src.utils.admin_utils import (
add_admin,
remove_admin,
list_admins
)
from src.utils.logger import setup_logging from src.utils.logger import setup_logging
async def shutdown(application: Application) -> None: async def shutdown(application: Application) -> None:
@@ -124,6 +129,11 @@ def main() -> None:
application.add_handler(CommandHandler("wakeup", wakeup_command)) application.add_handler(CommandHandler("wakeup", wakeup_command))
application.add_handler(CommandHandler("quota", quota_command)) application.add_handler(CommandHandler("quota", quota_command))
# Регистрация обработчиков для управления администраторами
application.add_handler(CommandHandler("addadmin", add_admin))
application.add_handler(CommandHandler("removeadmin", remove_admin))
application.add_handler(CommandHandler("admins", list_admins))
# Регистрация обработчиков callback-запросов # Регистрация обработчиков callback-запросов
# Сначала обрабатываем более специфичные паттерны # Сначала обрабатываем более специфичные паттерны
application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py

View File

@@ -172,16 +172,24 @@ async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -
try: try:
schedule = synology_api.get_power_schedule() schedule = synology_api.get_power_schedule()
# Проверяем, пустая ли структура расписания
if not schedule: if not schedule:
await message.edit_text("❌ <b>Ошибка получения информации о расписании питания</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") await message.edit_text("❌ <b>Ошибка получения информации о расписании питания</b>\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
return return
# Получаем задачи расписания
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
if not boot_tasks and not shutdown_tasks:
await message.edit_text(" <b>Расписание питания не настроено</b>\n\nНа вашем устройстве отсутствует настроенное расписание включения и выключения, либо API не поддерживается.", parse_mode="HTML")
return
except Exception as e: except Exception as e:
await message.edit_text(f"❌ <b>Ошибка получения информации о расписании питания</b>\n\nПричина: {str(e)}", parse_mode="HTML") await message.edit_text(f"❌ <b>Ошибка получения информации о расписании питания</b>\n\nПричина: {str(e)}", parse_mode="HTML")
return return
# Формируем сообщение о расписании питания # Формируем сообщение о расписании питания
boot_tasks = schedule.get("boot_tasks", [])
shutdown_tasks = schedule.get("shutdown_tasks", [])
reply_text = f"⏱️ <b>Расписание питания Synology NAS</b>\n\n" reply_text = f"⏱️ <b>Расписание питания Synology NAS</b>\n\n"
@@ -354,7 +362,7 @@ async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
# Получаем шаблон поиска из аргументов команды # Получаем шаблон поиска из аргументов команды
if not context.args: if not context.args:
await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") await update.message.reply_text("❌ Укажите шаблон для поиска: /search &lt;шаблон&gt;")
return return
pattern = " ".join(context.args) pattern = " ".join(context.args)

View File

@@ -14,20 +14,18 @@ from src.config.config import (
ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
) )
from src.api.synology import SynologyAPI from src.api.synology import SynologyAPI
from src.utils.admin_utils import admin_required
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Инициализация API Synology # Инициализация API Synology
synology_api = SynologyAPI() synology_api = SynologyAPI()
from src.utils.admin_utils import admin_required
@admin_required
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /status""" """Обработчик команды /status"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
is_online = synology_api.is_online() is_online = synology_api.is_online()
@@ -123,13 +121,9 @@ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
parse_mode="HTML" parse_mode="HTML"
) )
@admin_required
async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /power""" """Обработчик команды /power"""
user_id = update.effective_user.id
if user_id not in ADMIN_USER_IDS:
await update.message.reply_text("У вас нет доступа к этому боту.")
return
is_online = synology_api.is_online() is_online = synology_api.is_online()
@@ -168,15 +162,12 @@ async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
parse_mode="HTML" parse_mode="HTML"
) )
@admin_required
async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик callback-запросов для кнопок управления питанием""" """Обработчик callback-запросов для кнопок управления питанием"""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
user_id = query.from_user.id
if user_id not in ADMIN_USER_IDS:
return
action = query.data action = query.data
if action == "cancel": if action == "cancel":

View File

@@ -312,15 +312,26 @@ async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
reply_text += f"<b>Загрузка памяти:</b> {memory_usage}%\n\n" reply_text += f"<b>Загрузка памяти:</b> {memory_usage}%\n\n"
# Добавляем информацию о сетевой активности # Добавляем информацию о сетевой активности
network = system_load.get("network", {}) network = system_load.get("network", [])
if network: if network:
reply_text += "<b>Сетевая активность:</b>\n" reply_text += "<b>Сетевая активность:</b>\n"
for device, stats in network.items(): if isinstance(network, dict):
rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ # Если это словарь (старое API)
tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ for device, stats in network.items():
rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
reply_text += f"• <b>{device}</b>\n" tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
elif isinstance(network, list):
# Если это список (новое API)
for interface in network:
device = interface.get("device", "неизвестно")
rx = int(interface.get("rx", 0)) / (1024**2) # МБ
tx = int(interface.get("tx", 0)) / (1024**2) # МБ
reply_text += f"• <b>{device}</b>\n"
reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
await message.edit_text(reply_text, parse_mode="HTML") await message.edit_text(reply_text, parse_mode="HTML")

View File

@@ -61,7 +61,12 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"<b>СЛУЖЕБНЫЕ КОМАНДЫ:</b>\n" "<b>СЛУЖЕБНЫЕ КОМАНДЫ:</b>\n"
"/checkapi - Проверка API\n" "/checkapi - Проверка API\n"
"/help - Эта справка\n" "/help - Эта справка\n\n"
"<b>УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ:</b>\n"
"/admins - Список администраторов\n"
"/addadmin &lt;id&gt; - Добавить администратора\n"
"/removeadmin &lt;id&gt; - Удалить администратора\n"
) )
if update.message: if update.message:

283
src/utils/admin_utils.py Normal file
View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Утилиты для управления администраторами бота
"""
import os
import logging
from typing import List, Optional, Callable, Any, Union
from functools import wraps
from telegram import Update
from telegram.ext import ContextTypes
from src.config.config import ADMIN_USER_IDS
# Настройка логирования
logger = logging.getLogger(__name__)
def is_admin(user_id: int) -> bool:
"""Проверяет, является ли пользователь администратором бота
Args:
user_id: ID пользователя Telegram
Returns:
True если пользователь администратор, иначе False
"""
return user_id in ADMIN_USER_IDS
def admin_required(func: Callable) -> Callable:
"""Декоратор для проверки, является ли пользователь администратором
Args:
func: Оригинальная функция обработчика
Returns:
Обернутая функция с проверкой прав администратора
"""
@wraps(func)
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
# Проверяем доступность объекта update и effective_user
if not update or not update.effective_user:
logger.warning("Update object is incomplete, unable to check admin status")
return
user_id = update.effective_user.id
username = update.effective_user.username or "Unknown"
if not is_admin(user_id):
logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
# Если это сообщение, отправляем уведомление
if update.message:
await update.message.reply_text(
"⛔️ У вас нет прав на использование этой команды.\n"
"Обратитесь к владельцу бота, чтобы получить доступ."
)
# Если это callback query, отвечаем на него
elif update.callback_query:
await update.callback_query.answer(
"⛔️ У вас нет прав на использование этой функции."
)
return
# Если пользователь админ, вызываем оригинальную функцию
return await func(update, context, *args, **kwargs)
return wrapper
async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Добавляет нового администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID нового администратора
new_admin_id = int(context.args[0])
# Проверяем, не является ли пользователь уже администратором
if new_admin_id in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {new_admin_id} уже является администратором.")
return
# Добавляем нового администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Обновляем или добавляем строку с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip()
new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
else:
lines.append(f"ADMIN_USER_IDS={new_admin_id}")
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.append(new_admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{new_admin_id}</code> добавлен в список администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/addadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error adding admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при добавлении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Удаляет администратора бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
# Проверяем, есть ли аргументы команды
if not context.args or len(context.args) < 1:
await update.message.reply_text(
"❌ <b>Ошибка:</b> Необходимо указать ID пользователя.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
return
try:
# Парсим ID администратора для удаления
admin_id = int(context.args[0])
# Проверяем, не удаляет ли админ сам себя
if admin_id == update.effective_user.id:
await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
return
# Проверяем, является ли пользователь администратором
if admin_id not in ADMIN_USER_IDS:
await update.message.reply_text(f" Пользователь с ID {admin_id} не является администратором.")
return
# Удаляем администратора
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
# Читаем текущий файл .env
env_content = ""
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
# Находим строку с ADMIN_USER_IDS
lines = env_content.split('\n')
admin_line_idx = -1
for i, line in enumerate(lines):
if line.startswith('ADMIN_USER_IDS='):
admin_line_idx = i
break
# Удаляем ID из строки с администраторами
if admin_line_idx >= 0:
current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
new_ids = [id for id in current_ids if int(id) != admin_id]
if not new_ids:
# Если не осталось администраторов, добавляем текущего пользователя
# чтобы избежать ситуации, когда нет администраторов
new_ids = [str(update.effective_user.id)]
lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
# Записываем обновленный файл
with open(env_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
# Обновляем список в памяти
ADMIN_USER_IDS.remove(admin_id)
await update.message.reply_text(
f"✅ <b>Успешно!</b>\n\n"
f"Пользователь с ID <code>{admin_id}</code> удален из списка администраторов.",
parse_mode="HTML"
)
logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
else:
await update.message.reply_text("❌ <b>Ошибка:</b> Не удалось найти настройки администраторов.")
except ValueError:
await update.message.reply_text(
"❌ <b>Ошибка:</b> ID пользователя должен быть целым числом.\n\n"
"Пример использования:\n"
"<code>/removeadmin 123456789</code>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error removing admin: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при удалении администратора:</b>\n\n{str(e)}",
parse_mode="HTML"
)
async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Показывает список администраторов бота
Args:
update: Объект обновления Telegram
context: Контекст вызова
"""
# Проверяем, что команду выполняет администратор
if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
return
try:
if not ADMIN_USER_IDS:
await update.message.reply_text("⚠️ Список администраторов пуст.")
return
# Формируем сообщение со списком администраторов
admin_list = "\n".join([f"• <code>{admin_id}</code>" for admin_id in ADMIN_USER_IDS])
await update.message.reply_text(
f"👑 <b>Список администраторов бота:</b>\n\n"
f"{admin_list}\n\n"
f"Всего администраторов: {len(ADMIN_USER_IDS)}",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error listing admins: {str(e)}")
await update.message.reply_text(
f"❌ <b>Произошла ошибка при получении списка администраторов:</b>\n\n{str(e)}",
parse_mode="HTML"
)