Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/buzz-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ stored rules in `validation_error` so an owner can remove and repair them.
| | `open` | Open a DM (1–8 pubkeys) |
| | `add-member` | Add member to DM group |
| `users` | `get` | Get user profile(s) |
| | `set-profile` | Update your profile |
| | `set-profile` | Update your profile (merges — unset fields are preserved) |
| | `presence` | Get presence status |
| | `set-presence` | Set presence status |
| `workflows` | `list` | List workflows |
Expand Down
11 changes: 11 additions & 0 deletions crates/buzz-cli/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,13 @@ buzz users get --pubkey "$MY_PUBKEY" --pubkey "$MY_PUBKEY" | jq .
# users set-profile
buzz users set-profile --name "CLI Test Agent" --about "Testing buzz-cli" | jq .

# users set-profile — fields not passed are preserved, including ones the CLI
# does not model. Set the bot flag, then confirm --about did not clear it.
buzz users set-profile --bot true --website "https://example.com" | jq .
buzz users set-profile --about "still a bot" | jq .
buzz users get --pubkey "$MY_PUBKEY" | jq '.[0].content | fromjson | {bot, website, about}'
# expect: {"bot": true, "website": "https://example.com", "about": "still a bot"}

# users presence
buzz users presence --pubkeys "$MY_PUBKEY" | jq .

Expand Down Expand Up @@ -499,6 +506,10 @@ buzz messages vote --event "$(printf '0%.0s' {1..64})" \
buzz users set-profile 2>&1; echo "exit: $?"
# exit: 1 (at least one field required)

# Exit 1: Non-boolean --bot
buzz users set-profile --bot maybe 2>&1; echo "exit: $?"
# exit: 1

# Exit 3: No auth configured
env -u BUZZ_PRIVATE_KEY \
cargo run -p buzz-cli -- channels list 2>&1; echo "exit: $?"
Expand Down
74 changes: 29 additions & 45 deletions crates/buzz-cli/src/commands/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,64 +147,42 @@ async fn search_by_name(
Ok(())
}

#[allow(clippy::too_many_arguments)]
pub async fn cmd_set_profile(
client: &BuzzClient,
display_name: Option<&str>,
username: Option<&str>,
avatar_url: Option<&str>,
about: Option<&str>,
nip05_handle: Option<&str>,
website: Option<&str>,
bot: Option<bool>,
) -> Result<(), CliError> {
if display_name.is_none() && avatar_url.is_none() && about.is_none() && nip05_handle.is_none() {
let fields = buzz_sdk::ProfileFields {
display_name,
name: username,
picture: avatar_url,
about,
nip05: nip05_handle,
website,
bot,
..Default::default()
};
if fields == buzz_sdk::ProfileFields::default() {
return Err(CliError::Usage(
"at least one field required (--name, --avatar, --about, --nip05)".into(),
"at least one field required (--name, --username, --avatar, --about, --nip05, \
--website, --bot)"
.into(),
));
}

// Read-merge-write: fetch current profile, merge in the new fields, then sign.
// Read-merge-write. kind:0 is replaceable, so anything not carried forward is
// deleted — merge into the full current content, not a rebuilt subset, or
// fields Buzz does not model (bot, lud16, …) are silently destroyed.
let current = fetch_current_profile(client).await?;

// Merge: caller-supplied fields win; fall back to current profile values.
let merged_name = display_name
.map(|s| s.to_string())
.or_else(|| {
current
.get("display_name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.or_else(|| {
current
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});
let merged_picture = avatar_url.map(|s| s.to_string()).or_else(|| {
current
.get("picture")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});
let merged_about = about.map(|s| s.to_string()).or_else(|| {
current
.get("about")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});
let merged_nip05 = nip05_handle.map(|s| s.to_string()).or_else(|| {
current
.get("nip05")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});

let builder = buzz_sdk::build_profile(
merged_name.as_deref(),
None, // `name` field (username) — not exposed by CLI
merged_picture.as_deref(),
merged_about.as_deref(),
merged_nip05.as_deref(),
)
.map_err(|e| CliError::Other(format!("build_profile failed: {e}")))?;
let builder = buzz_sdk::build_profile_merged(&current, &fields)
.map_err(|e| CliError::Other(format!("build_profile failed: {e}")))?;

let event = client.sign_event(builder)?;

Expand Down Expand Up @@ -316,16 +294,22 @@ pub async fn dispatch(
}
UsersCmd::SetProfile {
name,
username,
avatar,
about,
nip05,
website,
bot,
} => {
cmd_set_profile(
client,
name.as_deref(),
username.as_deref(),
avatar.as_deref(),
about.as_deref(),
nip05.as_deref(),
website.as_deref(),
bot,
)
.await
}
Expand Down
14 changes: 13 additions & 1 deletion crates/buzz-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,11 +810,17 @@ pub enum UsersCmd {
name: Option<String>,
},
/// Update the current identity's profile
///
/// Fields you do not pass are left untouched, including profile keys Buzz
/// does not model.
#[command(name = "set-profile")]
SetProfile {
/// Display name
/// Display name (the `display_name` profile field)
#[arg(long)]
name: Option<String>,
/// Short username handle (the `name` profile field)
#[arg(long)]
username: Option<String>,
/// Avatar URL
#[arg(long)]
avatar: Option<String>,
Expand All @@ -824,6 +830,12 @@ pub enum UsersCmd {
/// NIP-05 identifier (e.g. user@example.com)
#[arg(long)]
nip05: Option<String>,
/// Homepage URL
#[arg(long)]
website: Option<String>,
/// NIP-24 automated-account flag
#[arg(long)]
bot: Option<bool>,
},
/// Get presence status for users
Presence {
Expand Down
Loading