use std::io::Read as _;
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, __test_helpers};
use crate::client::Client;
use crate::state::{
load_channels, load_friends, load_identity, load_party_keys, load_server, save_channels,
save_party_keys, ChannelEntry,
};
pub async fn run(home: &Path, recipient: &str, message: Option<String>) -> Result<()> {
let identity = load_identity(home)?;
let server = load_server(home)?;
if server.server_url.is_empty() {
anyhow::bail!("server not configured. Run `parley register --server URL`.");
}
let party = load_party_keys(home, &identity)?;
let client = Client::new(&server, &identity)?;
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());
crate::state::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 message_text = match message {
Some(m) => m,
None => {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
buf
}
};
let mut channels = load_channels(home).unwrap_or_default();
let (mut group, channel_id) = match channels.by_friend.get(&recipient_pk_b64).cloned() {
Some(entry) => {
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
)
})?;
(group, entry.channel_id)
}
None => {
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)?;
(create.group, chan_id)
}
};
let ciphertext = group
.encrypt_application(&party, message_text.as_bytes())
.map_err(|e| anyhow::anyhow!("encrypt: {e}"))?;
let _ = client
.post_mls_application(&channel_id, &ciphertext)
.await?;
save_party_keys(home, &party)?;
let handle_label = friends
.label(&recipient_pk_b64)
.map(str::to_owned)
.or_else(|| {
(recipient != recipient_pk_b64).then(|| recipient.to_string())
});
let _ = crate::history::append(
home,
&channel_id,
&crate::history::LogEntry {
seq: None,
ts: crate::history::now_unix(),
direction: "out".into(),
counterparty_pubkey: recipient_pk_b64,
counterparty_handle: handle_label,
kind: "text".into(),
body: message_text,
size: None,
saved_to: None,
},
);
println!("sent to {recipient} (channel {channel_id})");
Ok(())
}