Pre deploy commit.
This commit is contained in:
0
.env.example
Normal file
0
.env.example
Normal file
1873
.history/src/api/synology_20250830104812.py
Normal file
1873
.history/src/api/synology_20250830104812.py
Normal file
File diff suppressed because it is too large
Load Diff
1877
.history/src/api/synology_20250830104833.py
Normal file
1877
.history/src/api/synology_20250830104833.py
Normal file
File diff suppressed because it is too large
Load Diff
1877
.history/src/api/synology_20250830104945.py
Normal file
1877
.history/src/api/synology_20250830104945.py
Normal file
File diff suppressed because it is too large
Load Diff
1894
.history/src/api/synology_20250830105105.py
Normal file
1894
.history/src/api/synology_20250830105105.py
Normal file
File diff suppressed because it is too large
Load Diff
1908
.history/src/api/synology_20250830105130.py
Normal file
1908
.history/src/api/synology_20250830105130.py
Normal file
File diff suppressed because it is too large
Load Diff
1908
.history/src/api/synology_20250830110338.py
Normal file
1908
.history/src/api/synology_20250830110338.py
Normal file
File diff suppressed because it is too large
Load Diff
149
.history/src/bot_20250830110611.py
Normal file
149
.history/src/bot_20250830110611.py
Normal 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()
|
||||
154
.history/src/bot_20250830110630.py
Normal file
154
.history/src/bot_20250830110630.py
Normal 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()
|
||||
154
.history/src/bot_20250830110906.py
Normal file
154
.history/src/bot_20250830110906.py
Normal 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()
|
||||
972
.history/src/handlers/advanced_handlers_20250830104205.py
Normal file
972
.history/src/handlers/advanced_handlers_20250830104205.py
Normal 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 <шаблон>")
|
||||
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 "📄"
|
||||
972
.history/src/handlers/advanced_handlers_20250830104340.py
Normal file
972
.history/src/handlers/advanced_handlers_20250830104340.py
Normal 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 <шаблон>")
|
||||
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 "📄"
|
||||
982
.history/src/handlers/advanced_handlers_20250830105155.py
Normal file
982
.history/src/handlers/advanced_handlers_20250830105155.py
Normal 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 <шаблон>")
|
||||
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 "📄"
|
||||
980
.history/src/handlers/advanced_handlers_20250830105216.py
Normal file
980
.history/src/handlers/advanced_handlers_20250830105216.py
Normal 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 <шаблон>")
|
||||
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 "📄"
|
||||
980
.history/src/handlers/advanced_handlers_20250830110338.py
Normal file
980
.history/src/handlers/advanced_handlers_20250830110338.py
Normal 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 <шаблон>")
|
||||
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 "📄"
|
||||
328
.history/src/handlers/command_handlers_20250830110734.py
Normal file
328
.history/src/handlers/command_handlers_20250830110734.py
Normal 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
|
||||
329
.history/src/handlers/command_handlers_20250830110754.py
Normal file
329
.history/src/handlers/command_handlers_20250830110754.py
Normal 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
|
||||
325
.history/src/handlers/command_handlers_20250830110810.py
Normal file
325
.history/src/handlers/command_handlers_20250830110810.py
Normal 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
|
||||
322
.history/src/handlers/command_handlers_20250830110839.py
Normal file
322
.history/src/handlers/command_handlers_20250830110839.py
Normal 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
|
||||
322
.history/src/handlers/command_handlers_20250830110906.py
Normal file
322
.history/src/handlers/command_handlers_20250830110906.py
Normal 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
|
||||
378
.history/src/handlers/extended_handlers_20250830104501.py
Normal file
378
.history/src/handlers/extended_handlers_20250830104501.py
Normal 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")
|
||||
378
.history/src/handlers/extended_handlers_20250830104715.py
Normal file
378
.history/src/handlers/extended_handlers_20250830104715.py
Normal 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")
|
||||
116
.history/src/handlers/help_handlers_20250830110705.py
Normal file
116
.history/src/handlers/help_handlers_20250830110705.py
Normal 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 <запрос> - Поиск файлов\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 <id> - Добавить администратора\n"
|
||||
"/removeadmin <id> - Удалить администратора\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)
|
||||
116
.history/src/handlers/help_handlers_20250830110906.py
Normal file
116
.history/src/handlers/help_handlers_20250830110906.py
Normal 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 <запрос> - Поиск файлов\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 <id> - Добавить администратора\n"
|
||||
"/removeadmin <id> - Удалить администратора\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)
|
||||
283
.history/src/utils/admin_utils_20250830110540.py
Normal file
283
.history/src/utils/admin_utils_20250830110540.py
Normal 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"
|
||||
)
|
||||
283
.history/src/utils/admin_utils_20250830110906.py
Normal file
283
.history/src/utils/admin_utils_20250830110906.py
Normal 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"
|
||||
)
|
||||
302
.history/src/utils/admin_utils_20250830114406.py
Normal file
302
.history/src/utils/admin_utils_20250830114406.py
Normal 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"
|
||||
)
|
||||
283
.history/src/utils/admin_utils_20250830114514.py
Normal file
283
.history/src/utils/admin_utils_20250830114514.py
Normal 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"
|
||||
)
|
||||
@@ -1425,17 +1425,46 @@ class SynologyAPI:
|
||||
return {}
|
||||
|
||||
try:
|
||||
# Получаем расписание через API
|
||||
result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
|
||||
# Список возможных API для получения расписания питания
|
||||
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:
|
||||
return {}
|
||||
# Если нет результатов, вернем структуру, которую ожидает код
|
||||
logger.warning("No PowerSchedule API available, returning empty schedule structure")
|
||||
return {
|
||||
"boot_tasks": [],
|
||||
"shutdown_tasks": []
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
except Exception as 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:
|
||||
"""Настройка расписания включения/выключения
|
||||
@@ -1457,14 +1486,7 @@ class SynologyAPI:
|
||||
return False
|
||||
|
||||
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 = {
|
||||
"enabled": enabled,
|
||||
"type": schedule_type,
|
||||
@@ -1472,14 +1494,39 @@ class SynologyAPI:
|
||||
"time": time
|
||||
}
|
||||
|
||||
# Устанавливаем новое расписание
|
||||
result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
|
||||
# Список возможных API для установки расписания питания
|
||||
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:
|
||||
logger.error("Failed to set power schedule")
|
||||
success = False
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
|
||||
10
src/bot.py
10
src/bot.py
@@ -54,6 +54,11 @@ from src.handlers.advanced_handlers import (
|
||||
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:
|
||||
@@ -124,6 +129,11 @@ def main() -> None:
|
||||
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
|
||||
|
||||
@@ -172,16 +172,24 @@ async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
||||
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"
|
||||
|
||||
@@ -354,7 +362,7 @@ async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
# Получаем шаблон поиска из аргументов команды
|
||||
if not context.args:
|
||||
await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
|
||||
await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
|
||||
return
|
||||
|
||||
pattern = " ".join(context.args)
|
||||
|
||||
@@ -14,20 +14,18 @@ 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"""
|
||||
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...")
|
||||
|
||||
is_online = synology_api.is_online()
|
||||
@@ -123,13 +121,9 @@ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
@admin_required
|
||||
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()
|
||||
|
||||
@@ -168,15 +162,12 @@ async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
@admin_required
|
||||
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":
|
||||
|
||||
@@ -312,15 +312,26 @@ async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
reply_text += f"<b>Загрузка памяти:</b> {memory_usage}%\n\n"
|
||||
|
||||
# Добавляем информацию о сетевой активности
|
||||
network = system_load.get("network", {})
|
||||
network = system_load.get("network", [])
|
||||
if network:
|
||||
reply_text += "<b>Сетевая активность:</b>\n"
|
||||
for device, stats in network.items():
|
||||
rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
|
||||
tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
|
||||
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"
|
||||
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")
|
||||
|
||||
|
||||
@@ -61,7 +61,12 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
|
||||
"<b>СЛУЖЕБНЫЕ КОМАНДЫ:</b>\n"
|
||||
"/checkapi - Проверка API\n"
|
||||
"/help - Эта справка\n"
|
||||
"/help - Эта справка\n\n"
|
||||
|
||||
"<b>УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ:</b>\n"
|
||||
"/admins - Список администраторов\n"
|
||||
"/addadmin <id> - Добавить администратора\n"
|
||||
"/removeadmin <id> - Удалить администратора\n"
|
||||
)
|
||||
|
||||
if update.message:
|
||||
|
||||
283
src/utils/admin_utils.py
Normal file
283
src/utils/admin_utils.py
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user