Private
Public Access
1
0

client: implements event/updates websocket

This commit is contained in:
2025-05-01 18:07:18 -07:00
parent 13a78ccd47
commit f6ac3b5a58
14 changed files with 561 additions and 67 deletions

224
Cargo.lock generated
View File

@@ -181,6 +181,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.16.0"
@@ -189,9 +198,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.0" version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
@@ -297,6 +306,25 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]] [[package]]
name = "csv" name = "csv"
version = "1.3.1" version = "1.3.1"
@@ -363,6 +391,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]] [[package]]
name = "dbus" name = "dbus"
version = "0.9.7" version = "0.9.7"
@@ -471,6 +505,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "directories" name = "directories"
version = "6.0.0" version = "6.0.0"
@@ -657,12 +701,23 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-macro", "futures-macro",
"futures-sink",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
] ]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.14" version = "0.2.14"
@@ -671,7 +726,19 @@ checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
] ]
[[package]] [[package]]
@@ -691,7 +758,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http 0.2.12",
"indexmap", "indexmap",
"slab", "slab",
"tokio", "tokio",
@@ -743,6 +810,17 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.6" version = "0.4.6"
@@ -750,7 +828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http 0.2.12",
"pin-project-lite", "pin-project-lite",
] ]
@@ -777,7 +855,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http", "http 0.2.12",
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
@@ -907,6 +985,7 @@ dependencies = [
"chrono", "chrono",
"ctor", "ctor",
"env_logger", "env_logger",
"futures-util",
"hyper", "hyper",
"hyper-tls", "hyper-tls",
"log", "log",
@@ -915,6 +994,8 @@ dependencies = [
"serde_plain", "serde_plain",
"time", "time",
"tokio", "tokio",
"tokio-tungstenite",
"tungstenite",
"uuid", "uuid",
] ]
@@ -967,6 +1048,8 @@ dependencies = [
"dbus-codegen", "dbus-codegen",
"dbus-tree", "dbus-tree",
"dotenv", "dotenv",
"env_logger",
"futures-util",
"kordophone", "kordophone",
"kordophone-db", "kordophone-db",
"log", "log",
@@ -1083,7 +1166,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [ dependencies = [
"hermit-abi 0.3.9", "hermit-abi 0.3.9",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@@ -1297,6 +1380,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -1304,8 +1393,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
] ]
[[package]] [[package]]
@@ -1315,7 +1414,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
] ]
[[package]] [[package]]
@@ -1324,7 +1433,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.14",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.2",
] ]
[[package]] [[package]]
@@ -1342,7 +1460,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.14",
"libredox", "libredox",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@@ -1353,7 +1471,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.14",
"libredox", "libredox",
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
@@ -1505,6 +1623,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@@ -1713,6 +1842,18 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.10" version = "0.7.10"
@@ -1792,12 +1933,35 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.9.1",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]] [[package]]
name = "typed-arena" name = "typed-arena"
version = "2.0.2" version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"
@@ -1810,6 +1974,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
@@ -1822,8 +1992,8 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.14",
"rand", "rand 0.8.5",
"uuid-macro-internal", "uuid-macro-internal",
] ]
@@ -1850,6 +2020,12 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -1865,6 +2041,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.95" version = "0.2.95"
@@ -2108,6 +2293,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.5.0",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.25" version = "0.8.25"

View File

@@ -11,6 +11,7 @@ base64 = "0.22.1"
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
ctor = "0.2.8" ctor = "0.2.8"
env_logger = "0.11.5" env_logger = "0.11.5"
futures-util = "0.3.31"
hyper = { version = "0.14", features = ["full"] } hyper = { version = "0.14", features = ["full"] }
hyper-tls = "0.5.0" hyper-tls = "0.5.0"
log = { version = "0.4.21", features = [] } log = { version = "0.4.21", features = [] }
@@ -19,4 +20,6 @@ serde_json = "1.0.91"
serde_plain = "1.0.2" serde_plain = "1.0.2"
time = { version = "0.3.17", features = ["parsing", "serde"] } time = { version = "0.3.17", features = ["parsing", "serde"] }
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.37.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
tungstenite = "0.26.2"
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] } uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }

