release: v1.0 - Telegram CLI Bot
Основные изменения: - Перенос конфигурации из bot_config.json в .env - Удалено хранение токена в JSON (только переменные окружения) - Добавлена проверка прав доступа через ALLOWED_USERS - Декоратор @check_access для защиты хендлеров - Настройки бота: BOT_NAME, BOT_DESCRIPTION, BOT_ICON_EMOJI, WORKING_DIRECTORY - python-dotenv для загрузки переменных окружения - Обновлён run.sh для работы с .env - Убрана установка имени/описания при запуске (rate limit fix) - Удалён функционал изменения настроек через бота (только через .env) - Обновлена документация Безопасность: - Токен только в .env (не коммитится) - Проверка прав доступа по списку ALLOWED_USERS - bot_config.json удалён Файлы: - + .env.example (шаблон конфигурации) - - bot_config.json - ~ bot.py, run.sh, README.md, requirements.txt Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
c2f62295b7
commit
96d2577415
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Telegram Bot Token
|
||||||
|
# Получите токен у @BotFather в Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
||||||
|
|
||||||
|
# Настройки бота
|
||||||
|
BOT_NAME=CLI Assistant
|
||||||
|
BOT_DESCRIPTION=Бот для выполнения CLI команд
|
||||||
|
BOT_ICON_EMOJI=🤖
|
||||||
|
|
||||||
|
# Разрешённые пользователи (список ID через запятую)
|
||||||
|
# Пустой список = доступ открыт для всех
|
||||||
|
# Ваш ID можно узнать через @userinfobot
|
||||||
|
ALLOWED_USERS=
|
||||||
|
|
||||||
|
# Рабочая директория для команд
|
||||||
|
WORKING_DIRECTORY=/home/mirivlad
|
||||||
86
README.md
86
README.md
|
|
@ -41,11 +41,37 @@ pip install -r requirements.txt
|
||||||
3. Следуйте инструкциям
|
3. Следуйте инструкциям
|
||||||
4. Скопируйте полученный токен
|
4. Скопируйте полученный токен
|
||||||
|
|
||||||
### 5. Запуск бота
|
### 5. Настройка токена
|
||||||
|
|
||||||
|
**Способ 1: Через файл .env (рекомендуется)**
|
||||||
|
|
||||||
|
Скопируйте `.env.example` в `.env` и укажите токен:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Отредактируйте .env, вставив ваш токен
|
||||||
|
```
|
||||||
|
|
||||||
|
**Способ 2: Через переменную окружения**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export TELEGRAM_BOT_TOKEN='your_token_here'
|
export TELEGRAM_BOT_TOKEN='your_token_here'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Способ 3: Интерактивная настройка**
|
||||||
|
|
||||||
|
Запустите скрипт `run.sh` — он сам запросит токен:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Запуск бота
|
||||||
|
|
||||||
|
```bash
|
||||||
python bot.py
|
python bot.py
|
||||||
|
# или через скрипт
|
||||||
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
@ -139,27 +165,35 @@ async def my_custom_command(update, context):
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
Настройки хранятся в `bot_config.json`:
|
Все настройки хранятся в файле `.env`:
|
||||||
|
|
||||||
```json
|
```bash
|
||||||
{
|
# Токен бота
|
||||||
"bot_name": "CLI Assistant",
|
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
||||||
"bot_description": "Бот для выполнения CLI команд",
|
|
||||||
"bot_icon_emoji": "🤖",
|
# Настройки бота
|
||||||
"allowed_users": [],
|
BOT_NAME=CLI Assistant
|
||||||
"require_confirmation": true,
|
BOT_DESCRIPTION=Бот для выполнения CLI команд
|
||||||
"working_directory": "/home/user"
|
BOT_ICON_EMOJI=🤖
|
||||||
}
|
|
||||||
|
# Разрешённые пользователи (список ID через запятую)
|
||||||
|
# Пустой список = доступ открыт для всех
|
||||||
|
ALLOWED_USERS=123456789,987654321
|
||||||
|
|
||||||
|
# Рабочая директория для команд
|
||||||
|
WORKING_DIRECTORY=/home/user
|
||||||
```
|
```
|
||||||
|
|
||||||
| Параметр | Описание |
|
| Параметр | Описание |
|
||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `bot_name` | Имя бота |
|
| `TELEGRAM_BOT_TOKEN` | Токен бота от @BotFather |
|
||||||
| `bot_description` | Описание бота |
|
| `BOT_NAME` | Отображаемое имя бота |
|
||||||
| `bot_icon_emoji` | Emoji-иконка |
|
| `BOT_DESCRIPTION` | Описание бота |
|
||||||
| `allowed_users` | Список разрешённых user ID (пусто = все) |
|
| `BOT_ICON_EMOJI` | Emoji-иконка |
|
||||||
| `require_confirmation` | Требовать подтверждение перед выполнением |
|
| `ALLOWED_USERS` | Список разрешённых user ID через запятую (пусто = все) |
|
||||||
| `working_directory` | Рабочая директория для команд |
|
| `WORKING_DIRECTORY` | Рабочая директория для выполнения команд |
|
||||||
|
|
||||||
|
⚠️ **Важно:** После изменения `.env` требуется перезапуск бота.
|
||||||
|
|
||||||
## Безопасность
|
## Безопасность
|
||||||
|
|
||||||
|
|
@ -167,8 +201,14 @@ async def my_custom_command(update, context):
|
||||||
|
|
||||||
1. Бот выполняет команды от имени запустившего пользователя
|
1. Бот выполняет команды от имени запустившего пользователя
|
||||||
2. Не запускайте бота от root
|
2. Не запускайте бота от root
|
||||||
3. Ограничьте доступ через `allowed_users`
|
3. Ограничьте доступ через `ALLOWED_USERS` в `.env`:
|
||||||
|
```bash
|
||||||
|
ALLOWED_USERS=123456789,987654321
|
||||||
|
```
|
||||||
|
Ваш ID можно узнать через @userinfobot
|
||||||
4. Будьте осторожны с деструктивными командами (`rm`, `dd`, etc.)
|
4. Будьте осторожны с деструктивными командами (`rm`, `dd`, etc.)
|
||||||
|
5. **Никогда не передавайте файл `.env`** — он содержит токен бота
|
||||||
|
6. Добавьте `.env` в `.gitignore` (уже сделано)
|
||||||
|
|
||||||
## Логи
|
## Логи
|
||||||
|
|
||||||
|
|
@ -180,10 +220,12 @@ async def my_custom_command(update, context):
|
||||||
telegram-cli-bot/
|
telegram-cli-bot/
|
||||||
├── bot.py # Основной файл бота
|
├── bot.py # Основной файл бота
|
||||||
├── requirements.txt # Зависимости Python
|
├── requirements.txt # Зависимости Python
|
||||||
├── bot_config.json # Конфигурация (создаётся автоматически)
|
├── .env # Конфигурация (создаётся автоматически, не коммитить)
|
||||||
├── bot.log # Лог файл
|
├── .env.example # Пример конфигурации
|
||||||
├── .gitignore # Git ignore
|
├── .gitignore # Git ignore
|
||||||
└── README.md # Документация
|
├── bot.log # Лог файл
|
||||||
|
├── run.sh # Скрипт запуска
|
||||||
|
└── README.md # Документация
|
||||||
```
|
```
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
|
||||||
254
bot.py
254
bot.py
|
|
@ -6,14 +6,15 @@ Telegram CLI Bot - бот для выполнения CLI команд с мно
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Callable, Dict, Any, List
|
from typing import Optional, Callable, Dict, Any, List
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
|
||||||
from telegram.ext import (
|
from telegram.ext import (
|
||||||
Application,
|
Application,
|
||||||
|
|
@ -24,10 +25,11 @@ from telegram.ext import (
|
||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Загрузка переменных окружения из .env
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
# --- Конфигурация ---
|
# --- Конфигурация ---
|
||||||
BASE_DIR = Path(__file__).parent
|
BASE_DIR = Path(__file__).parent
|
||||||
CONFIG_FILE = BASE_DIR / "bot_config.json"
|
|
||||||
COMMANDS_FILE = BASE_DIR / "commands.yaml"
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
|
@ -40,16 +42,43 @@ logging.basicConfig(
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Конфигурация бота из переменных окружения ---
|
||||||
|
class BotConfig:
|
||||||
|
"""Конфигурация бота из переменных окружения."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = os.getenv("BOT_NAME", "CLI Assistant")
|
||||||
|
self.description = os.getenv("BOT_DESCRIPTION", "Бот для выполнения CLI команд")
|
||||||
|
self.icon = os.getenv("BOT_ICON_EMOJI", "🤖")
|
||||||
|
self.working_directory = os.getenv("WORKING_DIRECTORY", str(Path.home()))
|
||||||
|
|
||||||
|
# Парсинг списка разрешённых пользователей
|
||||||
|
allowed_users_str = os.getenv("ALLOWED_USERS", "")
|
||||||
|
if allowed_users_str.strip():
|
||||||
|
self.allowed_users = [
|
||||||
|
int(uid.strip())
|
||||||
|
for uid in allowed_users_str.split(",")
|
||||||
|
if uid.strip().isdigit()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.allowed_users = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_access_restricted(self) -> bool:
|
||||||
|
"""Проверка: ограничен ли доступ."""
|
||||||
|
return len(self.allowed_users) > 0
|
||||||
|
|
||||||
|
|
||||||
# --- Хранилище состояний пользователя ---
|
# --- Хранилище состояний пользователя ---
|
||||||
@dataclass
|
@dataclass
|
||||||
class UserState:
|
class UserState:
|
||||||
"""Состояние пользователя в диалоге."""
|
"""Состояние пользователя в диалоге."""
|
||||||
current_menu: str = "main"
|
current_menu: str = "main"
|
||||||
waiting_for_input: bool = False
|
waiting_for_input: bool = False
|
||||||
input_type: Optional[str] = None # "name", "description", "icon", "command"
|
input_type: Optional[str] = None
|
||||||
parent_menu: Optional[str] = None
|
parent_menu: Optional[str] = None
|
||||||
context: Dict[str, Any] = field(default_factory=dict)
|
context: Dict[str, Any] = field(default_factory=dict)
|
||||||
working_directory: Optional[str] = None # Текущая директория пользователя
|
working_directory: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class StateManager:
|
class StateManager:
|
||||||
|
|
@ -67,53 +96,6 @@ class StateManager:
|
||||||
self._states[user_id] = UserState()
|
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
|
@dataclass
|
||||||
class MenuItem:
|
class MenuItem:
|
||||||
|
|
@ -180,6 +162,31 @@ menu_builder = MenuBuilder()
|
||||||
command_registry = CommandRegistry()
|
command_registry = CommandRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Проверка прав доступа ---
|
||||||
|
def check_access(func):
|
||||||
|
"""Декоратор для проверки прав доступа пользователя."""
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
|
||||||
|
# Если доступ не ограничен — пропускаем всех
|
||||||
|
if not config.is_access_restricted:
|
||||||
|
return await func(update, context, *args, **kwargs)
|
||||||
|
|
||||||
|
if user_id not in config.allowed_users:
|
||||||
|
logger.warning(f"Попытка доступа от запрещённого пользователя {user_id}")
|
||||||
|
await update.message.reply_text(
|
||||||
|
"❌ *Доступ запрещён*\n\n"
|
||||||
|
"Ваш ID не добавлен в список разрешённых пользователей.\n"
|
||||||
|
f"Ваш ID: `{user_id}`",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
return await func(update, context, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# --- Инициализация меню ---
|
# --- Инициализация меню ---
|
||||||
def init_menus():
|
def init_menus():
|
||||||
"""Инициализация структуры меню."""
|
"""Инициализация структуры меню."""
|
||||||
|
|
@ -264,6 +271,7 @@ def init_menus():
|
||||||
|
|
||||||
# --- Хендлеры ---
|
# --- Хендлеры ---
|
||||||
|
|
||||||
|
@check_access
|
||||||
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Обработка команды /start."""
|
"""Обработка команды /start."""
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
|
|
@ -272,7 +280,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
state_manager.reset(user.id)
|
state_manager.reset(user.id)
|
||||||
|
|
||||||
# Показать текущую директорию
|
# Показать текущую директорию
|
||||||
working_dir = config.get("working_directory", str(Path.home()))
|
working_dir = config.working_directory
|
||||||
|
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
f"👋 Привет, {user.first_name}!\n\n"
|
f"👋 Привет, {user.first_name}!\n\n"
|
||||||
|
|
@ -288,6 +296,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@check_access
|
||||||
async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Обработка команды /menu - показывает главное меню."""
|
"""Обработка команды /menu - показывает главное меню."""
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
|
|
@ -298,7 +307,7 @@ async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
state.current_menu = "main"
|
state.current_menu = "main"
|
||||||
|
|
||||||
# Показать текущую директорию
|
# Показать текущую директорию
|
||||||
working_dir = state.working_directory or config.get("working_directory", str(Path.home()))
|
working_dir = state.working_directory or config.working_directory
|
||||||
|
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
f"🏠 *Главное меню*\n\n"
|
f"🏠 *Главное меню*\n\n"
|
||||||
|
|
@ -309,6 +318,7 @@ async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@check_access
|
||||||
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Обработка команды /help."""
|
"""Обработка команды /help."""
|
||||||
help_text = f"""
|
help_text = f"""
|
||||||
|
|
@ -347,6 +357,7 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
await update.message.reply_text(help_text, parse_mode="Markdown")
|
await update.message.reply_text(help_text, parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
@check_access
|
||||||
async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Обработка команды /settings."""
|
"""Обработка команды /settings."""
|
||||||
state = state_manager.get(update.effective_user.id)
|
state = state_manager.get(update.effective_user.id)
|
||||||
|
|
@ -445,65 +456,57 @@ async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
else:
|
else:
|
||||||
await query.edit_message_text("❌ Команда не найдена")
|
await query.edit_message_text("❌ Команда не найдена")
|
||||||
|
|
||||||
# Настройки бота
|
# Настройки бота - только просмотр, изменение через .env
|
||||||
elif callback == "set_name":
|
elif callback == "set_name":
|
||||||
state.waiting_for_input = True
|
|
||||||
state.input_type = "name"
|
|
||||||
await query.edit_message_text(
|
await query.edit_message_text(
|
||||||
"📝 *Изменение имени бота*\n\n"
|
"📝 *Изменение имени бота*\n\n"
|
||||||
f"Текущее имя: `{config.name}`\n\n"
|
f"Текущее имя: `{config.name}`\n\n"
|
||||||
"Отправьте новое имя бота:",
|
"Для изменения отредактируйте `.env`:\n"
|
||||||
|
"```\nBOT_NAME=Ваше имя\n```",
|
||||||
parse_mode="Markdown",
|
parse_mode="Markdown",
|
||||||
reply_markup=menu_builder.get_keyboard("settings")
|
reply_markup=menu_builder.get_keyboard("settings")
|
||||||
)
|
)
|
||||||
|
|
||||||
elif callback == "set_description":
|
elif callback == "set_description":
|
||||||
state.waiting_for_input = True
|
|
||||||
state.input_type = "description"
|
|
||||||
await query.edit_message_text(
|
await query.edit_message_text(
|
||||||
"📄 *Изменение описания бота*\n\n"
|
"📄 *Изменение описания бота*\n\n"
|
||||||
f"Текущее описание: `{config.description}`\n\n"
|
f"Текущее описание: `{config.description}`\n\n"
|
||||||
"Отправьте новое описание:",
|
"Для изменения отредактируйте `.env`:\n"
|
||||||
|
"```\nBOT_DESCRIPTION=Ваше описание\n```",
|
||||||
parse_mode="Markdown",
|
parse_mode="Markdown",
|
||||||
reply_markup=menu_builder.get_keyboard("settings")
|
reply_markup=menu_builder.get_keyboard("settings")
|
||||||
)
|
)
|
||||||
|
|
||||||
elif callback == "set_icon":
|
elif callback == "set_icon":
|
||||||
state.waiting_for_input = True
|
|
||||||
state.input_type = "icon"
|
|
||||||
await query.edit_message_text(
|
await query.edit_message_text(
|
||||||
"🎨 *Изменение иконки бота*\n\n"
|
"🎨 *Изменение иконки бота*\n\n"
|
||||||
f"Текущая иконка: `{config.icon}`\n\n"
|
f"Текущая иконка: `{config.icon}`\n\n"
|
||||||
"Отправьте новый emoji (один символ):",
|
"Для изменения отредактируйте `.env`:\n"
|
||||||
|
"```\nBOT_ICON_EMOJI=🤖\n```",
|
||||||
parse_mode="Markdown",
|
parse_mode="Markdown",
|
||||||
reply_markup=menu_builder.get_keyboard("settings")
|
reply_markup=menu_builder.get_keyboard("settings")
|
||||||
)
|
)
|
||||||
|
|
||||||
elif callback == "show_access":
|
elif callback == "show_access":
|
||||||
allowed = config.get("allowed_users", [])
|
if config.allowed_users:
|
||||||
if allowed:
|
text = "👥 *Разрешённые пользователи:*\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users)
|
||||||
text = "👥 *Разрешённые пользователи:*\n" + "\n".join(f"• `{uid}`" for uid in allowed)
|
|
||||||
else:
|
else:
|
||||||
text = "👥 *Доступ открыт для всех*\n\n(список разрешённых пользователей пуст)"
|
text = "👥 *Доступ открыт для всех*\n\n(список разрешённых пользователей пуст)"
|
||||||
await query.edit_message_text(text, parse_mode="Markdown")
|
await query.edit_message_text(text, parse_mode="Markdown")
|
||||||
|
|
||||||
elif callback == "add_access":
|
elif callback == "add_access":
|
||||||
state.waiting_for_input = True
|
|
||||||
state.input_type = "add_access"
|
|
||||||
await query.edit_message_text(
|
await query.edit_message_text(
|
||||||
"➕ *Добавление пользователя*\n\n"
|
"➕ *Добавление пользователя*\n\n"
|
||||||
"Отправьте ID пользователя Telegram:\n"
|
"Для добавления пользователя отредактируйте `.env`:\n"
|
||||||
"(можно получить через @userinfobot)",
|
"```\nALLOWED_USERS=123456789,987654321\n```\n"
|
||||||
|
"Ваш ID можно узнать через @userinfobot",
|
||||||
parse_mode="Markdown"
|
parse_mode="Markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
elif callback == "remove_access":
|
elif callback == "remove_access":
|
||||||
state.waiting_for_input = True
|
if config.allowed_users:
|
||||||
state.input_type = "remove_access"
|
text = "➖ *Удаление пользователя*\n\n" + "\n".join(f"• `{uid}`" for uid in config.allowed_users)
|
||||||
allowed = config.get("allowed_users", [])
|
text += "\n\nУдалите ID из `.env` чтобы убрать доступ"
|
||||||
if allowed:
|
|
||||||
text = "➖ *Удаление пользователя*\n\n" + "\n".join(f"• `{uid}`" for uid in allowed)
|
|
||||||
text += "\n\nОтправьте ID для удаления:"
|
|
||||||
else:
|
else:
|
||||||
text = "➖ Список пуст, некого удалять"
|
text = "➖ Список пуст, некого удалять"
|
||||||
await query.edit_message_text(text, parse_mode="Markdown")
|
await query.edit_message_text(text, parse_mode="Markdown")
|
||||||
|
|
@ -514,7 +517,7 @@ async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
f"*{config.icon} {config.name}*\n"
|
f"*{config.icon} {config.name}*\n"
|
||||||
f"_{config.description}_\n\n"
|
f"_{config.description}_\n\n"
|
||||||
f"Версия: 1.0.0\n"
|
f"Версия: 1.0.0\n"
|
||||||
f"Рабочая директория: `{config.get('working_directory')}`\n\n"
|
f"Рабочая директория: `{config.working_directory}`\n\n"
|
||||||
f"Бот позволяет выполнять CLI команды на вашем ПК\n"
|
f"Бот позволяет выполнять CLI команды на вашем ПК\n"
|
||||||
f"через интерфейс Telegram.",
|
f"через интерфейс Telegram.",
|
||||||
parse_mode="Markdown",
|
parse_mode="Markdown",
|
||||||
|
|
@ -529,7 +532,7 @@ async def execute_cli_command(query, command: str):
|
||||||
state = state_manager.get(user_id)
|
state = state_manager.get(user_id)
|
||||||
|
|
||||||
# Определяем рабочую директорию: сначала пользовательская, потом из конфига
|
# Определяем рабочую директорию: сначала пользовательская, потом из конфига
|
||||||
working_dir = state.working_directory or config.get("working_directory", str(Path.home()))
|
working_dir = state.working_directory or config.working_directory
|
||||||
|
|
||||||
logger.info(f"Выполнение команды: {command} в директории: {working_dir}")
|
logger.info(f"Выполнение команды: {command} в директории: {working_dir}")
|
||||||
|
|
||||||
|
|
@ -585,78 +588,13 @@ async def execute_cli_command(query, command: str):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_settings_input(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str):
|
@check_access
|
||||||
"""Обработка ввода в режиме настройки."""
|
|
||||||
user_id = update.effective_user.id
|
|
||||||
state = state_manager.get(user_id)
|
|
||||||
input_type = state.input_type
|
|
||||||
|
|
||||||
if input_type == "name":
|
|
||||||
config.set("bot_name", text)
|
|
||||||
await update.message.reply_text(
|
|
||||||
f"✅ Имя бота изменено на: `{text}`\n\n"
|
|
||||||
f"Используйте /start для возврата в главное меню",
|
|
||||||
parse_mode="Markdown"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif input_type == "description":
|
|
||||||
config.set("bot_description", text)
|
|
||||||
await update.message.reply_text(
|
|
||||||
f"✅ Описание изменено на: `{text}`\n\n"
|
|
||||||
f"Используйте /start для возврата в главное меню",
|
|
||||||
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 '🤖'}`\n\n"
|
|
||||||
f"Используйте /start для возврата в главное меню",
|
|
||||||
parse_mode="Markdown"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Обработка текстовых сообщений как CLI команд."""
|
"""Обработка текстовых сообщений как CLI команд."""
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
text = update.message.text.strip()
|
text = update.message.text.strip()
|
||||||
state = state_manager.get(user_id)
|
state = state_manager.get(user_id)
|
||||||
|
|
||||||
# Проверка: не в режиме настройки ли мы
|
|
||||||
if state.waiting_for_input:
|
|
||||||
await handle_settings_input(update, context, text)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Любое текстовое сообщение = CLI команда
|
# Любое текстовое сообщение = CLI команда
|
||||||
logger.info(f"Пользователь {user_id} отправил команду: {text}")
|
logger.info(f"Пользователь {user_id} отправил команду: {text}")
|
||||||
|
|
||||||
|
|
@ -673,7 +611,7 @@ async def execute_cli_command_from_message(update: Update, command: str):
|
||||||
state = state_manager.get(user_id)
|
state = state_manager.get(user_id)
|
||||||
|
|
||||||
# Определяем рабочую директорию: сначала пользовательская, потом из конфига
|
# Определяем рабочую директорию: сначала пользовательская, потом из конфига
|
||||||
working_dir = state.working_directory or config.get("working_directory", str(Path.home()))
|
working_dir = state.working_directory or config.working_directory
|
||||||
|
|
||||||
# Обработка команды cd - меняем директорию пользователя
|
# Обработка команды cd - меняем директорию пользователя
|
||||||
# Работает только с простыми командами cd, не с составными
|
# Работает только с простыми командами cd, не с составными
|
||||||
|
|
@ -829,33 +767,21 @@ async def post_init(application: Application):
|
||||||
]
|
]
|
||||||
await application.bot.set_my_commands(commands)
|
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("Бот инициализирован")
|
logger.info("Бот инициализирован")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Точка входа."""
|
"""Точка входа."""
|
||||||
# Проверка токена: сначала переменная окружения, потом конфиг
|
# Чтение токена только из переменной окружения
|
||||||
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
|
||||||
if not token and CONFIG_FILE.exists():
|
|
||||||
try:
|
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
||||||
config_data = json.load(f)
|
|
||||||
token = config_data.get("bot_token")
|
|
||||||
if token:
|
|
||||||
logger.info("Токен получен из конфигурации")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Не удалось прочитать токен из конфига: {e}")
|
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
print("❌ Ошибка: не установлен TELEGRAM_BOT_TOKEN")
|
print("❌ Ошибка: не установлен TELEGRAM_BOT_TOKEN")
|
||||||
print("Задайте переменную окружения:")
|
print("\nСпособы установки токена:")
|
||||||
print(" export TELEGRAM_BOT_TOKEN='your_token_here'")
|
print(" 1. Создайте файл .env по примеру .env.example")
|
||||||
print("Или запустите ./run.sh для интерактивной настройки")
|
print(" 2. Или задайте переменную окружения:")
|
||||||
|
print(" export TELEGRAM_BOT_TOKEN='your_token_here'")
|
||||||
|
print("\nИли запустите ./run.sh для интерактивной настройки")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Инициализация меню
|
# Инициализация меню
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
python-telegram-bot==21.0
|
python-telegram-bot==21.0
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
|
|
||||||
60
run.sh
60
run.sh
|
|
@ -6,23 +6,25 @@ set -e
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
CONFIG_FILE="$SCRIPT_DIR/bot_config.json"
|
ENV_FILE="$SCRIPT_DIR/.env"
|
||||||
|
|
||||||
# Функция для получения значения из JSON
|
# Функция для установки значения в .env
|
||||||
get_json_value() {
|
set_env_value() {
|
||||||
python3 -c "import json; data=json.load(open('$CONFIG_FILE')); print(data.get('$1', ''))" 2>/dev/null || echo ""
|
local key="$1"
|
||||||
}
|
local value="$2"
|
||||||
|
|
||||||
# Функция для установки значения в JSON
|
if [ -f "$ENV_FILE" ]; then
|
||||||
set_json_value() {
|
# Если ключ существует - обновляем
|
||||||
python3 -c "
|
if grep -q "^$key=" "$ENV_FILE"; then
|
||||||
import json
|
sed -i "s|^$key=.*|$key=$value|" "$ENV_FILE"
|
||||||
with open('$CONFIG_FILE', 'r') as f:
|
else
|
||||||
data = json.load(f)
|
# Иначе добавляем
|
||||||
data['$1'] = '$2'
|
echo "$key=$value" >> "$ENV_FILE"
|
||||||
with open('$CONFIG_FILE', 'w') as f:
|
fi
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
else
|
||||||
"
|
# Создаём файл
|
||||||
|
echo "$key=$value" > "$ENV_FILE"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Проверка виртуального окружения
|
# Проверка виртуального окружения
|
||||||
|
|
@ -49,20 +51,20 @@ pip install -q -r requirements.txt
|
||||||
# Работа с токеном
|
# Работа с токеном
|
||||||
TOKEN=""
|
TOKEN=""
|
||||||
|
|
||||||
# 1. Проверяем переменную окружения
|
# 1. Проверяем .env файл
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
TOKEN=$(grep "^TELEGRAM_BOT_TOKEN=" "$ENV_FILE" | cut -d'=' -f2)
|
||||||
|
if [ -n "$TOKEN" ]; then
|
||||||
|
echo "✅ Токен получен из .env"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Проверяем переменную окружения (имеет приоритет над .env)
|
||||||
if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
|
if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
|
||||||
TOKEN="$TELEGRAM_BOT_TOKEN"
|
TOKEN="$TELEGRAM_BOT_TOKEN"
|
||||||
echo "✅ Токен получен из переменной окружения"
|
echo "✅ Токен получен из переменной окружения"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Если нет в переменной, проверяем конфиг
|
|
||||||
if [ -z "$TOKEN" ] && [ -f "$CONFIG_FILE" ]; then
|
|
||||||
TOKEN=$(get_json_value "bot_token")
|
|
||||||
if [ -n "$TOKEN" ]; then
|
|
||||||
echo "✅ Токен получен из конфигурации"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Если токена нет нигде, запрашиваем у пользователя
|
# 3. Если токена нет нигде, запрашиваем у пользователя
|
||||||
if [ -z "$TOKEN" ]; then
|
if [ -z "$TOKEN" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -89,14 +91,10 @@ if [ -z "$TOKEN" ]; then
|
||||||
# Проверка формата токена (примерно 46 символов, содержит : и _)
|
# Проверка формата токена (примерно 46 символов, содержит : и _)
|
||||||
if [[ "$TOKEN" =~ ^[0-9]+:[A-Za-z0-9_-]+$ ]]; then
|
if [[ "$TOKEN" =~ ^[0-9]+:[A-Za-z0-9_-]+$ ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
read -p "💾 Сохранить токен в конфигурацию? (y/n): " SAVE
|
read -p "💾 Сохранить токен в .env? (y/n): " SAVE
|
||||||
if [[ "$SAVE" =~ ^[Yy]$ ]]; then
|
if [[ "$SAVE" =~ ^[Yy]$ ]]; then
|
||||||
# Создаём конфиг если нет
|
set_env_value "TELEGRAM_BOT_TOKEN" "$TOKEN"
|
||||||
if [ ! -f "$CONFIG_FILE" ]; then
|
echo "✅ Токен сохранён в $ENV_FILE"
|
||||||
echo '{}' > "$CONFIG_FILE"
|
|
||||||
fi
|
|
||||||
set_json_value "bot_token" "$TOKEN"
|
|
||||||
echo "✅ Токен сохранён в $CONFIG_FILE"
|
|
||||||
fi
|
fi
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue