cli: add display name lookup to get-users (#543)

This commit is contained in:
tlongwell-block
2026-05-11 20:41:02 -04:00
committed by GitHub
parent 11778f4250
commit 6b4f9f05d4
2 changed files with 74 additions and 6 deletions

View File

@@ -5,10 +5,24 @@ use crate::error::CliError;
use crate::validate::validate_hex64;
/// Get user profiles (kind:0 metadata events).
/// 0 pubkeys → query our own profile
/// 1 pubkey → query that user's profile
/// 2+ pubkeys → query batch
pub async fn cmd_get_users(client: &SproutClient, pubkeys: &[String]) -> Result<(), CliError> {
///
/// - 0 pubkeys, no name → query our own profile
/// - 1+ pubkeys → query those users' profiles
/// - --name "foo" → NIP-50 search on kind:0, then client-side filter
pub async fn cmd_get_users(
client: &SproutClient,
pubkeys: &[String],
name: Option<&str>,
) -> Result<(), CliError> {
if let Some(query) = name {
if !pubkeys.is_empty() {
return Err(CliError::Usage(
"--name and --pubkey are mutually exclusive".into(),
));
}
return search_by_name(client, query).await;
}
for pk in pubkeys {
validate_hex64(pk)?;
}
@@ -33,6 +47,55 @@ pub async fn cmd_get_users(client: &SproutClient, pubkeys: &[String]) -> Result<
Ok(())
}
/// Search for users by display name via NIP-50 full-text search on kind:0 profiles.
/// Returns [] if the relay does not implement NIP-50 search.
async fn search_by_name(client: &SproutClient, query: &str) -> Result<(), CliError> {
if query.trim().is_empty() {
return Err(CliError::Usage("--name cannot be empty".into()));
}
let filter = serde_json::json!({
"kinds": [0],
"search": query,
"limit": 100
});
let raw = client.query(&filter).await?;
// Parse and filter client-side for case-insensitive substring match
// on display_name or name fields (NIP-50 may return broader matches).
let events: serde_json::Value = serde_json::from_str(&raw)
.map_err(|e| CliError::Other(format!("failed to parse response: {e}")))?;
let Some(arr) = events.as_array() else {
println!("[]");
return Ok(());
};
let lower_query = query.to_ascii_lowercase();
let matches: Vec<&serde_json::Value> = arr
.iter()
.filter(|event| {
let Some(content_str) = event.get("content").and_then(|v| v.as_str()) else {
return false;
};
let Ok(content) = serde_json::from_str::<serde_json::Value>(content_str) else {
return false;
};
let display_name = content
.get("display_name")
.and_then(|v| v.as_str())
.unwrap_or("");
let name = content.get("name").and_then(|v| v.as_str()).unwrap_or("");
display_name.to_ascii_lowercase().contains(&lower_query)
|| name.to_ascii_lowercase().contains(&lower_query)
})
.collect();
let output = serde_json::to_string(&matches).expect("serializing parsed JSON values");
println!("{output}");
Ok(())
}
pub async fn cmd_set_profile(
client: &SproutClient,
display_name: Option<&str>,

View File

@@ -332,10 +332,13 @@ enum Cmd {
},
// ---- Users -------------------------------------------------------------
/// Get user profiles (0 = self, 1 = single, 2+ = batch)
/// Get user profiles by pubkey or display name
GetUsers {
#[arg(long = "pubkey")]
pubkeys: Vec<String>,
/// Search by display name (case-insensitive substring match)
#[arg(long = "name")]
name: Option<String>,
},
/// Update your profile
SetProfile {
@@ -788,7 +791,9 @@ async fn run(cli: Cli) -> Result<(), CliError> {
}
// ---- Users ---------------------------------------------------------
Cmd::GetUsers { pubkeys } => commands::users::cmd_get_users(&client, &pubkeys).await,
Cmd::GetUsers { pubkeys, name } => {
commands::users::cmd_get_users(&client, &pubkeys, name.as_deref()).await
}
Cmd::SetProfile {
name,
avatar,