Skip to content

feat(cli): user status expiry and status read - #8768

Closed
blakemcanally wants to merge 1 commit into
block:mainfrom
blakemcanally:bmcanally/cli-status-expiry
Closed

blakemcanally wants to merge 1 commit into
block:mainfrom
blakemcanally:bmcanally/cli-status-expiry

Conversation

@blakemcanally

Copy link
Copy Markdown
Contributor

Summary

This change adds two things to buzz-cli.

buzz users set-status now accepts --expires. The status then clears itself at that time.

buzz users status is a new command. It reads the visible status of one or more users.

Design

--expires takes a unix timestamp or a duration. A duration is a number with a unit: s, m, h, d, or w. A bare number is a unix timestamp. The CLI had no duration parser to reuse. The only one is private to buzz-workflow. A small parser now lives in users.rs.

The CLI rejects an expiry that is not in the future. It does this before it signs anything. The error text says that a bare number is a timestamp.

--expires cannot combine with --clear. A status with no text and no emoji cannot have an expiry. The SDK rejects that shape.

buzz-sdk has a new builder, build_user_status_expiring. It adds ["expiration", ts] after the emoji tag. This is the tag order desktop uses. build_user_status now calls the new builder with no expiry. Its output is unchanged. Existing callers need no change.

users status queries kind 30315 with #d: general. It picks the newest event for each author. A higher created_at wins. On a tie, the lower event id wins. Desktop uses the same rule.

The CLI then drops a user when the newest event has no text and no emoji. It also drops a user when the status has expired. An expired status means expiration <= now. An older status never shows behind a newer cleared or expired one.

The expiration tag is read like desktop's parseInt. A value such as 90abc reads as 90. A value such as soon reads as no expiry.

The output is an array: [{pubkey, text, emoji, expires_at, updated_at}]. emoji is an empty string when there is none. expires_at is null when the status does not expire. A user with no visible status has no row.

--pubkey takes hex, npub, or an exact display name. It reuses the name resolver of messages. It is repeatable. The limit is 100, because each name costs one profile search. With no --pubkey, the command reads your own status.

A relay response that is not valid JSON is an error. It is never an empty result.

Testing

What ran:

  • cargo test -p buzz-cli -p buzz-sdk passes.
  • cargo clippy -p buzz-cli -p buzz-sdk --all-targets -- -D warnings passes.
  • cargo fmt --all --check passes.
  • just file-size-check passes.

The CLI tests use an axum fake relay. They call the production functions publish_status and read_statuses. These are the same functions that the commands call. The tests cover these cases:

  • One publish carries the right tags.
  • Bad expiries cause no publish.
  • Expired, cleared, and malformed statuses.
  • Newest-event choice and tie-break.
  • Hex, npub, and name input.
  • The 100-pubkey limit.
  • An invalid relay response.
  • A publish followed by a read.

Interop evidence: I ran the real buzz binary against a local fake relay. No live relay was used. I fed the published event to desktop's own parseUserStatusEvent and fetchUserStatusLookup with node. Desktop showed the same text, emoji, and expiresAt that buzz users status printed. The check printed INTEROP OK.

Mutation checks: I broke each guard once. Each break made a test fail. The guards were:

  • the expiry comparison, <= changed to <
  • the future-expiry check
  • the d=general filter
  • the newest-first order
  • the tie-break
  • the 100-pubkey limit
  • the author de-duplication
  • the invalid-response error
  • the hide-cleared-status check
  • the duration overflow check
  • the SDK expiration tag
  • the SDK empty-status check

I restored each guard after the check.

One note on process: I wrote the code and the tests in the same pass. I did not write the tests first. The mutation checks cover the gap.

Manual test steps

Use a test identity on a relay that you may write to. Set BUZZ_RELAY_URL and BUZZ_PRIVATE_KEY first.

  1. Set a status that expires in two minutes.

    buzz users set-status --text "testing expiry" --emoji "🧪" --expires 2m

    Success: the output has "accepted":true.

  2. Read the status.

    buzz users status

    Success: the output is one row. text is testing expiry. emoji is 🧪. expires_at is about two minutes from now.

  3. Open the desktop app as the same user. Success: your status shows on your profile.

  4. Wait for the expiry. Read again.

    buzz users status

    Success: the output is []. Desktop also hides the status.

  5. Try a bad expiry.

    buzz users set-status --text x --expires 3600

    Success: exit code 1. The error says that the value must be in the future. Nothing is published.

  6. Try the clear conflict.

    buzz users set-status --clear --expires 1h

    Success: exit code 1. The error says --clear cannot be used with --expires.

  7. Read another user.

    buzz users status --pubkey <hex|npub|name>

    Success: you get that user's visible status. You get [] if they have none.

🤖 Generated with Claude Code

`users set-status --expires <unix|30m|2h|1d|1w>` adds a NIP-40 expiration
tag to the kind:30315 d=general event, the same shape desktop publishes.
An expiry that is not in the future is rejected before anything is signed.
buzz-sdk gains build_user_status_expiring; build_user_status is unchanged.

`users status [--pubkey <hex|npub|name>...]` reads the newest d=general
status per author (max 100) and omits expired or cleared ones, matching
desktop's parser.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: bmcanally <[email protected]>
@blakemcanally