View File

@@ -0,0 +1,45 @@
use crate::api::Credentials;
use crate::api::JwtToken;
use async_trait::async_trait;
#[async_trait]
pub trait AuthenticationStore {
async fn get_credentials(&mut self) -> Option<Credentials>;
async fn get_token(&mut self) -> Option<JwtToken>;
async fn set_token(&mut self, token: JwtToken);
}
pub struct InMemoryAuthenticationStore {
credentials: Option<Credentials>,
token: Option<JwtToken>,
}
impl Default for InMemoryAuthenticationStore {
fn default() -> Self {
Self::new(None)
}
}
impl InMemoryAuthenticationStore {
pub fn new(credentials: Option<Credentials>) -> Self {
Self {
credentials,
token: None,
}
}
}
#[async_trait]
impl AuthenticationStore for InMemoryAuthenticationStore {
async fn get_credentials(&mut self) -> Option<Credentials> {
self.credentials.clone()
}
async fn get_token(&mut self) -> Option<JwtToken> {
self.token.clone()
}
async fn set_token(&mut self, token: JwtToken) {
self.token = Some(token);
}
}

View File

@@ -0,0 +1,17 @@
use async_trait::async_trait;
use crate::model::update::UpdateItem;
use crate::model::event::Event;
use futures_util::stream::Stream;
#[async_trait]
pub trait EventSocket {
type Error;
type EventStream: Stream<Item = Result<Event, Self::Error>>;
type UpdateStream: Stream<Item = Result<Vec<UpdateItem>, Self::Error>>;
/// Modern event pipeline
async fn events(self) -> Self::EventStream;
/// Raw update items from the v1 API.
async fn raw_updates(self) -> Self::UpdateStream;
}

View File

