feat: add dynamic owner binding

This commit is contained in:
Alex
2026-05-24 21:53:23 +03:00
parent a36abd7fdd
commit 186139f796
7 changed files with 718 additions and 13 deletions
+20 -3
View File
@@ -64,7 +64,20 @@ STEP_GOAL = 10_000 # можно вынести в env при желании
# Auth helpers # Auth helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def is_allowed(update: Update) -> bool: def is_allowed(update: Update) -> bool:
global ALLOWED_USER_ID
uid = update.effective_user.id if update.effective_user else None 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 return uid == ALLOWED_USER_ID
@@ -1483,11 +1496,15 @@ def main() -> None:
logger.error("%s", exc) logger.error("%s", exc)
sys.exit(1) sys.exit(1)
BOT_TOKEN = SETTINGS.telegram_bot_token BOT_TOKEN = SETTINGS.telegram_bot_token
ALLOWED_USER_ID = SETTINGS.require_user_id() ALLOWED_USER_ID = SETTINGS.telegram_allowed_user_id
DB_PATH = str(SETTINGS.user_db_path(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() init_state_db()
logger.info("Стартую fitness-bot для пользователя: %s", ALLOWED_USER_ID)
app = Application.builder().token(BOT_TOKEN).build() app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", cmd_start)) app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("status", cmd_status)) app.add_handler(CommandHandler("status", cmd_status))
+9 -1
View File
@@ -46,8 +46,16 @@ class Settings:
def from_env(cls, *, require_bot: bool = False) -> Settings: def from_env(cls, *, require_bot: bool = False) -> Settings:
data_dir = Path(os.environ.get("DATA_DIR", "/opt/miband-tracker/data")) data_dir = Path(os.environ.get("DATA_DIR", "/opt/miband-tracker/data"))
allowed_user_id = parse_single_user_id( 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() bot_token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
if require_bot and not bot_token: if require_bot and not bot_token:
raise ConfigError("TELEGRAM_BOT_TOKEN не задан") raise ConfigError("TELEGRAM_BOT_TOKEN не задан")
+40 -6
View File
@@ -3,13 +3,24 @@
from __future__ import annotations from __future__ import annotations
import fcntl
import os import os
import time import time
from collections.abc import Iterator from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path 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): class LockUnavailable(RuntimeError):
"""Raised when another process already owns the sync lock.""" """Raised when another process already owns the sync lock."""
@@ -19,10 +30,23 @@ class LockUnavailable(RuntimeError):
def exclusive_file_lock(path: Path) -> Iterator[None]: def exclusive_file_lock(path: Path) -> Iterator[None]:
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a+", encoding="utf-8") as lock_file: with path.open("a+", encoding="utf-8") as lock_file:
try: fd = lock_file.fileno()
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc: if _HAS_FCNTL:
raise LockUnavailable(f"Lock is already held: {path}") from exc 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: try:
lock_file.seek(0) lock_file.seek(0)
@@ -31,4 +55,14 @@ def exclusive_file_lock(path: Path) -> Iterator[None]:
lock_file.flush() lock_file.flush()
yield yield
finally: 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
+12 -3
View File
@@ -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.") log(f"Running in daemon mode. Sync interval: {settings.sync_interval} seconds.")
while True: while True:
try: 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: except Exception as exc:
log(f"Unhandled error in main loop: {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)
+260
View File
@@ -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
Executable
+319
View File
@@ -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 ""
+58
View File
@@ -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