iris-chat 0.1.67

Iris Chat command line client and shared encrypted chat core
Documentation
use super::*;

pub(in crate::core) fn chat_invite_url(invite: &Invite) -> anyhow::Result<String> {
    let url = nostr_double_ratchet::invite_url(invite, CHAT_INVITE_ROOT_URL)?;
    Ok(route_wrapped_chat_invite_url(&url))
}

#[allow(clippy::large_enum_variant)]
pub(super) enum PublicInviteInput {
    Invite(Invite),
    DirectChat,
}

pub(super) fn parse_public_invite_or_direct_chat_input(
    input: &str,
) -> anyhow::Result<PublicInviteInput> {
    if let Ok(invite) = parse_public_invite_input(input) {
        return Ok(PublicInviteInput::Invite(invite));
    }
    parse_peer_input(input)?;
    Ok(PublicInviteInput::DirectChat)
}

pub(in crate::core) fn parse_public_invite_input(input: &str) -> anyhow::Result<Invite> {
    if let Ok(invite) = nostr_double_ratchet::parse_invite_url(input) {
        return Ok(invite);
    }

    let Ok(url) = url::Url::parse(input) else {
        return nostr_double_ratchet::parse_invite_url(input)
            .map_err(|error| anyhow::anyhow!(error.to_string()));
    };

    for (key, value) in url.query_pairs() {
        for candidate in [key.as_ref(), value.as_ref()] {
            if let Ok(invite) = parse_invite_candidate(candidate) {
                return Ok(invite);
            }
        }
    }

    if let Some(fragment) = url.fragment() {
        if let Ok(invite) = parse_invite_candidate(fragment) {
            return Ok(invite);
        }
        for (_, value) in url::form_urlencoded::parse(fragment.as_bytes()) {
            if let Ok(invite) = parse_invite_candidate(&value) {
                return Ok(invite);
            }
        }
        for part in fragment.split(['/', '?', '&', '=']) {
            if let Ok(invite) = parse_invite_candidate(part) {
                return Ok(invite);
            }
        }
    }

    nostr_double_ratchet::parse_invite_url(input)
        .map_err(|error| anyhow::anyhow!(error.to_string()))
}

fn parse_invite_candidate(candidate: &str) -> anyhow::Result<Invite> {
    let trimmed = candidate.trim().trim_start_matches('/');
    if let Ok(invite) = nostr_double_ratchet::parse_invite_url(trimmed) {
        return Ok(invite);
    }
    nostr_double_ratchet::parse_invite_url(&format!("{CHAT_INVITE_ROOT_URL}#{trimmed}"))
        .map_err(|error| anyhow::anyhow!(error.to_string()))
}

fn route_wrapped_chat_invite_url(url: &str) -> String {
    let Some((base, fragment)) = url.split_once('#') else {
        return url.to_string();
    };
    let payload = fragment.trim_start_matches('/');
    if payload.is_empty() || payload.starts_with("invite/") {
        return url.to_string();
    }
    format!("{base}#/invite/{payload}")
}

#[cfg(test)]
mod tests {
    use super::*;

    fn sample_invite_url() -> String {
        let keys = Keys::generate();
        let mut invite = Invite::create_new(keys.public_key(), Some("public".to_string()), None)
            .expect("invite");
        invite.owner_public_key = Some(keys.public_key());
        chat_invite_url(&invite).expect("invite url")
    }

    #[test]
    fn public_invite_url_uses_chat_iris_root() {
        assert!(sample_invite_url().starts_with("https://chat.iris.to/#/invite/"));
    }

    #[test]
    fn parse_public_invite_input_accepts_hash_route_wrapper() {
        let url = sample_invite_url();
        let encoded = url.split("/invite/").nth(1).expect("payload");
        let wrapped = format!("https://chat.iris.to/#/invite/{encoded}");

        let parsed = parse_public_invite_input(&wrapped).expect("parse wrapped invite");

        assert!(parsed.owner_public_key.is_some());
    }

    #[test]
    fn parse_public_invite_input_accepts_user_link_as_direct_chat() {
        let keys = Keys::generate();
        let npub = keys.public_key().to_bech32().expect("npub");
        let wrapped = format!("https://chat.iris.to/#/{npub}");

        let parsed =
            parse_public_invite_or_direct_chat_input(&wrapped).expect("parse direct chat link");

        assert!(matches!(parsed, PublicInviteInput::DirectChat));
    }

    #[test]
    fn route_wrapped_chat_invite_url_preserves_legacy_payload() {
        let legacy = "https://chat.iris.to/#%7B%22purpose%22%3A%22private%22%7D";

        let wrapped = route_wrapped_chat_invite_url(legacy);

        assert_eq!(
            wrapped,
            "https://chat.iris.to/#/invite/%7B%22purpose%22%3A%22private%22%7D"
        );
    }
}