Includes atomic migrations, per-rule reminder semantics, first-run helper script and the note on the undecided UI chat idea. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
63 lines
1.2 KiB
Python
63 lines
1.2 KiB
Python
from dataclasses import dataclass
|
|
from typing import NamedTuple, Protocol
|
|
from urllib.parse import quote
|
|
|
|
import httpx
|
|
|
|
MASK = "***"
|
|
|
|
|
|
class ChatError(Exception):
|
|
pass
|
|
|
|
|
|
def mask_secret(text: str, secret: str) -> str:
|
|
if not secret:
|
|
return text
|
|
for variant in (secret, quote(secret, safe=""), quote(secret)):
|
|
text = text.replace(variant, MASK)
|
|
return text
|
|
|
|
|
|
def json_body(response: httpx.Response) -> dict:
|
|
try:
|
|
body = response.json()
|
|
except ValueError:
|
|
return {}
|
|
return body if isinstance(body, dict) else {}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Button:
|
|
label: str
|
|
data: str = ""
|
|
url: str = ""
|
|
|
|
|
|
Buttons = tuple[tuple[Button, ...], ...]
|
|
|
|
|
|
class Target(NamedTuple):
|
|
backend: str
|
|
target: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MessageRef:
|
|
backend: str
|
|
target: str
|
|
message_id: str
|
|
|
|
|
|
class ChatAdapter(Protocol):
|
|
name: str
|
|
default_target: str
|
|
|
|
async def send(self, target: str, text: str, buttons: Buttons = (), silent: bool = False) -> MessageRef: ...
|
|
|
|
async def edit(self, ref: MessageRef, text: str, buttons: Buttons = (), silent: bool = False) -> None: ...
|
|
|
|
async def check(self) -> str: ...
|
|
|
|
async def aclose(self) -> None: ...
|