179 lines
7.3 KiB
Python
179 lines
7.3 KiB
Python
"""Encrypted, account-scoped recent work shared by signed-in devices."""
|
|
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from src.private_state import connect_private_sqlite
|
|
from src.state_encryption import PrivateStateCipher, PrivateStateEncryptionError, private_state_encryption_config
|
|
|
|
|
|
KINDS = frozenset({"issue", "filed", "pull", "review", "update"})
|
|
|
|
|
|
class RecentWorkStore:
|
|
def __init__(
|
|
self,
|
|
path: str | Path,
|
|
*,
|
|
timeout: float = 1.0,
|
|
encryption_key: bytes | None = None,
|
|
limit: int = 5,
|
|
pinned_limit: int = 20,
|
|
):
|
|
self.path = Path(path)
|
|
self.timeout = timeout
|
|
self.limit = max(1, int(limit))
|
|
self.pinned_limit = max(1, int(pinned_limit))
|
|
self._cipher = PrivateStateCipher(
|
|
encryption_key if encryption_key is not None else private_state_encryption_config(),
|
|
store="recent-work",
|
|
)
|
|
with self._connect() as connection:
|
|
connection.execute("PRAGMA journal_mode=WAL")
|
|
connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS recent_work ("
|
|
"login TEXT PRIMARY KEY, items TEXT NOT NULL)"
|
|
)
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
return connect_private_sqlite(self.path, timeout=self.timeout)
|
|
|
|
@staticmethod
|
|
def _login(login: str) -> str:
|
|
normalized = login.strip().lower()
|
|
if not normalized:
|
|
raise ValueError("login is required")
|
|
return normalized
|
|
|
|
@staticmethod
|
|
def _normalize(item: dict) -> dict:
|
|
if not isinstance(item, dict):
|
|
raise ValueError("recent work item is invalid")
|
|
kind = item.get("kind")
|
|
number = item.get("number")
|
|
title = item.get("title")
|
|
repository = item.get("repository", "")
|
|
if (
|
|
kind not in KINDS
|
|
or not isinstance(number, int)
|
|
or isinstance(number, bool)
|
|
or number < 1
|
|
or not isinstance(title, str)
|
|
or not title.strip()
|
|
):
|
|
raise ValueError("recent work item is invalid")
|
|
title = title.strip()[:180]
|
|
if kind == "update":
|
|
if repository:
|
|
raise ValueError("recent work item is invalid")
|
|
route = f"#/my-work/update/{number}"
|
|
normalized = {"kind": kind, "number": number, "title": title, "route": route}
|
|
else:
|
|
if (
|
|
not isinstance(repository, str)
|
|
or repository.count("/") != 1
|
|
or any(not part or not all(character.isalnum() or character in "_.-" for character in part)
|
|
for part in repository.split("/"))
|
|
):
|
|
raise ValueError("recent work item is invalid")
|
|
route = f"#/my-work/{kind}/{repository}/{number}"
|
|
normalized = {
|
|
"kind": kind,
|
|
"repository": repository,
|
|
"number": number,
|
|
"title": title,
|
|
"route": route,
|
|
}
|
|
if item.get("route", route) != route:
|
|
raise ValueError("recent work item is invalid")
|
|
return normalized
|
|
|
|
def _state(self, row, login: str) -> tuple[dict, bool]:
|
|
if row is None:
|
|
return {"items": [], "pinned": []}, False
|
|
payload, legacy = self._cipher.open(row[0], binding=f"items:{login}")
|
|
if isinstance(payload, list):
|
|
payload = {"items": payload, "pinned": []}
|
|
legacy = True
|
|
if not isinstance(payload, dict) or not isinstance(payload.get("items"), list) or not isinstance(payload.get("pinned"), list):
|
|
raise PrivateStateEncryptionError("private state could not be decrypted")
|
|
try:
|
|
items = [self._normalize(item) for item in payload["items"]][: self.limit]
|
|
pinned = [self._normalize(item) for item in payload["pinned"]][: self.pinned_limit]
|
|
if len({item["route"] for item in pinned}) != len(pinned):
|
|
raise ValueError("recent work item is invalid")
|
|
return {"items": items, "pinned": pinned}, legacy
|
|
except ValueError as error:
|
|
raise PrivateStateEncryptionError("private state could not be decrypted") from error
|
|
|
|
def _seal(self, state: dict, login: str) -> str:
|
|
return self._cipher.seal(state, binding=f"items:{login}")
|
|
|
|
def _write(self, connection: sqlite3.Connection, login: str, state: dict) -> None:
|
|
connection.execute(
|
|
"INSERT INTO recent_work(login, items) VALUES (?, ?) "
|
|
"ON CONFLICT(login) DO UPDATE SET items=excluded.items",
|
|
(login, self._seal(state, login)),
|
|
)
|
|
|
|
def get(self, login: str) -> dict:
|
|
login = self._login(login)
|
|
with self._connect() as connection:
|
|
row = connection.execute(
|
|
"SELECT items FROM recent_work WHERE login = ?", (login,)
|
|
).fetchone()
|
|
state, legacy = self._state(row, login)
|
|
if row is not None and legacy:
|
|
connection.execute(
|
|
"UPDATE recent_work SET items = ? WHERE login = ? AND items = ?",
|
|
(self._seal(state, login), login, row[0]),
|
|
)
|
|
return state
|
|
|
|
def record(self, login: str, item: dict) -> dict:
|
|
login = self._login(login)
|
|
normalized = self._normalize(item)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
row = connection.execute(
|
|
"SELECT items FROM recent_work WHERE login = ?", (login,)
|
|
).fetchone()
|
|
state, _legacy = self._state(row, login)
|
|
state["items"] = [normalized, *(entry for entry in state["items"] if entry["route"] != normalized["route"])][: self.limit]
|
|
state["pinned"] = [
|
|
normalized,
|
|
*(entry for entry in state["pinned"] if entry["route"] != normalized["route"]),
|
|
] if any(entry["route"] == normalized["route"] for entry in state["pinned"]) else state["pinned"]
|
|
self._write(connection, login, state)
|
|
return state
|
|
|
|
def pin(self, login: str, item: dict) -> dict:
|
|
login = self._login(login)
|
|
normalized = self._normalize(item)
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
row = connection.execute(
|
|
"SELECT items FROM recent_work WHERE login = ?", (login,)
|
|
).fetchone()
|
|
state, _legacy = self._state(row, login)
|
|
state["pinned"] = [
|
|
normalized,
|
|
*(entry for entry in state["pinned"] if entry["route"] != normalized["route"]),
|
|
][: self.pinned_limit]
|
|
self._write(connection, login, state)
|
|
return state
|
|
|
|
def unpin(self, login: str, route: str) -> dict:
|
|
login = self._login(login)
|
|
if not isinstance(route, str) or not route:
|
|
raise ValueError("recent work route is invalid")
|
|
with self._connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
row = connection.execute(
|
|
"SELECT items FROM recent_work WHERE login = ?", (login,)
|
|
).fetchone()
|
|
state, _legacy = self._state(row, login)
|
|
state["pinned"] = [entry for entry in state["pinned"] if entry["route"] != route]
|
|
self._write(connection, login, state)
|
|
return state
|