parley-md 0.1.2

Reference CLI for the Parley agent-to-agent messaging protocol. Installs the `parley` binary.
//! `parley log [<handle>]` — print local message history.
//!
//! Without arguments: shows the most recent N entries across all channels.
//! With a handle: shows only that channel's history.

use std::path::Path;

use anyhow::Result;

use crate::history::{self, LogEntry};
use crate::state::{load_channels, load_friends};

#[derive(Debug)]
pub struct Opts {
    pub recipient: Option<String>,
    pub limit: usize,
    pub all: bool,
    pub json: bool,
}

pub fn run(home: &Path, opts: Opts) -> Result<()> {
    let entries = match opts.recipient.as_deref() {
        Some(handle) => entries_for_handle(home, handle)?,
        None => history::read_all(home)?,
    };

    let total = entries.len();
    let to_show: Vec<&LogEntry> = if opts.all {
        entries.iter().collect()
    } else {
        let n = opts.limit.min(entries.len());
        entries.iter().skip(entries.len() - n).collect()
    };

    if opts.json {
        let payload = serde_json::json!({
            "total": total,
            "shown": to_show.len(),
            "entries": to_show.iter().collect::<Vec<_>>(),
        });
        println!("{}", serde_json::to_string_pretty(&payload)?);
        return Ok(());
    }

    if to_show.is_empty() {
        println!("(no history)");
        return Ok(());
    }
    for e in &to_show {
        let when = format_ts(e.ts);
        let arrow = if e.direction == "in" { "" } else { "" };
        let who = e
            .counterparty_handle
            .clone()
            .unwrap_or_else(|| short(&e.counterparty_pubkey));
        match e.kind.as_str() {
            "file" => {
                let size = e.size.unwrap_or(0);
                let saved = e.saved_to.as_deref().unwrap_or("");
                let extra = if saved.is_empty() {
                    String::new()
                } else {
                    format!("{saved}")
                };
                println!(
                    "{when}  {arrow} {who}  [file] {} ({size} bytes){extra}",
                    e.body
                );
            }
            _ => {
                println!("{when}  {arrow} {who}  {}", e.body);
            }
        }
    }
    if !opts.all && total > to_show.len() {
        println!(
            "\n({total} entries total; showing last {}; use --all for full)",
            to_show.len()
        );
    }
    Ok(())
}

fn entries_for_handle(home: &Path, handle: &str) -> Result<Vec<LogEntry>> {
    let friends = load_friends(home).unwrap_or_default();
    let pubkey = friends
        .resolve(handle)
        .or_else(|| {
            // Maybe it's a raw pubkey already.
            if handle.len() == 43 {
                Some(handle.to_string())
            } else {
                None
            }
        })
        .ok_or_else(|| {
            anyhow::anyhow!(
                "unknown recipient `{handle}` — not a local alias and not a 43-char pubkey. \
                 Run `parley friends list` or send them a message first to populate the alias."
            )
        })?;
    let channels = load_channels(home).unwrap_or_default();
    let entry = channels.by_friend.get(&pubkey).ok_or_else(|| {
        anyhow::anyhow!("no channel with `{handle}` yet — send them a message first")
    })?;
    history::read(home, &entry.channel_id)
}

fn format_ts(unix: i64) -> String {
    use time::OffsetDateTime;
    OffsetDateTime::from_unix_timestamp(unix)
        .ok()
        .and_then(|t| {
            t.format(&time::format_description::well_known::Rfc3339)
                .ok()
        })
        .unwrap_or_else(|| unix.to_string())
}

fn short(pubkey_b64: &str) -> String {
    let n = pubkey_b64.len().min(8);
    format!("{}", &pubkey_b64[..n])
}