feat(chat-cli): wire up logos-delivery transport and switch to client API (#91)

Replace the direct use of `conversations::Context` with `client::ChatClient`,
which is the intended public API for library consumers.

Remove `MessageEnvelope` and the username-keyed session model. The envelope
was never part of the wire protocol — sender identity was only tracked in the
CLI's local state. Chats are now keyed by conversation ID; add `/nickname` as
the user-facing replacement for named sessions.

Add a logos-delivery (Waku) transport alongside the existing file transport.
The active transport is selected at compile time: set `LOGOS_DELIVERY_LIB_DIR`
to link liblogosdelivery, otherwise the file transport is used.

Add logos-delivery as a Nix flake input and expose `.#logos-delivery` so the
library can be built with `nix build` and referenced by `LOGOS_DELIVERY_LIB_DIR`.

CI: rename `c-ffi-smoketest` to `smoketest`; add logos-delivery build step
and a `--smoketest` invocation of chat-cli to verify startup.
This commit is contained in:
osmaczko
2026-04-27 13:22:16 +02:00
committed by GitHub
parent df84fc87cf
commit eaeffcd21f
16 changed files with 2073 additions and 715 deletions
+162 -66
View File
@@ -1,93 +1,189 @@
//! Chat CLI - A terminal chat application using logos-chat.
//!
//! This application demonstrates how to use the logos-chat library
//! with file-based transport for local communication.
//!
//! # Usage
//!
//! Run two instances with different usernames:
//!
//! ```bash
//! # Terminal 1
//! cargo run -p chat-cli -- alice
//!
//! # Terminal 2
//! cargo run -p chat-cli -- bob
//! ```
//!
//! Then in alice's terminal:
//! 1. Type `/intro` to get your introduction bundle
//! 2. Copy the bundle string
//!
//! In bob's terminal:
//! 1. Type `/connect alice <bundle>` (paste alice's bundle)
//!
//! Now bob can send messages to alice, and alice can reply.
mod app;
mod transport;
mod ui;
mod utils;
use std::{env, path::PathBuf};
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use client::DeliveryService;
/// Get the data directory (in project folder).
fn get_data_dir() -> PathBuf {
env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("tmp/chat-cli-data")
use app::ChatApp;
#[derive(Parser, Debug)]
#[command(name = "chat-cli", about = "End-to-end encrypted terminal chat")]
struct Cli {
/// Your identity name.
#[arg(long, short)]
name: String,
// ── File-transport options ────────────────────────────────────────────────
/// Shared data directory for file transport (both peers must use the same path).
#[arg(long, default_value = "tmp/chat-cli-data")]
data: PathBuf,
// ── logos-delivery transport options ──────────────────────────────────────
/// Persistent SQLite database for logos-delivery transport (omit for ephemeral identity).
#[arg(long)]
db: Option<PathBuf>,
/// logos-delivery network preset (`logos.dev` or `twn`).
#[arg(long, default_value = "logos.dev")]
preset: String,
/// TCP port for the embedded logos-delivery node.
#[arg(long, default_value_t = 60000)]
port: u16,
/// Write logs to a file instead of stderr (keeps TUI output clean).
#[arg(long)]
log_file: Option<PathBuf>,
/// Initialize and immediately exit without launching the TUI (for CI).
#[arg(long)]
smoketest: bool,
}
fn main() -> Result<()> {
// Parse arguments
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <username>", args[0]);
eprintln!("\nExample:");
eprintln!(" Terminal 1: {} alice", args[0]);
eprintln!(" Terminal 2: {} bob", args[0]);
std::process::exit(1);
let cli = Cli::parse();
setup_logging(cli.log_file.as_deref())?;
#[cfg(logos_delivery)]
return run_logos_delivery(cli);
#[cfg(not(logos_delivery))]
run_file(cli)
}
#[cfg(not(logos_delivery))]
fn run_file(cli: Cli) -> Result<()> {
use transport::file::FileTransport;
std::fs::create_dir_all(&cli.data).context("failed to create data directory")?;
println!("Starting chat as '{}'...", cli.name);
println!("Data dir: {}", cli.data.display());
let transport_dir = cli.data.join("transport");
let (transport, inbound) =
FileTransport::new(&transport_dir).context("failed to create file transport")?;
let db_path = cli.data.join(format!("{}.db", cli.name));
let client = client::ChatClient::open(
cli.name.clone(),
client::StorageConfig::Encrypted {
path: db_path.to_string_lossy().to_string(),
key: "chat-cli".to_string(),
},
transport,
)
.map_err(|e| anyhow::anyhow!("{e:?}"))
.context("failed to open chat client")?;
let mut app = ChatApp::new(client, inbound, &cli.name, &cli.data)?;
if cli.smoketest {
return Ok(());
}
let user_name = &args[1];
// Setup data directory in project folder
let data_dir = get_data_dir();
std::fs::create_dir_all(&data_dir).context("Failed to create data directory")?;
println!("Starting chat as '{}'...", user_name);
println!("Data dir: {:?}", data_dir);
// Create app
let mut app = app::ChatApp::new(user_name, &data_dir).context("Failed to create chat app")?;
// Initialize terminal UI
let mut terminal = ui::init().context("Failed to initialize terminal")?;
// Main loop
let mut terminal = ui::init().context("failed to initialize terminal")?;
let result = run_app(&mut terminal, &mut app);
// Restore terminal
ui::restore().context("Failed to restore terminal")?;
ui::restore().context("failed to restore terminal")?;
result
}
fn run_app(terminal: &mut ui::Tui, app: &mut app::ChatApp) -> Result<()> {
#[cfg_attr(not(logos_delivery), allow(dead_code, unused_variables))]
fn run_logos_delivery(cli: Cli) -> Result<()> {
#[cfg(logos_delivery)]
{
use transport::logos_delivery::{Config, Service};
eprintln!("Starting logos-delivery node (preset={})...", cli.preset);
eprintln!("This may take a few seconds while connecting to the network.");
let logos_cfg = Config {
preset: cli.preset.clone(),
tcp_port: cli.port,
..Default::default()
};
let (delivery, inbound) =
Service::start(logos_cfg).context("failed to start logos-delivery")?;
eprintln!("Node connected. Initializing chat client...");
let data_dir = cli
.db
.as_ref()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| cli.data.clone());
let client = match cli.db {
Some(ref path) => {
let db_str = path
.to_str()
.context("db path contains non-UTF-8 characters")?
.to_string();
client::ChatClient::open(
cli.name.clone(),
client::StorageConfig::Encrypted {
path: db_str,
key: "chat-cli".to_string(),
},
delivery,
)
.map_err(|e| anyhow::anyhow!("{e:?}"))
.context("failed to open persistent client")?
}
None => client::ChatClient::new(cli.name.clone(), delivery),
};
let mut app = ChatApp::new(client, inbound, &cli.name, &data_dir)?;
if cli.smoketest {
return Ok(());
}
let mut terminal = ui::init().context("failed to initialize terminal")?;
let result = run_app(&mut terminal, &mut app);
ui::restore().context("failed to restore terminal")?;
return result;
}
#[cfg(not(logos_delivery))]
anyhow::bail!(
"logos-delivery transport is not available in this build.\n\
Build with LOGOS_DELIVERY_LIB_DIR set to enable it."
)
}
fn run_app<D: DeliveryService>(terminal: &mut ui::Tui, app: &mut ChatApp<D>) -> Result<()> {
loop {
// Process incoming messages
app.process_incoming()?;
// Draw UI
terminal.draw(|frame| ui::draw(frame, app))?;
// Handle input
if !ui::handle_events(app)? {
break;
}
}
Ok(())
}
fn setup_logging(log_file: Option<&std::path::Path>) -> Result<()> {
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"));
if let Some(path) = log_file {
let file = std::fs::File::create(path)
.with_context(|| format!("failed to create log file: {}", path.display()))?;
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(file)
.init();
} else {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new("off"))
.init();
}
Ok(())
}