mirror of
https://github.com/alexgetmancom/miband-bot.git
synced 2026-07-20 04:30:16 +03:00
feat: add miband bot application
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
"""Invalid runtime configuration."""
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
value = os.environ.get(name)
|
||||
if value is None:
|
||||
return default
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _env_int(name: str, default: int, *, min_value: int | None = None) -> int:
|
||||
raw = os.environ.get(name, str(default)).strip()
|
||||
try:
|
||||
value = int(raw)
|
||||
except ValueError as exc:
|
||||
raise ConfigError(f"{name} должен быть целым числом") from exc
|
||||
if min_value is not None and value < min_value:
|
||||
raise ConfigError(f"{name} должен быть не меньше {min_value}")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
data_dir: Path
|
||||
db_path: Path
|
||||
status_path: Path
|
||||
bot_state_db_path: Path
|
||||
telegram_bot_token: str
|
||||
telegram_allowed_user_id: int | None
|
||||
sync_interval: int
|
||||
query_duration: int
|
||||
enable_fds_sleep_details: bool
|
||||
|
||||
@classmethod
|
||||
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
|
||||
)
|
||||
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
|
||||
if require_bot and not bot_token:
|
||||
raise ConfigError("TELEGRAM_BOT_TOKEN не задан")
|
||||
return cls(
|
||||
data_dir=data_dir,
|
||||
db_path=Path(os.environ.get("DB_PATH", str(data_dir / "miband.db"))),
|
||||
status_path=Path(os.environ.get("STATUS_PATH", str(data_dir / "status.json"))),
|
||||
bot_state_db_path=Path(
|
||||
os.environ.get(
|
||||
"BOT_STATE_DB_PATH",
|
||||
str(data_dir / "fitness_bot_state.db"),
|
||||
)
|
||||
),
|
||||
telegram_bot_token=bot_token,
|
||||
telegram_allowed_user_id=allowed_user_id,
|
||||
sync_interval=_env_int("SYNC_INTERVAL", 900, min_value=0),
|
||||
query_duration=_env_int("QUERY_DURATION", 2, min_value=1),
|
||||
enable_fds_sleep_details=_env_bool("ENABLE_FDS_SLEEP_DETAILS", default=True),
|
||||
)
|
||||
|
||||
def require_user_id(self, user_id: int | None = None) -> int:
|
||||
resolved = user_id if user_id is not None else self.telegram_allowed_user_id
|
||||
if resolved is None:
|
||||
raise ConfigError("TELEGRAM_ALLOWED_USER_ID должен содержать ровно один user id")
|
||||
return int(resolved)
|
||||
|
||||
def token_path(self, user_id: int | None = None) -> Path:
|
||||
uid = self.require_user_id(user_id)
|
||||
preferred = self.data_dir / f"token_{uid}.json"
|
||||
legacy = self.data_dir / "token.json"
|
||||
if preferred.exists() or not legacy.exists():
|
||||
return preferred
|
||||
return legacy
|
||||
|
||||
def user_db_path(self, user_id: int | None = None) -> Path:
|
||||
uid = self.require_user_id(user_id)
|
||||
preferred = self.data_dir / f"miband_{uid}.db"
|
||||
if preferred.exists():
|
||||
return preferred
|
||||
return self.db_path
|
||||
|
||||
def user_status_path(self, user_id: int | None = None) -> Path:
|
||||
uid = self.require_user_id(user_id)
|
||||
preferred = self.data_dir / f"status_{uid}.json"
|
||||
if preferred.exists():
|
||||
return preferred
|
||||
return self.status_path
|
||||
|
||||
def canonical_user_db_path(self, user_id: int | None = None) -> Path:
|
||||
return self.data_dir / f"miband_{self.require_user_id(user_id)}.db"
|
||||
|
||||
def canonical_user_status_path(self, user_id: int | None = None) -> Path:
|
||||
return self.data_dir / f"status_{self.require_user_id(user_id)}.json"
|
||||
|
||||
|
||||
def parse_single_user_id(raw: str, *, required: bool = False) -> int | None:
|
||||
values = [item.strip() for item in raw.split(",") if item.strip()]
|
||||
if not values:
|
||||
if required:
|
||||
raise ConfigError("TELEGRAM_ALLOWED_USER_ID не задан или пуст")
|
||||
return None
|
||||
if len(values) > 1:
|
||||
raise ConfigError("TELEGRAM_ALLOWED_USER_ID должен содержать ровно один user id")
|
||||
try:
|
||||
return int(values[0])
|
||||
except ValueError as exc:
|
||||
raise ConfigError("TELEGRAM_ALLOWED_USER_ID должен быть целым числом") from exc
|
||||
Reference in New Issue
Block a user