use crate::cli::{print_value, DaemonClient};
use anyhow::Result;
use four_word_networking::IdentityEncoder;
fn identity_words(encoder: &IdentityEncoder, hex_id: &str) -> Option<String> {
encoder.encode_hex(hex_id).ok().map(|w| w.to_string())
}
pub fn inject_identity_words(encoder: &IdentityEncoder, value: &mut serde_json::Value) {
if let Some(obj) = value.as_object_mut() {
if let Some(agent_hex) = obj
.get("agent_id")
.and_then(|v| v.as_str())
.map(String::from)
{
if let Some(words) = identity_words(encoder, &agent_hex) {
obj.insert(
"identity_words".to_string(),
serde_json::Value::String(words),
);
}
}
if let Some(user_hex) = obj
.get("user_id")
.and_then(|v| v.as_str())
.map(String::from)
{
if let Some(words) = identity_words(encoder, &user_hex) {
obj.insert("user_words".to_string(), serde_json::Value::String(words));
}
}
}
}
pub async fn agent(client: &DaemonClient) -> Result<()> {
client.ensure_running().await?;
let mut resp = client.get("/agent").await?;
let encoder = IdentityEncoder::new();
inject_identity_words(&encoder, &mut resp);
print_value(client.format(), &resp);
Ok(())
}
pub async fn user_id(client: &DaemonClient) -> Result<()> {
client.ensure_running().await?;
let resp = client.get("/agent/user-id").await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn announce(client: &DaemonClient, include_user: bool, consent: bool) -> Result<()> {
client.ensure_running().await?;
let body = serde_json::json!({
"include_user_identity": include_user,
"human_consent": consent,
});
let resp = client.post("/announce", &body).await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn card(
client: &DaemonClient,
display_name: Option<&str>,
include_groups: bool,
) -> Result<()> {
client.ensure_running().await?;
let mut params = Vec::new();
if let Some(name) = display_name {
params.push(format!("display_name={name}"));
}
if include_groups {
params.push("include_groups=true".to_string());
}
let query = if params.is_empty() {
String::new()
} else {
format!("?{}", params.join("&"))
};
let resp = client.get(&format!("/agent/card{query}")).await?;
if let Some(link) = resp.get("link").and_then(|v| v.as_str()) {
eprintln!("\nYour shareable identity card:\n");
eprintln!(" {link}\n");
eprintln!("Share this link with anyone — they can import it with:");
eprintln!(" x0x agent import <link>\n");
}
print_value(client.format(), &resp);
Ok(())
}
pub async fn introduction(client: &DaemonClient) -> Result<()> {
client.ensure_running().await?;
let resp = client.get("/introduction").await?;
print_value(client.format(), &resp);
Ok(())
}
pub async fn import_card(
client: &DaemonClient,
card_link: &str,
trust_level: Option<&str>,
) -> Result<()> {
client.ensure_running().await?;
let mut body = serde_json::json!({ "card": card_link });
if let Some(tl) = trust_level {
body["trust_level"] = serde_json::Value::String(tl.to_string());
}
let resp = client.post("/agent/card/import", &body).await?;
print_value(client.format(), &resp);
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn identity_words_encodes_known_hex() {
let encoder = IdentityEncoder::new();
let hex_id = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let result = identity_words(&encoder, hex_id);
assert!(result.is_some(), "should encode valid hex");
let words = result.unwrap();
assert!(!words.is_empty(), "should produce non-empty words");
assert!(!words.is_empty(), "should produce non-empty words: {words}");
}
#[test]
fn identity_words_rejects_invalid_hex() {
let encoder = IdentityEncoder::new();
let result = identity_words(&encoder, "not-hex");
assert!(result.is_none(), "should reject invalid hex");
}
#[test]
fn identity_words_rejects_short_hex() {
let encoder = IdentityEncoder::new();
let result = identity_words(&encoder, "aabb");
assert!(result.is_none(), "should reject short hex");
}
#[test]
fn inject_identity_words_adds_words_to_object() {
let encoder = IdentityEncoder::new();
let agent_hex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let mut value = serde_json::json!({
"agent_id": agent_hex,
"name": "test-agent"
});
inject_identity_words(&encoder, &mut value);
assert!(
value.get("identity_words").is_some(),
"should add identity_words"
);
let words = value["identity_words"].as_str().unwrap().to_string();
assert!(!words.is_empty(), "should produce non-empty words: {words}");
}
#[test]
fn inject_identity_words_skips_missing_agent_id() {
let encoder = IdentityEncoder::new();
let mut value = serde_json::json!({"name": "no-id"});
inject_identity_words(&encoder, &mut value);
assert!(
value.get("identity_words").is_none(),
"should not add words without agent_id"
);
}
#[test]
fn inject_identity_words_adds_user_words() {
let encoder = IdentityEncoder::new();
let user_hex = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let mut value = serde_json::json!({
"agent_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"user_id": user_hex,
});
inject_identity_words(&encoder, &mut value);
assert!(
value.get("identity_words").is_some(),
"should add identity_words"
);
assert!(value.get("user_words").is_some(), "should add user_words");
}
#[test]
fn inject_identity_words_handles_non_object() {
let encoder = IdentityEncoder::new();
let mut value = serde_json::json!([1, 2, 3]);
inject_identity_words(&encoder, &mut value);
assert!(value.is_array());
}
}