mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-25 23:21:10 +00:00
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:
+162
-66
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user