Repository navigation
feat(cli): user status expiry and status read - #8768
Closed
blakemcanally wants to merge 1 commit into
Closed
blakemcanally wants to merge 1 commit into
blakemcanally wants to merge 1 commit into
Conversation
`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]>
Contributor
Author
|
🤖 Moved to #8777 so the branch lives in block/buzz. |
blakemcanally
temporarily deployed
to
codex-review
October 30, 2026 19:45 — with
GitHub Actions
Inactive
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This change adds two things to
buzz-cli.buzz users set-statusnow accepts--expires. The status then clears itself at that time.buzz users statusis a new command. It reads the visible status of one or more users.Design
--expirestakes a unix timestamp or a duration. A duration is a number with a unit:s,m,h,d, orw. A bare number is a unix timestamp. The CLI had no duration parser to reuse. The only one is private tobuzz-workflow. A small parser now lives inusers.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.
--expirescannot combine with--clear. A status with no text and no emoji cannot have an expiry. The SDK rejects that shape.buzz-sdkhas a new builder,build_user_status_expiring. It adds["expiration", ts]after the emoji tag. This is the tag order desktop uses.build_user_statusnow calls the new builder with no expiry. Its output is unchanged. Existing callers need no change.users statusqueries kind 30315 with#d: general. It picks the newest event for each author. A highercreated_atwins. 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 as90abcreads as 90. A value such assoonreads as no expiry.The output is an array:
[{pubkey, text, emoji, expires_at, updated_at}].emojiis an empty string when there is none.expires_atisnullwhen the status does not expire. A user with no visible status has no row.--pubkeytakes hex, npub, or an exact display name. It reuses the name resolver ofmessages. 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-sdkpasses.cargo clippy -p buzz-cli -p buzz-sdk --all-targets -- -D warningspasses.cargo fmt --all --checkpasses.just file-size-checkpasses.The CLI tests use an axum fake relay. They call the production functions
publish_statusandread_statuses. These are the same functions that the commands call. The tests cover these cases:Interop evidence: I ran the real
buzzbinary against a local fake relay. No live relay was used. I fed the published event to desktop's ownparseUserStatusEventandfetchUserStatusLookupwith node. Desktop showed the same text, emoji, andexpiresAtthatbuzz users statusprinted. The check printedINTEROP OK.Mutation checks: I broke each guard once. Each break made a test fail. The guards were:
<=changed to<d=generalfilterI 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_URLandBUZZ_PRIVATE_KEYfirst.Set a status that expires in two minutes.
Success: the output has
"accepted":true.Read the status.
Success: the output is one row.
textistesting expiry.emojiis🧪.expires_atis about two minutes from now.Open the desktop app as the same user. Success: your status shows on your profile.
Wait for the expiry. Read again.
Success: the output is
[]. Desktop also hides the status.Try a bad expiry.
Success: exit code 1. The error says that the value must be in the future. Nothing is published.
Try the clear conflict.
Success: exit code 1. The error says
--clearcannot be used with--expires.Read another user.
Success: you get that user's visible status. You get
[]if they have none.🤖 Generated with Claude Code