feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages - #7259
Conversation
Add two agent-facing capabilities to buzz-cli: 1. buzz gifs search / gifs share — lets agents search KLIPY GIFs via the relay's NIP-98-authenticated proxy, gate on the relay's NIP-11 buzz-gif extension descriptor, and report selections. Sending a GIF is a plain message containing the cdn_url; no send-path changes required. Adds post_json_authed helper to BuzzClient for NIP-98-authed JSON POSTs. 2. NIP-30 custom emoji tags on outgoing messages — buzz messages send now scans final content for :shortcode: sequences (hand-rolled scanner, no new dep, mirrors desktop customEmojiTags.ts exactly) and attaches ["emoji", shortcode, url] tags from the workspace palette. The palette fetch is skipped when content contains no colon sequences. Extends build_message with an emoji_tags parameter (additive; all existing call sites pass &[]). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…s arg Add missing `emoji_tags` (&[]) 7th argument to the SDK build_message call in examples/countdown-bot/src/main.rs, fixing E0061 compile error. This was the one remaining call site that wasn't updated when build_message's signature gained the additive emoji_tags parameter. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Four IMPORTANT findings from Thufir's pass-1 review, all in gifs.rs:
1. cdn_url normalization: cmd_search now emits typed [{cdn_url, slug, title,
width, height, preview_url?}] records via normalize_gif_response(), which
ports normalizeKlipyGifs() from desktop/src/features/gifs/api.ts. Asset
fallback order (md>hd>sm>xs for original; sm.webp>sm.gif>xs.webp>xs.gif>
md.webp for preview) mirrors the desktop exactly. Malformed response
envelopes (missing data.data array) now return a clear error instead of
silently emitting [].
2. safe_relay_path validation: resolve_gif_descriptor() now validates both
search and share paths with safe_relay_path() before either reaches
post_json_authed(). Ports the desktop safeRelayPath contract exactly
(api.ts:64-74): leading /, not //, no backslash, percent, query, fragment,
or dot/dot-dot segments. Adversarial corpus from api.test.mjs is bound as
Rust tests against the production validator.
3. customer_id anonymity: replaced SHA-256(pubkey_hex) with a domain-separated
derivation from secret key material: SHA-256(secret_bytes || NUL || relay_url),
truncated to 32 hex chars. Relay-scoped (different relay -> different ID),
not computable from public data, stateless. Tests verify relay-scoping,
key-scoping, and that the result differs from the old pubkey-hash approach.
4. Test teeth: new tests drive resolve_gif_descriptor gate logic through a
pure typed parse_descriptor() extractor; normalize_gif_response() is tested
directly for all normalization semantics (fallback order, type filtering,
missing-slug skip, malformed-envelope error, empty-array ok). Real HTTP
integration tests via axum fake server verify that cmd_search and cmd_share
hit the relay-advertised paths with NIP-98 Authorization headers and the
expected JSON request bodies; a gating test drives cmd_search against a
fake relay missing buzz-gif and confirms a clear error.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
(a) Missing send-path palette seam test: add 7 tokio integration tests to
emoji.rs that drive the production resolve_emoji_tags_for_content()
through a real BuzzClient against an axum fake /query server. Tests
cover: known shortcode -> correct tag, unknown shortcode filtered out,
dedup of repeated shortcode, first-appearance order, case-insensitive
match -> canonical lowercase tag, no-colon content -> zero palette
queries (short-circuit), and unknown-only content -> exactly one palette
query with empty tag result.
(b) Descriptor test helper re-introduced non-falsifiable pattern: extract the
synchronous NIP-11 parsing step from resolve_gif_descriptor() into a new
pub(crate) fn parse_gif_descriptor_info(), which resolve_gif_descriptor()
now calls after fetching the document. Descriptor gate tests are updated
to call the production function directly; the test-local parse_descriptor
helper is removed entirely.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
- gifs.rs: fake NIP-11 now advertises non-default paths /x/search-alt and /x/share-alt, proving production reads relay-advertised paths rather than using hardcoded defaults - gifs.rs: add test_client_with_tag() + search_forwards_x_auth_tag_header asserting the x-auth-tag header equals the exact tag JSON; add search_nip98_token_has_correct_u_method_and_payload_hash decoding the NIP-98 base64 token and asserting u, method, and payload tags - gifs.rs: search_output_contains_cdn_url replaced by search_entries_returns_top_level_cdn_url, which calls the production search_entries() helper and asserts entries[0].cdn_url is set to the expected normalized URL; raw-passthrough regression now fails the test - messages.rs: add cmd_send_message_attaches_emoji_tags_for_known_shortcodes and cmd_send_message_skips_palette_query_when_no_colon_in_content driving cmd_send_message against a fake /query + /events relay, asserting the submitted raw event carries emoji tags for known shortcodes and zero palette queries for no-colon content; removing the resolver call or passing &[] at :718 causes both tests to fail Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Forum sends (kind 45001/45003) previously resolved the workspace palette before kind selection, paying the relay query and then discarding the result because the forum SDK builders do not accept emoji_tags. Move the resolution into the None | Some(9) match arm so it only runs for the kind that actually uses it. The kind-9 happy and short-circuit paths are unchanged; the existing cmd_send_message seam tests cover both. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three needless-borrow / manual_contains / needless_splitn warnings in
gifs.rs that the pre-push hook missed (hook scopes clippy to the
desktop-tauri manifest only):
- splitn(2, '.') → split('.') (only .next() consumed)
- .iter().any(|&e| e == REQUIRED_EXTENSION) → .contains(&REQUIRED_EXTENSION)
- hex::encode(&sk) → hex::encode(sk) in test (needless borrow for generic arg)
Behavior unchanged.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: one P2
Reviewed head ef2adb4dc3e6be0daa07a5d31556ed2da0cf6c33 against base 0dbd036f5bff33e7ade75e7639f3218d424a6e73.
P2: Normalize and deduplicate each palette event before automatic emoji lookup
The new resolver at crates/buzz-cli/src/commands/emoji.rs:341–345 builds a case-sensitive map from the existing raw union, but the scanner lowercases every candidate. A current member kind-30030 set containing ["emoji","WAVE","https://example.com/wave.png"] is accepted by the relay and resolves on Desktop/Mobile. Sending :wave: through this new CLI path instead emits no NIP-30 tag. Recipients relying on the message tags receive literal text rather than the emoji.
The same seam can pin the wrong image: within one event, tags for wave ordered https://example.com/z.png, then https://example.com/a.png resolve to z in Desktop/Mobile (first normalized entry wins), but a in CLI (the cross-event lexical tie-break is also applied within the event).
These are valid stored inputs, not merely malformed fake responses: ingest.rs:145–158 validates the shortcode through the SDK normalizer but discards the normalized result and does not reject duplicates. Ingest passes the original signed event to replacement; storage serializes the original tags. The raw CLI parser predates this PR; its new use to pin outgoing message tags is the introduced failure.
Smallest fix / exit criteria: match customEmojiFromTags before applying the cross-event union: normalize shortcode keys, ignore missing/empty URLs, and keep the first normalized shortcode within each event. Add production-resolver regressions with uppercase palette keys, normalized duplicates and empty URLs. Keep the existing newest-set / equal-timestamp-smallest-URL rule across distinct events.
Scope and validation
Source-only review; no checkout, build, tests, or PR-code execution. The examples above are source-derived reproductions, not runtime results. Independent GIF and emoji lanes returned before this integrated review.
Reviewed CLI GIF capability discovery, authenticated request/response and relay authority boundaries, customer-ID derivation, emoji send/tag integration, and all exact-head Rust build_message references. Existing SDK callers supply the new argument; Desktop uses a separate builder. GIF sharing remains an ordinary CDN-URL message, with provider credentials staying at the relay. No additional blocker established in those paths.
Forum, ACP notices and CLI edits remain unchanged in capability; legacy tag-less edits preserve original emoji tags in the Desktop overlay. Broader pagination and unrelated hardening are not additional exit criteria.
P2: emoji_tags_of now normalizes shortcodes to lowercase (relay stores original case but scan_shortcodes always lowercases, so an uppercase stored key like "WAVE" would never resolve), skips entries with empty or missing URLs, and keeps only the first occurrence of each normalized shortcode within one event. Mirrors desktop customEmojiFromTags. P3: The ? on resolve_emoji_tags_for_content inside cmd_send_message is replaced with a match that logs a stderr warning and falls back to an empty emoji-tag list. Palette enrichment is decorative; a fetch failure must not abort delivery of an otherwise-valid message. Tests added: - emoji_tags_of_normalizes_uppercase_shortcode_to_lowercase - emoji_tags_of_skips_empty_url - emoji_tags_of_first_occurrence_wins_within_event - cmd_send_message_succeeds_when_palette_query_errors Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The previous fix applied only to_lowercase() to palette shortcodes. The relay validates shortcodes via normalize_custom_emoji_shortcode but stores the original signed tag, so a relay-valid key like " :WAVE: " (whitespace + colons + uppercase) would still fail resolution against scan_shortcodes output, which always emits lowercase undecorated strings. Replace the bare to_lowercase() with buzz_sdk::normalize_custom_emoji_shortcode: trim whitespace/colons, validate charset/length, lowercase. Entries that fail normalization (malformed tags not caught at ingest) are skipped. Regression test: resolve_tags_non_canonical_palette_key_resolves drives production resolve_emoji_tags_for_content through a fake palette whose entry carries the relay-valid key " :WAVE: " and asserts that content containing 👋 resolves to the canonical emoji tag. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-enforcement * origin/main: docs(nip-fi): document Git smart-HTTP credential exemption (#7268) feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259) feat(desktop): add persistent Bestie experience (#7223) fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188) Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
…eway-origin * origin/main: fix(desktop): derive agent availability from relay presence (#7127) fix(desktop): preserve spacing after multi-word mentions (#7128) docs(nip-fi): document Git smart-HTTP credential exemption (#7268) feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259) feat(desktop): add persistent Bestie experience (#7223) fix(desktop): harden profile batch and thread-reply fetches against relay slowness (#7188) docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#7254) fix(composer): align wrapped inline chip fragments (#7242) Add operation-aware database pool acquisition metrics (#7195) Signed-off-by: Tom Brow <tomb@block.xyz>
* origin/main: 🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270) Show status and huddle indicators beside names (#7112) Add mobile voice notes (#7121) perf(desktop): publish mention sends before waking agents (#7154) fix(desktop): unify owned-agent cloud provenance markers (#7129) fix(desktop): derive agent availability from relay presence (#7127) fix(desktop): preserve spacing after multi-word mentions (#7128) docs(nip-fi): document Git smart-HTTP credential exemption (#7268) feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
What
Two new agent-facing capabilities in
buzz-cli:1.
buzz gifscommand group (agent KLIPY picker path)Agents can now search and share GIFs via the relay's authenticated KLIPY proxy without holding a provider credential.
Output is a JSON array of GIF objects. Paste the
cdn_urlfield directly intobuzz messages send --content— sending a GIF is a plain message containing the CDN URL, no special send-path handling.Implementation details:
supported_extensionscontainingbuzz-gifandgif.provider == "klipy"gifdescriptor — no hardcoded paths; safe-path validation mirrorsdesktop/src/features/gifs/api.tspost_json_authedhelper inBuzzClienthandles NIP-98-signed JSON POSTs and 204 No Content responsescustomer_idderived asSHA-256(secret_key_bytes || '\0' || relay_url_bytes)[..16]→ 32 hex chars: stable, relay-scoped, not computable from public data, no storage neededlocaledefaults to$LANG(stripped of encoding suffix) oren_US2. NIP-30 custom emoji tags on outgoing messages
buzz messages sendnow automatically attaches["emoji", shortcode, url]tags for any:shortcode:patterns in the content that resolve in the workspace palette — identical to the desktop composer behavior.Implementation details:
:([a-z0-9_-]+):case-insensitively with canonical lowercase output — mirrorsdesktop/src/shared/lib/customEmojiTags.tsexactly:character; onequerywhen candidates exist but none matchunion_custom_emojilogic fromcommands/emoji.rsbuild_messageinbuzz-sdkgains a newemoji_tags: &[Vec<String>]parameter (additive — all existing callers pass&[]); NIP-30 tag attachment lives in the SDK alongsideimetatagsbuzz-acp) continues to pass&[]and is not affected; the MCP gap is noted in a commentFiles changed
buzz-clisrc/commands/gifs.rsbuzz-clisrc/commands/mod.rspub mod gifsbuzz-clisrc/lib.rsGifs(GifsCmd)variant, dispatch arm, inventory test updatebuzz-clisrc/client.rspost_json_authedhelperbuzz-clisrc/commands/emoji.rsscan_shortcodes+resolve_emoji_tags_for_content+ testsbuzz-clisrc/commands/messages.rscmd_send_message+ seam testsbuzz-cliREADME.mdbuzz gifssection + emoji-in-messages notebuzz-sdksrc/builders.rsbuild_messagegainsemoji_tagsparam + testsbuzz-acpsrc/pool.rsbuild_messagecall site (&[])buzz-acpsrc/setup_mode.rsbuild_messagecall site (&[])countdown-botsrc/main.rsbuild_messagecall site (&[])Relates to: https://buzz.block.builderlab.xyz — buzz-team channel thread on agent GIF/emoji support