Files
buzz/examples/countdown-bot
Will Pfleger 47d068e210 feat(cli): add buzz gifs command group and NIP-30 emoji tags on messages (#7259)
## What

Two new agent-facing capabilities in `buzz-cli`:

### 1. `buzz gifs` command group (agent KLIPY picker path)

Agents can now search and share GIFs via the relay's authenticated KLIPY
proxy without holding a provider credential.

```bash
buzz gifs search                         # trending GIFs
buzz gifs search --query "celebration"   # search GIFs
buzz gifs share --slug <slug>            # report selection to provider Recents
```

Output is a JSON array of GIF objects. Paste the `cdn_url` field
directly into `buzz messages send --content` — sending a GIF is a plain
message containing the CDN URL, no special send-path handling.

**Implementation details:**
- Gates on NIP-11 `supported_extensions` containing `buzz-gif` and
`gif.provider == "klipy"`
- Uses relay-relative paths from the NIP-11 `gif` descriptor — no
hardcoded paths; safe-path validation mirrors
`desktop/src/features/gifs/api.ts`
- New `post_json_authed` helper in `BuzzClient` handles NIP-98-signed
JSON POSTs and 204 No Content responses
- `customer_id` derived as `SHA-256(secret_key_bytes || '\0' ||
relay_url_bytes)[..16]` → 32 hex chars: stable, relay-scoped, not
computable from public data, no storage needed
- `locale` defaults to `$LANG` (stripped of encoding suffix) or `en_US`

### 2. NIP-30 custom emoji tags on outgoing messages

`buzz messages send` now 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.

```bash
buzz messages send --channel <uuid> --content "hello 👋 everyone 🎉"
# → event carries ["emoji", "wave", "..."] and ["emoji", "tada", "..."] tags
```

**Implementation details:**
- Hand-rolled single-pass scanner (no new dependency) implementing
`:([a-z0-9_-]+):` case-insensitively with canonical lowercase output —
mirrors `desktop/src/shared/lib/customEmojiTags.ts` exactly
- Zero extra relay round-trips when content contains no `:` character;
one `query` when candidates exist but none match
- Palette fetch reuses the existing `union_custom_emoji` logic from
`commands/emoji.rs`
- `build_message` in `buzz-sdk` gains a new `emoji_tags: &[Vec<String>]`
parameter (additive — all existing callers pass `&[]`); NIP-30 tag
attachment lives in the SDK alongside `imeta` tags
- MCP send path (`buzz-acp`) continues to pass `&[]` and is not
affected; the MCP gap is noted in a comment

## Files changed

| Crate | File | Change |
|-------|------|--------|
| `buzz-cli` | `src/commands/gifs.rs` | New — search + share handlers,
NIP-11 gating, tests |
| `buzz-cli` | `src/commands/mod.rs` | `pub mod gifs` |
| `buzz-cli` | `src/lib.rs` | `Gifs(GifsCmd)` variant, dispatch arm,
inventory test update |
| `buzz-cli` | `src/client.rs` | `post_json_authed` helper |
| `buzz-cli` | `src/commands/emoji.rs` | `scan_shortcodes` +
`resolve_emoji_tags_for_content` + tests |
| `buzz-cli` | `src/commands/messages.rs` | Emoji scan + tag injection
in `cmd_send_message` + seam tests |
| `buzz-cli` | `README.md` | `buzz gifs` section + emoji-in-messages
note |
| `buzz-sdk` | `src/builders.rs` | `build_message` gains `emoji_tags`
param + tests |
| `buzz-acp` | `src/pool.rs` | Update `build_message` call site (`&[]`)
|
| `buzz-acp` | `src/setup_mode.rs` | Update `build_message` call site
(`&[]`) |
| `countdown-bot` | `src/main.rs` | Update `build_message` call site
(`&[]`) |

Relates to: https://buzz.block.builderlab.xyz — buzz-team channel thread
on agent GIF/emoji support

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-02 20:03:38 -04:00
..
…

Countdown Bot

A tiny non-AI Buzz bot example.

The bot is deliberately boring and algorithmic: it listens to one Buzz channel and replies to simple commands:

  • !countdown 5 → 5 4 3 2 1 🚀
  • !fib 8 → 13 8 5 3 2 1 1 0
  • @Countdown Bot fib 8 → 13 8 5 3 2 1 1 0

It demonstrates that Buzz participants do not have to be LLM agents. Any process that can hold a Nostr key, answer NIP-42 auth, publish a kind 0 profile, subscribe to events, and publish kind 9 channel messages can be a bot.

On startup it publishes a profile named Countdown Bot with a small embedded SVG clock icon, then best-effort publishes a NIP-29 kind:9000 self-add with role=bot. That channel membership is what makes the bot show up in the members list and in Buzz's mention autocomplete.

Auth paths

1. Standalone bot identity

The bot authenticates with its own key only.

Use this when the bot should be admitted as its own independent relay identity.

BUZZ_RELAY_URL=ws://localhost:3000 \
BUZZ_CHANNEL_ID=<channel-uuid> \
BUZZ_BOT_PRIVATE_KEY=<bot-nsec-or-hex-secret> \
BUZZ_BOT_AUTH_MODE=standalone \
cargo run --manifest-path examples/countdown-bot/Cargo.toml

On a closed or allowlisted relay, add the bot pubkey as a relay member or to the configured pubkey allowlist before starting it. This path does not reuse an owner's access; revoking the bot requires removing this bot pubkey.

2. Owner-attested bot identity

The bot still signs messages with its own key, but its NIP-42 AUTH event also carries a NIP-OA auth tag signed by an owner key that is already allowed on the relay. This reuses the same owner-attestation credential path that Buzz agents receive after the owner/agent OAuth flow: the relay can let the bot connect because the owner is a relay member, without making the bot key a persistent relay member.

Generate the auth tag on the fly:

BUZZ_RELAY_URL=ws://localhost:3000 \
BUZZ_CHANNEL_ID=<channel-uuid> \
BUZZ_BOT_PRIVATE_KEY=<bot-nsec-or-hex-secret> \
BUZZ_OWNER_PRIVATE_KEY=<owner-or-agent-nsec-or-hex-secret> \
BUZZ_BOT_AUTH_MODE=owner-attested \
cargo run --manifest-path examples/countdown-bot/Cargo.toml

Or precompute and pass the tag explicitly:

BUZZ_AUTH_TAG='["auth","<owner-pubkey>","","<sig>"]' \
BUZZ_BOT_AUTH_MODE=owner-attested \
# plus BUZZ_RELAY_URL, BUZZ_CHANNEL_ID, BUZZ_BOT_PRIVATE_KEY
cargo run --manifest-path examples/countdown-bot/Cargo.toml

Relay requirements for this path:

  • BUZZ_REQUIRE_RELAY_MEMBERSHIP=true on closed relays.
  • BUZZ_ALLOW_NIP_OA_AUTH=true so owner-attested non-member bot keys can be admitted.
  • The owner pubkey must be an active relay member.

Relay access and channel access are separate. Owner-attested auth can admit the bot to the relay, but the bot still publishes as its own pubkey. The bot tries to self-add to open channels as a bot member on startup. For private channels, an owner/admin must add the bot pubkey to the channel membership before expecting it to appear in members, resolve in mention autocomplete, or read/write messages.

Try it locally

  1. Start Buzz:

    . ./bin/activate-hermit
    just setup
    just relay
    
  2. Create or choose a channel in the desktop app and copy its UUID.

  3. Run the bot with one of the auth paths above.

  4. In the channel, send:

    !countdown 5
    !fib 8
    @Countdown Bot fib 8
    

Notes

  • Commands are bounded (!countdown and !fib max 100) so one message cannot make the bot spam the relay. Out-of-range commands get an explicit help reply.
  • !fib replies in descending order because this example is a countdown bot.
  • Mention commands require both text like @Countdown Bot fib 8 and a p tag for the bot pubkey. The Buzz UI adds that tag when the bot is selected from mention autocomplete.
  • The bot ignores its own messages to avoid feedback loops.
  • The example uses direct WebSocket + NIP-42 instead of MCP so the protocol path is easy to inspect in one small file.