diff --git a/miband_tracker/bot/app.py b/miband_tracker/bot/app.py index bbf51c6..f443045 100644 --- a/miband_tracker/bot/app.py +++ b/miband_tracker/bot/app.py @@ -64,7 +64,20 @@ STEP_GOAL = 10_000 # можно вынести в env при желании # Auth helpers # --------------------------------------------------------------------------- def is_allowed(update: Update) -> bool: + global ALLOWED_USER_ID uid = update.effective_user.id if update.effective_user else None + if uid is None: + return False + if ALLOWED_USER_ID is None: + ALLOWED_USER_ID = uid + try: + allowed_user_file = SETTINGS.data_dir / "allowed_user.id" + SETTINGS.data_dir.mkdir(parents=True, exist_ok=True) + allowed_user_file.write_text(str(uid), encoding="utf-8") + logger.info("🎉 Бот успешно привязан к первому пользователю (ID: %s)!", uid) + except Exception as e: + logger.error("Не удалось сохранить ID владельца в файл: %s", e) + return True return uid == ALLOWED_USER_ID @@ -1483,11 +1496,15 @@ def main() -> None: logger.error("%s", exc) sys.exit(1) BOT_TOKEN = SETTINGS.telegram_bot_token - ALLOWED_USER_ID = SETTINGS.require_user_id() - DB_PATH = str(SETTINGS.user_db_path(ALLOWED_USER_ID)) + ALLOWED_USER_ID = SETTINGS.telegram_allowed_user_id + if ALLOWED_USER_ID is not None: + DB_PATH = str(SETTINGS.user_db_path(ALLOWED_USER_ID)) + logger.info("Стартую fitness-bot для пользователя: %s", ALLOWED_USER_ID) + else: + DB_PATH = str(SETTINGS.db_path) + logger.info("Стартую fitness-bot в режиме ожидания привязки владельца (первое входящее сообщение привяжет бота)...") init_state_db() - logger.info("Стартую fitness-bot для пользователя: %s", ALLOWED_USER_ID) app = Application.builder().token(BOT_TOKEN).build() app.add_handler(CommandHandler("start", cmd_start)) app.add_handler(CommandHandler("status", cmd_status)) diff --git a/miband_tracker/config.py b/miband_tracker/config.py index de384e5..c695a98 100644 --- a/miband_tracker/config.py +++ b/miband_tracker/config.py @@ -46,8 +46,16 @@ class Settings: def from_env(cls, *, require_bot: bool = False) -> Settings: data_dir = Path(os.environ.get("DATA_DIR", "/opt/miband-tracker/data")) allowed_user_id = parse_single_user_id( - os.environ.get("TELEGRAM_ALLOWED_USER_ID", ""), required=require_bot + os.environ.get("TELEGRAM_ALLOWED_USER_ID", ""), required=False ) + # Если ID не задан в env, пробуем загрузить из файла allowed_user.id + allowed_user_file = data_dir / "allowed_user.id" + if allowed_user_id is None and allowed_user_file.exists(): + try: + allowed_user_id = int(allowed_user_file.read_text(encoding="utf-8").strip()) + except Exception: + pass + bot_token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip() if require_bot and not bot_token: raise ConfigError("TELEGRAM_BOT_TOKEN не задан") diff --git a/miband_tracker/lock.py b/miband_tracker/lock.py index c7d6f6b..a8075c5 100644 --- a/miband_tracker/lock.py +++ b/miband_tracker/lock.py @@ -3,13 +3,24 @@ from __future__ import annotations -import fcntl import os import time from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path +# Попытка импорта fcntl (для Unix-систем) или msvcrt (для Windows) +try: + import fcntl + _HAS_FCNTL = True +except ImportError: + _HAS_FCNTL = False + try: + import msvcrt + _HAS_MSVCRT = True + except ImportError: + _HAS_MSVCRT = False + class LockUnavailable(RuntimeError): """Raised when another process already owns the sync lock.""" @@ -19,10 +30,23 @@ class LockUnavailable(RuntimeError): def exclusive_file_lock(path: Path) -> Iterator[None]: path.parent.mkdir(parents=True, exist_ok=True) with path.open("a+", encoding="utf-8") as lock_file: - try: - fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError as exc: - raise LockUnavailable(f"Lock is already held: {path}") from exc + fd = lock_file.fileno() + + if _HAS_FCNTL: + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError as exc: + raise LockUnavailable(f"Lock is already held: {path}") from exc + elif _HAS_MSVCRT: + try: + lock_file.seek(0) + # Блокируем первые 64 байта файла без ожидания (non-blocking) + msvcrt.locking(fd, msvcrt.LK_NBLCK, 64) + except (OSError, IOError) as exc: + raise LockUnavailable(f"Lock is already held: {path}") from exc + else: + # Резервный вариант, если блокировки недоступны на платформе + pass try: lock_file.seek(0) @@ -31,4 +55,14 @@ def exclusive_file_lock(path: Path) -> Iterator[None]: lock_file.flush() yield finally: - fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) + if _HAS_FCNTL: + try: + fcntl.flock(fd, fcntl.LOCK_UN) + except Exception: + pass + elif _HAS_MSVCRT: + try: + lock_file.seek(0) + msvcrt.locking(fd, msvcrt.LK_UNLCK, 64) + except Exception: + pass diff --git a/miband_tracker/sync.py b/miband_tracker/sync.py index 250b0df..a94b2e9 100644 --- a/miband_tracker/sync.py +++ b/miband_tracker/sync.py @@ -371,8 +371,17 @@ async def daemon_main(settings: Settings | None = None) -> int: log(f"Running in daemon mode. Sync interval: {settings.sync_interval} seconds.") while True: try: - await run_sync(settings=settings) + current_settings = Settings.from_env() + if current_settings.telegram_allowed_user_id is None: + log("Waiting for allowed user ID to be registered via Telegram bot...") + await asyncio.sleep(5) + continue + + await run_sync(settings=current_settings) except Exception as exc: log(f"Unhandled error in main loop: {exc}") - log(f"Sleeping for {settings.sync_interval} seconds...") - await asyncio.sleep(settings.sync_interval) + + # Load settings again to catch any runtime changes in sync interval + interval = Settings.from_env().sync_interval + log(f"Sleeping for {interval} seconds...") + await asyncio.sleep(interval) diff --git a/setup.bat b/setup.bat new file mode 100644 index 0000000..dc7cb1d --- /dev/null +++ b/setup.bat @@ -0,0 +1,260 @@ +@echo off +:: ============================================================================== +:: miband-bot Smart Setup Script for Windows (100% Flat robust version) +:: ============================================================================== +chcp 65001 > nul +cls + +echo ====================================================================== +echo 🚀 УМНЫЙ ЗАПУСК И НАСТРОЙКА miband-bot 🚀 +echo ====================================================================== +echo Этот скрипт поможет вам быстро развернуть личного Telegram-бота +echo для сбора данных Xiaomi Fitness / Mi Band. +echo. + +:: ------------------------------------------------------------------------------ +:: Шаг 1. Выбор метода установки +:: ------------------------------------------------------------------------------ +echo Как вы хотите запустить бота? +echo 1] Напрямую на Python (Рекомендуется. Потребляет минимум памяти) +echo 2] В Docker контейнерах (Всё работает в фоне) +echo. + +:CHOOSE_MODE +set install_mode=1 +set /p install_mode="Выберите вариант (1 или 2) [1]: " +if "%install_mode%"=="1" goto MODE_PYTHON +if "%install_mode%"=="2" goto MODE_DOCKER +echo Неверный ввод. Пожалуйста, введите 1 или 2. +echo. +goto CHOOSE_MODE + +:: ------------------------------------------------------------------------------ +:: Шаг 2. Проверка требований +:: ------------------------------------------------------------------------------ +:MODE_DOCKER +echo. +echo [1/3] Проверка окружения Docker... +where docker >nul 2>nul +if errorlevel 1 ( + echo ❌ Docker CLI не найден. Пожалуйста, установите Docker Desktop или настройте WSL 2. + pause + exit /b 1 +) +docker info >nul 2>nul +if errorlevel 1 ( + echo ❌ Демон Docker не запущен. Пожалуйста, запустите Docker и перезапустите скрипт. + pause + exit /b 1 +) +echo ✓ Движок Docker активен и готов к работе! +set DOCKER_ACTIVE=1 +goto SETUP_ENV + +:MODE_PYTHON +echo. +echo [1/3] Проверка окружения Python... +python --version >nul 2>nul +if not errorlevel 1 goto PYTHON_OK + +echo ❌ Python не найден или не настроен в вашей системе. +echo Вы можете скачать его вручную по ссылке: https://www.python.org/downloads/ +echo. +set install_python=y +set /p install_python="Хотите, чтобы я автоматически скачал и установил Python 3.11? [Y/n]: " +if /i not "%install_python%"=="y" ( + echo Установка отменена. Пожалуйста, установите Python вручную. + pause + exit /b 1 +) + +echo. +echo Скачивание установщика Python 3.11.9... +powershell -Command "Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe' -OutFile 'python_installer.exe'" +echo Установка Python (это займет около минуты, пожалуйста, подождите)... +python_installer.exe /quiet InstallAllUsers=0 PrependPath=1 Include_test=0 +del python_installer.exe + +:: Добавляем установленный Python в PATH текущей сессии +set "PATH=%LocalAppData%\Programs\Python\Python311;%LocalAppData%\Programs\Python\Python311\Scripts;%PATH%" + +python --version >nul 2>nul +if errorlevel 1 ( + echo ❌ Не удалось автоматически установить Python или добавить его в PATH. + echo Пожалуйста, установите Python вручную по ссылке: https://www.python.org/downloads/ + pause + exit /b 1 +) + +:PYTHON_OK +echo ✓ Python найден! +set DOCKER_ACTIVE=0 +goto SETUP_ENV + +:: ------------------------------------------------------------------------------ +:: Шаг 3. Настройка secrets.env +:: ------------------------------------------------------------------------------ +:SETUP_ENV +echo. +echo [2/3] Конфигурация параметров бота (secrets.env)... + +if not exist secrets.env goto INPUT_ENV_VALUES +echo Файл конфигурации secrets.env уже существует. +set overwrite_env=n +set /p overwrite_env="Хотите перезаписать его и настроить заново? [y/N]: " +if /i not "%overwrite_env%"=="y" ( + echo ✓ Сохраняем существующий secrets.env. + goto LAUNCH_PHASE +) + +:INPUT_ENV_VALUES +echo. +echo Сейчас мы настроим основные параметры безопасности. +echo. + +:INPUT_TOKEN +echo 🔑 Шаг 1. Получение Telegram Bot Token +echo Для работы бота необходим токен. Его можно получить бесплатно за 1 минуту: +echo 1. Откройте Telegram и перейдите к официальному боту @BotFather. +echo 2. Отправьте ему команду /newbot. +echo 3. Введите название для бота (например, Мой Mi Band Бот). +echo 4. Введите уникальное имя пользователя (username) на английском, +echo заканчивающееся на bot (например, my_miband_sync_bot). +echo 5. Скопируйте полученный токен (выглядит как 123456789:ABC-DEF...). +echo. +set /p bot_token="Вставьте ваш Telegram Bot Token: " +if "%bot_token%"=="" ( + echo Ошибка: Токен не может быть пустым. + echo. + goto INPUT_TOKEN +) +echo %bot_token% | findstr /c:":" >nul +if errorlevel 1 ( + echo Ошибка: Токен должен содержать символ ":" + echo. + goto INPUT_TOKEN +) +echo. + +echo 🔒 Шаг 2. Автоматическая привязка владельца (Whitelist ID) +echo Вам НЕ НУЖНО вручную искать и вводить ваш Telegram User ID! +echo Сразу после запуска бота откройте его в Telegram и отправьте команду /start. +echo Бот автоматически распознает ваш ID, запишет его в белый список +echo и заблокирует доступ для всех остальных пользователей. +echo. + +:: Создание secrets.env (запись построчно во избежание багов парсера скобок) +echo # ============================================================================== > secrets.env +echo # Конфигурация secrets.env для miband-bot >> secrets.env +echo # ============================================================================== >> secrets.env +echo. >> secrets.env +echo # Токен вашего Telegram-бота (полученный от @BotFather) >> secrets.env +echo TELEGRAM_BOT_TOKEN=%bot_token% >> secrets.env +echo. >> secrets.env +echo # Единственный разрешенный Telegram User ID (для безопасности данных) >> secrets.env +echo # Оставьте пустым - бот автоматически привяжется к первому, кто напишет /start! >> secrets.env +echo TELEGRAM_ALLOWED_USER_ID= >> secrets.env +echo. >> secrets.env +echo # Интервал фоновой синхронизации данных из облака Xiaomi Fitness (в секундах) >> secrets.env +echo SYNC_INTERVAL=900 >> secrets.env +echo. >> secrets.env +echo # Глубина запроса при автоматической синхронизации (в днях) >> secrets.env +echo QUERY_DURATION=2 >> secrets.env +echo. >> secrets.env +echo # Загружать детальные ночные данные о сне (FDS) >> secrets.env +echo ENABLE_FDS_SLEEP_DETAILS=true >> secrets.env + +echo ✓ Файл secrets.env успешно создан! +echo. + +:: ------------------------------------------------------------------------------ +:: Шаг 4. Установка зависимостей и запуск +:: ------------------------------------------------------------------------------ +:LAUNCH_PHASE +echo [3/3] Подготовка среды выполнения... + +if not "%DOCKER_ACTIVE%"=="1" goto PYTHON_LAUNCH + +:DOCKER_LAUNCH +set launch_now=y +set /p launch_now="Хотите запустить miband-bot в Docker прямо сейчас? [Y/n]: " +if /i not "%launch_now%"=="y" ( + echo. + echo Бот настроен, но не запущен. + echo Запустите вручную командой: docker compose up -d --build + goto END_LAUNCH +) +echo Сборка и запуск контейнеров... +docker compose up -d --build +if not errorlevel 1 ( + echo. + echo ====================================================================== + echo 🎉 miband-bot УСПЕШНО НАСТРОЕН И ЗАПУЩЕН В DOCKER! 🎉 + echo ====================================================================== + echo Теперь откройте бота в Telegram и отправьте команду /start. + echo. + echo Логи: docker compose logs -f fitness-bot + echo Стоп: docker compose down + echo ====================================================================== +) else ( + echo Ошибка при сборке или запуске Docker Compose. +) +goto END_LAUNCH + +:PYTHON_LAUNCH +echo Создаем виртуальное окружение .venv и устанавливаем зависимости... +if not exist .venv ( + python -m venv .venv +) +call .venv\Scripts\activate +echo Установка библиотек... +pip install -r requirements.txt -e mi-fitness-python + +:: Записываем рабочий запуск в run_local.bat построчно (полностью без скобочных блоков) +echo @echo off > run_local.bat +echo chcp 65001 ^> nul >> run_local.bat +echo cd /d "%%~dp0" >> run_local.bat +echo if not exist .venv ^( >> run_local.bat +echo echo Ошибка: Виртуальное окружение .venv не найдено. Запустите setup.bat сначала. >> run_local.bat +echo pause >> run_local.bat +echo exit /b 1 >> run_local.bat +echo ^) >> run_local.bat +echo. >> run_local.bat +echo :: Загрузка переменных из secrets.env >> run_local.bat +echo if exist secrets.env ^( >> run_local.bat +echo for /f "usebackq delims=" %%%%x in ^("secrets.env"^) do ^( >> run_local.bat +echo echo %%%%x ^| findstr /r "^#" ^>nul >> run_local.bat +echo if errorlevel 1 ^( >> run_local.bat +echo set %%%%x >> run_local.bat +echo ^) >> run_local.bat +echo ^) >> run_local.bat +echo ^) >> run_local.bat +echo. >> run_local.bat +echo :: Настройка локальных путей к папке data >> run_local.bat +echo set DATA_DIR=.\data >> run_local.bat +echo set DB_PATH=.\data\miband_%%TELEGRAM_ALLOWED_USER_ID%%.db >> run_local.bat +echo set STATUS_PATH=.\data\status_%%TELEGRAM_ALLOWED_USER_ID%%.json >> run_local.bat +echo set BOT_STATE_DB_PATH=.\data\fitness_bot_state.db >> run_local.bat +echo. >> run_local.bat +echo call .venv\Scripts\activate >> run_local.bat +echo === Запуск miband-bot локально ^(без Docker^) === >> run_local.bat +echo Для завершения работы закройте это окно консоли. >> run_local.bat +echo. >> run_local.bat +echo Запуск синхронизатора и Telegram-бота... >> run_local.bat +echo. >> run_local.bat +echo [Запуск фонового синхронизатора...] >> run_local.bat +echo start /b python -u miband_sync.py >> run_local.bat +echo [Запуск бота...] >> run_local.bat +echo python -u fitness_bot.py >> run_local.bat + +echo ✓ Создан удобный скрипт запуска: run_local.bat +echo. +set launch_now=y +set /p launch_now="Хотите запустить бота локально прямо сейчас? [Y/n]: " +if /i "%launch_now%"=="y" ( + call run_local.bat +) + +:END_LAUNCH +echo. +pause diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..6914b4d --- /dev/null +++ b/setup.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env bash + +# ============================================================================== +# miband-bot Smart Setup Script for macOS / Linux +# ============================================================================== + +# Цвета для вывода в консоль +RED='\033[0;31m' +# Приятный зеленый +GREEN='\033[0;32m' +# Насыщенный желтый +YELLOW='\033[1;33m' +# Глубокий синий +BLUE='\033[0;34m' +# Пурпурный +MAGENTA='\033[0;35m' +# Серый / обычный +NC='\033[0;37m' +BOLD='\033[1m' + +clear +echo -e "${BLUE}${BOLD}======================================================================" +echo -e " 🚀 УМНЫЙ ЗАПУСК И НАСТРОЙКА miband-bot 🚀" +echo -e "======================================================================${NC}" +echo -e "Этот скрипт поможет вам быстро развернуть личного Telegram-бота" +echo -e "для сбора данных Xiaomi Fitness / Mi Band." +echo "" + +# ------------------------------------------------------------------------------ +# Шаг 1. Выбор метода установки +# ------------------------------------------------------------------------------ +echo -e "${BOLD}Как вы хотите запустить бота?${NC}" +echo -e " ${BOLD}1)${NC} ${GREEN}Напрямую на Python${NC} (Рекомендуется. Потребляет минимум памяти)" +echo -e " ${BOLD}2)${NC} ${MAGENTA}В Docker контейнерах${NC} (Всё работает в фоне, легко управлять)" +echo "" + +while true; do + read -p "Выберите вариант (1 или 2) [1]: " install_mode + if [ -z "$install_mode" ]; then + install_mode="1" + fi + if [ "$install_mode" = "1" ] || [ "$install_mode" = "2" ]; then + break + else + echo -e "${RED}Неверный ввод. Пожалуйста, введите 1 или 2.${NC}" + fi +done +echo "" + +# ------------------------------------------------------------------------------ +# Шаг 2. Проверка требований выбранного режима +# ------------------------------------------------------------------------------ +if [ "$install_mode" = "2" ]; then + echo -e "${BOLD}[1/3] Проверка окружения Docker...${NC}" + + DOCKER_AVAILABLE=true + DOCKER_RUNNING=true + + if ! command -v docker &> /dev/null; then + DOCKER_AVAILABLE=false + fi + + if [ "$DOCKER_AVAILABLE" = true ]; then + if ! docker info &> /dev/null; then + DOCKER_RUNNING=false + fi + fi + + if [ "$DOCKER_AVAILABLE" = false ]; then + echo -e "${RED}❌ Docker CLI не найден в вашей системе.${NC}" + echo -e "Пожалуйста, установите Docker движок перед продолжением:" + echo -e " - ${BLUE}Linux:${NC} Установите 'docker' и 'docker-compose-plugin' через менеджер пакетов." + echo -e " - ${BLUE}macOS:${NC} Запустите легкий ${BOLD}Colima${NC} ('brew install colima') или Docker Desktop." + echo -e "Или вернитесь назад и выберите вариант запуска напрямую на Python." + exit 1 + elif [ "$DOCKER_RUNNING" = false ]; then + echo -e "${RED}❌ Служба Docker установлена, но сейчас не запущена.${NC}" + echo -e "Пожалуйста, запустите Docker-демон (например, 'colima start' на Mac" + echo -e "или 'sudo systemctl start docker' на Linux) и перезапустите скрипт." + exit 1 + else + echo -e "${GREEN}✓ Движок Docker активен и готов к работе!${NC}" + if docker compose version &> /dev/null; then + COMPOSE_CMD="docker compose" + elif command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + else + echo -e "${YELLOW}⚠️ Команда 'docker compose' не найдена. Убедитесь, что плагин Compose установлен.${NC}" + fi + fi + +else + echo -e "${BOLD}[1/3] Проверка окружения Python...${NC}" + + PYTHON_CMD="" + if command -v python3 &> /dev/null; then + PYTHON_CMD="python3" + elif command -v python &> /dev/null; then + # Проверяем версию python + py_ver=$(python -c 'import sys; print(sys.version_info[0])' 2>/dev/null) + if [ "$py_ver" = "3" ]; then + PYTHON_CMD="python" + fi + fi + + if [ -z "$PYTHON_CMD" ]; then + echo -e "${RED}❌ Python 3 не найден в вашей системе.${NC}" + echo -e "Пожалуйста, установите Python версии 3.10 или выше." + exit 1 + else + py_full_ver=$($PYTHON_CMD -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")') + echo -e "${GREEN}✓ Найден Python $py_full_ver!${NC}" + fi +fi +echo "" + +# ------------------------------------------------------------------------------ +# Шаг 3. Настройка secrets.env +# ------------------------------------------------------------------------------ +echo -e "${BOLD}[2/3] Конфигурация параметров бота (secrets.env)...${NC}" + +ENV_FILE="secrets.env" +SETUP_CONFIG=true + +if [ -f "$ENV_FILE" ]; then + echo -e "${YELLOW}Файл конфигурации secrets.env уже существует.${NC}" + read -p "Хотите перезаписать его и настроить параметры заново? [y/N]: " overwrite_env + if [ -z "$overwrite_env" ]; then + overwrite_env="n" + fi + if [[ ! $overwrite_env =~ ^[Yy]$ ]]; then + echo -e "${GREEN}✓ Сохраняем существующий secrets.env.${NC}" + SETUP_CONFIG=false + fi +fi + +if [ "$SETUP_CONFIG" = true ]; then + echo -e "Сейчас мы настроим основные параметры безопасности." + echo "" + + # Ввод токена бота с подробной инструкцией + while true; do + echo -e "🔑 ${BOLD}Шаг 1. Получение Telegram Bot Token${NC}" + echo -e "Для работы бота необходим токен. Его можно получить бесплатно за 1 минуту:" + echo -e " 1. Откройте Telegram и перейдите к официальному боту ${BLUE}@BotFather${NC}." + echo -e " 2. Отправьте ему команду ${BOLD}/newbot${NC}." + echo -e " 3. Введите название для бота (например, ${YELLOW}Мой Mi Band Бот${NC})." + echo -e " 4. Введите уникальное имя пользователя (username) на английском," + echo -e " заканчивающееся на ${BOLD}bot${NC} (например, ${YELLOW}my_miband_sync_bot${NC})." + echo -e " 5. Скопируйте полученный токен (выглядит как ${BLUE}123456789:ABC-DEF...${NC})." + echo "" + read -p "Вставьте ваш Telegram Bot Token: " bot_token + + if [ -n "$bot_token" ] && [[ "$bot_token" == *":"* ]]; then + break + else + echo -e "${RED}Ошибка: Токен должен быть непустым и содержать символ ':'${NC}\n" + fi + done + echo "" + + echo -e "🔒 ${BOLD}Шаг 2. Автоматическая привязка владельца (Whitelist ID)${NC}" + echo -e "Вам ${BOLD}НЕ НУЖНО${NC} вручную искать и вводить ваш Telegram User ID!" + echo -e "Сразу после запуска бота откройте его в Telegram и отправьте команду ${BOLD}/start${NC}." + echo -e "Бот автоматически распознает ваш ID, запишет его в белый список" + echo -e "и заблокирует доступ для всех остальных пользователей." + echo "" + + # Создание secrets.env + cat << EOF > "$ENV_FILE" +# ============================================================================== +# Конфигурация secrets.env для miband-bot +# ============================================================================== + +# Токен вашего Telegram-бота (полученный от @BotFather) +TELEGRAM_BOT_TOKEN=$bot_token + +# Единственный разрешенный Telegram User ID (для безопасности данных) +# Оставьте пустым - бот автоматически привяжется к первому, кто напишет /start! +TELEGRAM_ALLOWED_USER_ID= + +# Интервал фоновой синхронизации данных из облака Xiaomi Fitness (в секундах) +SYNC_INTERVAL=900 + +# Глубина запроса при автоматической синхронизации (в днях) +QUERY_DURATION=2 + +# Загружать детальные ночные данные о сне (FDS) +ENABLE_FDS_SLEEP_DETAILS=true +EOF + + chmod 600 "$ENV_FILE" 2>/dev/null + echo -e "${GREEN}✓ Файл secrets.env успешно создан!${NC}" +fi +echo "" + +# ------------------------------------------------------------------------------ +# Шаг 4. Установка зависимостей и запуск +# ------------------------------------------------------------------------------ +echo -e "${BOLD}[3/3] Подготовка среды выполнения...${NC}" + +if [ "$install_mode" = "2" ]; then + # Режим Docker + read -p "Хотите запустить miband-bot в Docker прямо сейчас? [Y/n]: " launch_now + if [ -z "$launch_now" ]; then + launch_now="y" + fi + if [[ $launch_now =~ ^[Yy]$ ]]; then + echo -e "${BLUE}Сборка и старт контейнеров в фоне...${NC}" + if $COMPOSE_CMD up -d --build; then + echo "" + echo -e "${GREEN}${BOLD}======================================================================" + echo -e " 🎉 miband-bot УСПЕШНО НАСТРОЕН И ЗАПУЩЕН В DOCKER! 🎉" + echo -e "======================================================================${NC}" + echo -e "Следующие шаги:" + echo -e "1. Откройте вашего бота в Telegram и отправьте ${BOLD}/start${NC}." + echo -e "2. Войдите в аккаунт Xiaomi по ссылке или QR-коду." + echo "" + echo -e "Команда для просмотра логов:" + echo -e " ${BLUE}$COMPOSE_CMD logs -f fitness-bot${NC}" + echo -e "Команда для остановки бота:" + echo -e " ${BLUE}$COMPOSE_CMD down${NC}" + echo "======================================================================" + else + echo -e "${RED}Ошибка при запуске Docker Compose.${NC}" + fi + else + echo -e "${YELLOW}Бот настроен, но не запущен.${NC}" + echo -e "Запустите вручную командой: ${BLUE}$COMPOSE_CMD up -d --build${NC}" + fi + +else + # Режим чистого Python + echo -e "Создаем виртуальное окружение .venv..." + + if [ ! -d ".venv" ]; then + if ! $PYTHON_CMD -m venv .venv; then + echo -e "${RED}❌ Не удалось создать виртуальное окружение .venv.${NC}" + echo -e "В вашей системе (вероятно, Debian/Ubuntu) отсутствует пакет для виртуальных сред." + echo -e "Пожалуйста, установите его с помощью команды:" + echo -e " ${BOLD}sudo apt update && sudo apt install -y python3-venv${NC}" + exit 1 + fi + fi + + source .venv/bin/activate + echo -e "${BLUE}Установка библиотек из requirements.txt и mi-fitness-python...${NC}" + if pip install -r requirements.txt -e mi-fitness-python; then + echo -e "${GREEN}✓ Все зависимости успешно установлены!${NC}" + else + echo -e "${RED}❌ Не удалось установить зависимости. Проверьте подключение к сети.${NC}" + exit 1 + fi + + # Создаем удобный скрипт для локального запуска + LAUNCHER_FILE="run_local.sh" + cat << 'EOF' > "$LAUNCHER_FILE" +#!/usr/bin/env bash +# ============================================================================== +# Скрипт локального запуска miband-bot (без Docker) +# ============================================================================== +cd "$(dirname "$0")" + +if [ ! -d ".venv" ]; then + echo "Ошибка: Папка .venv не найдена. Запустите setup.sh заново." + exit 1 +fi + +# Загружаем переменные из secrets.env в окружение текущего процесса +if [ -f secrets.env ]; then + while IFS= read -r line || [ -n "$line" ]; do + # Очищаем пробелы и переносы + line=$(echo "$line" | xargs 2>/dev/null || echo "$line") + # Игнорируем комментарии и пустые строки + if [[ ! "$line" =~ ^# ]] && [[ ! -z "$line" ]]; then + export "$line" + fi + done < secrets.env +fi + +# Настраиваем локальные пути к папке data (чтобы не писать в глобальные пути /opt) +export DATA_DIR="./data" +export DB_PATH="./data/miband_${TELEGRAM_ALLOWED_USER_ID}.db" +export STATUS_PATH="./data/status_${TELEGRAM_ALLOWED_USER_ID}.json" +export BOT_STATE_DB_PATH="./data/fitness_bot_state.db" + +source .venv/bin/activate +echo "=== Запуск miband-bot локально на Python ===" +echo "Нажмите Ctrl+C для завершения работы обоих процессов." +echo "" + +# При завершении скрипта (Ctrl+C) убиваем все запущенные нами фоновые задачи +trap "kill 0" EXIT + +# Запускаем фоновый синхронизатор и сам Telegram бот параллельно +python -u miband_sync.py & +python -u fitness_bot.py & + +# Ожидаем завершения +wait +EOF + chmod +x "$LAUNCHER_FILE" + echo -e "${GREEN}✓ Создан удобный скрипт запуска: ${BOLD}./run_local.sh${NC}" + echo "" + + read -p "Хотите запустить бота локально прямо сейчас? [Y/n]: " launch_now + if [ -z "$launch_now" ]; then + launch_now="y" + fi + if [[ $launch_now =~ ^[Yy]$ ]]; then + ./run_local.sh + else + echo -e "${YELLOW}Бот готов к запуску.${NC}" + echo -e "Запускайте его в любое время с помощью скрипта:" + echo -e " ${BLUE}./run_local.sh${NC}" + fi +fi + +echo "" diff --git a/tests/test_dynamic_whitelist.py b/tests/test_dynamic_whitelist.py new file mode 100644 index 0000000..1dd3460 --- /dev/null +++ b/tests/test_dynamic_whitelist.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2026 Alexey + +from __future__ import annotations + +import os +from pathlib import Path +from unittest.mock import MagicMock + +from telegram import Update, User + +from miband_tracker.bot import app +from miband_tracker.config import Settings + + +def test_dynamic_whitelisting(tmp_path: Path) -> None: + # Setup temporary directories and config + data_dir = tmp_path / "data" + data_dir.mkdir() + + # Override settings in app and os.environ + os.environ["DATA_DIR"] = str(data_dir) + os.environ["TELEGRAM_ALLOWED_USER_ID"] = "" + + app.SETTINGS = Settings.from_env() + app.ALLOWED_USER_ID = None + + # Verify initial state + assert app.ALLOWED_USER_ID is None + assert not (data_dir / "allowed_user.id").exists() + + # 1. First user tries to access + user_1 = MagicMock(spec=User) + user_1.id = 999999 + update_1 = MagicMock(spec=Update) + update_1.effective_user = user_1 + + # First access should be allowed and should bind user_1 + assert app.is_allowed(update_1) is True + assert app.ALLOWED_USER_ID == 999999 + assert (data_dir / "allowed_user.id").exists() + assert (data_dir / "allowed_user.id").read_text(encoding="utf-8").strip() == "999999" + + # 2. Second user tries to access + user_2 = MagicMock(spec=User) + user_2.id = 888888 + update_2 = MagicMock(spec=Update) + update_2.effective_user = user_2 + + # Access for user_2 must be blocked + assert app.is_allowed(update_2) is False + + # 3. First user accesses again + assert app.is_allowed(update_1) is True + + # 4. Verify settings reloading + reloaded_settings = Settings.from_env() + assert reloaded_settings.telegram_allowed_user_id == 999999