"""Outbound push so Lyra can reach Brian when he's not in the app (ntfy). This is the literal version of what she asked for — thinking "unprompted, without you" only matters if she can also *reach* you. When a thought tugs hard enough, the thought loop calls `push()` here and it lands on your phone with a tap-through to the Thoughts feed. One-way: you reply in the app, which feeds the loop. Transport only. Whether/when to ping (salience bar, cooldown, quiet hours) is the thought loop's call — see `thoughts.maybe_ping`. """ from __future__ import annotations import httpx from lyra import config, logbus def push(title: str, message: str, click: str | None = None, tags: str | None = None, priority: str | None = None) -> bool: """Publish a notification to the configured ntfy topic. Returns True on success. Never raises — a down ntfy must not break the thought loop. Uses ntfy's JSON publishing (POST to the base URL) rather than headers, so UTF-8 titles/messages (em-dashes, smart quotes, her actual words) go through — HTTP headers are latin-1 only and choke on them.""" cfg = config.load() if not cfg.ntfy_url: return False payload: dict = {"topic": cfg.ntfy_topic, "message": message} if title: payload["title"] = title if click: payload["click"] = click if tags: payload["tags"] = [t.strip() for t in tags.split(",") if t.strip()] if priority: payload["priority"] = priority try: r = httpx.post(cfg.ntfy_url, json=payload, timeout=8.0) ok = r.status_code < 400 if not ok: logbus.log("error", "ntfy push failed", status=r.status_code) return ok except Exception as exc: logbus.log("error", "ntfy push error", error=str(exc)[:160]) return False