Source code for humble_steam_key_redeemer.settings

"""Configuration for the redeemer.

Settings resolve from, in order of precedence: explicit arguments, environment
variables prefixed ``HSKR_``, a ``.env`` file, then the defaults below.

State is kept under a single application directory rather than scattered
through the working directory, so running the tool from a different folder
does not silently start from an empty history.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Literal

from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


def _default_state_dir() -> Path:
    """Return the per-user state directory for the current platform."""
    if os.name == "nt":
        base = os.environ.get("APPDATA")
        root = Path(base) if base else Path.home() / "AppData" / "Roaming"
    elif xdg := os.environ.get("XDG_STATE_HOME"):
        root = Path(xdg)
    else:
        root = Path.home() / ".local" / "state"
    return root / "humble-steam-key-redeemer"


[docs] class Settings(BaseSettings): """Runtime configuration. Attributes: state_dir: Directory holding the database and saved sessions. match_threshold: Similarity score above which a Humble title is considered to match an owned Steam app. confirm_threshold: Score at or above which a match is trusted without asking the user. cdp_endpoint: Attach to an existing browser at this CDP endpoint (for example ``http://localhost:9222``) rather than launching one. headless: Whether the Humble browser runs without a visible window. browser_timeout_ms: Per-operation browser timeout in milliseconds. request_timeout: HTTP timeout in seconds. locale: Browser locale, which determines Humble's currency display. """ model_config = SettingsConfigDict( env_prefix="HSKR_", env_file=".env", env_file_encoding="utf-8", extra="ignore", ) state_dir: Path = Field(default_factory=_default_state_dir) # Ownership matching. Humble and Steam spell titles differently # ("Game GOTY" vs "Game: Game of the Year Edition"), so exact comparison # misses and a low bar produces false matches that skip redeemable keys. match_threshold: int = Field(default=70, ge=0, le=100) confirm_threshold: int = Field(default=95, ge=0, le=100) # Attach to a browser already running with remote debugging instead of # launching one. Lets the tool reuse a browser that is already signed in — # including one an agent drives — rather than a fresh isolated profile. cdp_endpoint: str | None = None headless: bool = True browser: Literal["chromium", "firefox", "webkit"] = "chromium" browser_timeout_ms: int = Field(default=60_000, gt=0) request_timeout: float = Field(default=30.0, gt=0) locale: str = "en-US" # Steam permits roughly 50 activations per hour but only 10 failures. # Stopping on a rate limit rather than retrying avoids deepening it. max_redemptions_per_run: int = Field(default=0, ge=0) @field_validator("state_dir") @classmethod def _expand(cls, value: Path) -> Path: """Expand ``~`` and resolve the state directory.""" return value.expanduser() @property def database_path(self) -> Path: """Path to the SQLite database of keys and redemption attempts.""" return self.state_dir / "redeemer.db" @property def humble_session_path(self) -> Path: """Path to the stored Humble browser session.""" return self.state_dir / "humble-session.json" @property def steam_session_path(self) -> Path: """Path to the stored Steam session.""" return self.state_dir / "steam-session.json"
[docs] def ensure_state_dir(self) -> Path: """Create the state directory with owner-only permissions. Session files here are bearer credentials — a copied Steam session bypasses Steam Guard — so the directory must not be group- or world-accessible. ``mkdir(mode=...)`` only applies to a directory it actually creates and is further masked by the umask, so the mode is set explicitly afterwards. Otherwise a directory that already existed with loose permissions would silently keep them. Returns: The state directory path. """ self.state_dir.mkdir(parents=True, exist_ok=True, mode=0o700) if self.state_dir.stat().st_mode & 0o077: self.state_dir.chmod(0o700) return self.state_dir
__all__ = ["Settings"]