commit 75abe7bcf851951f90031f98c17a147ff3c40a83 Author: mirivlad Date: Mon Feb 23 15:41:48 2026 +0800 Initial commit: Telegram CLI Bot with multi-level menu Co-authored-by: Qwen-Coder diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9a8db5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ + +# Config +config.yaml +bot_config.json + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..74b3613 --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# Telegram CLI Bot + +Бот для выполнения CLI команд на вашем ПК через Telegram с многоуровневым меню и гибкой настройкой. + +## Возможности + +- 🖥️ **Выполнение CLI команд** - запуск любых команд от имени пользователя +- 📋 **Многоуровневое меню** - навигация через inline-кнопки +- ⚙️ **Настройка из бота** - изменение имени, описания, иконки прямо в диалоге +- 🎯 **Предустановленные команды** - готовые команды для файловой системы, поиска, системы и сети +- 👥 **Управление доступом** - ограничение круга пользователей +- 🔧 **Легкое добавление команд** - простая регистрация новых команд через код + +## Установка + +### 1. Клонирование репозитория + +```bash +cd /home/mirivlad/git +git clone telegram-cli-bot +cd telegram-cli-bot +``` + +### 2. Создание виртуального окружения + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +### 3. Установка зависимостей + +```bash +pip install -r requirements.txt +``` + +### 4. Получение токена бота + +1. Откройте [@BotFather](https://t.me/BotFather) в Telegram +2. Отправьте `/newbot` +3. Следуйте инструкциям +4. Скопируйте полученный токен + +### 5. Запуск бота + +```bash +export TELEGRAM_BOT_TOKEN='your_token_here' +python bot.py +``` + +## Использование + +### Команды бота + +| Команда | Описание | +|---------|----------| +| `/start` | Запустить бота, показать главное меню | +| `/help` | Показать справку | +| `/settings` | Открыть настройки бота | + +### Главное меню + +- **🖥️ Выполнить команду** - ввод произвольной CLI команды +- **📋 Предустановленные команды** - меню с готовыми командами +- **⚙️ Настройки бота** - конфигурация бота +- **ℹ️ О боте** - информация о боте + +### Предустановленные команды + +#### Файловая система +- `ls -la` - список файлов +- `pwd` - текущая директория +- `df -h` - свободное место +- `du -sh *` - размер папок + +#### Поиск +- `find . -name` - поиск файлов +- `grep пример` - поиск по содержимому +- `which command` - путь к командам + +#### Система +- `top -n 1` - процессы +- `ps aux` - список процессов +- `free -h` - использование памяти +- `uname -a` - информация о системе +- `uptime` - время работы + +#### Сеть +- `ip addr` - сетевые интерфейсы +- `ping google` - проверка связи +- `netstat` - сетевые подключения +- `curl ifconfig.me` - внешний IP + +### Настройка бота + +Через меню **⚙️ Настройки бота**: + +1. **📝 Изменить имя бота** - новое отображаемое имя +2. **📄 Изменить описание** - описание бота +3. **🎨 Изменить иконку** - emoji для бота +4. **👥 Управление доступом** - whitelist пользователей + +## Добавление новых команд + +### Быстрое добавление через меню + +Найдите функцию `init_menus()` в `bot.py` и добавьте новую кнопку: + +```python +# В нужное меню добавьте: +MenuItem("🔥 Ваша команда", "cmd_your", command="ваша_команда", icon="🔥"), +``` + +### Пример добавления команды для git: + +```python +# В init_menus() добавьте новое меню: +git_menu = [ + MenuItem("git status", "cmd_git_status", command="git status", icon="📊"), + MenuItem("git log", "cmd_git_log", command="git log --oneline -10", icon="📜"), + MenuItem("⬅️ Назад", "preset", icon="⬅️"), +] +menu_builder.add_menu("git", git_menu) + +# И добавьте кнопку в preset_menu: +MenuItem("🔗 Git", "git_menu", icon="🔗"), +``` + +### Продвинутое: регистрация через декоратор + +Используйте `command_registry` для сложной логики: + +```python +@command_registry.register("my_command") +async def my_custom_command(update, context): + # Ваша логика + pass +``` + +## Конфигурация + +Настройки хранятся в `bot_config.json`: + +```json +{ + "bot_name": "CLI Assistant", + "bot_description": "Бот для выполнения CLI команд", + "bot_icon_emoji": "🤖", + "allowed_users": [], + "require_confirmation": true, + "working_directory": "/home/user" +} +``` + +| Параметр | Описание | +|----------|----------| +| `bot_name` | Имя бота | +| `bot_description` | Описание бота | +| `bot_icon_emoji` | Emoji-иконка | +| `allowed_users` | Список разрешённых user ID (пусто = все) | +| `require_confirmation` | Требовать подтверждение перед выполнением | +| `working_directory` | Рабочая директория для команд | + +## Безопасность + +⚠️ **Важные предупреждения:** + +1. Бот выполняет команды от имени запустившего пользователя +2. Не запускайте бота от root +3. Ограничьте доступ через `allowed_users` +4. Будьте осторожны с деструктивными командами (`rm`, `dd`, etc.) + +## Логи + +Логи сохраняются в `bot.log` в директории бота. + +## Структура проекта + +``` +telegram-cli-bot/ +├── bot.py # Основной файл бота +├── requirements.txt # Зависимости Python +├── bot_config.json # Конфигурация (создаётся автоматически) +├── bot.log # Лог файл +├── .gitignore # Git ignore +└── README.md # Документация +``` + +## Требования + +- Python 3.8+ +- Библиотеки: `python-telegram-bot`, `pyyaml` +- Доступ к Telegram API + +## Лицензия + +MIT diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..9315a4f --- /dev/null +++ b/bot.py @@ -0,0 +1,724 @@ +#!/usr/bin/env python3 +""" +Telegram CLI Bot - бот для выполнения CLI команд с многоуровневым меню. +Легкое добавление новых команд через регистрацию хендлеров. +""" + +import os +import sys +import json +import asyncio +import subprocess +import logging +from pathlib import Path +from typing import Optional, Callable, Dict, Any, List +from dataclasses import dataclass, field + +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + ContextTypes, + filters, +) + +# --- Конфигурация --- +BASE_DIR = Path(__file__).parent +CONFIG_FILE = BASE_DIR / "bot_config.json" +COMMANDS_FILE = BASE_DIR / "commands.yaml" + +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + level=logging.INFO, + handlers=[ + logging.FileHandler(BASE_DIR / "bot.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + + +# --- Хранилище состояний пользователя --- +@dataclass +class UserState: + """Состояние пользователя в диалоге.""" + current_menu: str = "main" + waiting_for_input: bool = False + input_type: Optional[str] = None # "name", "description", "icon", "command" + parent_menu: Optional[str] = None + context: Dict[str, Any] = field(default_factory=dict) + + +class StateManager: + """Управление состояниями пользователей.""" + + def __init__(self): + self._states: Dict[int, UserState] = {} + + def get(self, user_id: int) -> UserState: + if user_id not in self._states: + self._states[user_id] = UserState() + return self._states[user_id] + + def reset(self, user_id: int): + self._states[user_id] = UserState() + + +# --- Конфигурация бота --- +class BotConfig: + """Конфигурация бота с сохранением в JSON.""" + + DEFAULT_CONFIG = { + "bot_name": "CLI Assistant", + "bot_description": "Бот для выполнения CLI команд", + "bot_icon_emoji": "🤖", + "allowed_users": [], # пустой список = все разрешены + "require_confirmation": True, + "working_directory": str(Path.home()), + } + + def __init__(self, config_file: Path = CONFIG_FILE): + self.config_file = config_file + self._config = self._load() + + def _load(self) -> dict: + if self.config_file.exists(): + with open(self.config_file, "r", encoding="utf-8") as f: + return json.load(f) + return self.DEFAULT_CONFIG.copy() + + def _save(self): + with open(self.config_file, "w", encoding="utf-8") as f: + json.dump(self._config, f, indent=2, ensure_ascii=False) + + def get(self, key: str, default=None): + return self._config.get(key, self.DEFAULT_CONFIG.get(key, default)) + + def set(self, key: str, value): + self._config[key] = value + self._save() + + @property + def name(self) -> str: + return self._config.get("bot_name", self.DEFAULT_CONFIG["bot_name"]) + + @property + def description(self) -> str: + return self._config.get("bot_description", self.DEFAULT_CONFIG["bot_description"]) + + @property + def icon(self) -> str: + return self._config.get("bot_icon_emoji", self.DEFAULT_CONFIG["bot_icon_emoji"]) + + +# --- Система команд --- +@dataclass +class MenuItem: + """Элемент меню.""" + label: str + callback: str # callback_data для кнопки + description: str = "" + icon: str = "" + children: List["MenuItem"] = field(default_factory=list) + command: Optional[str] = None # CLI команда для выполнения + is_command: bool = False + + +class MenuBuilder: + """Построитель многоуровневого меню.""" + + def __init__(self): + self._menus: Dict[str, List[MenuItem]] = {} + + def add_menu(self, menu_name: str, items: List[MenuItem]): + self._menus[menu_name] = items + + def get_menu(self, menu_name: str) -> List[MenuItem]: + return self._menus.get(menu_name, []) + + def get_keyboard(self, menu_name: str) -> InlineKeyboardMarkup: + """Создает InlineKeyboard для меню.""" + items = self._menus.get(menu_name, []) + keyboard = [] + for item in items: + icon = item.icon + " " if item.icon else "" + button = InlineKeyboardButton( + f"{icon}{item.label}", + callback_data=item.callback + ) + keyboard.append([button]) + return InlineKeyboardMarkup(keyboard) + + +class CommandRegistry: + """Реестр команд для легкого добавления.""" + + def __init__(self): + self._commands: Dict[str, Callable] = {} + + def register(self, name: str): + """Декоратор для регистрации команды.""" + def decorator(func: Callable): + self._commands[name] = func + return func + return decorator + + def get(self, name: str) -> Optional[Callable]: + return self._commands.get(name) + + def list_commands(self) -> List[str]: + return list(self._commands.keys()) + + +# --- Глобальные объекты --- +config = BotConfig() +state_manager = StateManager() +menu_builder = MenuBuilder() +command_registry = CommandRegistry() + + +# --- Инициализация меню --- +def init_menus(): + """Инициализация структуры меню.""" + + # Главное меню + main_menu = [ + MenuItem("🖥️ Выполнить команду", "exec_cmd", icon="🖥️", is_command=True), + MenuItem("📋 Предустановленные команды", "preset_menu", icon="📋"), + MenuItem("⚙️ Настройки бота", "settings_menu", icon="⚙️"), + MenuItem("ℹ️ О боте", "about", icon="ℹ️"), + ] + menu_builder.add_menu("main", main_menu) + + # Меню предустановленных команд + preset_menu = [ + MenuItem("📁 Файловая система", "fs_menu", icon="📁"), + MenuItem("🔍 Поиск", "search_menu", icon="🔍"), + MenuItem("📊 Система", "system_menu", icon="📊"), + MenuItem("🌐 Сеть", "network_menu", icon="🌐"), + MenuItem("⬅️ Назад", "main", icon="⬅️"), + ] + menu_builder.add_menu("preset", preset_menu) + + # Файловая система + fs_menu = [ + MenuItem("ls -la", "cmd_ls_la", command="ls -la", icon="📄"), + MenuItem("pwd", "cmd_pwd", command="pwd", icon="📍"), + MenuItem("df -h", "cmd_df", command="df -h", icon="💾"), + MenuItem("du -sh *", "cmd_du", command="du -sh * 2>/dev/null | sort -hr | head -20", icon="📊"), + MenuItem("⬅️ Назад", "preset", icon="⬅️"), + ] + menu_builder.add_menu("fs", fs_menu) + + # Поиск + search_menu = [ + MenuItem("find . -name", "cmd_find_name", command="find . -maxdepth 3 -name '*.txt' 2>/dev/null", icon="🔎"), + MenuItem("grep пример", "cmd_grep", command="grep -r 'example' . 2>/dev/null | head -20", icon="🔍"), + MenuItem("which command", "cmd_which", command="which python3 bash git", icon="📍"), + MenuItem("⬅️ Назад", "preset", icon="⬅️"), + ] + menu_builder.add_menu("search", search_menu) + + # Система + system_menu = [ + MenuItem("top -n 1", "cmd_top", command="top -bn1 | head -20", icon="📈"), + MenuItem("ps aux", "cmd_ps", command="ps aux | head -20", icon="🔄"), + MenuItem("free -h", "cmd_free", command="free -h", icon="💾"), + MenuItem("uname -a", "cmd_uname", command="uname -a", icon="ℹ️"), + MenuItem("uptime", "cmd_uptime", command="uptime", icon="⏱️"), + MenuItem("⬅️ Назад", "preset", icon="⬅️"), + ] + menu_builder.add_menu("system", system_menu) + + # Сеть + network_menu = [ + MenuItem("ip addr", "cmd_ip", command="ip addr 2>/dev/null || ifconfig 2>/dev/null", icon="🌐"), + MenuItem("ping google", "cmd_ping", command="ping -c 4 google.com 2>&1 | head -10", icon="📡"), + MenuItem("netstat", "cmd_netstat", command="ss -tuln 2>/dev/null || netstat -tuln 2>/dev/null | head -20", icon="🔌"), + MenuItem("curl ifconfig.me", "cmd_curl_ip", command="curl -s ifconfig.me 2>&1", icon="📍"), + MenuItem("⬅️ Назад", "preset", icon="⬅️"), + ] + menu_builder.add_menu("network", network_menu) + + # Настройки + settings_menu = [ + MenuItem("📝 Изменить имя бота", "set_name", icon="📝"), + MenuItem("📄 Изменить описание", "set_description", icon="📄"), + MenuItem("🎨 Изменить иконку", "set_icon", icon="🎨"), + MenuItem("👥 Управление доступом", "access_menu", icon="👥"), + MenuItem("⬅️ Назад", "main", icon="⬅️"), + ] + menu_builder.add_menu("settings", settings_menu) + + # Доступ + access_menu = [ + MenuItem("📋 Показать разрешённых", "show_access", icon="📋"), + MenuItem("➕ Добавить пользователя", "add_access", icon="➕"), + MenuItem("➖ Удалить пользователя", "remove_access", icon="➖"), + MenuItem("⬅️ Назад", "settings", icon="⬅️"), + ] + menu_builder.add_menu("access", access_menu) + + +# --- Хендлеры --- + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка команды /start.""" + user = update.effective_user + logger.info(f"Пользователь {user.username} ({user.id}) запустил бота") + + state_manager.reset(user.id) + + await update.message.reply_text( + f"👋 Привет, {user.first_name}!\n\n" + f"{config.icon} *{config.name}*\n" + f"_{config.description}_\n\n" + f"Используйте меню для навигации или команду /help для справки.", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("main") + ) + + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка команды /help.""" + help_text = f""" +📖 *Справка по боту {config.name}* + +*Основные возможности:* +• Выполнение CLI команд на вашем ПК +• Предустановленные команды в меню +• Гибкая настройка бота + +*Команды:* +/start - Запустить бота +/help - Эта справка +/settings - Настройки бота +/commands - Список доступных команд + +*Безопасность:* +Команды выполняются от имени пользователя. +Будьте осторожны с деструктивными командами! +""" + await update.message.reply_text(help_text, parse_mode="Markdown") + + +async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка команды /settings.""" + state = state_manager.get(update.effective_user.id) + state.current_menu = "settings" + + await update.message.reply_text( + "⚙️ *Настройки бота*", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("settings") + ) + + +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" + await query.edit_message_text( + "🏠 *Главное меню*", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("main") + ) + + 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 == "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("❌ Команда не найдена") + + # Настройки бота + elif callback == "set_name": + state.waiting_for_input = True + state.input_type = "name" + await query.edit_message_text( + "📝 *Изменение имени бота*\n\n" + f"Текущее имя: `{config.name}`\n\n" + "Отправьте новое имя бота:", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("settings") + ) + + elif callback == "set_description": + state.waiting_for_input = True + state.input_type = "description" + await query.edit_message_text( + "📄 *Изменение описания бота*\n\n" + f"Текущее описание: `{config.description}`\n\n" + "Отправьте новое описание:", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("settings") + ) + + elif callback == "set_icon": + state.waiting_for_input = True + state.input_type = "icon" + await query.edit_message_text( + "🎨 *Изменение иконки бота*\n\n" + f"Текущая иконка: `{config.icon}`\n\n" + "Отправьте новый emoji (один символ):", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("settings") + ) + + elif callback == "show_access": + allowed = config.get("allowed_users", []) + if allowed: + text = "👥 *Разрешённые пользователи:*\n" + "\n".join(f"• `{uid}`" for uid in allowed) + else: + text = "👥 *Доступ открыт для всех*\n\n(список разрешённых пользователей пуст)" + await query.edit_message_text(text, parse_mode="Markdown") + + elif callback == "add_access": + state.waiting_for_input = True + state.input_type = "add_access" + await query.edit_message_text( + "➕ *Добавление пользователя*\n\n" + "Отправьте ID пользователя Telegram:\n" + "(можно получить через @userinfobot)", + parse_mode="Markdown" + ) + + elif callback == "remove_access": + state.waiting_for_input = True + state.input_type = "remove_access" + allowed = config.get("allowed_users", []) + if allowed: + text = "➖ *Удаление пользователя*\n\n" + "\n".join(f"• `{uid}`" for uid in allowed) + text += "\n\nОтправьте ID для удаления:" + 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"Версия: 1.0.0\n" + f"Рабочая директория: `{config.get('working_directory')}`\n\n" + f"Бот позволяет выполнять CLI команды на вашем ПК\n" + f"через интерфейс Telegram.", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("main") + ) + state.current_menu = "main" + + elif callback == "exec_cmd": + state.waiting_for_input = True + state.input_type = "command" + await query.edit_message_text( + "🖥️ *Выполнение команды*\n\n" + "Отправьте команду для выполнения:\n\n" + "⚠️ _Будьте осторожны с деструктивными командами!_", + parse_mode="Markdown", + reply_markup=menu_builder.get_keyboard("main") + ) + + +async def execute_cli_command(query, command: str): + """Выполнение CLI команды.""" + working_dir = config.get("working_directory", str(Path.home())) + + logger.info(f"Выполнение команды: {command}") + + await query.edit_message_text( + f"⏳ *Выполнение...*\n\n`{command}`", + parse_mode="Markdown" + ) + + try: + process = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=working_dir + ) + + stdout, stderr = await asyncio.wait_for( + process.communicate(), + timeout=30 + ) + + output = stdout.decode("utf-8", errors="replace") + error = stderr.decode("utf-8", errors="replace") + + result = f"✅ *Результат:*\n\n" + result += f"```\n{command}\n```\n\n" + + if output: + # Ограничиваем вывод + if len(output) > 4000: + output = output[:4000] + "\n... (вывод обрезан)" + result += f"*Вывод:*\n```\n{output}\n```\n" + + if error: + if len(error) > 4000: + error = error[:4000] + "\n... (вывод обрезан)" + result += f"*Ошибки:*\n```\n{error}\n```\n" + + result += f"\n*Код возврата:* `{process.returncode}`" + + await query.edit_message_text(result, parse_mode="Markdown") + + except asyncio.TimeoutError: + await query.edit_message_text( + "❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд и была прервана.", + parse_mode="Markdown" + ) + except Exception as e: + logger.error(f"Ошибка выполнения команды: {e}") + await query.edit_message_text( + f"❌ *Ошибка:*\n```\n{str(e)}\n```", + parse_mode="Markdown" + ) + + +async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка текстовых сообщений (для ввода настроек и команд).""" + user_id = update.effective_user.id + state = state_manager.get(user_id) + text = update.message.text.strip() + + if not state.waiting_for_input: + return + + input_type = state.input_type + + if input_type == "name": + config.set("bot_name", text) + await update.message.reply_text( + f"✅ Имя бота изменено на: `{text}`", + parse_mode="Markdown" + ) + + elif input_type == "description": + config.set("bot_description", text) + await update.message.reply_text( + f"✅ Описание изменено на: `{text}`", + parse_mode="Markdown" + ) + + elif input_type == "icon": + config.set("bot_icon_emoji", text[0] if text else "🤖") + await update.message.reply_text( + f"✅ Иконка изменена на: `{text[0] if text else '🤖'}`", + parse_mode="Markdown" + ) + + elif input_type == "command": + # Выполнение произвольной команды + await update.message.reply_text( + f"⏳ *Выполнение...*\n\n`{text}`", + parse_mode="Markdown" + ) + await execute_cli_command_from_message(update, text) + + elif input_type == "add_access": + try: + uid = int(text) + allowed = config.get("allowed_users", []) + if uid not in allowed: + allowed.append(uid) + config.set("allowed_users", allowed) + await update.message.reply_text(f"✅ Пользователь `{uid}` добавлен", parse_mode="Markdown") + else: + await update.message.reply_text(f"⚠️ Пользователь `{uid}` уже в списке", parse_mode="Markdown") + except ValueError: + await update.message.reply_text("❌ Неверный формат ID") + + elif input_type == "remove_access": + try: + uid = int(text) + allowed = config.get("allowed_users", []) + if uid in allowed: + allowed.remove(uid) + config.set("allowed_users", allowed) + await update.message.reply_text(f"✅ Пользователь `{uid}` удалён", parse_mode="Markdown") + else: + await update.message.reply_text(f"⚠️ Пользователь `{uid}` не найден", parse_mode="Markdown") + except ValueError: + await update.message.reply_text("❌ Неверный формат ID") + + # Сброс состояния + state.waiting_for_input = False + state.input_type = None + + +async def execute_cli_command_from_message(update: Update, command: str): + """Выполнение CLI команды из сообщения.""" + working_dir = config.get("working_directory", str(Path.home())) + + try: + process = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=working_dir + ) + + stdout, stderr = await asyncio.wait_for( + process.communicate(), + timeout=30 + ) + + output = stdout.decode("utf-8", errors="replace") + error = stderr.decode("utf-8", errors="replace") + + result = f"✅ *Результат:*\n\n" + result += f"```\n{command}\n```\n\n" + + if output: + if len(output) > 4000: + output = output[:4000] + "\n... (вывод обрезан)" + result += f"*Вывод:*\n```\n{output}\n```\n" + + if error: + if len(error) > 4000: + error = error[:4000] + "\n... (вывод обрезан)" + result += f"*Ошибки:*\n```\n{error}\n```\n" + + result += f"\n*Код возврата:* `{process.returncode}`" + + await update.message.reply_text(result, parse_mode="Markdown") + + except asyncio.TimeoutError: + await update.message.reply_text( + "❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд и была прервана.", + parse_mode="Markdown" + ) + except Exception as e: + logger.error(f"Ошибка выполнения команды: {e}") + await update.message.reply_text( + f"❌ *Ошибка:*\n```\n{str(e)}\n```", + parse_mode="Markdown" + ) + + +async def post_init(application: Application): + """Инициализация после запуска бота.""" + # Установка команд бота + commands = [ + BotCommand("start", "Запустить бота"), + BotCommand("help", "Справка"), + BotCommand("settings", "Настройки"), + ] + await application.bot.set_my_commands(commands) + + # Установка имени и описания + await application.bot.set_my_name(config.name) + await application.bot.set_my_description(config.description) + + logger.info("Бот инициализирован") + + +def main(): + """Точка входа.""" + # Проверка токена + token = os.getenv("TELEGRAM_BOT_TOKEN") + if not token: + print("❌ Ошибка: не установлен TELEGRAM_BOT_TOKEN") + print("Задайте переменную окружения:") + print(" export TELEGRAM_BOT_TOKEN='your_token_here'") + sys.exit(1) + + # Инициализация меню + init_menus() + + # Создание приложения + application = Application.builder().token(token).post_init(post_init).build() + + # Регистрация хендлеров + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("settings", settings_command)) + application.add_handler(CallbackQueryHandler(menu_callback)) + application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_message)) + + # Запуск + logger.info("Запуск бота...") + print(f"🤖 {config.name} запущен!") + print(f"📝 Описание: {config.description}") + print(f"🎨 Иконка: {config.icon}") + print("\nОстановка: Ctrl+C") + + application.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..92aa08a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-telegram-bot==21.0 +pyyaml==6.0.1 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..3ca2a2c --- /dev/null +++ b/run.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Скрипт запуска Telegram CLI Bot + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Проверка токена +if [ -z "$TELEGRAM_BOT_TOKEN" ]; then + echo "❌ Ошибка: не установлен TELEGRAM_BOT_TOKEN" + echo "" + echo "Задайте переменную окружения одним из способов:" + echo "" + echo "1. Экспорт в shell:" + echo " export TELEGRAM_BOT_TOKEN='your_token_here'" + echo "" + echo "2. Запуск с переменной:" + echo " TELEGRAM_BOT_TOKEN='your_token_here' ./run.sh" + echo "" + echo "3. Создание файла .env:" + echo " echo 'TELEGRAM_BOT_TOKEN=your_token_here' > .env" + exit 1 +fi + +# Проверка виртуального окружения +if [ ! -d "venv" ]; then + echo "📦 Виртуальное окружение не найдено. Создаю..." + python3 -m venv venv + echo "✅ Виртуальное окружение создано" +fi + +# Активация виртуального окружения +source venv/bin/activate + +# Установка зависимостей +echo "📦 Проверка зависимостей..." +pip install -q -r requirements.txt + +# Запуск бота +echo "" +echo "🤖 Запуск Telegram CLI Bot..." +echo "" +python bot.py