Skip to content

feat(stickers): add Sonar sticker packs (supersedes #2968) - #4026

Draft
mattkanwisher wants to merge 4 commits into
block:mainfrom
mattkanwisher:feat/sonar-stickers-rebase
Draft

feat(stickers): add Sonar sticker packs (supersedes #2968)#4026
mattkanwisher wants to merge 4 commits into
block:mainfrom
mattkanwisher:feat/sonar-stickers-rebase

Conversation

@mattkanwisher

Copy link
Copy Markdown

Resurrects #1920 and #2968 — Sonar sticker packs, rebased onto current main

This PR brings back the sticker-packs work from #1920 (closed unmerged) and rebases it onto today's main (103 commits newer). All merge conflicts are resolved and every local quality gate is green.

Why Buzz must have this

Emoji reactions and custom emoji already proved that lightweight, expressive communication matters in Buzz — but every serious chat product our users compare us to (Slack, Signal, Telegram, Discord) ships stickers as the next tier of expression: larger, authored, pack-based, and fun. Today a Buzz user who wants to react with more personality than an emoji has to upload an image by hand, every time.

Stickers are also a community-culture primitive: teams build shared identity around their own packs. With this PR Buzz gets that — but with a safety model no competitor has:

Admin-approved catalog (kind 13536) — a community's sticker catalog is a materialized, curated list. Clients never fetch arbitrary pack events from the open network.
Content-addressed, approval-gated asset cache — the relay only serves sticker assets whose SHA-256 matches an approved pack revision (/media/sticker/{author}/{identifier}/{shortcode}/{sha256}). It never fetches unapproved assets, caps dimensions/frame counts, and pins DNS resolution.
Signal interop — users can import an official signal.art pack link and republish it as a Sonar pack, so existing sticker culture ports over instead of starting from zero.
Authoring built-in — create/edit packs in Settings, upload WebP covers and images from the desktop app, publish via buzz stickers CLI commands (agent-first, like everything else in Buzz).
Agents can sticker too — stickers ride ordinary channel messages as validated sticker tags, so managed agents and CLI flows can send them through the same pipeline.
What it looks like

Composer — sticker picker entry point (empty state prompts pack install):

Settings → Stickers — install curated packs, author your own, or import a Signal pack:

(Screenshots in the comment below — posted via scripts/post-screenshots.sh.)

Note on the screenshots: the E2E mock bridge has no seeded sticker catalog yet, so the picker renders its honest empty state. A follow-up will seed a mock catalog + pack events in e2eBridge.ts so we can capture the populated grid and add a screen recording of the full send flow. To try it live: buzz stickers --help against any relay running this branch.
What changed since #1920 (rebase notes)

Migration renumbered 0020_sticker_catalog.sql → 0021_sticker_catalog.sql (0020 is now join_policy_acceptances on main); migration tests updated for 21 migrations.
Merged with main's media-auth rework — kept authenticate_media_read / blob_cache_control alongside the new verified-sticker route; kept should_retry_legacy_upload upload fallback in both buzz-cli and the desktop app while exposing do_upload to the sticker commands.
Merged with main's mention-flow rework — sticker tags compose with audienceGeneration/audienceRevision, postSendContent, and the persistent-agent-audience send path; MessageComposerProps extraction kept and extended with main's newer props.
File-size guard compliance — split desktop sticker commands into commands/stickers.rs and the tag validator into sticker_events.rs (keeps media.rs, events.rs, lib.rs, AppShell.tsx under the 1000-line ceiling without overrides); added a small useLiveUpdates aggregator hook in the app shell.
Mobile — receive-only sticker rendering conflicts resolved against main's relay-provider changes.
Security properties (unchanged from #1920)

No fetching of unapproved assets; approval pins exact pack event IDs.
SHA-256 verified, dimension- and frame-capped animated media; zeroized secrets on import.
Pinned bitchat-to-sonar dependency (rev-locked).
Verification (this branch)

cargo clippy --workspace --all-targets -D warnings ✅
just desktop-tauri-check / just desktop-tauri-clippy / just desktop-tauri-test (1469 passed) ✅
Desktop: pnpm check (biome + file-size + px-text guards) ✅, pnpm test (3129 passed) ✅, tsc --noEmit ✅
Mobile: dart format ✅, flutter analyze ✅, flutter test (520 passed, incl. new sticker preview/reference tests) ✅
cargo test -p buzz-db -p buzz-core -p buzz-media -p buzz-sdk -p buzz-cli ✅ (incl. updated migration-sequence tests)
Design doc: docs/brainstorms/2026-07-15-sonar-sticker-packs-design.md.

Supersedes and [ ](#1920

mattkanwisher and others added 2 commits August 1, 2026 05:01
Rebases vincenzopalazzo:feat/sonar-stickers onto current main (182 commits
newer).

Conflict resolutions:
- ingest.rs / relay_admin.rs: union of main's project + team-catalog
  validation with the sticker pack/list/reference validators.
- buzz-db event.rs: main's NIP-09 created_at deletion predicate now runs
  inside the PR's advisory-lock transaction.
- Migration renumbered 0025 -> 0027 (0025_relay_invites and
  0026_replica_heartbeat landed on main); migration test expects 27.
- Desktop: adopted main's MessageComposer.types.ts and ComposerDockToolbar,
  dropping the PR's resurrected MessageComposerProps.ts; settings sections
  stay extracted in lib/settingsSections.ts with main's 'voice' section added.
- Mobile message_content.dart: main's KeyedSubtree + trailing-gallery body
  composed with the sticker preview branch.

main removed check-file-sizes.mjs's per-file override map, so the four files
the sticker work grew past the ceiling were split rather than exempted:
- desktop/src-tauri: NIP-IA builders + their tests -> identity_archive_events.rs
- shared/api/tauri.ts: NIP-44 + NIP-AB pairing calls -> api/pairing.ts
- MessageComposer.tsx: edit payload, emoji insertion, autocomplete key
  dispatch, and the sticker send path -> four sibling modules

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matthew Campbell <hyper@hyperworks.nu>
Co-authored-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Reviewed all 25 findings against current code. Most had already been fixed by
the author in later commits; the rest are fixed here, each with tests.

Security:
- desktop sticker import performed no SSRF check on relay hints from a Sonar
  link, so `ws://127.0.0.1:<port>` (or a hostname resolving to a private
  address) reached a WebSocket handshake from the desktop process. New
  commands/sticker_relay.rs validates the hint (wss:// only outside debug, no
  credentials), resolves it, rejects when ANY answer is a private/reserved
  address via buzz-core's existing is_private_ip, and then connects the vetted
  SocketAddr directly so DNS rebinding cannot slip past the check. Adds a
  message-size cap and an event ceiling for hostile relays.
- externally fetched sticker assets bypassed the metadata guard that uploads
  apply, so a pack could republish EXIF/XMP/comment data through the relay
  cache. validate_sticker_content now runs validate_image_metadata_free before
  the geometry parse.
- animation budgets used only the logical canvas, so a 1x1 GIF/WebP/APNG could
  declare a 65535x65535 frame and pass the pixel guard. Frame rectangles are
  now parsed and bounded for all three formats.

Correctness:
- buzz-core network.rs blocked the whole IPv6 2001::/23 IETF
  protocol-assignment block, which is mostly globally reachable — it rejected
  2001:1::1 (PCP anycast) and broke main's Teredo test. Narrowed to the
  registered non-reachable sub-blocks and tightened 3fff::/20 from a /16.
  Same over-broad-range class as the 192.0.1.0/24 finding.
- the sticker fetch-lock map evicted entries inline after the fetch, which
  leaks the entry whenever the request future is cancelled. Eviction moved to
  an RAII Drop handle.
- the admin approval queue derived from a single 500-row query, so packs past
  the cap were undiscoverable. Replaced with a paged until-cursor walk.
- APNG was enabled for sticker uploads by widening the shared upload MIME
  allowlist, which also widened ordinary attachments. Rescoped to a
  sticker-only superset.
- the sticker send path restored the pre-send draft unconditionally, clobbering
  anything typed while the send was in flight. Restores only when the composer
  is still empty.

Pre-existing and unrelated: buzz-relay's mesh_demo round-trip test fails
identically on upstream/main (verified in a clean worktree).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matthew Campbell <hyper@hyperworks.nu>
Co-authored-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
@mattkanwisher
mattkanwisher requested a review from a team as a code owner July 31, 2026 22:47
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

mattkanwisher and others added 2 commits August 1, 2026 06:03
Every Signal import failure was collapsed into "Could not import that Signal
sticker pack. Check the link and try again." via `map_err(|_| ...)`, discarding
the cause entirely.

That message is actively misleading for the most likely failure. The link is
validated against the official signal.art shape *before* any request is made,
so by the time the import runs its form is already known good. Meanwhile
cdn.signal.org is served under Signal's own private root CA
(`O=Signal Messenger, LLC, CN=Signal Messenger`), which no public trust store
contains — reqwest is built with `accept_invalid_certs: false` and the system
roots, so the handshake fails on a perfectly valid link and the user is told to
go re-check the one thing that cannot be wrong.

Reproduced directly:
  curl    https://cdn.signal.org/stickers/<id>/manifest.proto   -> exit 60
  curl -k https://cdn.signal.org/stickers/<id>/manifest.proto   -> HTTP 200

Not a local middlebox: signal.org and cloudflare.com both resolve and verify
fine from the same host; only cdn/cdn2.signal.org fail.

Distinguishes transport, decryption, and other failures, and logs the
underlying error (Signal's URLs carry the pack id, never the pack key). Does
not disable certificate verification.

Note this only fixes diagnosability: Signal import still cannot succeed until
the client trusts Signal's root CA. The pinned bitchat-to-sonar crate only
exposes `accept_invalid_certs`, so a real fix needs an upstream API to supply a
custom root via `add_root_certificate` — disabling verification is not an
acceptable substitute.

Co-authored-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matthew Campbell <hyper@hyperworks.nu>
PR block#2968 shipped with the sticker picker rendering only its empty state,
because the E2E mock bridge had no sticker catalog to serve — the PR body
listed seeding one as a follow-up. This is that follow-up.

Adds kind:30031 pack events, the kind:13536 approved-catalog snapshot, and the
viewer's kind:10031 installed list to the mock relay, with asset URLs shaped to
pass `isHttpsHashUrl`, plus a Playwright route that fulfils the sticker cache
path with real image bytes so the grid renders actual art instead of broken
images. The spec asserts `naturalWidth > 0`, so a regression fails the test
rather than silently capturing broken pixels.

Covers four states: the populated composer picker, a sticker sent and rendered
in the timeline, the settings catalog with its pending-approval queue, and the
picker before any pack is installed.

Co-authored-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matthew Campbell <hyper@hyperworks.nu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant