mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-07-15 20:39:31 +00:00
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:
parent
6897281826
commit
d131a69583
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>(
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
7
crates/client/src/config.rs
Normal file
7
crates/client/src/config.rs
Normal 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";
|
||||||
@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user