@@ -4,13 +4,25 @@ extern crate serde;
use std::{path::PathBuf, str}; use std::{path::PathBuf, str};
use crate::api::AuthenticationStore; use crate::api::AuthenticationStore;
use crate::api::event_socket::EventSocket;
use hyper::{Body, Client, Method, Request, Uri}; use hyper::{Body, Client, Method, Request, Uri};
use async_trait::async_trait; use async_trait::async_trait;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::net::TcpStream;
use futures_util::{StreamExt, TryStreamExt};
use futures_util::stream::{SplitStream, SplitSink, TryFilterMap, MapErr, Stream};
use futures_util::stream::Map;
use futures_util::stream::BoxStream;
use std::future::Future;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use crate::{ use crate::{
model::{Conversation, ConversationID, JwtToken, Message, MessageID}, model::{Conversation, ConversationID, JwtToken, Message, MessageID, UpdateItem, Event},
APIInterface APIInterface
}; };
@@ -63,6 +75,12 @@ impl From <serde_json::Error> for Error {
} }
} }
impl From <tungstenite::Error> for Error {
fn from(err: tungstenite::Error) -> Error {
Error::ClientError(err.to_string())
}
}
trait AuthBuilder { trait AuthBuilder {
fn with_auth(self, token: &Option<JwtToken>) -> Self; fn with_auth(self, token: &Option<JwtToken>) -> Self;
} }
@@ -90,6 +108,58 @@ impl<B> AuthSetting for hyper::http::Request<B> {
} }
} }
type WebsocketSink = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, tungstenite::Message>;
type WebsocketStream = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
pub struct WebsocketEventSocket {
_sink: WebsocketSink,
stream: WebsocketStream,
}
impl WebsocketEventSocket {
pub fn new(socket: WebSocketStream<MaybeTlsStream<TcpStream>>) -> Self {
let (sink, stream) = socket.split();
Self { _sink: sink, stream }
}
}
impl WebsocketEventSocket {
fn raw_update_stream(self) -> impl Stream<Item = Result<Vec<UpdateItem>, Error>> {
self.stream
.map_err(Error::from)
.try_filter_map(|msg| async move {
match msg {
tungstenite::Message::Text(text) => {
serde_json::from_str::<Vec<UpdateItem>>(&text)
.map(Some)
.map_err(Error::from)
}
_ => Ok(None)
}
})
}
}
#[async_trait]
impl EventSocket for WebsocketEventSocket {
type Error = Error;
type EventStream = BoxStream<'static, Result<Event, Error>>;
type UpdateStream = BoxStream<'static, Result<Vec<UpdateItem>, Error>>;
async fn events(self) -> Self::EventStream {
use futures_util::stream::iter;
self.raw_update_stream()
.map_ok(|updates| iter(updates.into_iter().map(|update| Ok(Event::from(update)))))
.try_flatten()
.boxed()
}
async fn raw_updates(self) -> Self::UpdateStream {
self.raw_update_stream().boxed()
}
}
#[async_trait] #[async_trait]
impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> { impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
type Error = Error; type Error = Error;
@@ -146,6 +216,44 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
let messages: Vec<Message> = self.request(&endpoint, Method::GET).await?; let messages: Vec<Message> = self.request(&endpoint, Method::GET).await?;
Ok(messages) Ok(messages)
} }
async fn open_event_socket(&mut self) -> Result<WebsocketEventSocket, Self::Error> {
use tungstenite::http::StatusCode;
use tungstenite::handshake::client::Request as TungsteniteRequest;
use tungstenite::handshake::client::generate_key;
let uri = self.uri_for_endpoint("updates", Some(self.websocket_scheme()));
log::debug!("Connecting to websocket: {:?}", uri);
let auth = self.auth_store.get_token().await;
let host = uri.authority().unwrap().host();
let mut request = TungsteniteRequest::builder()
.header("Host", host)
.header("Connection", "Upgrade")
.header("Upgrade", "websocket")
.header("Sec-WebSocket-Version", "13")
.header("Sec-WebSocket-Key", generate_key())
.uri(uri.to_string())
.body(())
.expect("Unable to build websocket request");
log::debug!("Websocket request: {:?}", request);
if let Some(token) = &auth {
let header_value = token.to_header_value().to_str().unwrap().parse().unwrap(); // ugh
request.headers_mut().insert("Authorization", header_value);
}
let (socket, response) = connect_async(request).await.unwrap();
log::debug!("Websocket connected: {:?}", response.status());
if response.status() != StatusCode::SWITCHING_PROTOCOLS {
return Err(Error::ClientError("Websocket connection failed".into()));
}
Ok(WebsocketEventSocket::new(socket))
}
} }
impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> { impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
@@ -157,15 +265,27 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
} }
} }
fn uri_for_endpoint(&self, endpoint: &str) -> Uri { fn uri_for_endpoint(&self, endpoint: &str, scheme: Option<&str>) -> Uri {
let mut parts = self.base_url.clone().into_parts(); let mut parts = self.base_url.clone().into_parts();
let root_path: PathBuf = parts.path_and_query.unwrap().path().into(); let root_path: PathBuf = parts.path_and_query.unwrap().path().into();
let path = root_path.join(endpoint); let path = root_path.join(endpoint);
parts.path_and_query = Some(path.to_str().unwrap().parse().unwrap()); parts.path_and_query = Some(path.to_str().unwrap().parse().unwrap());
if let Some(scheme) = scheme {
parts.scheme = Some(scheme.parse().unwrap());
}
Uri::try_from(parts).unwrap() Uri::try_from(parts).unwrap()
} }
fn websocket_scheme(&self) -> &str {
if self.base_url.scheme().unwrap() == "https" {
"wss"
} else {
"ws"
}
}
async fn request<T: DeserializeOwned>(&mut self, endpoint: &str, method: Method) -> Result<T, Error> { async fn request<T: DeserializeOwned>(&mut self, endpoint: &str, method: Method) -> Result<T, Error> {
self.request_with_body(endpoint, method, || { Body::empty() }).await self.request_with_body(endpoint, method, || { Body::empty() }).await
} }
@@ -188,7 +308,7 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
{ {
use hyper::StatusCode; use hyper::StatusCode;
let uri = self.uri_for_endpoint(endpoint); let uri = self.uri_for_endpoint(endpoint, None);
log::debug!("Requesting {:?} {:?}", method, uri); log::debug!("Requesting {:?} {:?}", method, uri);
let build_request = move |auth: &Option<JwtToken>| { let build_request = move |auth: &Option<JwtToken>| {
@@ -320,4 +440,18 @@ mod test {
let messages = client.get_messages(&conversation.guid, None, None, None).await.unwrap(); let messages = client.get_messages(&conversation.guid, None, None, None).await.unwrap();
assert!(!messages.is_empty()); assert!(!messages.is_empty());
} }
#[tokio::test]
async fn test_updates() {
if !mock_client_is_reachable().await {
log::warn!("Skipping http_client tests (mock server not reachable)");
return;
}
let mut client = local_mock_client();
// We just want to see if the connection is established, we won't wait for any events
let _ = client.open_event_socket().await.unwrap();
assert!(true);
}
} }

View File

@@ -2,11 +2,18 @@ use async_trait::async_trait;
pub use crate::model::{ pub use crate::model::{
Conversation, Message, ConversationID, MessageID, Conversation, Message, ConversationID, MessageID,
}; };
pub mod auth;
pub use crate::api::auth::{AuthenticationStore, InMemoryAuthenticationStore};
use crate::model::JwtToken; use crate::model::JwtToken;
pub mod http_client; pub mod http_client;
pub use http_client::HTTPAPIClient; pub use http_client::HTTPAPIClient;
pub mod event_socket;
pub use event_socket::EventSocket;
use self::http_client::Credentials; use self::http_client::Credentials;
#[async_trait] #[async_trait]
@@ -30,46 +37,7 @@ pub trait APIInterface {
// (POST) /authenticate // (POST) /authenticate
async fn authenticate(&mut self, credentials: Credentials) -> Result<JwtToken, Self::Error>; async fn authenticate(&mut self, credentials: Credentials) -> Result<JwtToken, Self::Error>;
}
#[async_trait] // (WS) /updates
pub trait AuthenticationStore { async fn open_event_socket(&mut self) -> Result<impl EventSocket, Self::Error>;
async fn get_credentials(&mut self) -> Option<Credentials>;
async fn get_token(&mut self) -> Option<JwtToken>;
async fn set_token(&mut self, token: JwtToken);
}
pub struct InMemoryAuthenticationStore {
credentials: Option<Credentials>,
token: Option<JwtToken>,
}
impl Default for InMemoryAuthenticationStore {
fn default() -> Self {
Self::new(None)
}
}
impl InMemoryAuthenticationStore {
pub fn new(credentials: Option<Credentials>) -> Self {
Self {
credentials,
token: None,
}
}
}
#[async_trait]
impl AuthenticationStore for InMemoryAuthenticationStore {
async fn get_credentials(&mut self) -> Option<Credentials> {
self.credentials.clone()
}
async fn get_token(&mut self) -> Option<JwtToken> {
self.token.clone()
}
async fn set_token(&mut self, token: JwtToken) {
self.token = Some(token);
}
} }

