One module per API area (base, files, activity, skills, conversations, notifications, forms, search, plans, projects, services, misc, diff, cron, agents) on a shared base.Schema; __init__.py re-exports every model so handlers keep writing schemas.<Name>. OpenAPI output is byte-identical. PlanSummary lives in conversations.py to keep the module graph a DAG. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
156 lines
4.4 KiB
Python
156 lines
4.4 KiB
Python
"""Notifications: the light feed behind the history + "new" badges, the
|
|
first-class webhooks + notify/ask log (``notify.py``) and the mail trigger
|
|
allowlist (``mail_trigger.py``)."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal, Optional
|
|
|
|
from .base import Schema
|
|
from .conversations import ConvNotification
|
|
|
|
__all__ = [
|
|
"NotifConversation",
|
|
"NotificationsResponse",
|
|
"FeedNotification",
|
|
"UnreadNotificationsResponse",
|
|
"Webhook",
|
|
"WebhooksResponse",
|
|
"MailTriggerSendersResponse",
|
|
"DeliveryStatus",
|
|
"NotifyResult",
|
|
"AskRecord",
|
|
"NotifyLogEntry",
|
|
"NotifyLogResponse",
|
|
]
|
|
|
|
|
|
# ── notification feed (light source for the history + "new" badges) ──────────
|
|
class NotifConversation(Schema):
|
|
"""A conversation that recorded notify-done pushes, feed-relevant bits only."""
|
|
id: str
|
|
title: str
|
|
endedAt: Optional[str] = None
|
|
projects: list[str]
|
|
services: list[str]
|
|
notifications: list[ConvNotification]
|
|
|
|
|
|
class NotificationsResponse(Schema):
|
|
conversations: list[NotifConversation]
|
|
count: Optional[int] = None
|
|
|
|
|
|
class FeedNotification(Schema):
|
|
"""A flat notification feed item (the server-side mirror of the frontend's
|
|
FeedNotif), as returned by ``/api/notifications/unread``."""
|
|
id: str
|
|
convId: str
|
|
convTitle: str
|
|
title: str
|
|
body: str
|
|
type: str
|
|
url: str
|
|
image: Optional[str] = None
|
|
at: Optional[str] = None
|
|
projects: list[str]
|
|
services: list[str]
|
|
kind: Optional[str] = None
|
|
spoken: Optional[str] = None
|
|
|
|
|
|
class UnreadNotificationsResponse(Schema):
|
|
notifications: list[FeedNotification]
|
|
count: int # items returned (after the limit)
|
|
total: int # total unread (before the limit)
|
|
|
|
|
|
# ── first-class notifications: webhooks + notify/ask log (notify.py) ─────────
|
|
class Webhook(Schema):
|
|
id: str
|
|
name: str
|
|
url: str
|
|
events: list[Literal["notify", "ask"]]
|
|
enabled: bool
|
|
|
|
|
|
class WebhooksResponse(Schema):
|
|
webhooks: list[Webhook]
|
|
|
|
|
|
# ── mail trigger allowlist (mail_trigger.py) ─────────────────────────────────
|
|
class MailTriggerSendersResponse(Schema):
|
|
"""``emails`` are allowed to trigger; ``disabled`` are kept but switched off."""
|
|
emails: list[str]
|
|
disabled: list[str]
|
|
|
|
|
|
class DeliveryStatus(Schema):
|
|
"""One webhook's delivery outcome for a forwarded notification."""
|
|
webhook: str
|
|
ok: bool
|
|
status: int
|
|
|
|
|
|
class NotifyResult(Schema):
|
|
ok: bool
|
|
id: str
|
|
delivered: list[DeliveryStatus]
|
|
|
|
|
|
class AskRecord(Schema):
|
|
"""An ask (choice question) with its lifecycle state."""
|
|
id: str
|
|
kind: Literal["ask"]
|
|
title: str
|
|
body: str
|
|
type: str
|
|
url: str
|
|
# Image shown in the notification (notify-done `--image`): full URL or a
|
|
# `/local/<file>` path served from Home Assistant's www/ dir.
|
|
image: Optional[str] = None
|
|
at: str
|
|
sessionId: str
|
|
delivered: list[DeliveryStatus]
|
|
options: list[str]
|
|
status: Literal["pending", "answered", "expired"]
|
|
answer: Optional[str] = None
|
|
answerIndex: Optional[int] = None
|
|
answeredAt: Optional[str] = None
|
|
expiresAt: Optional[str] = None
|
|
|
|
|
|
class NotifyLogEntry(Schema):
|
|
"""One recorded notification (kind "notify") or ask (kind "ask")."""
|
|
id: str
|
|
kind: Literal["notify", "ask"]
|
|
title: str
|
|
body: str
|
|
type: str
|
|
url: str
|
|
# Image shown in the notification (notify-done `--image`): full URL or a
|
|
# `/local/<file>` path served from Home Assistant's www/ dir.
|
|
image: Optional[str] = None
|
|
at: str
|
|
sessionId: str
|
|
delivered: list[DeliveryStatus]
|
|
# The line the desk phone read aloud, kept so the UI can replay the sound.
|
|
spoken: Optional[str] = None
|
|
# ask-only lifecycle fields (absent on plain pushes)
|
|
options: Optional[list[str]] = None
|
|
status: Optional[Literal["pending", "answered", "expired",
|
|
"sent", "acted"]] = None
|
|
answer: Optional[str] = None
|
|
answerIndex: Optional[int] = None
|
|
answeredAt: Optional[str] = None
|
|
expiresAt: Optional[str] = None
|
|
# push-only: extra buttons (`notify send --action-cmd`) and the tap
|
|
actions: Optional[list[dict]] = None
|
|
actionIndex: Optional[int] = None
|
|
actionTitle: Optional[str] = None
|
|
actedAt: Optional[str] = None
|
|
|
|
|
|
class NotifyLogResponse(Schema):
|
|
notifications: list[NotifyLogEntry]
|
|
count: Optional[int] = None
|