iris-chat 0.1.3

Iris Chat command line client and shared encrypted chat core
Documentation
use super::*;
use nostr::nips::nip19::{FromBech32, Nip19};

pub(crate) fn parse_peer_input(input: &str) -> anyhow::Result<(String, PublicKey)> {
    let normalized = normalize_peer_input_for_display(input);
    let pubkey = PublicKey::parse(&normalized)?;
    Ok((pubkey.to_hex(), pubkey))
}

pub(crate) fn normalize_peer_input_for_display(input: &str) -> String {
    let normalized = compact_identity_input(input);

    if let Some(pubkey) = extract_nip19_identity(&normalized) {
        return pubkey.to_bech32().unwrap_or_else(|_| pubkey.to_hex());
    }

    match PublicKey::parse(&normalized) {
        Ok(pubkey) if normalized.starts_with("npub1") => {
            pubkey.to_bech32().unwrap_or_else(|_| normalized.clone())
        }
        Ok(pubkey) => pubkey.to_hex(),
        Err(_) => normalized,
    }
}

fn compact_identity_input(input: &str) -> String {
    let compact = input
        .trim()
        .chars()
        .filter(|ch| !ch.is_whitespace())
        .collect::<String>()
        .to_ascii_lowercase();
    compact
        .strip_prefix("nostr:")
        .unwrap_or(&compact)
        .to_string()
}

fn extract_nip19_identity(input: &str) -> Option<PublicKey> {
    for prefix in ["npub1", "nprofile1"] {
        let Some(start) = input.find(prefix) else {
            continue;
        };
        let token = take_bech32_token(&input[start..]);
        if let Ok(nip19) = Nip19::from_bech32(token) {
            match nip19 {
                Nip19::Pubkey(pubkey) => return Some(pubkey),
                Nip19::Profile(profile) => return Some(profile.public_key),
                _ => {}
            }
        }
    }
    None
}

fn take_bech32_token(input: &str) -> &str {
    let end = input
        .find(|ch: char| !ch.is_ascii_alphanumeric())
        .unwrap_or(input.len());
    &input[..end]
}

pub(super) fn parse_owner_input(input: &str) -> anyhow::Result<OwnerPubkey> {
    let (_, pubkey) = parse_peer_input(input)?;
    Ok(pubkey)
}

pub(super) fn parse_owner_inputs(
    inputs: &[String],
    exclude_owner: OwnerPubkey,
) -> anyhow::Result<Vec<OwnerPubkey>> {
    let mut owners = inputs
        .iter()
        .map(|input| parse_owner_input(input))
        .collect::<anyhow::Result<Vec<_>>>()?;
    owners.retain(|owner| *owner != exclude_owner);
    owners.sort_by_key(|owner| owner.to_hex());
    owners.dedup();
    Ok(owners)
}

pub(super) fn parse_device_input(input: &str) -> anyhow::Result<DevicePubkey> {
    let (_, pubkey) = parse_peer_input(input)?;
    Ok(pubkey)
}

pub(super) fn local_device_from_keys(keys: &Keys) -> DevicePubkey {
    keys.public_key()
}

pub(super) fn owner_npub_from_owner(owner_pubkey: OwnerPubkey) -> Option<String> {
    owner_pubkey.to_bech32().ok()
}

pub(super) fn device_npub(device_hex: &str) -> Option<String> {
    PublicKey::parse(device_hex).ok()?.to_bech32().ok()
}

pub(super) fn public_authorization_state(
    state: LocalAuthorizationState,
) -> DeviceAuthorizationState {
    match state {
        LocalAuthorizationState::Authorized => DeviceAuthorizationState::Authorized,
        LocalAuthorizationState::AwaitingApproval => DeviceAuthorizationState::AwaitingApproval,
        LocalAuthorizationState::Revoked => DeviceAuthorizationState::Revoked,
    }
}

pub(super) fn chat_unavailable_message(logged_in: Option<&LoggedInState>) -> &'static str {
    match logged_in.map(|logged_in| logged_in.authorization_state) {
        Some(LocalAuthorizationState::AwaitingApproval) => {
            "This device is still waiting for approval."
        }
        Some(LocalAuthorizationState::Revoked) => {
            "This device has been removed from the profile. Log out to continue."
        }
        _ => "Create or restore a profile first.",
    }
}

pub(super) fn unix_now() -> UnixSeconds {
    UnixSeconds(
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs(),
    )
}