View File

@@ -0,0 +1,17 @@
use crate::model::{Conversation, Message, UpdateItem};
#[derive(Debug, Clone)]
pub enum Event {
ConversationChanged(Conversation),
MessageReceived(Conversation, Message),
}
impl From<UpdateItem> for Event {
fn from(update: UpdateItem) -> Self {
match update {
UpdateItem { conversation: Some(conversation), message: None, .. } => Event::ConversationChanged(conversation),
UpdateItem { conversation: Some(conversation), message: Some(message), .. } => Event::MessageReceived(conversation, message),
_ => panic!("Invalid update item: {:?}", update),
}
}
}

View File

@@ -1,5 +1,7 @@
pub mod conversation; pub mod conversation;
pub mod event;
pub mod message; pub mod message;
pub mod update;
pub use conversation::Conversation; pub use conversation::Conversation;
pub use conversation::ConversationID; pub use conversation::ConversationID;
@@ -7,6 +9,10 @@ pub use conversation::ConversationID;
pub use message::Message; pub use message::Message;
pub use message::MessageID; pub use message::MessageID;
pub use update::UpdateItem;
pub use event::Event;
pub mod jwt; pub mod jwt;
pub use jwt::JwtToken; pub use jwt::JwtToken;

View File

@@ -0,0 +1,21 @@
use serde::Deserialize;
use super::conversation::Conversation;
use super::message::Message;
#[derive(Debug, Clone, Deserialize)]
pub struct UpdateItem {
#[serde(rename = "messageSequenceNumber")]
pub seq: u64,
#[serde(rename = "conversation")]
pub conversation: Option<Conversation>,
#[serde(rename = "message")]
pub message: Option<Message>,
}
impl Default for UpdateItem {
fn default() -> Self {
Self { seq: 0, conversation: None, message: None }
}
}

