feat(backend): alerts come enriched and paged with an unseen count and a live alert frame

This commit is contained in:
hh committed 2026-10-19 02:57:27 +00:00
1 parent 4469938f47
commit 2bb8ca45fb
7 files changed
+682 -65

No files matched your search

+46 -12
View File
@@ -37,6 +37,7 @@ from utils.read.read_receipts import read_info
from utils.search.models import SearchFilters
from utils.search.repository import search_messages
from utils.storage import ContentAddressedStorage
from utils.watch_rules import WatchKind, WatchParamsError, clean_name, normalize_params
INSTRUCTIONS = """\
beavergram archives Telegram data (chats, messages, media, presence, stories,
@@ -76,8 +77,10 @@ Typical flows:
returns a public link valid 24h that needs no auth (`curl -O <link>`), plus
the content itself for small images and text files. `get_media` returns
metadata only.
- Monitoring: `set_watch` creates a local rule, `list_watches` lists rules, and
`list_alerts` reads what those rules fired.
- Monitoring: `set_watch` creates a local rule (kinds `keyword`,
`new_message`, `message_deleted`, `message_edited`, `peer_online`; params in
its description), `list_watches` lists rules, and `list_alerts` reads what
those rules fired, with chat, message preview and a web client `path`.
Watched bots:
- a bot whose token the user added is just another archived account: it has its
@@ -420,13 +423,37 @@ async def get_annotations(
@mcp.tool
async def set_watch(
account_id: int,
kind: str,
kind: WatchKind,
params: dict[str, Any] | None = None,
name: str | None = None,
enabled: bool = True,
) -> dict[str, Any]:
"""Create a local watch rule (the only local write MCP is allowed)."""
"""Create a local watch rule (the only local write MCP is allowed).
A watch fires an alert into the web client (live counter and list) and to
the user's webhooks. The user's own messages never fire. Kinds and params:
- `keyword`: `{"pattern": str, "regex": bool = false, "chat_id"?: int,
"sender_id"?: int}`. Case-insensitive substring, or a Python regex when
`regex` is true (at most 200 characters, no nested repeats like
`(a+)+`). Checked on new messages, on edits (the new text) and on voice
transcripts. Without `chat_id` it watches every chat.
- `new_message`: `{"chat_id"?: int, "sender_id"?: int}`, at least one: any
new message in that chat, or from that person anywhere, or both.
- `message_deleted`: `{"chat_id"?: int}`. A stored message deleted in that
chat; without `chat_id`, in any private chat.
- `message_edited`: `{"chat_id"?: int}`. Same scopes as `message_deleted`.
- `peer_online`: `{"peer_id": int}`. The person comes online after at
least 15 minutes away.
`name` is an optional human label shown in the list and in alerts.
"""
try:
clean = normalize_params(kind, params or {})
except WatchParamsError as exc:
raise ToolError(str(exc)) from exc
watch = await watches.create_watch(
await _pool(), account_id, kind, params or {}, enabled=enabled
await _pool(), account_id, kind, clean, name=clean_name(name), enabled=enabled
)
return watch.model_dump(mode="json")
@@ -441,12 +468,19 @@ async def list_watches(account_id: int) -> list[dict[str, Any]]:
async def list_alerts(
account_id: int,
seen: bool | None = None,
before: int | None = None,
limit: int = DEFAULT_LIMIT,
offset: int = 0,
) -> list[dict[str, Any]]:
"""List fired alerts from watch rules."""
return _dump(
await watches.list_alerts(
await _pool(), account_id, Page(limit=limit, offset=offset), seen=seen
)
) -> dict[str, Any]:
"""List fired alerts from watch rules, newest first.
Each alert names its watch (id, kind, name), the event (`message`, `edit`,
`transcript`, `delete`, `online`), the chat, the message preview with its
sender and deleted/edited flags, the peer for `peer_online`, and `path`,
the web client link. `seen=false` keeps unseen ones. Page older with
`before` set to `next_before` from the previous call; `unseen` is the
account's unseen count.
"""
page = await watches.list_alerts(
await _pool(), account_id, seen=seen, before=before, limit=limit
)
return page.model_dump(mode="json")