From 41c9c9ae88efff0376f86066cbaefc542897c37b Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Sun, 18 Jan 2026 09:39:52 -0500 Subject: [PATCH] wip: high-level server component API --- rust-analyzer.toml | 7 ++ xmpp/Cargo.toml | 8 +- xmpp/comile.sh | 1 + xmpp/compile.sh | 1 + xmpp/src/component.rs | 249 ++++++++++++++++++++++++++++++++++++++++++ xmpp/src/lib.rs | 3 + 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 rust-analyzer.toml create mode 100644 xmpp/comile.sh create mode 100755 xmpp/compile.sh create mode 100644 xmpp/src/component.rs diff --git a/rust-analyzer.toml b/rust-analyzer.toml new file mode 100644 index 0000000000000000000000000000000000000000..014a7cf80d7b9cc0cb2e38c9167f22fd38c5f3a4 --- /dev/null +++ b/rust-analyzer.toml @@ -0,0 +1,7 @@ +[cargo] +features = [ + "xmpp/component", + "xmpp/insecure-tcp", + "tokio-xmpp/component", + "tokio-xmpp/insecure-tcp", +] diff --git a/xmpp/Cargo.toml b/xmpp/Cargo.toml index 79ee241ad3c7e8dd2b11b0536e00020ff41d7cb0..2bcc78c440ed380d7494e2fdad9a819b3e13d3a5 100644 --- a/xmpp/Cargo.toml +++ b/xmpp/Cargo.toml @@ -21,7 +21,8 @@ log = "0.4" reqwest = { version = "0.12", features = ["stream"], default-features = false } tokio-util = { version = "0.7", features = ["codec"] } # same repository dependencies -tokio-xmpp = { version = "5.0", path = "../tokio-xmpp", default-features = false } +tokio-xmpp = { version = "5.0", features = ["component", "insecure-tcp"], path = "../tokio-xmpp", default-features = false } +thiserror = "2.0" [dev-dependencies] env_logger = { version = "0.11", default-features = false, features = ["auto-color", "humantime"] } @@ -31,7 +32,7 @@ name = "hello_bot" required-features = ["avatars"] [features] -default = ["avatars", "aws_lc_rs", "starttls", "rustls-native-certs"] +default = ["avatars", "aws_lc_rs", "starttls", "rustls-native-certs", "component", "insecure-tcp"] aws_lc_rs = ["rustls-any-backend", "tokio-xmpp/aws_lc_rs", "reqwest/rustls-tls-no-provider"] ring = ["rustls-any-backend", "tokio-xmpp/ring", "reqwest/rustls-tls-no-provider"] @@ -45,6 +46,9 @@ webpki-roots = ["tokio-xmpp/webpki-roots"] starttls = ["tokio-xmpp/starttls"] +insecure-tcp = ["tokio-xmpp/insecure-tcp"] +component = ["tokio-xmpp/component"] + avatars = [] escape-hatch = [] syntax-highlighting = [ "tokio-xmpp/syntax-highlighting" ] diff --git a/xmpp/comile.sh b/xmpp/comile.sh new file mode 100644 index 0000000000000000000000000000000000000000..97eb2226a36b79952e2ac68d1504fa2c0c141105 --- /dev/null +++ b/xmpp/comile.sh @@ -0,0 +1 @@ +cargo b --features component,insecure diff --git a/xmpp/compile.sh b/xmpp/compile.sh new file mode 100755 index 0000000000000000000000000000000000000000..ae2519ac3e175947084f3589dae25c820c89cae0 --- /dev/null +++ b/xmpp/compile.sh @@ -0,0 +1 @@ +cargo b --features component,insecure-tcp diff --git a/xmpp/src/component.rs b/xmpp/src/component.rs new file mode 100644 index 0000000000000000000000000000000000000000..6bff3400e34db8193ec841992d746c153f000b40 --- /dev/null +++ b/xmpp/src/component.rs @@ -0,0 +1,249 @@ +use futures::stream::StreamExt; +use std::{fmt::Debug, marker::PhantomData}; + +use thiserror::Error; +use tokio_xmpp::{ + Component as TokioComponent, Stanza, + connect::ServerConnector, + jid::{BareJid, Jid}, + parsers::{ + data_forms::DataForm, + ibr::{FormQuery, LegacyQuery}, + iq::Iq, + ns, + stanza_error::{DefinedCondition, ErrorType, StanzaError}, + }, + xmlstream::Timeouts, +}; + +pub trait IbrData: Clone + Send + Sync + Sized {} + +pub trait LegacyIbrData: Clone + Send + Sync + Sized + IbrData { + fn from_legacy(query: &LegacyQuery) -> Self; +} + +pub trait DataFormIbrData: Clone + Send + Sync + Sized + IbrData { + fn from_data_form(form: &FormQuery) -> Self; +} + +pub trait IbrStore: Send + Sync { + type Data: IbrData; + + type E: std::error::Error + Send + Sync; + + fn get(&self, jid: &BareJid) -> impl Future, Self::E>>; + + fn register( + &self, + jid: &BareJid, + data: Self::Data, + ) -> impl Future>; + + fn unregister( + &self, + jid: &BareJid, + data: Self::Data, + ) -> impl Future>; +} + +// Must be empty +pub enum IbrFields { + Legacy(LegacyQuery), + DataForm(DataForm), +} + +// Must not be empty +pub enum IbrSubmission { + Legacy(LegacyQuery), + DataForm(DataForm), +} + +/// Errors that can occur during IBR operations. +#[derive(Debug)] +pub enum IbrError { + /// User is not registered (for cancel/change operations). + NotRegistered, + /// Username conflict. + Conflict, + /// Required fields missing. + NotAcceptable, + /// Unsupported submission type (e.g., legacy when only DataForm is implemented). + UnsupportedSubmissionType, + /// Storage error. + Store(Box), +} + +pub trait IbrHandler: IbrStore { + fn fields( + &self, + from: &BareJid, + status: Option, + ) -> impl Future + Send; + + fn on_register( + &self, + from: &BareJid, + submission: IbrSubmission, + ) -> impl Future>; + + fn on_cancel(&self, from: &BareJid) -> impl Future> + Send; +} + +impl IbrData for () {} + +#[derive(Debug, Error)] +pub enum IbrNotSupportedShouldBeUnreachableError { + #[error("If you see this, there is a serious bug in xmpp-rs. Please report")] + CriticalUnreachableError, +} + +pub struct IbrNotSupported {} + +impl IbrStore for IbrNotSupported { + type Data = (); + type E = IbrNotSupportedShouldBeUnreachableError; + + fn get(&self, _: &BareJid) -> impl Future, Self::E>> { + unreachable!(); + std::future::ready(Err( + IbrNotSupportedShouldBeUnreachableError::CriticalUnreachableError, + )) + } + + fn register(&self, _: &BareJid, _: Self::Data) -> impl Future> { + unreachable!(); + std::future::ready(Err( + IbrNotSupportedShouldBeUnreachableError::CriticalUnreachableError, + )) + } + + fn unregister(&self, _: &BareJid, _: Self::Data) -> impl Future> { + unreachable!(); + std::future::ready(Err( + IbrNotSupportedShouldBeUnreachableError::CriticalUnreachableError, + )) + } +} + +impl IbrHandler for IbrNotSupported {} + +#[derive(Debug)] +pub enum ComponentError { + IbrError(IbrError), +} + +struct Config +where + C: ServerConnector, + Handler: ComponentHandler, +{ + jid: Jid, + password: String, + hander: Handler, + phantom_connector: PhantomData, +} + +pub enum Continuation { + Next(Stanza), + Error(StanzaError), +} + +pub trait ComponentHandler: Sized +where + C: ServerConnector, +{ + fn handle_register() -> impl Future>; + + async fn handle_iq(&mut self, iq: Iq) -> Result { + match iq { + Iq::Get { + from, + to, + id, + payload, + } => match (payload.name(), payload.ns().as_str()) { + ("query", ns::REGISTER) => { + let fields = self.ibr_fields().await; + todo!() + } + _ => { + let err = StanzaError::new( + ErrorType::Cancel, + DefinedCondition::ServiceUnavailable, + "en", + "No handler defined for this kind of iq.", + ); + + Ok(Continuation::Error(err)) + } + }, + + _ => todo!(), + } + } +} + +struct Component +where + C: ServerConnector, + Handler: ComponentHandler, + Ibr: IbrHandler, +{ + jid: Jid, + password: String, + handler: Handler, + inner: TokioComponent, // low-level component implementation + ibr: Ibr, +} + +impl Component +where + C: ServerConnector, + Handler: ComponentHandler, + Ibr: IbrHandler, +{ + async fn run(mut self) -> Result<(), ComponentError> { + while let Some(stanza) = self.inner.next().await { + match stanza { + Stanza::Iq(iq) => { + self.handler.handle_iq(iq); + } + _ => todo!(), + } + } + + Ok(()) + } +} + +impl Config +where + C: ServerConnector, + Handler: ComponentHandler, + Ibr: IbrHandler, +{ + pub async fn run( + self, + connector: C, + timeouts: Timeouts, + handler: Handler, + ) -> Result<(), ComponentError> { + let inner_component = TokioComponent::new_with_connector( + &self.jid.as_str(), + &self.password.as_str(), + connector, + timeouts, + ) + .await + .unwrap(); // TODO: Handle this error + + let inner_component = Component { + jid: self.jid, + password: self.password, + handler, + inner: inner_component, + }; + + inner_component.run().await + } +} diff --git a/xmpp/src/lib.rs b/xmpp/src/lib.rs index 05db676542cc74c8ff9f126ffbe9ffdc1a84e853..c8a7f3992ea97d83a7b70eed689f713cdf227bdd 100644 --- a/xmpp/src/lib.rs +++ b/xmpp/src/lib.rs @@ -65,6 +65,9 @@ use parsers::message::Id as MessageId; pub mod agent; pub mod builder; +#[cfg(feature = "component")] +#[cfg(feature = "insecure-tcp")] +pub mod component; pub mod config; pub mod delay; pub mod disco;