mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-04 06:03:12 +00:00
feat: separate embedded logos client (#166)
* feat: separate embedded p2p delievery to its own crate * feat: separate p2p config in its own crate * chore: split embed module * chore: refactor registry config * feat: split logos chat crate * chore: refactor
This commit is contained in:
parent
b19d0c6e67
commit
939a63e8bc
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -20,12 +20,12 @@ jobs:
|
|||||||
- run: rustup update stable && rustup default stable
|
- run: rustup update stable && rustup default stable
|
||||||
# hashgraph-like-consensus's build.rs shells out to protoc via prost-build.
|
# hashgraph-like-consensus's build.rs shells out to protoc via prost-build.
|
||||||
- run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
|
- run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
|
||||||
# chat-cli pulls in components' embedded_p2p_delivery feature, whose
|
# chat-cli and the embedded-logos-delivery crate link liblogosdelivery
|
||||||
# build.rs links liblogosdelivery (built via Nix or LOGOS_DELIVERY_LIB_DIR).
|
# (built via Nix or LOGOS_DELIVERY_LIB_DIR). The smoketest job builds and
|
||||||
# The smoketest job builds and exercises it under Nix; here we keep the
|
# exercises them under Nix; here we keep the toolchain-only job fast by
|
||||||
# toolchain-only job fast by skipping it.
|
# skipping them.
|
||||||
- run: cargo build --verbose --workspace --exclude chat-cli
|
- run: cargo build --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery
|
||||||
- run: cargo test --verbose --workspace --exclude chat-cli
|
- run: cargo test --verbose --workspace --exclude chat-cli --exclude embedded-logos-delivery
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
name: Clippy
|
name: Clippy
|
||||||
|
|||||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -2072,6 +2072,20 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-logos-delivery"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"libchat",
|
||||||
|
"logos-generic-chat",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum-ordinalize"
|
name = "enum-ordinalize"
|
||||||
version = "4.3.2"
|
version = "4.3.2"
|
||||||
@ -3599,6 +3613,18 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "logos-chat"
|
name = "logos-chat"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"components",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"embedded-logos-delivery",
|
||||||
|
"libchat",
|
||||||
|
"logos-account",
|
||||||
|
"logos-generic-chat",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "logos-generic-chat"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chat-sqlite",
|
"chat-sqlite",
|
||||||
"components",
|
"components",
|
||||||
|
|||||||
10
Cargo.toml
10
Cargo.toml
@ -12,8 +12,10 @@ members = [
|
|||||||
"core/shared-traits",
|
"core/shared-traits",
|
||||||
"core/sqlite",
|
"core/sqlite",
|
||||||
"core/storage",
|
"core/storage",
|
||||||
"crates/client",
|
"crates/logos-chat",
|
||||||
|
"crates/generic-chat",
|
||||||
"extensions/components",
|
"extensions/components",
|
||||||
|
"extensions/embedded-logos-delivery",
|
||||||
]
|
]
|
||||||
|
|
||||||
default-members = [
|
default-members = [
|
||||||
@ -25,7 +27,7 @@ default-members = [
|
|||||||
"core/shared-traits",
|
"core/shared-traits",
|
||||||
"core/sqlite",
|
"core/sqlite",
|
||||||
"core/storage",
|
"core/storage",
|
||||||
"crates/client",
|
"crates/generic-chat",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@ -34,8 +36,10 @@ logos-account = { path = "core/account" }
|
|||||||
chat-sqlite = { path = "core/sqlite" }
|
chat-sqlite = { path = "core/sqlite" }
|
||||||
components = { path = "extensions/components" }
|
components = { path = "extensions/components" }
|
||||||
crypto = { path = "core/crypto" }
|
crypto = { path = "core/crypto" }
|
||||||
|
embedded-logos-delivery = { path = "extensions/embedded-logos-delivery" }
|
||||||
libchat = { path = "core/conversations" }
|
libchat = { path = "core/conversations" }
|
||||||
logos-chat = { path = "crates/client" }
|
logos-chat = { path = "crates/logos-chat" }
|
||||||
|
logos-generic-chat = { path = "crates/generic-chat" }
|
||||||
shared-traits = { path = "core/shared-traits" }
|
shared-traits = { path = "core/shared-traits" }
|
||||||
storage = { path = "core/storage" }
|
storage = { path = "core/storage" }
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ path = "src/main.rs"
|
|||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
logos-account = { workspace = true, features = ["dev"] }
|
logos-account = { workspace = true, features = ["dev"] }
|
||||||
logos-chat = { workspace = true, features = ["embedded-p2p-delivery"] }
|
logos-chat = { workspace = true }
|
||||||
|
|
||||||
# External dependencies (sorted)
|
# External dependencies (sorted)
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|||||||
@ -8,11 +8,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use logos_account::TestLogosAccount;
|
|
||||||
use logos_chat::{
|
use logos_chat::{
|
||||||
AccountDirectory, ChatClient, ChatClientBuilder, ChatStore, DelegateSigner, Event,
|
AccountDirectory, ChatClient, ChatStore, Event, LogosConfig, P2pConfig, RegistrationService,
|
||||||
HttpRegistry, LogosChatClient, LogosConfig, NETWORK_PRESET, REGISTRY_ENDPOINT,
|
Transport,
|
||||||
RegistrationService, StorageConfig, Transport,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use app::ChatApp;
|
use app::ChatApp;
|
||||||
@ -78,24 +76,27 @@ fn main() -> Result<()> {
|
|||||||
let db_str = db_path(&cli)?;
|
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 => {
|
TransportKind::LogosDelivery => {
|
||||||
let preset = cli.preset.as_deref().unwrap_or(NETWORK_PRESET);
|
let mut p2p_config = P2pConfig::default();
|
||||||
println!("Starting logos-delivery node (preset={preset})...");
|
if let Some(port) = cli.port {
|
||||||
|
p2p_config.tcp_port = port;
|
||||||
|
}
|
||||||
|
if let Some(preset) = cli.preset.as_deref() {
|
||||||
|
p2p_config.preset = preset.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Starting logos-delivery node (preset={})...",
|
||||||
|
p2p_config.preset
|
||||||
|
);
|
||||||
println!("This may take a few seconds while connecting to the network.");
|
println!("This may take a few seconds while connecting to the network.");
|
||||||
|
|
||||||
let mut config = LogosConfig::new(db_str, "chat-cli");
|
let mut config = LogosConfig::new(db_str, "chat-cli");
|
||||||
if let Some(port) = cli.port {
|
|
||||||
config.set_tcp_port(port);
|
|
||||||
}
|
|
||||||
if let Some(preset) = cli.preset.as_deref() {
|
|
||||||
config.set_preset(preset);
|
|
||||||
}
|
|
||||||
if let Some(registry_url) = cli.registry_url.as_deref() {
|
if let Some(registry_url) = cli.registry_url.as_deref() {
|
||||||
config.set_registry_url(registry_url);
|
config.set_registry_url(registry_url);
|
||||||
}
|
}
|
||||||
let (client, events) = LogosChatClient::open(config)
|
config.set_p2p_config(p2p_config);
|
||||||
|
let (client, events) = logos_chat::open(config)
|
||||||
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
||||||
.context("failed to open chat client")?;
|
.context("failed to open chat client")?;
|
||||||
|
|
||||||
@ -104,32 +105,17 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
// The file transport is a local-only path: it reuses the Logos service
|
// The file transport is a local-only path: it reuses the Logos service
|
||||||
// stack (delegate identity, HTTP registry, encrypted storage) but swaps
|
// stack (delegate identity, HTTP registry, encrypted storage) but swaps
|
||||||
// the transport, so it builds a client directly instead of going through
|
// the transport in via `open_with_transport`.
|
||||||
// `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")?;
|
||||||
|
|
||||||
let endpoint = cli.registry_url.as_deref().unwrap_or(REGISTRY_ENDPOINT);
|
let mut config = LogosConfig::new(db_str, "chat-cli");
|
||||||
// A fresh dev account endorsing a fresh delegate each launch,
|
if let Some(registry_url) = cli.registry_url.as_deref() {
|
||||||
// mirroring `LogosChatClient::open`.
|
config.set_registry_url(registry_url);
|
||||||
let account = TestLogosAccount::new();
|
}
|
||||||
let delegate = DelegateSigner::random();
|
let (client, events) = logos_chat::open_with_transport(config, transport)
|
||||||
let mut registry = HttpRegistry::new(endpoint);
|
|
||||||
account
|
|
||||||
.add_delegate_signer(&mut registry, delegate.public_key())
|
|
||||||
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
|
||||||
.context("failed to publish the device bundle")?;
|
|
||||||
let (client, events) = ChatClientBuilder::new(account.address())
|
|
||||||
.ident(delegate)
|
|
||||||
.transport(transport)
|
|
||||||
.registration(registry)
|
|
||||||
.storage_config(StorageConfig::Encrypted {
|
|
||||||
path: db_str,
|
|
||||||
key: "chat-cli".to_string(),
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
||||||
.context("failed to open chat client")?;
|
.context("failed to open chat client")?;
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
//! 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";
|
|
||||||
|
|
||||||
/// Default TCP port for the embedded logos-delivery node.
|
|
||||||
pub const DEFAULT_TCP_PORT: u16 = 60000;
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
//! The opinionated Logos client.
|
|
||||||
//!
|
|
||||||
//! `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.
|
|
||||||
//!
|
|
||||||
//! 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 logos_account::TestLogosAccount;
|
|
||||||
|
|
||||||
use crate::ChatClientBuilder;
|
|
||||||
use crate::client::{ChatClient, Transport};
|
|
||||||
use crate::config::{DEFAULT_TCP_PORT, NETWORK_PRESET, REGISTRY_ENDPOINT};
|
|
||||||
use crate::delegate::DelegateSigner;
|
|
||||||
use crate::errors::ClientError;
|
|
||||||
use crate::event::Event;
|
|
||||||
|
|
||||||
// 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<Vec<u8>> {
|
|
||||||
self.inbound_queue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration for opening a [`LogosChatClient`].
|
|
||||||
///
|
|
||||||
/// `db_path` (a per-client location) and `db_key` (a secret) are required and
|
|
||||||
/// never baked into the library. The TCP port and the network config default to
|
|
||||||
/// the baked-in Logos values; override them with the setters (e.g. to point at a
|
|
||||||
/// local deployment).
|
|
||||||
pub struct LogosConfig {
|
|
||||||
db_path: String,
|
|
||||||
db_key: String,
|
|
||||||
tcp_port: u16,
|
|
||||||
preset: String,
|
|
||||||
registry_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogosConfig {
|
|
||||||
/// Config for the required per-client `db_path` and `db_key`. The TCP port,
|
|
||||||
/// network preset, and registry endpoint default to the baked-in Logos
|
|
||||||
/// values; override them with [`set_tcp_port`](Self::set_tcp_port),
|
|
||||||
/// [`set_preset`](Self::set_preset), and
|
|
||||||
/// [`set_registry_url`](Self::set_registry_url).
|
|
||||||
pub fn new(db_path: impl Into<String>, db_key: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
db_path: db_path.into(),
|
|
||||||
db_key: db_key.into(),
|
|
||||||
tcp_port: DEFAULT_TCP_PORT,
|
|
||||||
preset: NETWORK_PRESET.to_string(),
|
|
||||||
registry_url: REGISTRY_ENDPOINT.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Override the TCP port for the embedded logos-delivery node.
|
|
||||||
pub fn set_tcp_port(&mut self, tcp_port: u16) {
|
|
||||||
self.tcp_port = tcp_port;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Override the logos-delivery network preset (defaults to the baked-in
|
|
||||||
/// [`NETWORK_PRESET`]).
|
|
||||||
pub fn set_preset(&mut self, preset: impl Into<String>) {
|
|
||||||
self.preset = preset.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Override the registry endpoint (account + keypackage store; defaults to
|
|
||||||
/// the baked-in [`REGISTRY_ENDPOINT`]).
|
|
||||||
pub fn set_registry_url(&mut self, registry_url: impl Into<String>) {
|
|
||||||
self.registry_url = registry_url.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [`ChatClient`] wired to the Logos service stack: a [`DelegateSigner`]
|
|
||||||
/// identity acting for a fresh dev account, the HTTP keypackage + account
|
|
||||||
/// registry ([`HttpRegistry`], which is both the keypackage store and the
|
|
||||||
/// account → device directory), encrypted [`ChatStorage`], and the
|
|
||||||
/// logos-delivery transport.
|
|
||||||
pub type LogosChatClient = ChatClient<EmbeddedP2pDeliveryService, HttpRegistry, ChatStorage>;
|
|
||||||
|
|
||||||
impl LogosChatClient {
|
|
||||||
/// Open a client on the Logos stack per `config`, starting a logos-delivery
|
|
||||||
/// node as its transport and persisting to the encrypted database.
|
|
||||||
pub fn open(config: LogosConfig) -> Result<(Self, Receiver<Event>), ClientError> {
|
|
||||||
let transport = EmbeddedP2pDeliveryService::start(P2pConfig {
|
|
||||||
preset: config.preset,
|
|
||||||
tcp_port: config.tcp_port,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.map_err(|e| ClientError::Transport(e.to_string()))?;
|
|
||||||
|
|
||||||
// A fresh account endorsing a fresh delegate each open: the account
|
|
||||||
// key is dropped after publishing the bundle, so devices cannot be
|
|
||||||
// added later. A caller-supplied, custody-holding account replaces
|
|
||||||
// this once the platform provides one.
|
|
||||||
let account = TestLogosAccount::new();
|
|
||||||
let delegate = DelegateSigner::random();
|
|
||||||
let mut registry = HttpRegistry::new(config.registry_url);
|
|
||||||
account
|
|
||||||
.add_delegate_signer(&mut registry, delegate.public_key())
|
|
||||||
.map_err(|e| ClientError::BundlePublish(e.to_string()))?;
|
|
||||||
ChatClientBuilder::new(account.address())
|
|
||||||
.ident(delegate)
|
|
||||||
.transport(transport)
|
|
||||||
.registration(registry)
|
|
||||||
.storage_config(StorageConfig::Encrypted {
|
|
||||||
path: config.db_path,
|
|
||||||
key: config.db_key,
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "logos-chat"
|
name = "logos-generic-chat"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
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 }
|
||||||
@ -1,9 +1,9 @@
|
|||||||
# message-exchange
|
# message-exchange
|
||||||
|
|
||||||
An example Rust application built on top of [`crates/client`](../../).
|
An example Rust application built on top of [`crates/generic-chat`](../../).
|
||||||
|
|
||||||
It demonstrates that creating a working chat client in pure Rust is trivial: depend on
|
It demonstrates that creating a working chat client in pure Rust is trivial: depend on
|
||||||
`crates/client`, pick a `DeliveryService` implementation (here the in-memory
|
`crates/generic-chat`, pick a `DeliveryService` implementation (here the in-memory
|
||||||
`InProcessDelivery` shipped with the crate), and wire up `ChatClient`. No boilerplate, no FFI.
|
`InProcessDelivery` shipped with the crate), and wire up `ChatClient`. No boilerplate, no FFI.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use components::EphemeralRegistry;
|
use components::EphemeralRegistry;
|
||||||
use logos_account::TestLogosAccount;
|
use logos_account::TestLogosAccount;
|
||||||
use logos_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus};
|
use logos_generic_chat::{ChatClientBuilder, Event, InProcessDelivery, MessageBus};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -1,22 +1,16 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
pub use builder::{ChatClientBuilder, Unset};
|
pub use builder::{ChatClientBuilder, Unset};
|
||||||
pub use client::{ChatClient, Transport};
|
pub use client::{ChatClient, Transport};
|
||||||
pub use config::{DEFAULT_TCP_PORT, 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, LogosConfig};
|
|
||||||
|
|
||||||
// Re-export types callers need to interact with ChatClient.
|
// Re-export types callers need to interact with ChatClient.
|
||||||
pub use libchat::{
|
pub use libchat::{
|
||||||
@ -4,7 +4,7 @@ use components::EphemeralRegistry;
|
|||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use crypto::Ed25519VerifyingKey;
|
use crypto::Ed25519VerifyingKey;
|
||||||
use logos_account::TestLogosAccount;
|
use logos_account::TestLogosAccount;
|
||||||
use logos_chat::{
|
use logos_generic_chat::{
|
||||||
AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event,
|
AddressedEnvelope, ChatClient, ChatClientBuilder, DelegateSigner, DeliveryService, Event,
|
||||||
InProcessDelivery, MessageBus, Transport,
|
InProcessDelivery, MessageBus, Transport,
|
||||||
};
|
};
|
||||||
@ -30,7 +30,7 @@ fn create_test_client(
|
|||||||
ChatClient<InProcessDelivery, EphemeralRegistry, libchat::ChatStorage>,
|
ChatClient<InProcessDelivery, EphemeralRegistry, libchat::ChatStorage>,
|
||||||
Receiver<Event>,
|
Receiver<Event>,
|
||||||
),
|
),
|
||||||
logos_chat::ClientError,
|
logos_generic_chat::ClientError,
|
||||||
> {
|
> {
|
||||||
let account = TestLogosAccount::new();
|
let account = TestLogosAccount::new();
|
||||||
let delegate = DelegateSigner::random();
|
let delegate = DelegateSigner::random();
|
||||||
@ -375,10 +375,16 @@ fn unpublished_account_address_is_an_error() {
|
|||||||
let err = saro
|
let err = saro
|
||||||
.create_direct_conversation(&unpublished.address())
|
.create_direct_conversation(&unpublished.address())
|
||||||
.expect_err("no bundle published for the account");
|
.expect_err("no bundle published for the account");
|
||||||
assert!(matches!(err, logos_chat::ClientError::AccountResolution(_)));
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
logos_generic_chat::ClientError::AccountResolution(_)
|
||||||
|
));
|
||||||
|
|
||||||
let err = saro
|
let err = saro
|
||||||
.create_direct_conversation("not-an-account-address")
|
.create_direct_conversation("not-an-account-address")
|
||||||
.expect_err("not an account key");
|
.expect_err("not an account key");
|
||||||
assert!(matches!(err, logos_chat::ClientError::AccountResolution(_)));
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
logos_generic_chat::ClientError::AccountResolution(_)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
16
crates/logos-chat/Cargo.toml
Normal file
16
crates/logos-chat/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "logos-chat"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Workspace dependencies (sorted)
|
||||||
|
components = { workspace = true }
|
||||||
|
crossbeam-channel = { workspace = true }
|
||||||
|
embedded-logos-delivery = { workspace = true }
|
||||||
|
libchat = { workspace = true }
|
||||||
|
logos-account = { workspace = true, features = ["dev"] }
|
||||||
|
logos-generic-chat = { workspace = true }
|
||||||
12
crates/logos-chat/src/lib.rs
Normal file
12
crates/logos-chat/src/lib.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
mod logos;
|
||||||
|
|
||||||
|
pub use logos::{LogosChatClient, LogosConfig, REGISTRY_ENDPOINT, open, open_with_transport};
|
||||||
|
// Facade re-exports so callers need no direct dependency on the transport
|
||||||
|
// crate.
|
||||||
|
pub use embedded_logos_delivery::{
|
||||||
|
DEFAULT_NETWORK_PRESET, DEFAULT_TCP_PORT, EmbeddedLogosDelivery, P2pConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export the transport-generic client surface so callers depend on this
|
||||||
|
// crate alone.
|
||||||
|
pub use logos_generic_chat::*;
|
||||||
117
crates/logos-chat/src/logos.rs
Normal file
117
crates/logos-chat/src/logos.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
//! The opinionated Logos client.
|
||||||
|
//!
|
||||||
|
//! [`open`] 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, and encrypted
|
||||||
|
//! on-disk storage. The stack is generic over the transport — any
|
||||||
|
//! [`Transport`] can be injected via [`open_with_transport`] — and the
|
||||||
|
//! concrete [`LogosChatClient`] commits to the embedded logos-delivery node,
|
||||||
|
//! whose native `liblogosdelivery` link keeps this crate outside the
|
||||||
|
//! workspace's default members.
|
||||||
|
//!
|
||||||
|
//! [`LogosChatClient`] points at `ChatClient`, which lives in
|
||||||
|
//! `logos-generic-chat`, so Rust's inherent-impl rule keeps the open
|
||||||
|
//! constructors off the alias; they are crate-level functions ([`open`],
|
||||||
|
//! [`open_with_transport`]) taking the all-inclusive [`LogosConfig`] instead.
|
||||||
|
|
||||||
|
use components::HttpRegistry;
|
||||||
|
use crossbeam_channel::Receiver;
|
||||||
|
use embedded_logos_delivery::{EmbeddedLogosDelivery, P2pConfig};
|
||||||
|
use libchat::{ChatStorage, StorageConfig};
|
||||||
|
use logos_account::TestLogosAccount;
|
||||||
|
|
||||||
|
use logos_generic_chat::{
|
||||||
|
ChatClient, ChatClientBuilder, ClientError, DelegateSigner, Event, Transport,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The endpoint for the account and keypackage registration service.
|
||||||
|
pub const REGISTRY_ENDPOINT: &str = "https://devnet.chat-kc.logos.co";
|
||||||
|
|
||||||
|
/// Configuration for opening a Logos client.
|
||||||
|
///
|
||||||
|
/// `db_path` (a per-client location) and `db_key` (a secret) are required and
|
||||||
|
/// never baked into the library. Everything else defaults: the registry
|
||||||
|
/// endpoint to the baked-in Logos value and the embedded node's p2p settings
|
||||||
|
/// to [`P2pConfig::default`]; override them with
|
||||||
|
/// [`set_registry_url`](Self::set_registry_url) and
|
||||||
|
/// [`set_p2p_config`](Self::set_p2p_config).
|
||||||
|
pub struct LogosConfig {
|
||||||
|
db_path: String,
|
||||||
|
db_key: String,
|
||||||
|
registry_url: String,
|
||||||
|
p2p_config: P2pConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogosConfig {
|
||||||
|
/// Config for the required per-client `db_path` and `db_key`. The registry
|
||||||
|
/// endpoint defaults to the baked-in Logos value; override it with
|
||||||
|
/// [`set_registry_url`](Self::set_registry_url).
|
||||||
|
pub fn new(db_path: impl Into<String>, db_key: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
db_path: db_path.into(),
|
||||||
|
db_key: db_key.into(),
|
||||||
|
registry_url: REGISTRY_ENDPOINT.to_string(),
|
||||||
|
p2p_config: P2pConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override the registry endpoint (account + keypackage store; defaults to
|
||||||
|
/// the baked-in [`REGISTRY_ENDPOINT`]).
|
||||||
|
pub fn set_registry_url(&mut self, registry_url: impl Into<String>) {
|
||||||
|
self.registry_url = registry_url.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override the embedded node's p2p settings (defaults to
|
||||||
|
/// [`P2pConfig::default`]). Only [`open`] starts an embedded node, so
|
||||||
|
/// [`open_with_transport`] ignores this.
|
||||||
|
pub fn set_p2p_config(&mut self, p2p_config: P2pConfig) {
|
||||||
|
self.p2p_config = p2p_config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a client on the Logos stack per `config`, starting an embedded
|
||||||
|
/// logos-delivery node per its p2p settings as the transport. A convenience
|
||||||
|
/// over [`open_with_transport`] that commits to the [`LogosChatClient`]
|
||||||
|
/// transport.
|
||||||
|
pub fn open(config: LogosConfig) -> Result<(LogosChatClient, Receiver<Event>), ClientError> {
|
||||||
|
let transport = EmbeddedLogosDelivery::start(config.p2p_config.clone())
|
||||||
|
.map_err(|e| ClientError::Transport(e.to_string()))?;
|
||||||
|
open_with_transport(config, transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a client on the Logos stack per `config` with the injected transport,
|
||||||
|
/// persisting to the encrypted database.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn open_with_transport<T: Transport>(
|
||||||
|
config: LogosConfig,
|
||||||
|
transport: T,
|
||||||
|
) -> Result<(ChatClient<T, HttpRegistry, ChatStorage>, Receiver<Event>), ClientError> {
|
||||||
|
// A fresh account endorsing a fresh delegate each open: the account
|
||||||
|
// key is dropped after publishing the bundle, so devices cannot be
|
||||||
|
// added later. A caller-supplied, custody-holding account replaces
|
||||||
|
// this once the platform provides one.
|
||||||
|
let account = TestLogosAccount::new();
|
||||||
|
let delegate = DelegateSigner::random();
|
||||||
|
let mut registry = HttpRegistry::new(config.registry_url);
|
||||||
|
account
|
||||||
|
.add_delegate_signer(&mut registry, delegate.public_key())
|
||||||
|
.map_err(|e| ClientError::BundlePublish(e.to_string()))?;
|
||||||
|
ChatClientBuilder::new(account.address())
|
||||||
|
.ident(delegate)
|
||||||
|
.transport(transport)
|
||||||
|
.registration(registry)
|
||||||
|
.storage_config(StorageConfig::Encrypted {
|
||||||
|
path: config.db_path,
|
||||||
|
key: config.db_key,
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Logos client: a [`ChatClient`] wired to the Logos service stack — a
|
||||||
|
/// [`DelegateSigner`] identity acting for a fresh dev account, the HTTP
|
||||||
|
/// keypackage + account registry ([`HttpRegistry`], which is both the
|
||||||
|
/// keypackage store and the account → device directory), and encrypted
|
||||||
|
/// [`ChatStorage`] — running an embedded logos-delivery node as its
|
||||||
|
/// transport. Open one with [`open`], or swap the transport via
|
||||||
|
/// [`open_with_transport`].
|
||||||
|
pub type LogosChatClient = ChatClient<EmbeddedLogosDelivery, HttpRegistry, ChatStorage>;
|
||||||
@ -2,10 +2,6 @@
|
|||||||
name = "components"
|
name = "components"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
links = "logosdelivery"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
embedded_p2p_delivery = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
|
|||||||
@ -1,9 +1,3 @@
|
|||||||
mod local_broadcaster;
|
mod local_broadcaster;
|
||||||
|
|
||||||
pub use local_broadcaster::LocalBroadcaster;
|
pub use local_broadcaster::LocalBroadcaster;
|
||||||
|
|
||||||
#[cfg(feature = "embedded_p2p_delivery")]
|
|
||||||
pub mod embedded_p2p_delivery;
|
|
||||||
|
|
||||||
#[cfg(feature = "embedded_p2p_delivery")]
|
|
||||||
pub use embedded_p2p_delivery::{EmbeddedP2pDeliveryService, P2pConfig};
|
|
||||||
|
|||||||
18
extensions/embedded-logos-delivery/Cargo.toml
Normal file
18
extensions/embedded-logos-delivery/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "embedded-logos-delivery"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
links = "logosdelivery"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Workspace dependencies (sorted)
|
||||||
|
crossbeam-channel = { workspace = true }
|
||||||
|
libchat = { workspace = true }
|
||||||
|
logos-generic-chat = { workspace = true }
|
||||||
|
|
||||||
|
# External dependencies (sorted)
|
||||||
|
base64 = "0.22"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "2"
|
||||||
|
tracing = "0.1"
|
||||||
@ -5,17 +5,12 @@ use std::process::Command;
|
|||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR");
|
println!("cargo:rerun-if-env-changed=LOGOS_DELIVERY_LIB_DIR");
|
||||||
|
|
||||||
if std::env::var_os("CARGO_FEATURE_EMBEDDED_P2P_DELIVERY").is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(lib_dir) = locate_lib_dir() else {
|
let Some(lib_dir) = locate_lib_dir() else {
|
||||||
println!(
|
println!(
|
||||||
"cargo:warning=embedded_p2p_delivery feature is enabled but \
|
"cargo:warning=liblogosdelivery could not be located; `cargo check`/\
|
||||||
liblogosdelivery could not be located; `cargo check`/`clippy` will \
|
`clippy` will pass, but building or testing will fail at link. Enter \
|
||||||
pass, but building or testing will fail at link. Enter the dev shell \
|
the dev shell with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to \
|
||||||
with `nix develop` or set LOGOS_DELIVERY_LIB_DIR to the directory \
|
the directory containing the library."
|
||||||
containing the library."
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -1,8 +1,12 @@
|
|||||||
//! logos-delivery backed [`client::DeliveryService`] implementation.
|
//! The embedded logos-delivery transport service.
|
||||||
//!
|
//!
|
||||||
//! `LogosDeliveryService` wraps an embedded logos-delivery node running on a
|
//! [`EmbeddedLogosDelivery`] implements [`DeliveryService`] by wrapping an
|
||||||
//! dedicated `std::thread`. All interaction is via synchronous `std::sync::mpsc`
|
//! embedded logos-delivery node running on a dedicated `std::thread`. All
|
||||||
//! channels.
|
//! interaction is via synchronous `std::sync::mpsc` channels.
|
||||||
|
//!
|
||||||
|
//! This crate links the native `liblogosdelivery` library, so it lives outside
|
||||||
|
//! the workspace's default members; depend on it (e.g. via the `logos-chat`
|
||||||
|
//! crate) only when shipping the embedded node.
|
||||||
//!
|
//!
|
||||||
//! ## Content topic mapping
|
//! ## Content topic mapping
|
||||||
//!
|
//!
|
||||||
@ -51,6 +55,12 @@ type SubscriberList = Arc<Mutex<Vec<Sender<Vec<u8>>>>>;
|
|||||||
|
|
||||||
// ── P2pConfig ───────────────────────────────────────────────────────────────────
|
// ── P2pConfig ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// The logos-delivery network preset joined by default.
|
||||||
|
pub const DEFAULT_NETWORK_PRESET: &str = "logos.dev";
|
||||||
|
|
||||||
|
/// Default TCP port for the embedded logos-delivery node.
|
||||||
|
pub const DEFAULT_TCP_PORT: u16 = 60000;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct P2pConfig {
|
pub struct P2pConfig {
|
||||||
pub preset: String,
|
pub preset: String,
|
||||||
@ -61,8 +71,8 @@ pub struct P2pConfig {
|
|||||||
impl Default for P2pConfig {
|
impl Default for P2pConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
preset: "logos.dev".into(),
|
preset: DEFAULT_NETWORK_PRESET.into(),
|
||||||
tcp_port: 60000,
|
tcp_port: DEFAULT_TCP_PORT,
|
||||||
log_level: "ERROR".into(),
|
log_level: "ERROR".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,21 +125,21 @@ impl WakuPayload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── EmbeddedP2pDeliveryService ──────────────────────────────────────────────────
|
// ── EmbeddedLogosDelivery ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/// logos-delivery backed delivery service. Cheap to clone — all clones share
|
/// logos-delivery backed delivery service. Cheap to clone — all clones share
|
||||||
/// the same background node.
|
/// the same background node.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct EmbeddedP2pDeliveryService {
|
pub struct EmbeddedLogosDelivery {
|
||||||
outbound: mpsc::SyncSender<OutboundCmd>,
|
outbound: mpsc::SyncSender<OutboundCmd>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
subscribers: SubscriberList,
|
subscribers: SubscriberList,
|
||||||
inbound_rx: Option<Receiver<Vec<u8>>>,
|
inbound_rx: Option<Receiver<Vec<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmbeddedP2pDeliveryService {
|
impl EmbeddedLogosDelivery {
|
||||||
/// Start the embedded logos-delivery node. The client drains inbound
|
/// Start the embedded logos-delivery node. The client drains inbound
|
||||||
/// payloads via [`Transport::inbound`].
|
/// payloads via [`Self::inbound_queue`].
|
||||||
pub fn start(cfg: P2pConfig) -> Result<Self, DeliveryError> {
|
pub fn start(cfg: P2pConfig) -> Result<Self, DeliveryError> {
|
||||||
let (out_tx, out_rx) = mpsc::sync_channel::<OutboundCmd>(256);
|
let (out_tx, out_rx) = mpsc::sync_channel::<OutboundCmd>(256);
|
||||||
let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new()));
|
let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new()));
|
||||||
@ -284,7 +294,7 @@ impl EmbeddedP2pDeliveryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeliveryService for EmbeddedP2pDeliveryService {
|
impl DeliveryService for EmbeddedLogosDelivery {
|
||||||
type Error = DeliveryError;
|
type Error = DeliveryError;
|
||||||
|
|
||||||
fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), DeliveryError> {
|
fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), DeliveryError> {
|
||||||
@ -312,3 +322,13 @@ impl DeliveryService for EmbeddedP2pDeliveryService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Teaching the service the inbound half makes it a full client transport, so
|
||||||
|
// callers need no wrapper newtype. The impl lives here (the crate owning the
|
||||||
|
// type) because the orphan rule bars it from the `logos-chat` crate, which
|
||||||
|
// owns neither the trait nor the type.
|
||||||
|
impl logos_generic_chat::Transport for EmbeddedLogosDelivery {
|
||||||
|
fn inbound(&mut self) -> Receiver<Vec<u8>> {
|
||||||
|
self.inbound_queue()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user