ndr 0.0.79

CLI for encrypted Nostr messaging using double ratchet
Documentation
use anyhow::Result;

use crate::output::Output;
use crate::storage::Storage;

pub async fn add(npub_or_hex: &str, name: &str, storage: &Storage, output: &Output) -> Result<()> {
    // Accept:
    // - npub / nprofile
    // - Iris-style chat links like `https://chat.iris.to/#npub...`
    // - 64-char hex pubkey
    let npub = if let Some(pk) = crate::commands::nip19::parse_pubkey(npub_or_hex) {
        nostr::ToBech32::to_bech32(&pk)?
    } else if npub_or_hex.len() == 64 && npub_or_hex.chars().all(|c| c.is_ascii_hexdigit()) {
        let pk = nostr::PublicKey::from_hex(npub_or_hex)?;
        nostr::ToBech32::to_bech32(&pk)?
    } else {
        anyhow::bail!("Invalid pubkey: must be npub/nprofile, chat link, or 64-char hex");
    };

    storage.add_contact(&npub, name)?;
    output.success(
        "contact_add",
        serde_json::json!({
            "npub": npub,
            "name": name,
        }),
    );
    Ok(())
}

pub async fn list(storage: &Storage, output: &Output) -> Result<()> {
    let contacts = storage.list_contacts()?;
    output.success(
        "contact_list",
        serde_json::json!({
            "contacts": contacts.iter().map(|(npub, name)| serde_json::json!({
                "npub": npub,
                "name": name,
            })).collect::<Vec<_>>(),
        }),
    );
    Ok(())
}

pub async fn remove(name: &str, storage: &Storage, output: &Output) -> Result<()> {
    if storage.remove_contact(name)? {
        output.success_message("contact_remove", &format!("Removed contact '{}'", name));
    } else {
        anyhow::bail!("Contact '{}' not found", name);
    }
    Ok(())
}