mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter-coverage (push) Waiting to run
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
"""One extra shutdown hour on a day with a credited paper-book reading session.
|
|
|
|
book-guard (``~/src/book-guard``) publishes reading through its ledger, the
|
|
same way leetcode-guard publishes solves, and steam-backlog-enforcer reads the
|
|
same fact for an hour of gaming. Like the LeetCode hour it is a *term* of the
|
|
daily derivation in :mod:`screen_locker._shutdown_base`, never an increment
|
|
another program writes into the config.
|
|
|
|
The contract, pinned in book-guard's ``_quiz.record_verdict``:
|
|
|
|
**Only ``credit`` rows count**, and only those with ``detail["bonus"] ==
|
|
"1"`` -- book-guard sets that when the session passed its quiz *and* was big
|
|
enough (20+ pages, 20+ minutes). A ``reject`` is a failed quiz; an
|
|
``escape`` forgives a lock and earns nothing.
|
|
|
|
**The reading time is ``detail["ended_at"]``**, not the row's creation: a
|
|
session read at 22:30 and quizzed next morning earns for the evening it was
|
|
read, which by then is over -- never for the quiz day.
|
|
|
|
**"Cannot check" is not "did not read"**: an unreadable ledger or key yields
|
|
``None`` and the caller fails closed to no hour.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Final
|
|
|
|
from gatelock.log_integrity import verify_entry_hmac
|
|
|
|
from screen_locker._constants import HMAC_KEY_FILE
|
|
from screen_locker._leetcode_bonus import _read_entries, _today_window
|
|
|
|
_logger: Final = logging.getLogger(__name__)
|
|
|
|
READING_BONUS_HOURS: Final = 1
|
|
|
|
# book-guard's real ledger. Read-only from here; resolved at call time so the
|
|
# test suite's path redirect (tests/_isolated_state.py) applies.
|
|
READING_LEDGER_FILE: Final = Path.home() / ".local/share/book_guard/ledger.json"
|
|
|
|
|
|
def _read_today(entry: dict[str, object], *, window: tuple[float, float]) -> bool:
|
|
"""Whether this verified credit is bonus-eligible and was read today."""
|
|
detail = entry.get("detail")
|
|
if not isinstance(detail, dict) or detail.get("bonus") != "1":
|
|
return False
|
|
try:
|
|
ended = float(str(detail.get("ended_at")))
|
|
except ValueError:
|
|
_logger.warning(
|
|
"reading credit %r has no usable ended_at", entry.get("entry_id")
|
|
)
|
|
return False
|
|
start, end = window
|
|
return start <= ended <= end
|
|
|
|
|
|
def read_today(
|
|
ledger_path: Path | None = None,
|
|
*,
|
|
now: datetime | None = None,
|
|
) -> bool | None:
|
|
"""Whether a verified, bonus-eligible reading session ended today, locally.
|
|
|
|
Returns:
|
|
True or False when the ledger could be read and verified, ``None``
|
|
when it could not -- which is never "did not read".
|
|
"""
|
|
target = READING_LEDGER_FILE if ledger_path is None else ledger_path
|
|
try:
|
|
key_usable = bool(HMAC_KEY_FILE.read_bytes().strip())
|
|
except OSError as exc:
|
|
_logger.warning("Cannot read the integrity key at %s (%s)", HMAC_KEY_FILE, exc)
|
|
return None
|
|
if not key_usable:
|
|
_logger.warning("Integrity key at %s is empty", HMAC_KEY_FILE)
|
|
return None
|
|
if not target.exists():
|
|
return False # book-guard never registered a book: no reading, no fault
|
|
rows = _read_entries(target, "book-guard")
|
|
if rows is None:
|
|
return None
|
|
window = _today_window(now or datetime.now(tz=UTC))
|
|
return any(
|
|
isinstance(row, dict)
|
|
and row.get("kind") == "credit"
|
|
and verify_entry_hmac(row, key_file=HMAC_KEY_FILE)
|
|
and _read_today(row, window=window)
|
|
for row in rows
|
|
)
|
|
|
|
|
|
def reading_bonus_hours(ledger_path: Path | None = None) -> int:
|
|
"""The shutdown hours today's reading has earned, failing closed.
|
|
|
|
A ledger that does not exist yet (book-guard never ran) is a plain 0,
|
|
logged by :func:`_read_entries` like any unreadable one.
|
|
"""
|
|
done = read_today(ledger_path)
|
|
if done is None:
|
|
_logger.warning("Reading state could not be checked; no shutdown hour")
|
|
return 0
|
|
return READING_BONUS_HOURS if done else 0
|