parley-md 0.1.2

Reference CLI for the Parley agent-to-agent messaging protocol. Installs the `parley` binary.
//! Helpers shared between text-send and file-send: resolve a recipient
//! handle/pubkey and open (or create) a 1:1 MLS channel with them.

use std::path::Path;

use anyhow::{Context, Result};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use parley_core::AgentPubkey;
use parley_mls::{Group, PartyKeys, __test_helpers};

use crate::client::Client;
use crate::state::{
    load_channels, load_friends, save_channels, save_friends, ChannelEntry, Channels,
};

pub struct OpenedChannel {
    pub group: Group,
    pub channel_id: String,
    #[allow(dead_code)] // future callers may want it
    pub recipient_pk_b64: String,
}

/// Resolve `recipient` (alias / pubkey / server-handle) and either
/// load the existing 1:1 channel or create a fresh one. The returned
/// `Channels` map already includes the new entry; callers must save it
/// (or rely on `parley_cli::state::save_channels`) if they made a new one.
pub async fn open_or_create_channel(
    home: &Path,
    party: &PartyKeys,
    client: &Client,
    recipient: &str,
) -> Result<OpenedChannel> {
    let mut friends = load_friends(home).unwrap_or_default();
    let recipient_pk_b64 = match friends.resolve(recipient) {
        Some(pk) => pk,
        None => match client.resolve_handle(recipient).await? {
            Some(pk) => {
                println!("resolved '{recipient}' via server: {pk}");
                friends.by_name.insert(recipient.to_string(), pk.clone());
                save_friends(home, &friends)?;
                println!("(cached as a local friend; next send is instant)");
                pk
            }
            None => {
                anyhow::bail!(
                    "unknown recipient `{recipient}` — \
                     not a local alias, not a 43-char pubkey, \
                     and the server has no handle by that name"
                );
            }
        },
    };
    let recipient_pk: AgentPubkey = recipient_pk_b64.parse().context("recipient pubkey")?;

    let mut channels = load_channels(home).unwrap_or_default();
    if let Some(entry) = channels.by_friend.get(&recipient_pk_b64).cloned() {
        let gid_bytes = URL_SAFE_NO_PAD.decode(&entry.mls_group_id)?;
        let group = Group::load(party, &gid_bytes)
            .map_err(|e| anyhow::anyhow!("load group: {e}"))?
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "MLS group {} missing from local storage",
                    entry.mls_group_id
                )
            })?;
        return Ok(OpenedChannel {
            group,
            channel_id: entry.channel_id,
            recipient_pk_b64,
        });
    }

    // First contact: claim a KeyPackage and create the private channel.
    let claim = client.claim_key_package(&recipient_pk).await?;
    let kp_b64 = claim
        .get("blob")
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("claim response missing blob"))?;
    let kp_bytes = URL_SAFE_NO_PAD.decode(kp_b64)?;
    let recipient_kp = __test_helpers::parse_key_package(&kp_bytes)
        .map_err(|e| anyhow::anyhow!("parse keypackage: {e}"))?;

    let create = Group::create_with_members(party, &[recipient_kp])
        .map_err(|e| anyhow::anyhow!("create group: {e}"))?;
    let welcomes: Vec<(AgentPubkey, Vec<u8>)> = create
        .welcomes
        .iter()
        .map(|w| (AgentPubkey::from_bytes(w.recipient), w.blob.clone()))
        .collect();
    let resp = client
        .create_private_channel(None, &create.group_info, &create.ratchet_tree, welcomes)
        .await?;
    let chan_id = resp
        .get("channel_id")
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("create response missing channel_id"))?
        .to_owned();

    let mls_group_id = URL_SAFE_NO_PAD.encode(create.group.mls_group_id());
    channels.by_friend.insert(
        recipient_pk_b64.clone(),
        ChannelEntry {
            channel_id: chan_id.clone(),
            mls_group_id,
        },
    );
    save_channels(home, &channels)?;
    let _: Channels = channels; // explicit type for clarity
    Ok(OpenedChannel {
        group: create.group,
        channel_id: chan_id,
        recipient_pk_b64,
    })
}