telegram-cli-bot/bot/handlers/callbacks.py

482 lines
22 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Обработчик callback-запросов от меню."""
import logging
from telegram import Update
from telegram.ext import ContextTypes
from bot.config import config, state_manager, server_manager, menu_builder
from bot.utils.decorators import check_access
logger = logging.getLogger(__name__)
async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка нажатий на кнопки меню."""
query = update.callback_query
user_id = query.from_user.id
state = state_manager.get(user_id)
await query.answer()
callback = query.data
logger.info(f"Callback: {callback} от пользователя {user_id}")
# Обработка навигации
if callback == "main":
state.current_menu = "main"
# Проверяем режим чата с ИИ для обновления текста кнопки
ai_status = "✅ ВКЛ" if state.ai_chat_mode else "❌ ВЫКЛ"
await query.edit_message_text(
f"🏠 *Главное меню*\n\n"
f"💬 *Чат с ИИ:* {ai_status}",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
)
elif callback == "preset_menu":
state.current_menu = "preset"
await query.edit_message_text(
"📋 *Предустановленные команды*",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("preset")
)
elif callback == "fs_menu":
await query.edit_message_text(
"📁 *Файловая система*",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("fs")
)
elif callback == "search_menu":
await query.edit_message_text(
"🔍 *Поиск*",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("search")
)
elif callback == "system_menu":
await query.edit_message_text(
"📊 *Система*",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("system")
)
elif callback == "network_menu":
await query.edit_message_text(
"🌐 *Сеть*",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("network")
)
elif callback == "server_menu":
# Динамическое обновление меню серверов с кнопками управления
servers = server_manager.list_servers()
keyboard = []
for srv in servers:
# Кнопка выбора сервера + кнопка управления (для не-local)
row = [InlineKeyboardButton(
srv.display_name,
callback_data=f"server_select_{srv.name}"
)]
if srv.name != "local":
row.append(InlineKeyboardButton(
"⚙️",
callback_data=f"server_manage_{srv.name}"
))
keyboard.append(row)
keyboard.append([
InlineKeyboardButton(" Добавить", callback_data="server_add"),
InlineKeyboardButton("⬅️ Назад", callback_data="main")
])
state.current_menu = "server"
await query.edit_message_text(
"🖥️ *Управление серверами*\n\n"
"Выберите сервер для подключения или добавьте новый.\n"
"⚙️ — редактировать/удалить сервер",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup(keyboard)
)
elif callback == "server_add":
state.waiting_for_input = True
state.input_type = "add_server_name"
state.context["new_server"] = {}
await query.edit_message_text(
" *Добавление сервера*\n\n"
"Введите *имя сервера* (латиница, без пробелов):\n"
"Пример: `web-prod`, `db-backup`",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
]])
)
elif callback.startswith("server_manage_"):
server_name = callback.replace("server_manage_", "")
server = server_manager.get(server_name)
if server and server_name != "local":
state.editing_server = server_name
await query.edit_message_text(
f"⚙️ *Управление сервером*\n\n"
f"{server.display_name}\n"
f"📍 `{server.description}`\n"
f"🏷️ Теги: `{','.join(server.tags) if server.tags else 'нет'}`\n\n"
f"Выберите действие:",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("✏️ Редактировать", callback_data=f"server_edit_{server_name}")],
[InlineKeyboardButton("🗑️ Удалить", callback_data=f"server_delete_{server_name}")],
[InlineKeyboardButton("⬅️ Назад", callback_data="server_menu")]
])
)
else:
await query.edit_message_text(
f"❌ *Сервер не найден*\n\n"
f"Сервер `{server_name}` отсутствует в конфигурации.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("server")
)
elif callback.startswith("server_edit_"):
server_name = callback.replace("server_edit_", "")
server = server_manager.get(server_name)
if server and server_name != "local":
state.editing_server = server_name
state.waiting_for_input = True
state.input_type = "edit_server_field"
password_status = "установлен" if server.password else "не установлен"
await query.edit_message_text(
f"✏️ *Редактирование сервера: {server_name}*\n\n"
f"Текущие значения:\n"
f"• Host: `{server.host}`\n"
f"• Port: `{server.port}`\n"
f"• User: `{server.user}`\n"
f"• Tags: `{','.join(server.tags) if server.tags else 'нет'}`\n"
f"• Password: {password_status}\n\n"
f"Введите номер поля для изменения:\n"
f"1 — Host\n"
f"2 — Port\n"
f"3 — User\n"
f"4 — Tags\n"
f"5 — Password",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("❌ Отмена", callback_data="server_menu")
]])
)
else:
await query.edit_message_text(
"❌ Ошибка: сервер не найден",
reply_markup=menu_builder.get_keyboard("server")
)
elif callback.startswith("server_delete_"):
server_name = callback.replace("server_delete_", "")
server = server_manager.get(server_name)
if server and server_name != "local":
# Удаляем сразу с подтверждением
if server_manager.delete_server(server_name):
await query.edit_message_text(
f"🗑️ *Сервер удалён*\n\n"
f"Сервер `{server_name}` успешно удалён из конфигурации.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("server")
)
else:
await query.edit_message_text(
"❌ Ошибка при удалении сервера",
reply_markup=menu_builder.get_keyboard("server")
)
else:
await query.edit_message_text(
"❌ Нельзя удалить local сервер",
reply_markup=menu_builder.get_keyboard("server")
)
elif callback == "srv_skip_password":
# Пропуск пароля при добавлении сервера
user_id = query.from_user.id
state = state_manager.get(user_id)
state.context["new_server"]["password"] = ""
state.input_type = "add_server_tags"
await query.edit_message_text(
"✅ Пароль пропущен (будет использоваться только ключ)\n\n"
"Введите *теги* через запятую (или нажмите Пропустить):\n"
"Пример: `web,prod`, `db,backup`\n\n"
"Теги помогают группировать серверы.",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("⏭️ Пропустить", callback_data="srv_skip_tags")],
[InlineKeyboardButton("❌ Отмена", callback_data="server_menu")]
])
)
elif callback == "srv_skip_tags":
# Пропуск тегов при добавлении сервера
user_id = query.from_user.id
state = state_manager.get(user_id)
new_server = state.context.get("new_server", {})
if new_server.get("name") and new_server.get("host") and new_server.get("port") and new_server.get("user"):
if server_manager.add_server(
name=new_server["name"],
host=new_server["host"],
port=new_server["port"],
user=new_server["user"],
tags=[],
password=new_server.get("password", "")
):
await query.edit_message_text(
"✅ *Сервер добавлен*\n\n"
f"Имя: `{new_server['name']}`\n"
f"Host: `{new_server['host']}`\n"
f"Port: `{new_server['port']}`\n"
f"User: `{new_server['user']}`\n"
f"Tags: нет\n"
f"Password: {'установлен' if new_server.get('password') else 'не установлен'}\n\n"
f"Сервер сохранён в `.env` и доступен для выбора.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("server")
)
else:
await query.edit_message_text(
"❌ Ошибка: сервер с таким именем уже существует",
reply_markup=menu_builder.get_keyboard("server")
)
else:
await query.edit_message_text(
"❌ Ошибка: неполные данные сервера",
reply_markup=menu_builder.get_keyboard("server")
)
state.waiting_for_input = False
state.input_type = None
state.context.clear()
elif callback.startswith("server_select_"):
server_name = callback.replace("server_select_", "")
server = server_manager.get(server_name)
if server:
state.current_server = server_name
# Сброс рабочей директории при смене сервера
state.working_directory = None
await query.edit_message_text(
f"✅ *Сервер изменён*\n\n"
f"{server.display_name}\n"
f"📍 `{server.description}`\n\n"
f"Теперь команды выполняются на этом сервере.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
)
state.current_menu = "main"
else:
await query.edit_message_text(
f"❌ *Сервер не найден*\n\n"
f"Сервер `{server_name}` отсутствует в конфигурации.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("server")
)
elif callback == "settings_menu":
state.current_menu = "settings"
await query.edit_message_text(
"⚙️ *Настройки бота*",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("settings")
)
elif callback == "access_menu":
await query.edit_message_text(
"👥 *Управление доступом*",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("access")
)
# Обработка команд выполнения
elif callback.startswith("cmd_"):
# Поиск команды в меню
command = None
for menu_items in menu_builder._menus.values():
for item in menu_items:
if item.callback == callback and item.command:
command = item.command
break
if command:
await execute_cli_command(query, command)
else:
await query.edit_message_text("❌ Команда не найдена")
# Настройки бота - только просмотр, изменение через .env
elif callback == "set_name":
await query.edit_message_text(
"📝 *Изменение имени бота*\n\n"
f"Текущее имя: `{config.name}`\n\n"
"Для изменения отредактируйте `.env`:\n"
"```\nBOT_NAME=Ваше имя\n```",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("settings")
)
elif callback == "set_description":
await query.edit_message_text(
"📄 *Изменение описания бота*\n\n"
f"Текущее описание: `{config.description}`\n\n"
"Для изменения отредактируйте `.env`:\n"
"```\nBOT_DESCRIPTION=Ваше описание\n```",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("settings")
)
elif callback == "set_icon":
await query.edit_message_text(
"🎨 *Изменение иконки бота*\n\n"
f"Текущая иконка: `{config.icon}`\n\n"
"Для изменения отредактируйте `.env`:\n"
"```\nBOT_ICON_EMOJI=🤖\n```",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("settings")
)
elif callback == "show_access":
if config.allowed_users:
text = "👥 *Разрешённые пользователи:*\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users)
else:
text = "👥 *Доступ открыт для всех*\n\n(список разрешённых пользователей пуст)"
await query.edit_message_text(text, parse_mode="Markdown")
elif callback == "add_access":
await query.edit_message_text(
" *Добавление пользователя*\n\n"
"Для добавления пользователя отредактируйте `.env`:\n"
"```\nALLOWED_USERS=123456789,987654321\n```\n"
"Ваш ID можно узнать через @userinfobot",
parse_mode="Markdown"
)
elif callback == "remove_access":
if config.allowed_users:
text = " *Удаление пользователя*\n\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users)
text += "\n\nУдалите ID из `.env` чтобы убрать доступ"
else:
text = " Список пуст, некого удалять"
await query.edit_message_text(text, parse_mode="Markdown")
elif callback == "about":
await query.edit_message_text(
f" *О боте*\n\n"
f"*{config.icon} {config.name}*\n"
f"_{config.description}_\n\n"
f"*Версия:* `2.1.0`\n\n"
f"*Возможности:*\n"
f"• Выполнение CLI команд через Telegram\n"
f"• Поддержка локальных команд и SSH\n"
f"• Интерактивный ввод пароля (sudo)\n"
f"• Предустановленные команды\n"
f"• Управление серверами\n"
f"• Очистка ANSI-кодов и прогресс-баров\n"
f"• Форматирование длинного вывода\n"
f"• 💬 Чат с ИИ агентом (Qwen Code)\n\n"
f"*Рабочая директория:*\n"
f"`{config.working_directory}`\n\n"
f"Бот позволяет безопасно выполнять команды\n"
f"на вашем сервере через интерфейс Telegram.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
)
state.current_menu = "main"
elif callback in ["toggle_ai_chat", "toggle_ai_chat_on", "toggle_ai_chat_off"]:
# Переключаем режим чата с ИИ
state.ai_chat_mode = not state.ai_chat_mode
logger.info(f"toggle_ai_chat: user_id={user_id}, new_mode={state.ai_chat_mode}")
ai_status = "✅ ВКЛЮЧЕН" if state.ai_chat_mode else "❌ ВЫКЛЮЧЕН"
action = "включён" if state.ai_chat_mode else "выключен"
await query.edit_message_text(
f"🏠 *Главное меню*\n\n"
f"💬 *Чат с ИИ:* {ai_status}\n\n"
f"Режим чата с агентом {action}.\n"
f"Теперь все сообщения будут отправляться в Qwen Code.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("main", user_id=query.from_user.id, state=state)
)
state.current_menu = "main"
# --- Обработчики меню памяти ---
elif callback == "memory_menu":
state.current_menu = "memory"
await query.edit_message_text(
"🧠 *Память ИИ*\n\n"
"Управление памятью чата с ИИ:\n"
"• Профиль — факты о вас, которые запомнил ИИ\n"
"• Статистика — количество сообщений и сессий\n"
"• Очистить — удалить историю переписки",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("memory")
)
elif callback == "memory_profile":
profile_summary = get_user_profile_summary(user_id)
if not profile_summary:
profile_summary = "📭 Профиль пуст\n\nФакты ещё не извлечены.\nНачните общаться с ИИ в чате."
await query.edit_message_text(
f"📋 *Ваш профиль*\n\n{profile_summary}",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("memory")
)
elif callback == "memory_stats":
stats = memory_manager.get_stats(user_id)
await query.edit_message_text(
f"📊 *Статистика памяти*\n\n"
f"• Сессий: `{stats['total_sessions']}`\n"
f"• Сообщений: `{stats['total_messages']}`\n"
f"• Фактов: `{stats['total_facts']}`",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("memory")
)
elif callback == "memory_clear":
# Показываем подтверждение
await query.edit_message_text(
"🗑️ *Очистка истории*\n\n"
"Вы уверены?\n"
"Это удалит всю историю сообщений.\n"
"Факты останутся (их можно удалить отдельно).",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("🗑️ Да, очистить", callback_data="memory_clear_confirm")],
[InlineKeyboardButton("❌ Отмена", callback_data="memory_menu")]
])
)
elif callback == "memory_clear_confirm":
# Очищаем историю сообщений (в будущем можно добавить метод в memory_manager)
from memory_system import MemoryStorage
# Пока просто уведомляем
await query.edit_message_text(
"✅ *История очищена*\n\n"
"Функция полной очистки будет добавлена в следующей версии.\n"
"Пока очищается только история сессии в памяти бота.",
parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("memory")
)
# Сбрасываем историю чата в состоянии
state.ai_chat_history = []