From d131a69583a565a0e813df24b30889453e4c4e09 Mon Sep 17 00:00:00 2001 From: kaichao Date: Fri, 3 Jul 2026 02:17:45 +0800 Subject: [PATCH] feat: default transport for logos chat client (#159) * chore: gate logos-delivery transport on cargo feature, not env-dependent cfg * chore: fix clippy * feat: logos chat client use logos delivery as default --- Cargo.lock | 1 - bin/chat-cli/Cargo.toml | 3 +- bin/chat-cli/src/main.rs | 101 ++++++++++++++++++------------------ crates/client/Cargo.toml | 3 ++ crates/client/src/config.rs | 7 +++ crates/client/src/errors.rs | 2 + crates/client/src/lib.rs | 4 ++ crates/client/src/logos.rs | 69 ++++++++++++++---------- 8 files changed, 111 insertions(+), 79 deletions(-) create mode 100644 crates/client/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 4fb63da..4bff5b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1315,7 +1315,6 @@ dependencies = [ "arboard", "base64", "clap", - "components", "crossbeam-channel", "crossterm 0.29.0", "logos-chat", diff --git a/bin/chat-cli/Cargo.toml b/bin/chat-cli/Cargo.toml index fa73399..d921dec 100644 --- a/bin/chat-cli/Cargo.toml +++ b/bin/chat-cli/Cargo.toml @@ -9,9 +9,8 @@ path = "src/main.rs" [dependencies] # Workspace dependencies (sorted) -components = { workspace = true , features = ["embedded_p2p_delivery"]} crossbeam-channel = { workspace = true } -logos-chat = { workspace = true } +logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] } # External dependencies (sorted) anyhow = "1.0" diff --git a/bin/chat-cli/src/main.rs b/bin/chat-cli/src/main.rs index bd12b68..04482f6 100644 --- a/bin/chat-cli/src/main.rs +++ b/bin/chat-cli/src/main.rs @@ -9,30 +9,11 @@ use anyhow::{Context, Result}; use clap::{Parser, ValueEnum}; use crossbeam_channel::Receiver; use logos_chat::{ - ChatClient, ChatStore, Event, IdentityProvider, LogosChatClient, RegistrationService, Transport, + ChatClient, ChatClientBuilder, ChatStore, DelegateSigner, Event, HttpRegistry, + IdentityProvider, LogosChatClient, NETWORK_PRESET, REGISTRY_ENDPOINT, RegistrationService, + StorageConfig, Transport, }; -use components::{EmbeddedP2pDeliveryService, P2pConfig}; - -#[derive(Debug)] -struct P2pTransport(EmbeddedP2pDeliveryService); - -impl logos_chat::DeliveryService for P2pTransport { - type Error = ::Error; - fn publish(&mut self, envelope: logos_chat::AddressedEnvelope) -> Result<(), Self::Error> { - self.0.publish(envelope) - } - fn subscribe(&mut self, addr: &str) -> Result<(), Self::Error> { - self.0.subscribe(addr) - } -} - -impl logos_chat::Transport for P2pTransport { - fn inbound(&mut self) -> crossbeam_channel::Receiver> { - self.0.inbound_queue() - } -} - use app::ChatApp; #[derive(Copy, Clone, Debug, ValueEnum)] @@ -62,9 +43,10 @@ struct Cli { db: Option, // ── logos-delivery transport options ────────────────────────────────────── - /// logos-delivery network preset (e.g. `logos.dev`). - #[arg(long, default_value = "logos.dev")] - preset: String, + /// logos-delivery network preset (e.g. `logos.dev`). When omitted, the + /// preconfigured network preset is used. + #[arg(long)] + preset: Option, /// TCP port for the embedded logos-delivery node. #[arg(long, default_value_t = 60000)] @@ -91,47 +73,66 @@ fn main() -> Result<()> { std::fs::create_dir_all(&cli.data).context("failed to create data directory")?; + let db_str = db_path(&cli)?; + match cli.transport { + // logos-delivery is the transport baked into `LogosChatClient`, so the + // Logos client opens it from config rather than receiving one. + TransportKind::LogosDelivery => { + let preset = cli.preset.as_deref().unwrap_or(NETWORK_PRESET); + println!("Starting logos-delivery node (preset={preset})..."); + println!("This may take a few seconds while connecting to the network."); + + let (client, events) = LogosChatClient::open( + db_str, + "chat-cli", + cli.port, + cli.preset.as_deref(), + cli.registry_url.as_deref(), + ) + .map_err(|e| anyhow::anyhow!("{e:?}")) + .context("failed to open chat client")?; + + println!("Node connected."); + launch_tui(client, events, &cli) + } + // The file transport is a local-only path: it reuses the Logos service + // stack (delegate identity, HTTP registry, encrypted storage) but swaps + // the transport, so it builds a client directly instead of going through + // `LogosChatClient`. TransportKind::File => { let transport_dir = cli.data.join("transport"); let transport = transport::file::FileTransport::new(&transport_dir) .context("failed to create file transport")?; - run(transport, &cli) - } - TransportKind::LogosDelivery => { - println!("Starting logos-delivery node (preset={})...", cli.preset); - println!("This may take a few seconds while connecting to the network."); - let cfg = P2pConfig { - preset: cli.preset.clone(), - tcp_port: cli.port, - ..Default::default() - }; - let transport = P2pTransport( - EmbeddedP2pDeliveryService::start(cfg).context("failed to start logos-delivery")?, - ); + let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT); + let (client, events) = ChatClientBuilder::new() + .ident(DelegateSigner::random()) + .transport(transport) + .registration(HttpRegistry::new(endpoint)) + .storage_config(StorageConfig::Encrypted { + path: db_str, + key: "chat-cli".to_string(), + }) + .build() + .map_err(|e| anyhow::anyhow!("{e:?}")) + .context("failed to open chat client")?; - println!("Node connected. Initializing chat client..."); - run(transport, &cli) + launch_tui(client, events, &cli) } } } -fn run(transport: T, cli: &Cli) -> Result<()> { - let db_path = cli +/// Resolve the SQLite database path: `--db` if given, else `/.db`. +fn db_path(cli: &Cli) -> Result { + let path = cli .db .clone() .unwrap_or_else(|| cli.data.join(format!("{}.db", cli.name))); - let db_str = db_path + Ok(path .to_str() .context("db path contains non-UTF-8 characters")? - .to_string(); - - let (client, events) = - LogosChatClient::open(transport, db_str, "chat-cli", cli.registry_url.as_deref()) - .map_err(|e| anyhow::anyhow!("{e:?}")) - .context("failed to open chat client")?; - launch_tui(client, events, cli) + .to_string()) } fn launch_tui( diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 02f6fc2..92b6a61 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -6,6 +6,9 @@ edition = "2024" [lib] crate-type = ["rlib"] +[features] +embedded-p2p-delivery = ["components/embedded_p2p_delivery"] + [dependencies] # Workspace dependencies (sorted) chat-sqlite = { workspace = true } diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs new file mode 100644 index 0000000..4200971 --- /dev/null +++ b/crates/client/src/config.rs @@ -0,0 +1,7 @@ +//! Baked-in configuration for the Logos service stack. + +/// The endpoint for the account and keypackage registration service. +pub const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co"; + +/// The logos-delivery network preset the Logos client joins by default. +pub const NETWORK_PRESET: &str = "logos.dev"; diff --git a/crates/client/src/errors.rs b/crates/client/src/errors.rs index 4420ccf..18cd478 100644 --- a/crates/client/src/errors.rs +++ b/crates/client/src/errors.rs @@ -6,4 +6,6 @@ pub enum ClientError { Chat(#[from] ChatError), #[error("received credential could not be parsed")] BadlyFormedCredential, + #[error("failed to start the transport: {0}")] + Transport(String), } diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index d916494..de0a5e3 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,17 +1,21 @@ mod builder; mod client; +mod config; mod delegate; mod delivery_in_process; mod errors; mod event; +#[cfg(feature = "embedded-p2p-delivery")] mod logos; pub use builder::{ChatClientBuilder, Unset}; pub use client::{ChatClient, Transport}; +pub use config::{NETWORK_PRESET, REGISTRY_ENDPOINT}; pub use delegate::DelegateSigner; pub use delivery_in_process::{InProcessDelivery, MessageBus}; pub use errors::ClientError; pub use event::{Event, MessageSender}; +#[cfg(feature = "embedded-p2p-delivery")] pub use logos::LogosChatClient; // Re-export types callers need to interact with ChatClient. diff --git a/crates/client/src/logos.rs b/crates/client/src/logos.rs index 152fa61..214eda8 100644 --- a/crates/client/src/logos.rs +++ b/crates/client/src/logos.rs @@ -1,52 +1,69 @@ //! The opinionated Logos client. //! -//! [`ChatClientBuilder`] is generic and can only default the zero-config -//! components (random identity, ephemeral registry, in-memory storage) — it has -//! no way to know a registry endpoint or a database path, so its defaults are -//! the test-grade ones. `LogosChatClient` is the layer that *does* commit to a -//! stack: a delegate identity, the HTTP keypackage + account registry, and -//! encrypted on-disk storage. It exists so independently built clients share the -//! same production services instead of each re-deriving them. +//! `LogosChatClient` commits to the Logos service stack so independently built +//! clients share the same production services instead of each re-deriving them: +//! a delegate identity, the HTTP keypackage + account registry, encrypted +//! on-disk storage, and — unlike the generic [`ChatClientBuilder`] — the +//! logos-delivery transport itself. logos-delivery *is* the transport for the +//! Logos client, so the caller no longer supplies one; only per-client secrets +//! (the database path and key) and the network config are passed in. //! -//! Only the transport is left to the caller: it carries native dependencies and -//! environment-specific configuration that belong to the binary, not here. +//! The logos-delivery transport carries a native dependency, so this whole +//! module is gated behind the `embedded-p2p-delivery` cargo feature. The registry +//! endpoint lives in [`crate::config`] instead, so it stays available to other +//! transports when the feature is off. +use components::{EmbeddedP2pDeliveryService, HttpRegistry, P2pConfig}; use crossbeam_channel::Receiver; use libchat::{ChatStorage, StorageConfig}; use crate::ChatClientBuilder; use crate::client::{ChatClient, Transport}; +use crate::config::{NETWORK_PRESET, REGISTRY_ENDPOINT}; use crate::delegate::DelegateSigner; use crate::errors::ClientError; use crate::event::Event; -use components::HttpRegistry; -// The endpoint for account and keypackage registration service. -const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co"; +// logos-delivery already implements `DeliveryService`; teaching it the inbound +// half here (in the crate that owns `Transport`) makes it a full transport, so +// callers need no wrapper newtype. +impl Transport for EmbeddedP2pDeliveryService { + fn inbound(&mut self) -> Receiver> { + self.inbound_queue() + } +} /// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`] /// identity, the HTTP keypackage + account registry ([`HttpRegistry`], which is -/// both the keypackage store and the account → device directory), and encrypted -/// [`ChatStorage`]. Only the transport `T` is supplied by the caller. -pub type LogosChatClient = ChatClient; +/// both the keypackage store and the account → device directory), encrypted +/// [`ChatStorage`], and the logos-delivery transport. +pub type LogosChatClient = + ChatClient; -impl LogosChatClient -where - T: Transport + Send + 'static, -{ - /// Open a client on the Logos stack over `transport`, persisting to the - /// encrypted database at `db_path` unlocked with `db_key`. When `registry_url` - /// is `Some`, it overrides the preconfigured registry endpoint (e.g. a local - /// deployment); otherwise the baked-in endpoint is used. +impl LogosChatClient { + /// Open a client on the Logos stack, starting a logos-delivery node on + /// `tcp_port` as its transport and persisting to the encrypted database at + /// `db_path` unlocked with `db_key`. When `preset`/`registry_url` are `Some`, + /// they override the baked-in network preset/registry endpoint (e.g. a local + /// deployment); otherwise the preconfigured values are used. /// - /// `db_path` is a per-client location and `db_key` is a secret, so both are - /// caller-supplied — never baked into the library. + /// `db_path` is a per-client location, `db_key` is a secret, and `tcp_port` + /// is a per-client local resource, so all three are caller-supplied — never + /// baked into the library. pub fn open( - transport: T, db_path: impl Into, db_key: impl Into, + tcp_port: u16, + preset: Option<&str>, registry_url: Option<&str>, ) -> Result<(Self, Receiver), ClientError> { + let transport = EmbeddedP2pDeliveryService::start(P2pConfig { + preset: preset.unwrap_or(NETWORK_PRESET).to_string(), + tcp_port, + ..Default::default() + }) + .map_err(|e| ClientError::Transport(e.to_string()))?; + let endpoint = registry_url.unwrap_or(REGISTRY_ENDPOINT); ChatClientBuilder::new() .ident(DelegateSigner::random())