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:
mirivlad 2026-02-23 16:52:06 +08:00
parent c2f62295b7
commit 96d2577415
5 changed files with 219 additions and 236 deletions

16
.env.example Normal file
View File

@ -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

View File

@ -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,9 +220,11 @@ 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
├── bot.log # Лог файл
├── run.sh # Скрипт запуска
└── README.md # Документация └── README.md # Документация
``` ```

252
bot.py
View File

@ -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(" 1. Создайте файл .env по примеру .env.example")
print(" 2. Или задайте переменную окружения:")
print(" export TELEGRAM_BOT_TOKEN='your_token_here'") print(" export TELEGRAM_BOT_TOKEN='your_token_here'")
print("Или запустите ./run.sh для интерактивной настройки") print("\nИли запустите ./run.sh для интерактивной настройки")
sys.exit(1) sys.exit(1)
# Инициализация меню # Инициализация меню

View File

@ -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
View File

@ -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