mirror of
https://github.com/prdlk/omarchy-beeper.git
synced 2026-11-02 13:12:48 +00:00
feat: omarchy-beeper bar widget for unread Beeper chats
This commit is contained in:
commit
7fc78f1075
17 files changed
+3483
No files matched your search
@@ -0,0 +1,186 @@
|
||||
"""Shared helpers for mocked contract tests. Never talks to Beeper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
from contextlib import contextmanager, redirect_stdout
|
||||
from io import BytesIO, StringIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LIB = ROOT / "lib"
|
||||
|
||||
if str(LIB) not in sys.path:
|
||||
sys.path.insert(0, str(LIB))
|
||||
|
||||
|
||||
def capture_json(fn, *args, **kwargs) -> dict:
|
||||
buf = StringIO()
|
||||
with redirect_stdout(buf):
|
||||
try:
|
||||
fn(*args, **kwargs)
|
||||
except SystemExit:
|
||||
pass
|
||||
text = buf.getvalue().strip()
|
||||
if not text:
|
||||
raise AssertionError("expected JSON on stdout")
|
||||
return json.loads(text.splitlines()[-1])
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
"""Enough of an http.client.HTTPResponse for read_http_body()."""
|
||||
|
||||
def __init__(self, payload: object, status: int = 200, content_length: object | None = None):
|
||||
if isinstance(payload, (bytes, bytearray)):
|
||||
raw = bytes(payload)
|
||||
else:
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
self._buf = BytesIO(raw)
|
||||
self.status = status
|
||||
declared = len(raw) if content_length is None else content_length
|
||||
self.headers = {} if declared is False else {"Content-Length": str(declared)}
|
||||
|
||||
def read(self, n: int = -1) -> bytes:
|
||||
return self._buf.read(n)
|
||||
|
||||
def __enter__(self) -> FakeResponse:
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class Call:
|
||||
def __init__(self, method: str, url: str, body: object) -> None:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.host = parsed.netloc
|
||||
self.path = parsed.path
|
||||
self.query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
|
||||
self.body = body
|
||||
|
||||
def one(self, key: str, default: str = "") -> str:
|
||||
values = self.query.get(key) or []
|
||||
return values[0] if values else default
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
||||
return f"<Call {self.method} {self.path} {self.query}>"
|
||||
|
||||
|
||||
class FakeHttp:
|
||||
"""Route (METHOD, path) to a callable or a fixed response."""
|
||||
|
||||
def __init__(self, routes: dict[tuple[str, str], object] | None = None) -> None:
|
||||
self.routes = dict(routes or {})
|
||||
self.calls: list[Call] = []
|
||||
|
||||
def route(self, method: str, path: str, handler: object) -> None:
|
||||
self.routes[(method, path)] = handler
|
||||
|
||||
def open(self, req, timeout: int | None = None): # noqa: D102 - urlopen shape
|
||||
body = None
|
||||
if req.data:
|
||||
body = json.loads(req.data.decode("utf-8"))
|
||||
call = Call(req.get_method(), req.full_url, body)
|
||||
self.calls.append(call)
|
||||
handler = self.routes.get((call.method, call.path))
|
||||
if handler is None:
|
||||
raise urllib.error.HTTPError(call.url, 404, "Not Found", {}, BytesIO(b"{}"))
|
||||
result = handler(call) if callable(handler) else handler
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
if isinstance(result, FakeResponse):
|
||||
return result
|
||||
return FakeResponse(result)
|
||||
|
||||
def paths(self, method: str = "") -> list[str]:
|
||||
return [c.path for c in self.calls if not method or c.method == method]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def fake_api(http: FakeHttp):
|
||||
import beeper
|
||||
|
||||
with patch.object(beeper, "_OPENER", http):
|
||||
yield http
|
||||
|
||||
|
||||
def http_error(status: int, message: str = "nope", url: str = "http://localhost:23373/v1/x"):
|
||||
body = json.dumps({"message": message, "code": "error"}).encode("utf-8")
|
||||
return urllib.error.HTTPError(url, status, message, {"Content-Length": str(len(body))}, BytesIO(body))
|
||||
|
||||
|
||||
def url_error(reason: Exception | None = None):
|
||||
return urllib.error.URLError(reason or ConnectionRefusedError(111, "Connection refused"))
|
||||
|
||||
|
||||
def chat(
|
||||
chat_id: str,
|
||||
*,
|
||||
title: str = "Ada Lovelace",
|
||||
network: str = "WhatsApp",
|
||||
account: str = "local-whatsapp_1",
|
||||
last_activity: str = "2026-02-11T10:00:00.000Z",
|
||||
unread: int = 2,
|
||||
) -> dict:
|
||||
return {
|
||||
"id": chat_id,
|
||||
"localChatID": "1",
|
||||
"accountID": account,
|
||||
"network": network,
|
||||
"title": title,
|
||||
"type": "single",
|
||||
"participants": {"items": [], "hasMore": False, "total": 1},
|
||||
"lastActivity": last_activity,
|
||||
"unreadCount": unread,
|
||||
"isArchived": False,
|
||||
"isMuted": False,
|
||||
"isPinned": False,
|
||||
}
|
||||
|
||||
|
||||
def message(
|
||||
chat_id: str,
|
||||
*,
|
||||
text: str = "are you coming tonight?",
|
||||
sender: str = "Ada",
|
||||
timestamp: str = "2026-02-11T10:00:00.000Z",
|
||||
kind: str = "TEXT",
|
||||
sort_key: str = "100",
|
||||
) -> dict:
|
||||
return {
|
||||
"id": "m1",
|
||||
"chatID": chat_id,
|
||||
"accountID": "local-whatsapp_1",
|
||||
"senderID": "@ada:example",
|
||||
"senderName": sender,
|
||||
"timestamp": timestamp,
|
||||
"sortKey": sort_key,
|
||||
"type": kind,
|
||||
"text": text,
|
||||
}
|
||||
|
||||
|
||||
def account(account_id: str, network: str) -> dict:
|
||||
return {
|
||||
"accountID": account_id,
|
||||
"loginID": account_id,
|
||||
"bridge": {"id": network.lower(), "type": network.lower(), "provider": "local"},
|
||||
"network": network,
|
||||
"user": {"id": "@me:example", "isSelf": True},
|
||||
"status": "connected",
|
||||
}
|
||||
|
||||
|
||||
def search_page(chats: list[dict], has_more: bool = False, cursor: str | None = None) -> dict:
|
||||
return {
|
||||
"items": chats,
|
||||
"hasMore": has_more,
|
||||
"oldestCursor": cursor,
|
||||
"newestCursor": None,
|
||||
}
|
||||
Reference in new issue
Block a user