View File

@@ -4,9 +4,13 @@ use std::collections::HashMap;
pub use crate::APIInterface; pub use crate::APIInterface;
use crate::{ use crate::{
api::http_client::Credentials, api::http_client::Credentials,
model::{Conversation, ConversationID, JwtToken, Message, MessageID} model::{Conversation, ConversationID, JwtToken, Message, MessageID, UpdateItem, Event},
api::event_socket::EventSocket,
}; };
use futures_util::StreamExt;
use futures_util::stream::BoxStream;
pub struct TestClient { pub struct TestClient {
pub version: &'static str, pub version: &'static str,
pub conversations: Vec<Conversation>, pub conversations: Vec<Conversation>,
@@ -28,6 +32,32 @@ impl TestClient {
} }
} }
pub struct TestEventSocket {
pub events: Vec<Event>,
}
impl TestEventSocket {
pub fn new() -> Self {
Self { events: vec![] }
}
}
#[async_trait]
impl EventSocket for TestEventSocket {
type Error = TestError;
type EventStream = BoxStream<'static, Result<Event, TestError>>;
type UpdateStream = BoxStream<'static, Result<Vec<UpdateItem>, TestError>>;
async fn events(self) -> Self::EventStream {
futures_util::stream::iter(self.events.into_iter().map(Ok)).boxed()
}
async fn raw_updates(self) -> Self::UpdateStream {
let results: Vec<Result<Vec<UpdateItem>, TestError>> = vec![];
futures_util::stream::iter(results.into_iter()).boxed()
}
}
#[async_trait] #[async_trait]
impl APIInterface for TestClient { impl APIInterface for TestClient {
type Error = TestError; type Error = TestError;
@@ -57,4 +87,10 @@ impl APIInterface for TestClient {
Err(TestError::ConversationNotFound) Err(TestError::ConversationNotFound)
} }
async fn open_event_socket(&mut self) -> Result<impl EventSocket, Self::Error> {
Ok(TestEventSocket::new())
} }
}

View File

