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
This commit is contained in:
kaichao 2026-07-03 02:17:45 +08:00 committed by GitHub
parent 6897281826
commit d131a69583
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 111 additions and 79 deletions

1
Cargo.lock generated
View File

@ -1315,7 +1315,6 @@ dependencies = [
"arboard", "arboard",
"base64", "base64",
"clap", "clap",
"components",
"crossbeam-channel", "crossbeam-channel",
"crossterm 0.29.0", "crossterm 0.29.0",
"logos-chat", "logos-chat",

View File

@ -9,9 +9,8 @@ path = "src/main.rs"
[dependencies] [dependencies]
# Workspace dependencies (sorted) # Workspace dependencies (sorted)
components = { workspace = true , features = ["embedded_p2p_delivery"]}
crossbeam-channel = { workspace = true } crossbeam-channel = { workspace = true }
logos-chat = { workspace = true } logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] }
# External dependencies (sorted) # External dependencies (sorted)
anyhow = "1.0" anyhow = "1.0"

View File

@ -9,30 +9,11 @@ use anyhow::{Context, Result};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use crossbeam_channel::Receiver; use crossbeam_channel::Receiver;
use logos_chat::{ 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 = <EmbeddedP2pDeliveryService as logos_chat::DeliveryService>::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<Vec<u8>> {
self.0.inbound_queue()
}
}
use app::ChatApp; use app::ChatApp;
#[derive(Copy, Clone, Debug, ValueEnum)] #[derive(Copy, Clone, Debug, ValueEnum)]
@ -62,9 +43,10 @@ struct Cli {
db: Option<PathBuf>, db: Option<PathBuf>,
// ── logos-delivery transport options ────────────────────────────────────── // ── logos-delivery transport options ──────────────────────────────────────
/// logos-delivery network preset (e.g. `logos.dev`). /// logos-delivery network preset (e.g. `logos.dev`). When omitted, the
#[arg(long, default_value = "logos.dev")] /// preconfigured network preset is used.
preset: String, #[arg(long)]
preset: Option<String>,
/// TCP port for the embedded logos-delivery node. /// TCP port for the embedded logos-delivery node.
#[arg(long, default_value_t = 60000)] #[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")?; std::fs::create_dir_all(&cli.data).context("failed to create data directory")?;
let db_str = db_path(&cli)?;
match cli.transport { 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 => { TransportKind::File => {
let transport_dir = cli.data.join("transport"); let transport_dir = cli.data.join("transport");
let transport = transport::file::FileTransport::new(&transport_dir) let transport = transport::file::FileTransport::new(&transport_dir)
.context("failed to create file transport")?; .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 { let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT);
preset: cli.preset.clone(), let (client, events) = ChatClientBuilder::new()
tcp_port: cli.port, .ident(DelegateSigner::random())
..Default::default() .transport(transport)
}; .registration(HttpRegistry::new(endpoint))
let transport = P2pTransport( .storage_config(StorageConfig::Encrypted {
EmbeddedP2pDeliveryService::start(cfg).context("failed to start logos-delivery")?, 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..."); launch_tui(client, events, &cli)
run(transport, &cli)
} }
} }
} }
fn run<T: Transport>(transport: T, cli: &Cli) -> Result<()> { /// Resolve the SQLite database path: `--db` if given, else `<data>/<name>.db`.
let db_path = cli fn db_path(cli: &Cli) -> Result<String> {
let path = cli
.db .db
.clone() .clone()
.unwrap_or_else(|| cli.data.join(format!("{}.db", cli.name))); .unwrap_or_else(|| cli.data.join(format!("{}.db", cli.name)));
let db_str = db_path Ok(path
.to_str() .to_str()
.context("db path contains non-UTF-8 characters")? .context("db path contains non-UTF-8 characters")?
.to_string(); .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)
} }
fn launch_tui<I, T, R, S>( fn launch_tui<I, T, R, S>(

View File

@ -6,6 +6,9 @@ edition = "2024"
[lib] [lib]
crate-type = ["rlib"] crate-type = ["rlib"]
[features]
embedded-p2p-delivery = ["components/embedded_p2p_delivery"]
[dependencies] [dependencies]
# Workspace dependencies (sorted) # Workspace dependencies (sorted)
chat-sqlite = { workspace = true } chat-sqlite = { workspace = true }

View File

@ -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";

View File

@ -6,4 +6,6 @@ pub enum ClientError {
Chat(#[from] ChatError), Chat(#[from] ChatError),
#[error("received credential could not be parsed")] #[error("received credential could not be parsed")]
BadlyFormedCredential, BadlyFormedCredential,
#[error("failed to start the transport: {0}")]
Transport(String),
} }

View File

@ -1,17 +1,21 @@
mod builder; mod builder;
mod client; mod client;
mod config;
mod delegate; mod delegate;
mod delivery_in_process; mod delivery_in_process;
mod errors; mod errors;
mod event; mod event;
#[cfg(feature = "embedded-p2p-delivery")]
mod logos; mod logos;
pub use builder::{ChatClientBuilder, Unset}; pub use builder::{ChatClientBuilder, Unset};
pub use client::{ChatClient, Transport}; pub use client::{ChatClient, Transport};
pub use config::{NETWORK_PRESET, REGISTRY_ENDPOINT};
pub use delegate::DelegateSigner; pub use delegate::DelegateSigner;
pub use delivery_in_process::{InProcessDelivery, MessageBus}; pub use delivery_in_process::{InProcessDelivery, MessageBus};
pub use errors::ClientError; pub use errors::ClientError;
pub use event::{Event, MessageSender}; pub use event::{Event, MessageSender};
#[cfg(feature = "embedded-p2p-delivery")]
pub use logos::LogosChatClient; pub use logos::LogosChatClient;
// Re-export types callers need to interact with ChatClient. // Re-export types callers need to interact with ChatClient.

View File

@ -1,52 +1,69 @@
//! The opinionated Logos client. //! The opinionated Logos client.
//! //!
//! [`ChatClientBuilder`] is generic and can only default the zero-config //! `LogosChatClient` commits to the Logos service stack so independently built
//! components (random identity, ephemeral registry, in-memory storage) — it has //! clients share the same production services instead of each re-deriving them:
//! no way to know a registry endpoint or a database path, so its defaults are //! a delegate identity, the HTTP keypackage + account registry, encrypted
//! the test-grade ones. `LogosChatClient` is the layer that *does* commit to a //! on-disk storage, and — unlike the generic [`ChatClientBuilder`] — the
//! stack: a delegate identity, the HTTP keypackage + account registry, and //! logos-delivery transport itself. logos-delivery *is* the transport for the
//! encrypted on-disk storage. It exists so independently built clients share the //! Logos client, so the caller no longer supplies one; only per-client secrets
//! same production services instead of each re-deriving them. //! (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 //! The logos-delivery transport carries a native dependency, so this whole
//! environment-specific configuration that belong to the binary, not here. //! 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 crossbeam_channel::Receiver;
use libchat::{ChatStorage, StorageConfig}; use libchat::{ChatStorage, StorageConfig};
use crate::ChatClientBuilder; use crate::ChatClientBuilder;
use crate::client::{ChatClient, Transport}; use crate::client::{ChatClient, Transport};
use crate::config::{NETWORK_PRESET, REGISTRY_ENDPOINT};
use crate::delegate::DelegateSigner; use crate::delegate::DelegateSigner;
use crate::errors::ClientError; use crate::errors::ClientError;
use crate::event::Event; use crate::event::Event;
use components::HttpRegistry;
// The endpoint for account and keypackage registration service. // logos-delivery already implements `DeliveryService`; teaching it the inbound
const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co"; // 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<Vec<u8>> {
self.inbound_queue()
}
}
/// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`] /// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`]
/// identity, the HTTP keypackage + account registry ([`HttpRegistry`], which is /// identity, the HTTP keypackage + account registry ([`HttpRegistry`], which is
/// both the keypackage store and the account → device directory), and encrypted /// both the keypackage store and the account → device directory), encrypted
/// [`ChatStorage`]. Only the transport `T` is supplied by the caller. /// [`ChatStorage`], and the logos-delivery transport.
pub type LogosChatClient<T> = ChatClient<DelegateSigner, T, HttpRegistry, ChatStorage>; pub type LogosChatClient =
ChatClient<DelegateSigner, EmbeddedP2pDeliveryService, HttpRegistry, ChatStorage>;
impl<T> LogosChatClient<T> impl LogosChatClient {
where /// Open a client on the Logos stack, starting a logos-delivery node on
T: Transport + Send + 'static, /// `tcp_port` as its transport and persisting to the encrypted database at
{ /// `db_path` unlocked with `db_key`. When `preset`/`registry_url` are `Some`,
/// Open a client on the Logos stack over `transport`, persisting to the /// they override the baked-in network preset/registry endpoint (e.g. a local
/// encrypted database at `db_path` unlocked with `db_key`. When `registry_url` /// deployment); otherwise the preconfigured values are used.
/// is `Some`, it overrides the preconfigured registry endpoint (e.g. a local
/// deployment); otherwise the baked-in endpoint is used.
/// ///
/// `db_path` is a per-client location and `db_key` is a secret, so both are /// `db_path` is a per-client location, `db_key` is a secret, and `tcp_port`
/// caller-supplied — never baked into the library. /// is a per-client local resource, so all three are caller-supplied — never
/// baked into the library.
pub fn open( pub fn open(
transport: T,
db_path: impl Into<String>, db_path: impl Into<String>,
db_key: impl Into<String>, db_key: impl Into<String>,
tcp_port: u16,
preset: Option<&str>,
registry_url: Option<&str>, registry_url: Option<&str>,
) -> Result<(Self, Receiver<Event>), ClientError> { ) -> Result<(Self, Receiver<Event>), 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); let endpoint = registry_url.unwrap_or(REGISTRY_ENDPOINT);
ChatClientBuilder::new() ChatClientBuilder::new()
.ident(DelegateSigner::random()) .ident(DelegateSigner::random())