Copy link
Copy Markdown
Contributor Author

🤖 Moved to #8777 so the branch lives in block/buzz.

blakemcanally added a commit that referenced this pull request Nov 5, 2026
> Replaces #8768, which was opened from a fork.

## Summary

This change adds two things to `buzz-cli`.

`buzz users set-status` now accepts `--expires`. The status then clears
itself at that time.

`buzz users status` is a new command. It reads the visible status of one
or more users.

## Design

`--expires` takes a unix timestamp or a duration. A duration is a number
with a unit: `s`, `m`, `h`, `d`, or `w`. A bare number is a unix
timestamp. The CLI had no duration parser to reuse. The only one is
private to `buzz-workflow`. A small parser now lives in `users.rs`.

The CLI rejects an expiry that is not in the future. It does this before
it signs anything. The error text says that a bare number is a
timestamp.

`--expires` cannot combine with `--clear`. A status with no text and no
emoji cannot have an expiry. The SDK rejects that shape.

`buzz-sdk` has a new builder, `build_user_status_expiring`. It adds
`["expiration", ts]` after the emoji tag. This is the tag order desktop
uses. `build_user_status` now calls the new builder with no expiry. Its
output is unchanged. Existing callers need no change.

`users status` queries kind 30315 with `#d: general`. It picks the
newest event for each author. A higher `created_at` wins. On a tie, the
lower event id wins. Desktop uses the same rule.

The CLI then drops a user when the newest event has no text and no
emoji. It also drops a user when the status has expired. An expired
status means `expiration <= now`. An older status never shows behind a
newer cleared or expired one.

The expiration tag is read like desktop's `parseInt`. A value such as
`90abc` reads as 90. A value such as `soon` reads as no expiry.

The output is an array: `[{pubkey, text, emoji, expires_at,
updated_at}]`. `emoji` is an empty string when there is none.
`expires_at` is `null` when the status does not expire. A user with no
visible status has no row.

`--pubkey` takes hex, npub, or an exact display name. It reuses the name
resolver of `messages`. It is repeatable. The limit is 100, because each
name costs one profile search. With no `--pubkey`, the command reads
your own status.

A relay response that is not valid JSON is an error. It is never an
empty result.

## Testing

What ran:

- `cargo test -p buzz-cli -p buzz-sdk` passes.
- `cargo clippy -p buzz-cli -p buzz-sdk --all-targets -- -D warnings`
passes.
- `cargo fmt --all --check` passes.
- `just file-size-check` passes.

The CLI tests use an axum fake relay. They call the production functions
`publish_status` and `read_statuses`. These are the same functions that
the commands call. The tests cover these cases:

- One publish carries the right tags.
- Bad expiries cause no publish.
- Expired, cleared, and malformed statuses.
- Newest-event choice and tie-break.
- Hex, npub, and name input.
- The 100-pubkey limit.
- An invalid relay response.
- A publish followed by a read.

Interop evidence: I ran the real `buzz` binary against a local fake
relay. No live relay was used. I fed the published event to desktop's
own `parseUserStatusEvent` and `fetchUserStatusLookup` with node.
Desktop showed the same text, emoji, and `expiresAt` that `buzz users
status` printed. The check printed `INTEROP OK`.

Mutation checks: I broke each guard once. Each break made a test fail.
The guards were:

- the expiry comparison, `<=` changed to `<`
- the future-expiry check
- the `d=general` filter
- the newest-first order
- the tie-break
- the 100-pubkey limit
- the author de-duplication
- the invalid-response error
- the hide-cleared-status check
- the duration overflow check
- the SDK expiration tag
- the SDK empty-status check

I restored each guard after the check.

One note on process: I wrote the code and the tests in the same pass. I
did not write the tests first. The mutation checks cover the gap.

## Manual test steps

Use a test identity on a relay that you may write to. Set
`BUZZ_RELAY_URL` and `BUZZ_PRIVATE_KEY` first.

1. Set a status that expires in two minutes.

   ```bash
buzz users set-status --text "testing expiry" --emoji "🧪" --expires 2m
   ```

   Success: the output has `"accepted":true`.

2. Read the status.

   ```bash
   buzz users status
   ```

Success: the output is one row. `text` is `testing expiry`. `emoji` is
`🧪`. `expires_at` is about two minutes from now.

3. Open the desktop app as the same user. Success: your status shows on
your profile.

4. Wait for the expiry. Read again.

   ```bash
   buzz users status
   ```

   Success: the output is `[]`. Desktop also hides the status.

5. Try a bad expiry.

   ```bash
   buzz users set-status --text x --expires 3600
   ```

Success: exit code 1. The error says that the value must be in the
future. Nothing is published.

6. Try the clear conflict.

   ```bash
   buzz users set-status --clear --expires 1h
   ```

Success: exit code 1. The error says `--clear` cannot be used with
`--expires`.

7. Read another user.

   ```bash
   buzz users status --pubkey <hex|npub|name>
   ```

Success: you get that user's visible status. You get `[]` if they have
none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: bmcanally <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>

This branch was previously deployed

1 inactive deployment
codex-review — 0232faf1 Deployed Oct 30, 2026 by blakemcanally via Run Codex Security Review #11050
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant