mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-11 09:33:11 +00:00
Add Delegate Signer and wire into Client (#143)
* Add encoded_credential to CovnoOutcome * Add DelegateSigner * Add test for DirectV1 * Add support for undecodable credentials * Add docs * Clean + fixes * clippy fixes * Add unit tests * Update trait bounds
This commit is contained in:
parent
1c984f442c
commit
d02689c764
115
Cargo.lock
generated
115
Cargo.lock
generated
@ -774,7 +774,7 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1814,7 +1814,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "de-mls"
|
name = "de-mls"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
source = "git+https://github.com/vacp2p/de-mls?branch=develop#d838e832994fd1d14f624783741bc60b31510fa0"
|
source = "git+https://github.com/vacp2p/de-mls?branch=develop#2dfcd8c71668856e8d7027968c2c64d87ec7fea7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashgraph-like-consensus",
|
"hashgraph-like-consensus",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
@ -2104,7 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3601,9 +3601,12 @@ dependencies = [
|
|||||||
"chat-sqlite",
|
"chat-sqlite",
|
||||||
"components",
|
"components",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
"crypto",
|
||||||
|
"hex",
|
||||||
"libchat",
|
"libchat",
|
||||||
"logos-account",
|
"logos-account",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"shared-traits",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
@ -3703,7 +3706,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4534,7 +4537,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5028,7 +5031,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5086,7 +5089,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5696,7 +5699,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6394,7 +6397,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6468,7 +6471,7 @@ version = "0.52.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6477,16 +6480,7 @@ version = "0.59.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.60.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.53.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6504,31 +6498,14 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu",
|
||||||
"windows_i686_gnullvm 0.52.6",
|
"windows_i686_gnullvm",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.53.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
"windows_aarch64_gnullvm 0.53.1",
|
|
||||||
"windows_aarch64_msvc 0.53.1",
|
|
||||||
"windows_i686_gnu 0.53.1",
|
|
||||||
"windows_i686_gnullvm 0.53.1",
|
|
||||||
"windows_i686_msvc 0.53.1",
|
|
||||||
"windows_x86_64_gnu 0.53.1",
|
|
||||||
"windows_x86_64_gnullvm 0.53.1",
|
|
||||||
"windows_x86_64_msvc 0.53.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6537,96 +6514,48 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use logos_chat::{ChatClient, DeliveryService, EphemeralRegistry, Event, RegistrationService};
|
use logos_chat::{ChatClient, EphemeralRegistry, Event, RegistrationService, Transport};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::utils::now;
|
use crate::utils::now;
|
||||||
@ -41,7 +41,7 @@ pub struct AppState {
|
|||||||
pub active_chat: Option<String>,
|
pub active_chat: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChatApp<T: DeliveryService, R: RegistrationService = EphemeralRegistry> {
|
pub struct ChatApp<T: Transport, R: RegistrationService = EphemeralRegistry> {
|
||||||
pub client: ChatClient<T, R>,
|
pub client: ChatClient<T, R>,
|
||||||
events: Receiver<Event>,
|
events: Receiver<Event>,
|
||||||
pub state: AppState,
|
pub state: AppState,
|
||||||
@ -55,7 +55,7 @@ pub struct ChatApp<T: DeliveryService, R: RegistrationService = EphemeralRegistr
|
|||||||
|
|
||||||
impl<T, R> ChatApp<T, R>
|
impl<T, R> ChatApp<T, R>
|
||||||
where
|
where
|
||||||
T: DeliveryService + Send + 'static,
|
T: Transport,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|||||||
@ -8,9 +8,7 @@ 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_chat::{
|
use logos_chat::{ChatClient, Event, HttpRegistry, RegistrationService, StorageConfig, Transport};
|
||||||
ChatClient, DeliveryService, Event, HttpRegistry, RegistrationService, StorageConfig, Transport,
|
|
||||||
};
|
|
||||||
|
|
||||||
use app::ChatApp;
|
use app::ChatApp;
|
||||||
|
|
||||||
@ -132,7 +130,7 @@ fn run<T: Transport>(transport: T, cli: &Cli) -> Result<()> {
|
|||||||
|
|
||||||
fn launch_tui<T, R>(client: ChatClient<T, R>, events: Receiver<Event>, cli: &Cli) -> Result<()>
|
fn launch_tui<T, R>(client: ChatClient<T, R>, events: Receiver<Event>, cli: &Cli) -> Result<()>
|
||||||
where
|
where
|
||||||
T: DeliveryService + Send + 'static,
|
T: Transport,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + Send + 'static,
|
||||||
{
|
{
|
||||||
let mut app = ChatApp::new(client, events, &cli.name, &cli.data)?;
|
let mut app = ChatApp::new(client, events, &cli.name, &cli.data)?;
|
||||||
@ -213,7 +211,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> {
|
|||||||
|
|
||||||
fn run_app<T, R>(terminal: &mut ui::Tui, app: &mut ChatApp<T, R>) -> Result<()>
|
fn run_app<T, R>(terminal: &mut ui::Tui, app: &mut ChatApp<T, R>) -> Result<()>
|
||||||
where
|
where
|
||||||
T: DeliveryService + Send + 'static,
|
T: Transport,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + Send + 'static,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ use ratatui::{
|
|||||||
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
||||||
};
|
};
|
||||||
|
|
||||||
use logos_chat::{DeliveryService, RegistrationService};
|
use logos_chat::{RegistrationService, Transport};
|
||||||
|
|
||||||
use crate::app::ChatApp;
|
use crate::app::ChatApp;
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ pub fn restore() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the UI.
|
/// Draw the UI.
|
||||||
pub fn draw<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
pub fn draw<D: Transport, R: RegistrationService + Send + 'static>(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &ChatApp<D, R>,
|
app: &ChatApp<D, R>,
|
||||||
) {
|
) {
|
||||||
@ -58,7 +58,7 @@ pub fn draw<D: DeliveryService + Send + 'static, R: RegistrationService + Send +
|
|||||||
draw_status(frame, app, chunks[3]);
|
draw_status(frame, app, chunks[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_header<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
fn draw_header<D: Transport, R: RegistrationService + Send + 'static>(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &ChatApp<D, R>,
|
app: &ChatApp<D, R>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
@ -85,7 +85,7 @@ fn draw_header<D: DeliveryService + Send + 'static, R: RegistrationService + Sen
|
|||||||
frame.render_widget(header, area);
|
frame.render_widget(header, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_messages<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
fn draw_messages<D: Transport, R: RegistrationService + Send + 'static>(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &ChatApp<D, R>,
|
app: &ChatApp<D, R>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
@ -175,7 +175,7 @@ fn draw_messages<D: DeliveryService + Send + 'static, R: RegistrationService + S
|
|||||||
frame.render_stateful_widget(messages_widget, area, &mut list_state);
|
frame.render_stateful_widget(messages_widget, area, &mut list_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_input<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
fn draw_input<D: Transport, R: RegistrationService + Send + 'static>(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &ChatApp<D, R>,
|
app: &ChatApp<D, R>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
@ -206,7 +206,7 @@ fn draw_input<D: DeliveryService + Send + 'static, R: RegistrationService + Send
|
|||||||
frame.set_cursor_position((cursor_x, area.y + 1));
|
frame.set_cursor_position((cursor_x, area.y + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
fn draw_status<D: Transport, R: RegistrationService + Send + 'static>(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &ChatApp<D, R>,
|
app: &ChatApp<D, R>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
@ -220,10 +220,7 @@ fn draw_status<D: DeliveryService + Send + 'static, R: RegistrationService + Sen
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle keyboard events.
|
/// Handle keyboard events.
|
||||||
pub fn handle_events<
|
pub fn handle_events<D: Transport, R: RegistrationService + Send + 'static>(
|
||||||
D: DeliveryService + Send + 'static,
|
|
||||||
R: RegistrationService + Send + 'static,
|
|
||||||
>(
|
|
||||||
app: &mut ChatApp<D, R>,
|
app: &mut ChatApp<D, R>,
|
||||||
) -> io::Result<bool> {
|
) -> io::Result<bool> {
|
||||||
// Poll for events with a short timeout to allow checking incoming messages
|
// Poll for events with a short timeout to allow checking incoming messages
|
||||||
|
|||||||
@ -255,12 +255,15 @@ impl<S: ExternalServices> Convo<S> for GroupV1Convo {
|
|||||||
.process_message(&cx.mls_provider, protocol_message)
|
.process_message(&cx.mls_provider, protocol_message)
|
||||||
.map_err(ChatError::generic)?;
|
.map_err(ChatError::generic)?;
|
||||||
|
|
||||||
|
let cred_bytes = processed.credential().serialized_content().to_vec();
|
||||||
|
|
||||||
let content = match processed.into_content() {
|
let content = match processed.into_content() {
|
||||||
ProcessedMessageContent::ApplicationMessage(msg) => {
|
ProcessedMessageContent::ApplicationMessage(msg) => {
|
||||||
let reliable = ReliablePayload::decode(msg.into_bytes().as_slice())?;
|
let reliable = ReliablePayload::decode(msg.into_bytes().as_slice())?;
|
||||||
cx.causal.on_receive(&self.convo_id, &reliable);
|
cx.causal.on_receive(&self.convo_id, &reliable);
|
||||||
Some(Content {
|
Some(Content {
|
||||||
bytes: reliable.content.to_vec(),
|
bytes: reliable.content.to_vec(),
|
||||||
|
encoded_credential: cred_bytes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ProcessedMessageContent::StagedCommitMessage(commit) => {
|
ProcessedMessageContent::StagedCommitMessage(commit) => {
|
||||||
|
|||||||
@ -184,7 +184,7 @@ impl GroupV2Convo {
|
|||||||
pub fn new<S: ExternalServices>(
|
pub fn new<S: ExternalServices>(
|
||||||
service_ctx: &mut ServiceContext<S>,
|
service_ctx: &mut ServiceContext<S>,
|
||||||
) -> Result<Self, ChatError> {
|
) -> Result<Self, ChatError> {
|
||||||
let setup = DemlsSetup::new(service_ctx.mls_identity.display_name())?;
|
let setup = DemlsSetup::new(service_ctx.mls_identity.id().as_str().to_string())?;
|
||||||
let convo_id = rand_string(5);
|
let convo_id = rand_string(5);
|
||||||
let conversation = Conversation::create(&convo_id, setup.deps())?;
|
let conversation = Conversation::create(&convo_id, setup.deps())?;
|
||||||
let convo = GroupV2Convo {
|
let convo = GroupV2Convo {
|
||||||
@ -205,7 +205,7 @@ impl GroupV2Convo {
|
|||||||
pub fn new_pending<S: ExternalServices>(
|
pub fn new_pending<S: ExternalServices>(
|
||||||
service_ctx: &mut ServiceContext<S>,
|
service_ctx: &mut ServiceContext<S>,
|
||||||
) -> Result<Self, ChatError> {
|
) -> Result<Self, ChatError> {
|
||||||
let name = service_ctx.mls_identity.display_name();
|
let name = service_ctx.mls_identity.id().as_str().to_string();
|
||||||
let setup = DemlsSetup::new(name.clone())?;
|
let setup = DemlsSetup::new(name.clone())?;
|
||||||
let kp = setup.factory.generate_key_package()?;
|
let kp = setup.factory.generate_key_package()?;
|
||||||
|
|
||||||
@ -458,12 +458,16 @@ impl GroupV2Convo {
|
|||||||
events.iter().find_map(|evt| match evt {
|
events.iter().find_map(|evt| match evt {
|
||||||
ConversationEvent::AppMessage(AppMessageProto {
|
ConversationEvent::AppMessage(AppMessageProto {
|
||||||
payload: Some(app_message::Payload::ConversationMessage(cm)),
|
payload: Some(app_message::Payload::ConversationMessage(cm)),
|
||||||
}) => Some(ConvoOutcome {
|
}) => {
|
||||||
convo_id: self.convo_id.clone(),
|
let cred = cm.sender.as_bytes().to_vec();
|
||||||
content: Some(Content {
|
Some(ConvoOutcome {
|
||||||
bytes: cm.message.clone(),
|
convo_id: self.convo_id.clone(),
|
||||||
}),
|
content: Some(Content {
|
||||||
}),
|
bytes: cm.message.clone(),
|
||||||
|
encoded_credential: cred,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,6 +197,7 @@ impl PrivateV1Convo {
|
|||||||
fn handle_content(&self, bytes: Bytes) -> Content {
|
fn handle_content(&self, bytes: Bytes) -> Content {
|
||||||
Content {
|
Content {
|
||||||
bytes: bytes.into(),
|
bytes: bytes.into(),
|
||||||
|
encoded_credential: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,4 +31,4 @@ pub use service_traits::{DeliveryService, RegistrationService, WakeupService};
|
|||||||
pub use shared_traits::{IdentId, IdentIdRef, IdentityProvider};
|
pub use shared_traits::{IdentId, IdentIdRef, IdentityProvider};
|
||||||
pub use storage::ConversationKind;
|
pub use storage::ConversationKind;
|
||||||
pub use types::AddressedEnvelope;
|
pub use types::AddressedEnvelope;
|
||||||
pub use utils::hex_trunc;
|
pub use utils::{hex_trunc, trunc};
|
||||||
|
|||||||
@ -13,6 +13,9 @@ use crate::conversation::ConversationId;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Content {
|
pub struct Content {
|
||||||
pub bytes: Vec<u8>,
|
pub bytes: Vec<u8>,
|
||||||
|
/// Hex-encoded [`DelegateCredential`] of the sender, if present in the message.
|
||||||
|
/// Empty when the sender did not attach a credential.
|
||||||
|
pub encoded_credential: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@ -71,3 +71,19 @@ pub fn hex_trunc(data: &[u8]) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn trunc(data: &str) -> String {
|
||||||
|
if data.chars().count() <= 8 {
|
||||||
|
return data.to_string();
|
||||||
|
}
|
||||||
|
let head: String = data.chars().take(4).collect();
|
||||||
|
let tail: String = data
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.take(4)
|
||||||
|
.collect::<String>()
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.collect();
|
||||||
|
format!("{head}..{tail}")
|
||||||
|
}
|
||||||
|
|||||||
@ -11,10 +11,13 @@ crate-type = ["rlib"]
|
|||||||
chat-sqlite = { workspace = true }
|
chat-sqlite = { workspace = true }
|
||||||
components = { workspace = true }
|
components = { workspace = true }
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
|
crypto = { workspace = true }
|
||||||
libchat = { workspace = true }
|
libchat = { workspace = true }
|
||||||
logos-account = { workspace = true, features = ["dev"]}
|
logos-account = { workspace = true, features = ["dev"]}
|
||||||
|
shared-traits = { workspace = true }
|
||||||
|
|
||||||
# External dependencies (sorted)
|
# External dependencies (sorted)
|
||||||
|
hex = "0.4.3"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|||||||
@ -7,13 +7,13 @@ use libchat::{
|
|||||||
ChatError, ChatStorage, ConversationId, ConvoOutcome, Core, DeliveryService, IdentId,
|
ChatError, ChatStorage, ConversationId, ConvoOutcome, Core, DeliveryService, IdentId,
|
||||||
IdentIdRef, InboxOutcome, Introduction, PayloadOutcome, RegistrationService, StorageConfig,
|
IdentIdRef, InboxOutcome, Introduction, PayloadOutcome, RegistrationService, StorageConfig,
|
||||||
};
|
};
|
||||||
use logos_account::TestLogosAccount;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
use crate::delegate::{DelegateCredential, DelegateSigner};
|
||||||
use crate::errors::ClientError;
|
use crate::errors::ClientError;
|
||||||
use crate::event::Event;
|
use crate::event::Event;
|
||||||
|
|
||||||
type ClientCore<T, R> = Core<(TestLogosAccount, T, R, ThreadedWakeupService, ChatStorage)>;
|
type ClientCore<T, R> = Core<(DelegateSigner, T, R, ThreadedWakeupService, ChatStorage)>;
|
||||||
type AccountAddressRef<'a> = &'a str;
|
type AccountAddressRef<'a> = &'a str;
|
||||||
type LocalSignerId = IdentId;
|
type LocalSignerId = IdentId;
|
||||||
|
|
||||||
@ -51,13 +51,14 @@ pub struct ChatClient<T: DeliveryService, R: RegistrationService = EphemeralRegi
|
|||||||
|
|
||||||
impl<T: Transport> ChatClient<T, EphemeralRegistry> {
|
impl<T: Transport> ChatClient<T, EphemeralRegistry> {
|
||||||
/// Create an in-memory, ephemeral client. Identity is lost on drop.
|
/// Create an in-memory, ephemeral client. Identity is lost on drop.
|
||||||
pub fn new(name: impl Into<String>, mut transport: T) -> (Self, Receiver<Event>) {
|
pub fn new(_: impl Into<String>, mut transport: T) -> (Self, Receiver<Event>) {
|
||||||
let inbound = transport.inbound();
|
let inbound = transport.inbound();
|
||||||
let ident = TestLogosAccount::new(name);
|
let delegate = DelegateSigner::random();
|
||||||
|
|
||||||
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
||||||
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
||||||
let core = Core::new_with_name(
|
let core = Core::new_with_name(
|
||||||
ident,
|
delegate,
|
||||||
transport,
|
transport,
|
||||||
EphemeralRegistry::new(),
|
EphemeralRegistry::new(),
|
||||||
wakeup_service,
|
wakeup_service,
|
||||||
@ -72,17 +73,17 @@ impl<T: Transport> ChatClient<T, EphemeralRegistry> {
|
|||||||
/// If an identity already exists in storage it is loaded; otherwise a new
|
/// If an identity already exists in storage it is loaded; otherwise a new
|
||||||
/// one is created and saved.
|
/// one is created and saved.
|
||||||
pub fn open(
|
pub fn open(
|
||||||
name: impl Into<String>,
|
_: impl Into<String>,
|
||||||
config: StorageConfig,
|
config: StorageConfig,
|
||||||
mut transport: T,
|
mut transport: T,
|
||||||
) -> Result<(Self, Receiver<Event>), ClientError> {
|
) -> Result<(Self, Receiver<Event>), ClientError> {
|
||||||
let store = ChatStorage::new(config).map_err(ChatError::from)?;
|
let store = ChatStorage::new(config).map_err(ChatError::from)?;
|
||||||
let inbound = transport.inbound();
|
let inbound = transport.inbound();
|
||||||
let ident = TestLogosAccount::new(name);
|
let delegate = DelegateSigner::random();
|
||||||
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
||||||
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
||||||
let core = Core::new_from_store(
|
let core = Core::new_from_store(
|
||||||
ident,
|
delegate,
|
||||||
transport,
|
transport,
|
||||||
EphemeralRegistry::new(),
|
EphemeralRegistry::new(),
|
||||||
wakeup_service,
|
wakeup_service,
|
||||||
@ -96,7 +97,7 @@ impl<T: Transport> ChatClient<T, EphemeralRegistry> {
|
|||||||
|
|
||||||
impl<T, R> ChatClient<T, R>
|
impl<T, R> ChatClient<T, R>
|
||||||
where
|
where
|
||||||
T: DeliveryService + Send + 'static,
|
T: Transport + Send + 'static,
|
||||||
R: RegistrationService + Send + 'static,
|
R: RegistrationService + Send + 'static,
|
||||||
{
|
{
|
||||||
/// Open or create a persistent client with a caller-supplied registration
|
/// Open or create a persistent client with a caller-supplied registration
|
||||||
@ -108,7 +109,7 @@ where
|
|||||||
/// when a real registry is wired in we want each session to publish so
|
/// when a real registry is wired in we want each session to publish so
|
||||||
/// other clients can fetch it.
|
/// other clients can fetch it.
|
||||||
pub fn open_with_registry(
|
pub fn open_with_registry(
|
||||||
name: impl Into<String>,
|
_: impl Into<String>,
|
||||||
config: StorageConfig,
|
config: StorageConfig,
|
||||||
mut transport: T,
|
mut transport: T,
|
||||||
registry: R,
|
registry: R,
|
||||||
@ -118,14 +119,34 @@ where
|
|||||||
{
|
{
|
||||||
let store = ChatStorage::new(config).map_err(ChatError::from)?;
|
let store = ChatStorage::new(config).map_err(ChatError::from)?;
|
||||||
let inbound = transport.inbound();
|
let inbound = transport.inbound();
|
||||||
let ident = TestLogosAccount::new(name);
|
let delegate = DelegateSigner::random();
|
||||||
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
||||||
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
||||||
let mut core = Core::new_from_store(ident, transport, registry, wakeup_service, store)?;
|
let mut core = Core::new_from_store(delegate, transport, registry, wakeup_service, store)?;
|
||||||
core.register_keypackage()?;
|
core.register_keypackage()?;
|
||||||
Ok(Self::spawn(core, inbound, wakeup_rx))
|
Ok(Self::spawn(core, inbound, wakeup_rx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a client with ephemeral storage with the provided Transport and RegistrationService.
|
||||||
|
pub fn new_ephemeral(
|
||||||
|
delegate: DelegateSigner,
|
||||||
|
mut transport: T,
|
||||||
|
reg: R,
|
||||||
|
) -> Result<(Self, Receiver<Event>), ClientError> {
|
||||||
|
let inbound = transport.inbound();
|
||||||
|
|
||||||
|
let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded();
|
||||||
|
let wakeup_service = ThreadedWakeupService::new(wakeup_tx);
|
||||||
|
let core = Core::new_with_name(
|
||||||
|
delegate,
|
||||||
|
transport,
|
||||||
|
reg,
|
||||||
|
wakeup_service,
|
||||||
|
ChatStorage::in_memory(),
|
||||||
|
)?;
|
||||||
|
Ok(Self::spawn(core, inbound, wakeup_rx))
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn(
|
fn spawn(
|
||||||
core: ClientCore<T, R>,
|
core: ClientCore<T, R>,
|
||||||
inbound: Receiver<Vec<u8>>,
|
inbound: Receiver<Vec<u8>>,
|
||||||
@ -287,12 +308,24 @@ fn events_from_inbound(result: PayloadOutcome) -> Vec<Event> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_credential(encoded: Vec<u8>) {
|
||||||
|
if let Ok(data) = hex::decode(encoded)
|
||||||
|
&& let Ok(cred) = DelegateCredential::try_from(data)
|
||||||
|
{
|
||||||
|
tracing::debug!(?cred, "decoded sender credential");
|
||||||
|
// TODO: Integration Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn convo_events(outcome: ConvoOutcome) -> Vec<Event> {
|
fn convo_events(outcome: ConvoOutcome) -> Vec<Event> {
|
||||||
let ConvoOutcome { convo_id, content } = outcome;
|
let ConvoOutcome { convo_id, content } = outcome;
|
||||||
content
|
content
|
||||||
.map(|c| Event::MessageReceived {
|
.map(|c| {
|
||||||
convo_id: Arc::from(convo_id),
|
decode_credential(c.encoded_credential);
|
||||||
content: c.bytes,
|
Event::MessageReceived {
|
||||||
|
convo_id: Arc::from(convo_id),
|
||||||
|
content: c.bytes,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect()
|
.collect()
|
||||||
@ -310,6 +343,7 @@ fn inbox_events(outcome: InboxOutcome) -> Vec<Event> {
|
|||||||
class: new_conversation.class,
|
class: new_conversation.class,
|
||||||
});
|
});
|
||||||
if let Some(c) = initial.and_then(|co| co.content) {
|
if let Some(c) = initial.and_then(|co| co.content) {
|
||||||
|
decode_credential(c.encoded_credential);
|
||||||
events.push(Event::MessageReceived {
|
events.push(Event::MessageReceived {
|
||||||
convo_id: Arc::clone(&id),
|
convo_id: Arc::clone(&id),
|
||||||
content: c.bytes,
|
content: c.bytes,
|
||||||
|
|||||||
290
crates/client/src/delegate.rs
Normal file
290
crates/client/src/delegate.rs
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
|
||||||
|
use libchat::{IdentId, IdentityProvider, trunc};
|
||||||
|
|
||||||
|
use crate::ClientError;
|
||||||
|
|
||||||
|
type AccountAddr = String;
|
||||||
|
|
||||||
|
/// A local signing identity that holds an Ed25519 keypair.
|
||||||
|
///
|
||||||
|
/// Can be standalone (unassociated) or authorized to act on behalf of an account
|
||||||
|
/// via [`DelegateSigner::associate`].
|
||||||
|
pub struct DelegateSigner {
|
||||||
|
signing_key: Ed25519SigningKey,
|
||||||
|
verifying_key: Ed25519VerifyingKey,
|
||||||
|
identifier: IdentId,
|
||||||
|
account_addr: Option<AccountAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DelegateSigner {
|
||||||
|
/// Create a new signer with a randomly generated keypair.
|
||||||
|
pub fn random() -> Self {
|
||||||
|
let signing_key = Ed25519SigningKey::generate();
|
||||||
|
let verifying_key = signing_key.verifying_key();
|
||||||
|
let identifier = DelegateCredential::unassociated(&verifying_key).into();
|
||||||
|
Self {
|
||||||
|
signing_key,
|
||||||
|
verifying_key,
|
||||||
|
identifier,
|
||||||
|
account_addr: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Associate a DelegateSigner with an Account.
|
||||||
|
pub fn associate(&mut self, account_addr: AccountAddr) {
|
||||||
|
self.identifier =
|
||||||
|
DelegateCredential::associated(&self.verifying_key, account_addr.as_str()).into();
|
||||||
|
self.account_addr = Some(account_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn account_addr(&self) -> Option<&str> {
|
||||||
|
self.account_addr.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdentityProvider for DelegateSigner {
|
||||||
|
fn id(&self) -> libchat::IdentIdRef<'_> {
|
||||||
|
&self.identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> String {
|
||||||
|
trunc(self.identifier.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
|
||||||
|
self.signing_key.sign(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_key(&self) -> &Ed25519VerifyingKey {
|
||||||
|
&self.verifying_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A credential issued to a delegate key, optionally bound to an account address.
|
||||||
|
///
|
||||||
|
/// Serialized as a TLV byte sequence prefixed with magic bytes `0x23 0x23`.
|
||||||
|
/// A credential without an `account_addr` is *unassociated* — it identifies the
|
||||||
|
/// delegate key but has not yet been linked to an account.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DelegateCredential {
|
||||||
|
delegate_id: Ed25519VerifyingKey,
|
||||||
|
account_addr: Option<AccountAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DelegateCredential {
|
||||||
|
const TAG_DELEGATE_ID: u8 = 0x01;
|
||||||
|
const TAG_ACCOUNT_ADDR: u8 = 0x02;
|
||||||
|
|
||||||
|
pub fn unassociated(delegate: &Ed25519VerifyingKey) -> Self {
|
||||||
|
Self {
|
||||||
|
delegate_id: delegate.clone(),
|
||||||
|
account_addr: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn associated(delegate: &Ed25519VerifyingKey, account: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
delegate_id: delegate.clone(),
|
||||||
|
account_addr: Some(account.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(self) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.extend_from_slice(&[0x23, 0x23]);
|
||||||
|
let key_bytes = self.delegate_id.as_ref();
|
||||||
|
debug_assert!(
|
||||||
|
key_bytes.len() <= 255,
|
||||||
|
"delegate_id too large for 1-byte TLV length"
|
||||||
|
);
|
||||||
|
data.extend_from_slice(&[Self::TAG_DELEGATE_ID, key_bytes.len() as u8]);
|
||||||
|
data.extend_from_slice(key_bytes);
|
||||||
|
if let Some(addr) = self.account_addr {
|
||||||
|
let addr_bytes = addr.as_bytes();
|
||||||
|
debug_assert!(
|
||||||
|
addr_bytes.len() <= 255,
|
||||||
|
"account_addr too large for 1-byte TLV length"
|
||||||
|
);
|
||||||
|
data.extend_from_slice(&[Self::TAG_ACCOUNT_ADDR, addr_bytes.len() as u8]);
|
||||||
|
data.extend_from_slice(addr_bytes);
|
||||||
|
}
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DelegateCredential> for Vec<u8> {
|
||||||
|
fn from(value: DelegateCredential) -> Self {
|
||||||
|
value.serialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for DelegateCredential {
|
||||||
|
type Error = ClientError;
|
||||||
|
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
if value.get(..2) != Some(&[0x23, 0x23]) {
|
||||||
|
return Err(ClientError::BadlyFormedCredential);
|
||||||
|
}
|
||||||
|
let mut delegate_id = None;
|
||||||
|
let mut account_addr = None;
|
||||||
|
let mut i = 2;
|
||||||
|
while i + 2 <= value.len() {
|
||||||
|
let tag = value[i];
|
||||||
|
let len = value[i + 1] as usize;
|
||||||
|
i += 2;
|
||||||
|
let v = value
|
||||||
|
.get(i..i + len)
|
||||||
|
.ok_or(ClientError::BadlyFormedCredential)?;
|
||||||
|
i += len;
|
||||||
|
match tag {
|
||||||
|
DelegateCredential::TAG_DELEGATE_ID => {
|
||||||
|
let bytes: &[u8; 32] = v
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ClientError::BadlyFormedCredential)?;
|
||||||
|
delegate_id = Some(
|
||||||
|
Ed25519VerifyingKey::from_bytes(bytes)
|
||||||
|
.map_err(|_| ClientError::BadlyFormedCredential)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DelegateCredential::TAG_ACCOUNT_ADDR => {
|
||||||
|
account_addr = Some(
|
||||||
|
String::from_utf8(v.to_vec())
|
||||||
|
.map_err(|_| ClientError::BadlyFormedCredential)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
delegate_id: delegate_id.ok_or(ClientError::BadlyFormedCredential)?,
|
||||||
|
account_addr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DelegateCredential> for IdentId {
|
||||||
|
fn from(value: DelegateCredential) -> Self {
|
||||||
|
IdentId::new(hex::encode(value.serialize()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<IdentId> for DelegateCredential {
|
||||||
|
type Error = ClientError;
|
||||||
|
|
||||||
|
fn try_from(value: IdentId) -> Result<Self, Self::Error> {
|
||||||
|
hex::decode(value.as_str())
|
||||||
|
.map_err(|_| ClientError::BadlyFormedCredential)?
|
||||||
|
.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crypto::Ed25519SigningKey;
|
||||||
|
|
||||||
|
fn test_key() -> Ed25519VerifyingKey {
|
||||||
|
Ed25519SigningKey::generate().verifying_key()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_unassociated() {
|
||||||
|
let key = test_key();
|
||||||
|
let bytes = DelegateCredential::unassociated(&key).serialize();
|
||||||
|
let recovered: DelegateCredential = bytes.clone().try_into().unwrap();
|
||||||
|
assert_eq!(recovered.serialize(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_associated() {
|
||||||
|
let key = test_key();
|
||||||
|
let bytes = DelegateCredential::associated(&key, "user@example.com").serialize();
|
||||||
|
let recovered: DelegateCredential = bytes.clone().try_into().unwrap();
|
||||||
|
assert_eq!(recovered.serialize(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ident_id_roundtrip_unassociated() {
|
||||||
|
let key = test_key();
|
||||||
|
let original = DelegateCredential::unassociated(&key).serialize();
|
||||||
|
let ident_id: IdentId = DelegateCredential::unassociated(&key).into();
|
||||||
|
let recovered: DelegateCredential = ident_id.try_into().unwrap();
|
||||||
|
assert_eq!(recovered.serialize(), original);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ident_id_roundtrip_associated() {
|
||||||
|
let key = test_key();
|
||||||
|
let addr = "user@example.com";
|
||||||
|
let original = DelegateCredential::associated(&key, addr).serialize();
|
||||||
|
let ident_id: IdentId = DelegateCredential::associated(&key, addr).into();
|
||||||
|
let recovered: DelegateCredential = ident_id.try_into().unwrap();
|
||||||
|
assert_eq!(recovered.serialize(), original);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_addr_preserved_across_roundtrip() {
|
||||||
|
let key = test_key();
|
||||||
|
let addr = "alice@libchat.example";
|
||||||
|
let recovered: DelegateCredential = DelegateCredential::associated(&key, addr)
|
||||||
|
.serialize()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(recovered.account_addr.as_deref(), Some(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unassociated_has_no_account_after_roundtrip() {
|
||||||
|
let key = test_key();
|
||||||
|
let recovered: DelegateCredential = DelegateCredential::unassociated(&key)
|
||||||
|
.serialize()
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
assert!(recovered.account_addr.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_magic_bytes_rejected() {
|
||||||
|
let bytes = vec![0x00, 0x00, 0x01, 0x20];
|
||||||
|
assert!(matches!(
|
||||||
|
DelegateCredential::try_from(bytes),
|
||||||
|
Err(ClientError::BadlyFormedCredential)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncated_payload_rejected() {
|
||||||
|
// Magic + TAG_DELEGATE_ID + len=32, but only 16 bytes of key data
|
||||||
|
let mut bytes = vec![0x23, 0x23, 0x01, 32];
|
||||||
|
bytes.extend_from_slice(&[0u8; 16]);
|
||||||
|
assert!(matches!(
|
||||||
|
DelegateCredential::try_from(bytes),
|
||||||
|
Err(ClientError::BadlyFormedCredential)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_delegate_id_rejected() {
|
||||||
|
// Valid magic but no TLV fields
|
||||||
|
let bytes = vec![0x23, 0x23];
|
||||||
|
assert!(matches!(
|
||||||
|
DelegateCredential::try_from(bytes),
|
||||||
|
Err(ClientError::BadlyFormedCredential)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_utf8_account_addr_rejected() {
|
||||||
|
let key = test_key();
|
||||||
|
// Build a valid credential then corrupt the account_addr bytes
|
||||||
|
let mut bytes = DelegateCredential::unassociated(&key).serialize();
|
||||||
|
// Append a TAG_ACCOUNT_ADDR field with invalid UTF-8
|
||||||
|
bytes.push(DelegateCredential::TAG_ACCOUNT_ADDR);
|
||||||
|
bytes.push(3); // len
|
||||||
|
bytes.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // invalid UTF-8
|
||||||
|
assert!(matches!(
|
||||||
|
DelegateCredential::try_from(bytes),
|
||||||
|
Err(ClientError::BadlyFormedCredential)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,4 +4,6 @@ use libchat::ChatError;
|
|||||||
pub enum ClientError {
|
pub enum ClientError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Chat(#[from] ChatError),
|
Chat(#[from] ChatError),
|
||||||
|
#[error("received credential could not be parsed")]
|
||||||
|
BadlyFormedCredential,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
mod client;
|
mod client;
|
||||||
|
mod delegate;
|
||||||
mod delivery_in_process;
|
mod delivery_in_process;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod event;
|
mod event;
|
||||||
|
|
||||||
pub use client::{ChatClient, Transport};
|
pub use client::{ChatClient, Transport};
|
||||||
|
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;
|
pub use event::Event;
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use components::EphemeralRegistry;
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
|
use libchat::IdentityProvider;
|
||||||
|
use logos_account::TestLogosAccount;
|
||||||
use logos_chat::{
|
use logos_chat::{
|
||||||
AddressedEnvelope, ChatClient, DeliveryService, Event, InProcessDelivery, MessageBus,
|
AddressedEnvelope, ChatClient, DelegateSigner, DeliveryService, Event, InProcessDelivery,
|
||||||
StorageConfig, Transport,
|
MessageBus, StorageConfig, Transport,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Block until the next event arrives and matches; panic on timeout/mismatch.
|
/// Block until the next event arrives and matches; panic on timeout/mismatch.
|
||||||
@ -17,6 +20,52 @@ where
|
|||||||
f(event).unwrap_or_else(|other| panic!("expected {label}, got {other:?}"))
|
f(event).unwrap_or_else(|other| panic!("expected {label}, got {other:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direct_v1_integration() {
|
||||||
|
let bus = MessageBus::default();
|
||||||
|
let saro_delivery = InProcessDelivery::new(bus.clone());
|
||||||
|
let raya_delivery = InProcessDelivery::new(bus);
|
||||||
|
|
||||||
|
let reg_service = EphemeralRegistry::new();
|
||||||
|
|
||||||
|
// Create Accounts, Deletage and Associate the two.
|
||||||
|
let saro_account = TestLogosAccount::new("Saro");
|
||||||
|
let mut saro_delegate = DelegateSigner::random();
|
||||||
|
// TODO: Submit Delegate to Account for auth.
|
||||||
|
saro_delegate.associate(saro_account.id().to_string());
|
||||||
|
|
||||||
|
let raya_account = TestLogosAccount::new("Raya");
|
||||||
|
let mut raya_delegate = DelegateSigner::random();
|
||||||
|
// TODO: Submit Delegate to Account for auth.
|
||||||
|
raya_delegate.associate(raya_account.id().to_string());
|
||||||
|
let raya_delegate_id = raya_delegate.id().clone();
|
||||||
|
|
||||||
|
let (mut saro, _saro_events) =
|
||||||
|
ChatClient::new_ephemeral(saro_delegate, saro_delivery, reg_service.clone()).unwrap();
|
||||||
|
let (_raya, raya_events) =
|
||||||
|
ChatClient::new_ephemeral(raya_delegate, raya_delivery, reg_service.clone()).unwrap();
|
||||||
|
|
||||||
|
let convo_id = saro
|
||||||
|
.create_direct_conversation(raya_delegate_id.as_str())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The invite payload yields ConversationStarted then MessageReceived.
|
||||||
|
expect_event(&raya_events, "ConversationStarted", |e| match e {
|
||||||
|
Event::ConversationStarted { convo_id, .. } => Ok(convo_id),
|
||||||
|
other => Err(other),
|
||||||
|
});
|
||||||
|
|
||||||
|
saro.send_message(&convo_id, b"Hey from saro")
|
||||||
|
.expect("payload mismatch");
|
||||||
|
expect_event(&raya_events, "MessageReceived", |e| match e {
|
||||||
|
Event::MessageReceived { content, .. } => {
|
||||||
|
assert_eq!(content.as_slice(), b"Hey from saro");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
other => Err(other),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn saro_raya_message_exchange() {
|
fn saro_raya_message_exchange() {
|
||||||
let bus = MessageBus::default();
|
let bus = MessageBus::default();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user