feat(stickers): add Sonar sticker packs (resurrects #1920) - #2968
feat(stickers): add Sonar sticker packs (resurrects #1920)#2968vincenzopalazzo wants to merge 9 commits into
Conversation
ScreenshotsSticker picker — populated (from the original #1920 build, same UI)The composer sticker button opens the pack picker with the installed "Herecomesbitcoin.org" pack. Sticker rendered in the message timelineSettings → Stickers (current main rebase)Install curated Sonar packs, author your own pack with WebP cover + images, or import an official Signal pack. Picker empty state (current main rebase)Honest empty state when no packs are installed yet — points the user to Settings. |
There was a problem hiding this comment.
💡 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".
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
aec7cea to
dc76471
Compare
There was a problem hiding this comment.
💡 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".
| if !ALLOWED_MIMES.contains(&mime) { | ||
| return Err(CliError::Usage(format!("unsupported file type: {mime}"))); | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
| if (!sticker?.[1] && cachedRelayOrigin) { | ||
| const urlOrigin = canonicalOrigin(url); | ||
| if (urlOrigin !== cachedRelayOrigin) { | ||
| return url; |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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]]}'" |
There was a problem hiding this comment.
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 👍 / 👎.
| class StickerUnavailablePlaceholder extends StatelessWidget { | ||
| const StickerUnavailablePlaceholder({super.key}); |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| bytes.extend_from_slice(&[0; 4]); | ||
| } | ||
|
|
||
| let mut apng = TINY_PNG.to_vec(); |
There was a problem hiding this comment.
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 👍 / 👎.
| child: Image.network( | ||
| url, |
There was a problem hiding this comment.
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 👍 / 👎.
| trimmed: trimmed || sticker.fallback, | ||
| audienceGeneration: persistentAudience.generation, | ||
| audienceRevision: audienceScope ? persistentAudience.revision : null, | ||
| stickerTags: sticker.tags, |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| if kind.mime_type() == "image/png" | ||
| && buzz_core::stickers::apng_frame_count(bytes).is_some() | ||
| { | ||
| "image/apng".to_owned() |
There was a problem hiding this comment.
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 👍 / 👎.
| 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; |
There was a problem hiding this comment.
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 👍 / 👎.
| static STICKER_FETCH_LOCKS: LazyLock<DashMap<String, Arc<tokio::sync::Mutex<()>>>> = | ||
| LazyLock::new(DashMap::new); |
There was a problem hiding this comment.
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>
1e4a543 to
1e58ee2
Compare
There was a problem hiding this comment.
💡 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".
| return [ | ||
| { | ||
| coordinate: entry.coordinate, | ||
| author: address.author, | ||
| identifier: address.identifier, | ||
| title: `${address.identifier} (superseded)`, | ||
| stickers: [], |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| && newest | ||
| .as_ref() | ||
| .is_none_or(|current| event.created_at > current.created_at) => |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| parsed.username === "" && | ||
| parsed.password === "" && | ||
| parsed.port === "" && | ||
| parsed.pathname.includes(sha256) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| if !matches!(parsed.scheme(), "wss" | "ws") { | ||
| return Err(format!("Relay hints must be ws(s) URLs: {relay}")); | ||
| } | ||
| valid_relays.push(relay.clone()); |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| 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)?; |
There was a problem hiding this comment.
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 👍 / 👎.
| setComposerContent(savedDraftContent); | ||
| richText.setContent(savedDraftContent); | ||
| media.setPendingImeta(savedDraftImeta); | ||
| setSpoileredAttachmentUrls(savedDraftSpoilered); |
There was a problem hiding this comment.
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 👍 / 👎.
| const events = await relayClient.fetchEvents({ | ||
| kinds: [KIND_STICKER_PACK], | ||
| limit: MAX_STICKER_CATALOG_PACKS, | ||
| }); |
There was a problem hiding this comment.
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 👍 / 👎.
Review —
|
|
@vincenzopalazzo you still working on this? this is a great feature, that blocks from moving over from telegram to buzz. you need help |
|
@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 |
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>
|
@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 |
|
@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 |
|
@vincenzopalazzo ok feel free to close this. I also included your commits so you'll get credit #4026 |
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>
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>
|
Continuing this on #4026 |




Resurrects #1920 — Sonar sticker packs, rebased onto current
mainThis 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:
/media/sticker/{author}/{identifier}/{shortcode}/{sha256}). It never fetches unapproved assets, caps dimensions/frame counts, and pins DNS resolution.signal.artpack link and republish it as a Sonar pack, so existing sticker culture ports over instead of starting from zero.buzz stickersCLI commands (agent-first, like everything else in Buzz).stickertags, 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.)What changed since #1920 (rebase notes)
0020_sticker_catalog.sql→0021_sticker_catalog.sql(0020is nowjoin_policy_acceptanceson main); migration tests updated for 21 migrations.authenticate_media_read/blob_cache_controlalongside the new verified-sticker route; keptshould_retry_legacy_uploadupload fallback in bothbuzz-cliand the desktop app while exposingdo_uploadto the sticker commands.audienceGeneration/audienceRevision,postSendContent, and the persistent-agent-audience send path;MessageComposerPropsextraction kept and extended with main's newer props.commands/stickers.rsand the tag validator intosticker_events.rs(keepsmedia.rs,events.rs,lib.rs,AppShell.tsxunder the 1000-line ceiling without overrides); added a smalluseLiveUpdatesaggregator hook in the app shell.Security properties (unchanged from #1920)
bitchat-to-sonardependency (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) ✅pnpm check(biome + file-size + px-text guards) ✅,pnpm test(3129 passed) ✅,tsc --noEmit✅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.