wip: high-level server component API

Phillip Davis created

Change summary

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(-)

Detailed changes

rust-analyzer.toml 🔗

@@ -0,0 +1,7 @@
+[cargo]
+features = [
+  "xmpp/component",
+  "xmpp/insecure-tcp",
+  "tokio-xmpp/component",
+  "tokio-xmpp/insecure-tcp",
+]

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" ]

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<Output = Result<Option<Self::Data>, Self::E>>;
+
+    fn register(
+        &self,
+        jid: &BareJid,
+        data: Self::Data,
+    ) -> impl Future<Output = Result<(), Self::E>>;
+
+    fn unregister(
+        &self,
+        jid: &BareJid,
+        data: Self::Data,
+    ) -> impl Future<Output = Result<(), Self::E>>;
+}
+
+// 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<dyn std::error::Error + Send + Sync>),
+}
+
+pub trait IbrHandler: IbrStore {
+    fn fields(
+        &self,
+        from: &BareJid,
+        status: Option<Self::Data>,
+    ) -> impl Future<Output = IbrFields> + Send;
+
+    fn on_register(
+        &self,
+        from: &BareJid,
+        submission: IbrSubmission,
+    ) -> impl Future<Output = Result<(), IbrError>>;
+
+    fn on_cancel(&self, from: &BareJid) -> impl Future<Output = Result<(), IbrError>> + 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<Output = Result<Option<Self::Data>, Self::E>> {
+        unreachable!();
+        std::future::ready(Err(
+            IbrNotSupportedShouldBeUnreachableError::CriticalUnreachableError,
+        ))
+    }
+
+    fn register(&self, _: &BareJid, _: Self::Data) -> impl Future<Output = Result<(), Self::E>> {
+        unreachable!();
+        std::future::ready(Err(
+            IbrNotSupportedShouldBeUnreachableError::CriticalUnreachableError,
+        ))
+    }
+
+    fn unregister(&self, _: &BareJid, _: Self::Data) -> impl Future<Output = Result<(), Self::E>> {
+        unreachable!();
+        std::future::ready(Err(
+            IbrNotSupportedShouldBeUnreachableError::CriticalUnreachableError,
+        ))
+    }
+}
+
+impl IbrHandler for IbrNotSupported {}
+
+#[derive(Debug)]
+pub enum ComponentError {
+    IbrError(IbrError),
+}
+
+struct Config<C, Handler>
+where
+    C: ServerConnector,
+    Handler: ComponentHandler<C>,
+{
+    jid: Jid,
+    password: String,
+    hander: Handler,
+    phantom_connector: PhantomData<C>,
+}
+
+pub enum Continuation {
+    Next(Stanza),
+    Error(StanzaError),
+}
+
+pub trait ComponentHandler<C>: Sized
+where
+    C: ServerConnector,
+{
+    fn handle_register() -> impl Future<Output = Result<(), ComponentError>>;
+
+    async fn handle_iq(&mut self, iq: Iq) -> Result<Continuation, ComponentError> {
+        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<C, Handler, Ibr>
+where
+    C: ServerConnector,
+    Handler: ComponentHandler<C>,
+    Ibr: IbrHandler,
+{
+    jid: Jid,
+    password: String,
+    handler: Handler,
+    inner: TokioComponent<C>, // low-level component implementation
+    ibr: Ibr,
+}
+
+impl<C, Handler> Component<C, Handler, Ibr>
+where
+    C: ServerConnector,
+    Handler: ComponentHandler<C>,
+    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<C, Handler> Config<C, Handler, Ibr>
+where
+    C: ServerConnector,
+    Handler: ComponentHandler<C>,
+    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
+    }
+}

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;