@@ -54,9 +54,6 @@ impl DbusRepository for ServerImpl {
fn get_conversations(&mut self) -> Result<Vec<arg::PropMap>, dbus::MethodErr> { fn get_conversations(&mut self) -> Result<Vec<arg::PropMap>, dbus::MethodErr> {
self.send_event_sync(Event::GetAllConversations) self.send_event_sync(Event::GetAllConversations)
.map(|conversations| { .map(|conversations| {
// Convert conversations to DBus property maps
conversations.into_iter().map(|conv| { conversations.into_iter().map(|conv| {
let mut map = arg::PropMap::new(); let mut map = arg::PropMap::new();
map.insert("guid".into(), arg::Variant(Box::new(conv.guid))); map.insert("guid".into(), arg::Variant(Box::new(conv.guid)));
@@ -87,8 +84,6 @@ impl DbusRepository for ServerImpl {
self.send_event_sync(|r| Event::GetMessages(conversation_id, last_message_id_opt, r)) self.send_event_sync(|r| Event::GetMessages(conversation_id, last_message_id_opt, r))
.map(|messages| { .map(|messages| {
messages.into_iter().map(|msg| { messages.into_iter().map(|msg| {
let mut map = arg::PropMap::new(); let mut map = arg::PropMap::new();
map.insert("id".into(), arg::Variant(Box::new(msg.id))); map.insert("id".into(), arg::Variant(Box::new(msg.id)));

View File

@@ -11,6 +11,8 @@ clap = { version = "4.5.20", features = ["derive"] }
dbus = "0.9.7" dbus = "0.9.7"
dbus-tree = "0.9.2" dbus-tree = "0.9.2"
dotenv = "0.15.0" dotenv = "0.15.0"
env_logger = "0.11.8"
futures-util = "0.3.31"
kordophone = { path = "../kordophone" } kordophone = { path = "../kordophone" }
kordophone-db = { path = "../kordophone-db" } kordophone-db = { path = "../kordophone-db" }
log = "0.4.22" log = "0.4.22"

View File

@@ -2,10 +2,14 @@ use kordophone::APIInterface;
use kordophone::api::http_client::HTTPAPIClient; use kordophone::api::http_client::HTTPAPIClient;
use kordophone::api::http_client::Credentials; use kordophone::api::http_client::Credentials;
use kordophone::api::InMemoryAuthenticationStore; use kordophone::api::InMemoryAuthenticationStore;
use kordophone::api::event_socket::EventSocket;
use anyhow::Result; use anyhow::Result;
use clap::Subcommand; use clap::Subcommand;
use crate::printers::{ConversationPrinter, MessagePrinter}; use crate::printers::{ConversationPrinter, MessagePrinter};
use kordophone::model::event::Event;
use futures_util::StreamExt;
pub fn make_api_client_from_env() -> HTTPAPIClient<InMemoryAuthenticationStore> { pub fn make_api_client_from_env() -> HTTPAPIClient<InMemoryAuthenticationStore> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
@@ -37,6 +41,12 @@ pub enum Commands {
/// Prints the server Kordophone version. /// Prints the server Kordophone version.
Version, Version,
/// Prints all events from the server.
Events,
/// Prints all raw updates from the server.
RawUpdates,
} }
impl Commands { impl Commands {
@@ -46,6 +56,8 @@ impl Commands {
Commands::Version => client.print_version().await, Commands::Version => client.print_version().await,
Commands::Conversations => client.print_conversations().await, Commands::Conversations => client.print_conversations().await,
Commands::Messages { conversation_id } => client.print_messages(conversation_id).await, Commands::Messages { conversation_id } => client.print_messages(conversation_id).await,
Commands::RawUpdates => client.print_raw_updates().await,
Commands::Events => client.print_events().await,
} }
} }
} }
@@ -82,6 +94,35 @@ impl ClientCli {
} }
Ok(()) Ok(())
} }
pub async fn print_events(&mut self) -> Result<()> {
let socket = self.api.open_event_socket().await?;
let mut stream = socket.events().await;
while let Some(Ok(event)) = stream.next().await {
match event {
Event::ConversationChanged(conversation) => {
println!("Conversation changed: {}", conversation.guid);
}
Event::MessageReceived(conversation, message) => {
println!("Message received: msg: {} conversation: {}", message.guid, conversation.guid);
}
}
}
Ok(())
}
pub async fn print_raw_updates(&mut self) -> Result<()> {
let socket = self.api.open_event_socket().await?;
println!("Listening for raw updates...");
let mut stream = socket.raw_updates().await;
while let Some(update) = stream.next().await {
println!("Got update: {:?}", update);
}
Ok(())
}
} }

View File

@@ -5,6 +5,7 @@ mod daemon;
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use log::LevelFilter;
/// A command line interface for the Kordophone library and daemon /// A command line interface for the Kordophone library and daemon
#[derive(Parser)] #[derive(Parser)]
@@ -43,8 +44,22 @@ async fn run_command(command: Commands) -> Result<()> {
} }
} }
fn initialize_logging() {
// Weird: is this the best way to do this?
let log_level = std::env::var("RUST_LOG")
.map(|s| s.parse::<LevelFilter>().unwrap_or(LevelFilter::Info))
.unwrap_or(LevelFilter::Info);
env_logger::Builder::from_default_env()
.format_timestamp_secs()
.filter_level(log_level)
.init();
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
initialize_logging();
let cli = Cli::parse(); let cli = Cli::parse();
run_command(cli.command).await run_command(cli.command).await