Skip to content

feat(stickers): add Sonar sticker packs (resurrects #1920) - #2968

Closed
vincenzopalazzo wants to merge 9 commits into
block:mainfrom
vincenzopalazzo:feat/sonar-stickers
Closed

feat(stickers): add Sonar sticker packs (resurrects #1920)#2968
vincenzopalazzo wants to merge 9 commits into
block:mainfrom
vincenzopalazzo:feat/sonar-stickers

Conversation

@vincenzopalazzo

Copy link
Copy Markdown

Resurrects #1920 — 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.sql0021_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 #1920.

@vincenzopalazzo
vincenzopalazzo requested a review from a team as a code owner July 26, 2026 11:24
vincenzopalazzo added a commit to vincenzopalazzo/buzz that referenced this pull request Jul 26, 2026
@vincenzopalazzo

Copy link
Copy Markdown
Author

Screenshots

Sticker picker — populated (from the original #1920 build, same UI)

The composer sticker button opens the pack picker with the installed "Herecomesbitcoin.org" pack.

picker

Sticker rendered in the message timeline

message

Settings → Stickers (current main rebase)

Install curated Sonar packs, author your own pack with WebP cover + images, or import an official Signal pack.

settings

Picker empty state (current main rebase)

Honest empty state when no packs are installed yet — points the user to Settings.

picker-empty

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aec7cea21f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-relay/src/api/media.rs Outdated
Comment thread desktop/src/shared/api/stickers.ts Outdated
Comment thread crates/buzz-core/src/network.rs Outdated
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc7647168a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1105 to +1107
if !ALLOWED_MIMES.contains(&mime) {
return Err(CliError::Usage(format!("unsupported file type: {mime}")));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept APNG in sticker byte uploads

When stickers create, update, or import receives an APNG, sniff_sticker_asset deliberately classifies it as image/apng, but this new upload path checks the pre-existing ALLOWED_MIMES list, which does not contain that MIME. Every otherwise-valid APNG therefore fails locally with “unsupported file type” before reaching the relay, despite APNG being advertised as supported throughout the sticker validators.

Useful? React with 👍 / 👎.

Comment thread desktop/src/shared/lib/mediaUrl.ts Outdated
Comment on lines 283 to 286
if (!sticker?.[1] && cachedRelayOrigin) {
const urlOrigin = canonicalOrigin(url);
if (urlOrigin !== cachedRelayOrigin) {
return url;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the origin gate to absolute sticker URLs

For every match of RELAY_STICKER_RE, capture group 1 is the path regardless of whether the input was relative or absolute, so !sticker?.[1] is always false and the origin comparison never runs. Consequently an external URL such as https://other-relay/.../media/sticker/... passed by global callers like the markdown image renderer is rewritten to the active relay's localhost proxy instead of being loaded from its original host, producing the wrong asset or a 404.

Useful? React with 👍 / 👎.

Comment thread scripts/diagnose-managed-agent.sh Outdated
found=0
while IFS= read -r -d '' f; do
if grep -qE "${PUBKEY}" "${f}" 2>/dev/null || grep -qiE 'buzz-acp|managed.agent' "${f}" 2>/dev/null; then
hits=$(grep -hE "${TELLTALES}" "${f}" 2>/dev/null | tail -n 8)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep scanning when a relevant log has no telltales

When a log contains the agent pubkey or buzz-acp but none of the TELLTALES patterns, grep exits 1; with both set -e and pipefail, that status propagates through the command substitution and terminates the entire script at this assignment. The diagnostic then stops before scanning subsequent logs or printing the relay checks and decision key, so the no-match pipeline needs to be made non-fatal.

Useful? React with 👍 / 👎.

Comment thread scripts/diagnose-managed-agent.sh Outdated
Comment on lines +60 to +62
echo " curl -s ${RELAY}/query -d '{"kinds":[39002],"#p":["${PUBKEY}"]}' | jq '.[].tags'"
echo " >> does your @mention carry the agent's exact hex p-tag? (kinds 9=stream msg, 40002=v2)"
echo " curl -s ${RELAY}/query -d '{"kinds":[9,40002],"#p":["${PUBKEY}"],"limit":5}' | jq '.[]|{kind,p_tags:[.tags[]|select(.[0]=="p")[1]]}'"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve JSON quoting in the printed relay commands

The unescaped inner double quotes terminate and reopen the shell string instead of being emitted. The first suggested command consequently prints a body like '{kinds:[39002],#p:[abc]}', and the second also prints select(.[0]==p), so copied commands contain invalid JSON and an invalid jq expression rather than performing the advertised relay checks.

Useful? React with 👍 / 👎.

Comment on lines +56 to +57
class StickerUnavailablePlaceholder extends StatelessWidget {
const StickerUnavailablePlaceholder({super.key});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the sticker placeholder widget private

StickerUnavailablePlaceholder is only referenced inside this source file, but declaring it public creates a second public widget alongside StickerPreview. Rename it to _StickerUnavailablePlaceholder or move it to its own file to preserve the mobile repository's one-public-widget-per-file invariant.

AGENTS.md reference: AGENTS.md:L528-L530

Useful? React with 👍 / 👎.

- Route sticker asset reads through authenticate_media_read so
  BUZZ_REQUIRE_MEDIA_GET_AUTH applies to /media/sticker/* (P1)
- Keep superseded catalog entries as removable placeholders instead of
  dropping them from the curation list (P2)
- Re-add sticker settings section + sendChannelMessage stickerTags param
  lost in the rebase auto-merge
- Refresh stale file-size overrides (runtime.rs drift on main, events.rs)

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea9600a069

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-media/src/validation.rs Outdated
bytes.extend_from_slice(&[0; 4]);
}

let mut apng = TINY_PNG.to_vec();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Insert APNG chunks before the existing IEND

TINY_PNG already contains a complete IEND chunk, so initializing this fixture from it and then appending acTL, fcTL, IDAT, and another IEND places all animation chunks after the image has ended. apng_frame_count stops at the first IEND and requires that offset to equal the byte length, so it returns None and the Some(1) assertion below fails. Remove the original IEND or construct the chunks in valid PNG order.

Useful? React with 👍 / 👎.

Comment on lines +40 to +41
child: Image.network(
url,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the authenticated media provider for mobile stickers

When BUZZ_REQUIRE_MEDIA_GET_AUTH=true, this Image.network request carries no Blossom authorization header, while the new sticker route authenticates every /media/sticker/* read. Mobile therefore renders every otherwise-valid sticker as unavailable in authenticated-media deployments. Use the existing shared MediaImage, which obtains headers from MediaGetAuthService, instead of issuing an unsigned network image request.

Useful? React with 👍 / 👎.

Comment on lines +618 to +621
trimmed: trimmed || sticker.fallback,
audienceGeneration: persistentAudience.generation,
audienceRevision: audienceScope ? persistentAudience.revision : null,
stickerTags: sticker.tags,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent sticker sends from silently hiding typed text

When the composer already contains text and the user selects a sticker, this sends the typed trimmed content rather than the sticker fallback while also attaching the sticker tag. Both MessageRow.renderBody and mobile MessageContent take the valid-sticker branch and omit that content, so a message such as “look at this” silently disappears whenever the sticker loads. Either disallow/clear mixed text when selecting a sticker, always send the shortcode fallback, or render the caption alongside the sticker.

Useful? React with 👍 / 👎.

- Add the Stickers descriptor to settingsSections so the nav item and
  section actually render (the rebase carried the type + render case but
  dropped the descriptor entry, hiding the section).
- Replace the composer picker's empty state with inline install buttons
  for curated catalog packs and an Open-sticker-settings shortcut, so
  users can install/create without leaving the chat.

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8897192014

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +422 to +425
if kind.mime_type() == "image/png"
&& buzz_core::stickers::apng_frame_count(bytes).is_some()
{
"image/apng".to_owned()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate APNG uploads with the PNG metadata guard

When an uploaded PNG contains a valid APNG control sequence, this reclassifies it as image/apng; the subsequent validate_image_metadata_free match handles only image/png, so the APNG falls through to Ok(()). An APNG containing arbitrary eXIf, iTXt, zTXt, or private ancillary chunks is therefore accepted even though ordinary PNG uploads reject those metadata channels, potentially publishing location or other private metadata. Route image/apng through validate_png_metadata_free as well.

Useful? React with 👍 / 👎.

Comment thread crates/buzz-relay/src/api/media.rs Outdated
Comment on lines +592 to +596
let fetch_lock = STICKER_FETCH_LOCKS
.entry(object_key.clone())
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone();
let _fetch_guard = fetch_lock.lock().await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid locking every cached sticker read

Once a sticker is present in object storage, every request for that hash still acquires this per-object mutex before performing the S3 HEAD and GET. Concurrent loads of a popular sticker are consequently serialized even though no origin materialization is needed, making latency grow with the number of simultaneous viewers. Check the cache before taking the fetch lock, then acquire and recheck only on a miss to retain deduplication of upstream fetches.

Useful? React with 👍 / 👎.

Comment on lines +32 to +33
static STICKER_FETCH_LOCKS: LazyLock<DashMap<String, Arc<tokio::sync::Mutex<()>>>> =
LazyLock::new(DashMap::new);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove unused sticker fetch locks from the global map

Each distinct approved sticker hash inserts a mutex into this process-global DashMap, but no path removes the entry after the request completes or after a pack is removed. Catalog turnover across tenants therefore grows relay memory for the lifetime of the process even though the corresponding assets may never be requested again. Evict the entry when the last waiter releases it, or use weak references/a bounded keyed-lock structure.

Useful? React with 👍 / 👎.

isSettingsSection() validates /settings?section= against
SETTINGS_SECTION_VALUES, which was missing "stickers" — the nav
descriptor and render case existed, but the route search validation
stripped the param and fell back to the default profile section, so
clicking Stickers in settings (or "Open sticker settings" in the
composer picker) never opened the panel.

Extract the section registry into features/settings/lib/settingsSections.ts
(with "stickers" added) and re-export it from SettingsPanels.tsx, plus a
regression test asserting every nav section passes the guard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e58ee2287

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +370 to +376
return [
{
coordinate: entry.coordinate,
author: address.author,
identifier: address.identifier,
title: `${address.identifier} (superseded)`,
stickers: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not expose superseded placeholders as installable packs

When an author replaces an approved pack, the newly added placeholder is returned as an ordinary StickerPack. The same useStickerCatalogQuery data feeds the Install controls in StickerSettingsCard and ComposerStickerPicker, so members can install this superseded coordinate; useInstalledStickerPacks then treats the empty placeholder as installed and presents a pack containing no stickers. Keep placeholders available to the admin removal UI, but exclude or explicitly mark them as unavailable in member-facing catalog/install flows.

Useful? React with 👍 / 👎.

Comment thread scripts/diagnose-managed-agent.sh Outdated
relay_url, model, provider}' "${RECORD_FILE}" 2>/dev/null || echo " (pubkey not found in ${RECORD_FILE})"
else
echo " jq not installed; grep'ing for the pubkey:"
grep -o ""pubkey":"[^"]*"\|"respond_to":"[^"]*"" "${RECORD_FILE}" | head

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve quoting in the jq-free record fallback

On machines without jq, the shell consumes the inner double quotes in this grep expression, so it does not match normal JSON such as {"pubkey":"..."}. Because the command is also a pipeline under set -euo pipefail, the resulting grep status 1 terminates the diagnostic before the log and relay checks; quote the regex as a single shell argument and make a no-match non-fatal.

Useful? React with 👍 / 👎.

The settings import only accepted Signal signal.art links, so a pack
already published as kind:30031 on public relays (e.g. a
https://sonarprivacy.xyz/stickers?a=30031:<author>:<identifier>&relay=...
link) could not be brought into a Buzz community at all.

Add an import_nostr_sticker_pack Tauri command that parses the pack link
(coordinate + ws(s) relay hints, capped at 8), fetches the newest
kind:30031 event via read-only unauthenticated REQs against the hint
relays, and parses it with sonar-stickers. The draft keeps the original
content-addressed asset URLs — the relay's approval-gated sticker cache
re-fetches and hash-verifies them — and the importer republishes the pack
under their own key through the normal publish path (the relay rejects
foreign-pubkey events, so re-ingesting the original event is not
possible).

The settings import field now branches on link type: signal.art links keep
the existing zeroized Signal flow, anything else goes through the Nostr
import. Draft gains optional description/license so imported packs
round-trip their metadata.

Verified against the live pack from the bug report: the REQ returns
"Herecomesbitcoin.org" (103 stickers, blossom.primal.net assets) from
wss://relay.damus.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 901f8f6b8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +343 to +348
RelayMessage::Event {
subscription_id: id,
event,
} if id == subscription_id
&& event.kind.as_u16() == sonar_stickers::STICKER_PACK_KIND
&& event.pubkey.to_hex() == address.author_pubkey_hex

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify signatures on imported Nostr pack events

When a Sonar link includes a malicious or compromised relay hint, the relay can return an event that merely claims the requested kind and pubkey but has a forged ID or signature. NostrWsConnection only deserializes incoming EVENT messages, and this guard never calls event.verify() before the structurally parsed pack is offered for republication under the importing user's identity. Verify every candidate event cryptographically before accepting it.

Useful? React with 👍 / 👎.

Comment on lines +349 to +351
&& newest
.as_ref()
.is_none_or(|current| event.created_at > current.created_at) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the canonical event-ID tiebreak when importing

When a relay returns multiple valid revisions with the same created_at, this strict timestamp comparison retains whichever event arrived first, although the repository's canonical replaceable-event ordering selects the smallest event ID for a timestamp tie. Relay response order is not guaranteed, so the importer can republish a losing revision instead of the actual pack head; compare (created_at, reverse event ID) as canonical_head does in the CLI.

Useful? React with 👍 / 👎.

ImportedStickerAsset serialized None width/height/alt/emoji as JSON null,
but publishStickerPack validates with `=== undefined` checks — a null
width passes the xor guard yet fails `null !== undefined && null < 1`,
so every dimension-less sticker (the common case for Nostr packs without
dim fields, e.g. :s0: in the herecomesbitcoin pack) was rejected with
"not a valid Sonar HTTPS asset". Signal imports never hit this because
uploaded assets always carry dimensions.

Add skip_serializing_if to the optional asset fields plus a serde
regression test asserting the keys are omitted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2bec9c59fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread desktop/src/shared/api/stickers.ts Outdated
parsed.username === "" &&
parsed.password === "" &&
parsed.port === "" &&
parsed.pathname.includes(sha256)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize the asset path before matching its hash

When an asset URL contains an uppercase hexadecimal rendering of its lowercase SHA-256, relay-side validation accepts the pack because buzz-core/src/stickers.rs lowercases the URL path before matching the hash, while this desktop check rejects it. parseStickerPack consequently returns null, and fetchStickerCatalog misrepresents the approved current pack as a superseded empty placeholder, preventing desktop users from installing or using it. Normalize parsed.pathname consistently or reject this URL shape at the relay as well.

Useful? React with 👍 / 👎.

Comment on lines +334 to +338
conn.send_raw(&request)
.await
.map_err(|error| error.to_string())?;
let mut newest: Option<nostr::Event> = None;
let deadline = tokio::time::Instant::now() + PACK_FETCH_TIMEOUT;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use one timeout for the entire relay fetch

When a relay hint takes most of PACK_FETCH_TIMEOUT to connect, this creates a fresh ten-second query deadline afterward, and the intervening send_raw call is not timeout-bounded at all. A link with eight slow or hostile hints can therefore keep the import UI busy for roughly 160 seconds or longer despite the constant being documented as the combined per-relay connect-and-query budget. Establish the deadline before connecting or wrap the complete per-relay operation in one timeout.

Useful? React with 👍 / 👎.

Relay:
- media: check the sticker cache before taking the per-object fetch lock
  (cached reads no longer serialize) and evict idle STICKER_FETCH_LOCKS
  entries so catalog turnover stops growing process memory

Media validation:
- route image/apng through validate_png_metadata_free so APNGs can no
  longer smuggle eXIf/iTXt/zTXt metadata past the PNG guard (P1)
- fix APNG test fixtures appending animation chunks after TINY_PNG's
  existing IEND (P1)
- buzz-cli: accept image/apng in sticker byte uploads

Desktop:
- tauri import: verify event signatures, apply the canonical
  (created_at, event-id) head tiebreak, and bound the whole per-relay
  fetch by one 10s deadline (P1)
- stickers: mark superseded placeholders and exclude them from member
  install flows while keeping the admin Remove control
- stickers: lowercase asset path before hash matching (relay parity)
- mediaUrl: fix dead origin gate — absolute sticker URLs from other
  relays now pass through unchanged
- composer: send the shortcode fallback for sticker messages instead of
  silently attaching (and hiding) typed text

Mobile:
- sticker_preview: private placeholder widget (one-public-widget rule)
  and MediaImage so sticker reads carry Blossom auth

Scripts:
- diagnose-managed-agent: fix JSON/jq quoting in printed commands and
  make no-match greps non-fatal under set -e/pipefail

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61a33308ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +282 to +285
if !matches!(parsed.scheme(), "wss" | "ws") {
return Err(format!("Relay hints must be ws(s) URLs: {relay}"));
}
valid_relays.push(relay.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject private relay hints before connecting

A Sonar link can supply relay=ws://127.0.0.1:<port> (or a hostname resolving to a private or link-local address), and this validation accepts it before NostrWsConnection::connect issues a WebSocket handshake from the desktop process. Importing an untrusted link therefore enables client-side SSRF against local or internal services; resolve relay hosts and reject private/reserved addresses before connecting, and restrict plaintext ws to an explicit local-development exception.

Useful? React with 👍 / 👎.

Blossom servers commonly 302 to a CDN (e.g. blossom.primal.net →
r2a.primal.net), but fetch_sticker_bytes used redirect::Policy::none(),
so every lazily-materialized sticker from such a pack failed with 502
and clients rendered the emoji fallback instead of the image.

Follow up to 3 redirects manually, re-running the same SSRF validation
on every hop: HTTPS-only, no credentials, default port, DNS lookup with
private-IP rejection, and per-hop address pinning with proxies disabled.
Redirect loops and unbounded chains still fail closed.

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7600167251

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

trimmed: hasSticker ? sticker.fallback : trimmed,
audienceGeneration: persistentAudience.generation,
audienceRevision: audienceScope ? persistentAudience.revision : null,
stickerTags: sticker.tags,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep attachments visible when sending a sticker

When the composer already has pending attachments and the user selects a sticker, this sends both the imeta data and the sticker tag, but desktop's MessageRow.renderBody and mobile's MessageContent replace the entire markdown body with the valid-sticker view. Because buildOutgoingMessage puts the attachment links in that hidden body, recipients see only the sticker even though the composer showed and sent the attachments. Prevent combining these states, clear the attachments when selecting a sticker, or render the attachment body alongside the sticker.

Useful? React with 👍 / 👎.

return Err(MediaError::ImageTooLarge);
}

Ok(StickerContentMeta {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply metadata rejection to fetched sticker assets

When an approved pack references an externally hosted PNG/APNG, GIF, or WebP containing EXIF, XMP, comments, or private metadata chunks, verify_sticker_bytes reaches this successful return after only MIME, geometry, and frame checks, so the relay caches and serves those metadata-bearing bytes unchanged. The generic upload path rejects the same containers through validate_image_metadata_free at lines 450–451. Fresh evidence beyond the prior APNG-upload finding is that this separate validator for externally fetched sticker assets still bypasses that guard; apply the metadata-free validation here as well.

Useful? React with 👍 / 👎.

Signal-style sticker UX: clicking a sticker in the composer picker now
sends it directly as its own message (shortcode fallback content +
sticker tag), removing the pending-sticker preview step and the ability
to attach typed text to a sticker (renderers take the sticker branch and
would silently drop it anyway).

Any in-flight draft (text, attachments, spoilered URLs) is snapshotted
before the send and restored afterwards, since the mention send flow
clears composer state on success — a sticker click never eats an unsent
draft.

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e9c410785

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +397 to +400
let animation_pixels = (size.width as u64)
.checked_mul(size.height as u64)
.and_then(|pixels| pixels.checked_mul(u64::from(frames)))
.ok_or(MediaError::ImageTooLarge)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate per-frame geometry before accepting animations

For animated GIFs, this budget uses only the logical-screen dimensions returned by imagesize, while gif_frame_count skips each image descriptor without validating its per-frame width and height. An approved, externally fetched GIF can therefore advertise a tiny canvas but contain a frame with dimensions up to 65535×65535, pass the 100-million-pixel guard, and then expose clients to an oversized decoder allocation. Parse and bound every frame rectangle, or validate the complete animation with a bounded decoder, before caching it.

Useful? React with 👍 / 👎.

Comment on lines +658 to +661
setComposerContent(savedDraftContent);
richText.setContent(savedDraftContent);
media.setPendingImeta(savedDraftImeta);
setSpoileredAttachmentUrls(savedDraftSpoilered);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve edits made while the sticker send is pending

When a host leaves the editor enabled during isSending—for example, InboxDetailPane passes disabled={!canReply} and supplies isSendingReply separately—this unconditional restoration overwrites anything the user types while the sticker request is in flight with the pre-click snapshot once the request completes. Restore only if the composer still contains the post-click state, or merge/preserve edits made after the sticker send began.

Useful? React with 👍 / 👎.

Comment on lines +393 to +396
const events = await relayClient.fetchEvents({
kinds: [KIND_STICKER_PACK],
limit: MAX_STICKER_CATALOG_PACKS,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Paginate the pack list before building the approval queue

If a community has more than 500 live pack coordinates, this query permanently returns only the newest 500, while StickerSettingsCard derives the entire admin approval queue from this result. Older packs are therefore impossible to discover or approve through the UI even when the catalog still has capacity; paginate through all live packs or provide a coordinate/search-based curation path rather than using the catalog-size cap as the candidate-query limit.

Useful? React with 👍 / 👎.

@dophsquare

Copy link
Copy Markdown

Review — feat(stickers): add Sonar sticker packs (resurrects #1920)

Verdict: auth boundary is right; structurally blocked — hold line-level review until it's clean. Reviewed at commit 8e9c410 (77 files, ~8.3k+/1.7k-).

🐝 Migration 0025 is a three-way collision

origin/main already ships 0025_relay_invites.sql (merged). This PR's 0025_sticker_catalog.sql and #2948's 0025_channel_owner_recovery.sql both also claim 0025. sqlx keys migrations by the numeric prefix (version), so a duplicate version fails at startup. This PR must renumber to the next free slot — coordinate with #2948 so you don't re-collide. This is part of why the PR shows CONFLICTING.

Auth boundary — verified

  • KIND_STICKER_PACK = 30031 is parameterized-replaceable (compile-time assert!(is_parameterized_replaceable(...))).
  • KIND_STICKER_CATALOG = 13536 mutations route through relay_admin.rs with a sticker_catalog_command_requires_admin_users ingest test and coordinate validation (30031:{author}:{identifier}).
  • Correct gating — catalog is admin-only, packs are user-replaceable.

Why it's blocked for now

CONFLICTING + the 0025 collision + 77 files across relay/db/cli/desktop/mobile. A meaningful line-level review isn't worth doing until it's structurally clean.

Action items

  • Rebase (currently CONFLICTING).
  • Renumber migration off 0025 to the next free slot (coordinate with feat(channels): add audited owner recovery #2948 so they don't re-collide).
  • After rebase + renumber, request a full line-level review — ~8k lines is too large to review meaningfully in the current state.

Independent review by Fizz 🐝 — auth boundary + structure triaged against the checked-out branch; not yet line-reviewed.

@mattkanwisher

Copy link
Copy Markdown

@vincenzopalazzo you still working on this? this is a great feature, that blocks from moving over from telegram to buzz. you need help

@vincenzopalazzo

Copy link
Copy Markdown
Author

@mattkanwisher no i just need to have a feedback from the block team that they want this then I can rebase and move along with the review!

The code is mostly ready

mattkanwisher added a commit to mattkanwisher/buzz that referenced this pull request Jul 31, 2026
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>
mattkanwisher added a commit to mattkanwisher/buzz that referenced this pull request Jul 31, 2026
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

Copy link
Copy Markdown

@vincenzopalazzo they already reviewed it and said why its blocked , I fixed their comment in my branch can you merge in my branch? mattkanwisher#1

@vincenzopalazzo

Copy link
Copy Markdown
Author

@mattkanwisher if you the credit to me for the stickers idea you can carry on the PR and I will close it, feel free to ping me if you need any input on the design

@mattkanwisher

Copy link
Copy Markdown

@vincenzopalazzo ok feel free to close this. I also included your commits so you'll get credit #4026

mattkanwisher added a commit to mattkanwisher/buzz that referenced this pull request Jul 31, 2026
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 the populated picker, a sticker sent and rendered in the timeline, the
settings catalog with its approval queue, and the not-yet-installed picker.

Note: the bulk of this harness was swept into fda40d6 by an overlapping
`git add -A`; only the spec's final revision is isolated here. The two are
worth splitting before this goes upstream.

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>
mattkanwisher added a commit to mattkanwisher/buzz that referenced this pull request Jul 31, 2026
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>
@vincenzopalazzo

Copy link
Copy Markdown
Author

Continuing this on #4026

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.

3 participants