feat(notifications): Telegram, Discord, ntfy and webhook channels, with publicUrl, screenshots and self-destruct - #27
Merged
Conversation
…rl and screenshots Spec 08 gains publicUrl (§5.8), the per-kind --notificationChannel parameters and the channels CLI; spec 03 the channels API, the channels WS topic, the publicUrl check route and the platform mapping (§9.5); spec 04 the channels pages; specs 09 and 10 the tests, error codes and redaction sinks. D-33 to D-39 record what N1 implements, and D-40 the platform message formats (Telegram HTML, Discord embeds).
The channels REST DTOs (views that never carry a secret value, input with environment variable names, preview, delivery log, env check, Telegram connect), the channels WS topic and events, channels:read and channels:write scopes, the publicUrl key and grammar, GET /health instance_id, the public-url status DTO, per-platform requirements with one shared config check, preview samples and delivery reason texts, and the channel error codes.
…ports The pure renderer half of a platform adapter (shared by sends and the preview), the notification image store and reader, the setup-only Telegram connect calls and the one-shot URL probe of the publicUrl check.
…l image rule Parses --notificationChannel into startup channels with secrets as variable names only (an inline secret is refused without echoing it), answers the env and config-file spellings with a hint naming the flag, captures attention, vault-confirm (before the fill) and crash screenshots when a channel wants them, keeps per channel only the image variant it may see, adds the public link builder, the delivery log filters and per-channel stats.
Realistic messages for every preview sample (attention, its resolution, vault confirm, tool errors, crash, degraded, test), built through the real producers with fixed ids and times, optionally with a masked or unmasked screenshot block.
…ters Pure renderers shared with the preview and one transport per platform: Telegram HTML messages and photos with inline keyboards, edits of text or caption and deletes; Discord webhook embeds with link buttons and a kept screenshot on edit (bot-mode buttons rendered for the preview); ntfy JSON publishes or uploads replaced by sequence id and deleted; the generic webhook posting the signed contract with same-origin redirects only. One HTTP helper classifies every failure and never echoes a secret. Also the Telegram connect calls, the screenshot store and the publicUrl probe.
…nnels CLI The channels API (views without secret values, CRUD with read-only startup channels, pause and resume, the test send, the pure preview, the delivery log, the environment check and the Telegram connect flow), the channels WS events, GET /system/public-url with the health instance id, publicUrl host and origin trust, the --notificationChannel flag on serve and doctor, browserhive channels list|test|preview, and the doctor publicUrl and channel checks.
Bun.serve fakes of the Telegram Bot API, Discord webhooks, ntfy and a webhook receiver with scripted failures; renderer golden files per platform, sample and variant (screenshot, local links, counts, edits, Discord bot mode); escaping and length properties; every adapter against the fakes including 429, 5xx, timeouts and vanished messages; the full path event to send, edit and TTL delete on SQLite per platform; and the redaction sentinel rendered through every platform and webhook body.
A non-required ntfy job runs the adapter against binwiederhier/ntfy v2.28.0 (publish, read back, upload, replace by sequence id, delete). notify-live.yml runs weekly, on dispatch and on PRs labelled live-notify from this repository, in the notify-live environment: real Telegram, a Discord webhook and ntfy, send with a screenshot, read back, edit and delete, skipping a platform without secrets and opening an issue on failure.
…icUrl into the server Composition registers the Telegram, Discord, ntfy and webhook factories and renderers, the screenshot store and snapshots, the channel service, the publicUrl link builder, host trust on /mcp and the check; startup channels come from --notificationChannel. The help lists the flag and the channels command; route, doctor and help tests cover them; OpenAPI and the references are regenerated.
… anywhere in a preview A secret parameter can appear in a request body as well as the path (an ntfy topic kept in a variable); the preview shows the variable name in both.
…ws and delivery log Notifications gains Inbox, Channels and Delivery log sections and a sidebar entry. Channel cards show status, secret variables (set or missing, never values), 24 h counts and test sends; startup channels are read-only. The add-channel wizard keeps its draft in localStorage and walks platform, credentials with launch-method snippets, connect (Telegram one-tap link with QR, ntfy subscribe QR, webhook URL), rules (presets, quiet hours, content level, screenshots with masking, self-destruct capped at 47 h on Telegram) and a preview drawn from the renderer's own requests. The Discord difference panel draws webhook and bot messages side by side. The System page shows the public address check.
…ust and screenshots Also sends an ntfy notification without its screenshot when a self-hosted server refuses uploads, names a startup/dashboard clash in the boot error, shows channels and the link target in the banner, and aligns the specs (viewport-size screenshots, two Telegram buttons per row, D-40 wording).
…enshots and self-destruct The notifications guide gains per-platform setup (BotFather, Discord webhooks, the ntfy app and QR code, the webhook contract and its signature), the public address with Tailscale, reverse proxies and the check, screenshots, self-destruct, startup channels, the delivery log and what leaves the machine; the CLI, security and dashboard guides and a minor changeset follow.
…ld be refused The Bot API refuses URL buttons whose host has no dot (localhost, a bare machine name) or is an IPv6 literal, which failed every send with a localhost publicUrl. Found against the real Bot API; domains and IPv4 addresses keep their buttons.
A live run against ntfy.sh read the topic back before the publish showed up; the check now polls for up to ten seconds per step.
…view fixes Component tests over responses captured from a real daemon (cards, wizard, Telegram connect, previews, public address), the channel API coverage check, Discord and Telegram timestamps in the mocks, one publicUrl note, the delivery log stacked below 1024 px, clearer save and connect errors.
…the test starts Adds a webhook channel through the wizard, previews it, saves it, sends a real test that the receiver records, finds it in the delivery log and deletes it, on desktop and phone. Links the rules step to the guide's what-leaves-your-machine section.
arg1998
marked this pull request as ready for review
September 29, 2026 01:25
The preview listed the missing-publicUrl hint twice in different words. The server note is now the single source (the CLI prints it too), worded as what happens and what to do, with publicUrl shown as code in the dashboard. The act-button note now says what the buttons do today.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Notifications on your phone (N1)
N1 of the notification channels program (plan
scratchpad/notification-channels-design.md, owner-approved). It builds on N0 (#25): the message contract and the delivery outbox.What it does
Four channels: a Telegram bot, a Discord webhook, an ntfy topic and a generic webhook. Each is a pure renderer (contract → platform request) plus a transport, behind N0's outbox (retries,
Retry-After, breaker, coalescing).Edits in place, silently. Resolving an attention request edits the message and removes its buttons. A growing tool-error group edits its count.
Self-destruct per channel × category (default never; Telegram capped at 47 h because bots may delete for 48 h only). Delete when resolved is available, off by default. Deletes that fall due while BrowserHive is off happen at the next start.
Screenshots, opt-in per channel × category: an attention request (CAPTCHAs included), a vault confirmation (taken before the fill; never during one or in a secret window), a crash's last frame. Masked or unmasked, per channel. Never with
recordToolResults=noneor below content levelfull.publicUrl(--publicUrl/BROWSERHIVE_PUBLIC_URL/ JSON). All links use it. Its host joins the Host allow-list, and its origin passes the CSRF guard, which fixes proxies that rewriteHost.doctor, the System page andGET /system/public-urlcheck<publicUrl>/healthagainst aninstance_idminted at each start: ok / elsewhere / behind a login / unreachable. Without it, links are labelled "Open on this computer".Startup channels: a flag-only
--notificationChannel "telegram:name=phone,token=env:BH_TG_TOKEN,chat=…", repeatable. Secrets go only asenv:NAME; an inline secret exits 64 without echoing it, and so does a name clash with a dashboard channel. These channels show read-only in the dashboard.API:
/api/v1/channelsCRUD, pause/resume, test send (rate-limited), a pure preview (the same renderer as a send, secrets shown as variable names), the delivery log with filters and cursor, an env-var set/missing check (never the value), and a Telegram one-tap connect (at.me/<bot>?start=<code>link, then a 2-minute setup-only long poll). Scopes arechannels:read|write, and achannelsWS topic carries changes live.CLI:
browserhive channels list | test <name> | preview <name> [--sample]against a running server.doctorchecks every channel's variables andpublicUrl. The banner gains aNotifyline.Dashboard: Notifications → Channels, with cards (status, last delivery, 24 h counts, pause/resume/edit/duplicate/delete/test) and a 5-step wizard:
The Delivery log is live, with filters and "why wasn't this sent?", and the System page gains a Public address card.
Setup in short
/newbot, runexport BH_TELEGRAM_TOKEN=…, and restart. Then use the wizard's one-tap link.export BH_DISCORD_WEBHOOK=…and restart.--publicUrl https://…, for example with Tailscaleserveor your reverse proxy. The test message's Open dashboard is the proof.The full walkthroughs are in
docs/guide/notifications.md.Spikes
sendRichMessagewithhtml(h3, table, footer) plus an inline keyboard was accepted, and so waseditMessageText(rich_message). Rendering on phones was not compared. D-40: classic HTML for now; two spike messages are left in the test group for you to compare.localhost, dotless hosts, IPv6 literals. Accepted: domains and IPv4. ApublicUrlofhttp://localhost:…failed every send, so it is fixed: links go into the text.files[n]/embeds, so a webhook V2 message cannot upload a screenshot. We use embeds plus a link-button action row (with_components=true). Not verified live; there is no webhook yet.binwiederhier/ntfy:v2.28.0: sequence-id replace,DELETE /<topic>/<seq>,PUTupload (only the path form or the header carries the sequence id; the query form is ignored), and a maximum of 3 actions. A server without an attachment cache refuses uploads, so we fall back to text.Verification
bun run checkpasses (lint, typecheck, depcruise, unit ≈3 000 server tests plus 384 dashboard tests, and the openapi/docs/db-types checks).test:goldens,build,package:checkand the website build pass.test:integrationgives 67 passed / 2 skipped (the real-ntfy test needsBHDEV_NTFY_URL). E2e, run the CI way against the built daemon, gives 11 passed / 1 skipped: the 9 existing tests plus the new channels journey on desktop and phone.covered.message_deleteevent arrives). Resolving after a TTL delete givessuperseded: message_deleted.publicUrlcheck reportsok.BROWSERHIVE_NOTIFICATION_CHANNELexits 64 with a hint. A name clash between a startup and a dashboard channel exits 64.sendPhotowith the masked screenshot, and an ntfy.sh upload with the attachment and view actions.deleteMessageand ntfy.shDELETEboth succeeded.scripts/notify-live.ts: Telegram ✅, ntfy.sh ✅ (after adding cache polling), Discord skipped.publicUrl, and ntfy.sh read-back lags a publish.Contract change
listChannels,createChannel,getChannel,updateChannel,deleteChannel,pauseChannel,resumeChannel,testChannel,previewChannel,listDeliveries,getDelivery,checkChannelEnv,startTelegramConnect,getTelegramConnectandgetPublicUrlStatus;HealthResponse.instance_id(optional);CHANNEL_NOT_FOUND,CHANNEL_NAME_TAKEN,CHANNEL_READ_ONLY,CHANNEL_NOT_READY,CHANNEL_KIND_UNAVAILABLE,CHANNEL_PLATFORM_ERRORandDELIVERY_NOT_FOUND.publicUrl, plus the flag-only--notificationChannel.BROWSERHIVE_NOTIFICATION_CHANNELand anotificationChannelfile key fail with a hint.channels(channel.changed,channel.removed,delivery.updated); the scopeschannels:readandchannels:write.NotificationChannelRules): gainsmask_images(additive;schemastays 1).packages/core/test/goldens/notifications/.Please check on your phone
publicUrlis reachable from the phone.live-notifylabel to this PR to runnotify-live.yml(it needs your approval of thenotify-liveenvironment).Also in this PR
uqr(MIT) for QR codes.Known and not chased
integration (macos-latest)andintegration (windows-latest)are known flaky/failing (macOS: integration tests are flaky (timeouts, then Chromium killed) #23, Windows: integration failures #2).Follow-ups for N2 (handoff
scratchpad/notifications-handoff-N1.md)notification_actions; mint tokens and pass them toRenderContext.actToken.http-action spike.