mirror of
https://github.com/alexgetmancom/miband-bot.git
synced 2026-07-22 13:40:14 +03:00
feat: add dynamic owner binding
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 ""
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user