diff options
| -rw-r--r-- | filamento/Cargo.toml | 4 | ||||
| -rw-r--r-- | filamento/src/caps.rs | 222 | ||||
| -rw-r--r-- | filamento/src/error.rs | 6 | 
3 files changed, 227 insertions, 5 deletions
| diff --git a/filamento/Cargo.toml b/filamento/Cargo.toml index 3530ab9..1c28c39 100644 --- a/filamento/Cargo.toml +++ b/filamento/Cargo.toml @@ -8,7 +8,7 @@ futures = "0.3.31"  lampada = { version = "0.1.0", path = "../lampada" }  tokio = "1.42.0"  thiserror = "2.0.11" -stanza = { version = "0.1.0", path = "../stanza", features = ["rfc_6121", "xep_0203", "xep_0030", "xep_0060", "xep_0172", "xep_0390", "xep_0128"] } +stanza = { version = "0.1.0", path = "../stanza", features = ["rfc_6121", "xep_0203", "xep_0030", "xep_0060", "xep_0172", "xep_0390", "xep_0128", "xep_0115"] }  sqlx = { version = "0.8.3", features = ["sqlite", "runtime-tokio", "uuid", "chrono"] }  # TODO: re-export jid?  jid = { version = "0.1.0", path = "../jid", features = ["sqlx"] } @@ -18,9 +18,11 @@ chrono = "0.4.40"  sha2 = "0.10.8"  sha3 = "0.10.8"  base64 = "0.22.1" +sha1 = "0.10.6"  [dev-dependencies]  tracing-subscriber = "0.3.19" +peanuts = { version = "0.1.0", path = "../../peanuts" }  [[example]]  name = "example" diff --git a/filamento/src/caps.rs b/filamento/src/caps.rs index c87e48a..a0709eb 100644 --- a/filamento/src/caps.rs +++ b/filamento/src/caps.rs @@ -1,20 +1,119 @@  use std::str::FromStr;  use base64::{Engine, prelude::BASE64_STANDARD}; +use sha1::Sha1;  use sha2::{Digest, Sha256};  use sha3::Sha3_256;  use stanza::{      xep_0030::info, +    xep_0115::{self, C},      xep_0300::{self, Algo, Hash}, -    xep_0390::C, +    xep_0390,  }; +use tracing::trace;  use crate::{      disco::{Identity, Info, identity::Category}, -    error::{CapsDecodeError, HashNodeConversionError}, +    error::{CapsDecodeError, CapsEncodeError, HashNodeConversionError},  }; -pub fn caps(query: info::Query) -> C { +pub fn caps(node: String, query: info::Query) -> Result<xep_0115::C, CapsEncodeError> { +    let mut string = String::new(); + +    // identities string +    let mut identities = Vec::new(); +    for identity in query.identities { +        let mut string = String::new(); +        string.push_str(&identity.category); +        string.push('/'); +        string.push_str(&identity.r#type); +        string.push('/'); +        string.push_str(&identity.lang.unwrap_or_default()); +        string.push('/'); +        string.push_str(&identity.name.unwrap_or_default()); +        string.push('<'); +        identities.push(string); +    } +    identities.sort(); +    let identities_string = identities.concat(); +    string.push_str(&identities_string); + +    // features string +    let mut features = Vec::new(); +    for feature in query.features { +        let mut string = String::new(); +        string.push_str(&feature.var); +        string.push('<'); +        features.push(string); +    } +    features.sort(); +    let features_string = features.concat(); +    string.push_str(&features_string); + +    // extensions string +    let mut extensions = Vec::new(); +    for extension in query.extensions { +        let mut string = String::new(); +        let form_type = extension +            .fields +            .iter() +            .find(|field| field.var.as_deref() == Some("FORM_TYPE")) +            .ok_or(CapsEncodeError::InvalidDataForm)? +            .values +            .clone() +            .into_iter() +            .map(|value| value.0) +            .collect::<Vec<String>>() +            .concat(); +        string.push_str(&form_type); +        string.push('<'); +        let mut fields = Vec::new(); +        for field in extension.fields { +            if field.var.as_deref() == Some("FORM_TYPE") { +                continue; +            } +            let mut string = String::new(); +            string.push_str(&field.var.unwrap_or_default()); +            string.push('<'); +            let mut values = Vec::new(); +            for value in field.values { +                let mut string = String::new(); +                string.push_str(&value.0); +                string.push('<'); +                values.push(string); +            } +            values.sort(); +            let values_string = values.concat(); +            string.push_str(&values_string); +            fields.push(string); +        } +        fields.sort(); +        let fields_string = fields.concat(); +        string.push_str(&fields_string); +        extensions.push(string); +    } +    extensions.sort(); +    let extensions_string = extensions.concat(); +    string.push_str(&extensions_string); + +    trace!("generated caps string: `{}`", string); + +    let mut sha1 = Sha1::new(); + +    sha1.update(&string); + +    let result = sha1.finalize(); +    let sha1_result = BASE64_STANDARD.encode(result); + +    Ok(C { +        ext: None, +        hash: "sha-1".to_string(), +        node, +        ver: sha1_result, +    }) +} + +pub fn caps2(query: info::Query) -> xep_0390::C {      let mut string = String::new();      // features string @@ -97,7 +196,7 @@ pub fn caps(query: info::Query) -> C {      let result = sha3_256.finalize();      let sha3_256_result = BASE64_STANDARD.encode(result); -    C(vec![ +    xep_0390::C(vec![          Hash {              algo: Algo::SHA256,              hash: sha256_result, @@ -182,3 +281,118 @@ static CLIENT_INFO: Info = Info {      features: vec![],      identities: vec![],  }; + +#[cfg(test)] +mod tests { +    use peanuts::{Writer, element::IntoElement}; +    use stanza::{ +        xep_0004::{Field, FieldType, Value, X, XType}, +        xep_0030::info::{Feature, Identity}, +    }; + +    use super::*; + +    #[tokio::test] +    async fn test_caps() { +        tracing_subscriber::fmt().init(); + +        let info = info::Query { +            node: Some("http://psi-im.org".to_string()), +            features: vec![ +                Feature { +                    var: "http://jabber.org/protocol/caps".to_string(), +                }, +                Feature { +                    var: "http://jabber.org/protocol/disco#items".to_string(), +                }, +                Feature { +                    var: "http://jabber.org/protocol/disco#info".to_string(), +                }, +                Feature { +                    var: "http://jabber.org/protocol/muc".to_string(), +                }, +            ], +            identities: vec![ +                Identity { +                    category: "client".to_string(), +                    name: Some("Ψ 0.11".to_string()), +                    r#type: "pc".to_string(), +                    lang: Some("el".to_string()), +                }, +                Identity { +                    category: "client".to_string(), +                    name: Some("Psi 0.11".to_string()), +                    r#type: "pc".to_string(), +                    lang: Some("en".to_string()), +                }, +            ], +            extensions: vec![X { +                r#type: XType::Result, +                instructions: Vec::new(), +                title: None, +                fields: vec![ +                    Field { +                        label: None, +                        r#type: None, +                        var: Some("ip_version".to_string()), +                        desc: None, +                        required: false, +                        values: vec![Value("ipv4".to_string()), Value("ipv6".to_string())], +                        options: Vec::new(), +                    }, +                    Field { +                        label: None, +                        r#type: None, +                        var: Some("software".to_string()), +                        desc: None, +                        required: false, +                        values: vec![Value("Psi".to_string())], +                        options: Vec::new(), +                    }, +                    Field { +                        label: None, +                        r#type: Some(FieldType::Hidden), +                        var: Some("FORM_TYPE".to_string()), +                        desc: None, +                        required: false, +                        values: vec![Value("urn:xmpp:dataforms:softwareinfo".to_string())], +                        options: Vec::new(), +                    }, +                    Field { +                        label: None, +                        r#type: None, +                        var: Some("os".to_string()), +                        desc: None, +                        required: false, +                        values: vec![Value("Mac".to_string())], +                        options: Vec::new(), +                    }, +                    Field { +                        label: None, +                        r#type: None, +                        var: Some("os_version".to_string()), +                        desc: None, +                        required: false, +                        values: vec![Value("10.5.1".to_string())], +                        options: Vec::new(), +                    }, +                    Field { +                        label: None, +                        r#type: None, +                        var: Some("software_version".to_string()), +                        desc: None, +                        required: false, +                        values: vec![Value("0.11".to_string())], +                        options: Vec::new(), +                    }, +                ], +                reported: None, +                items: Vec::new(), +            }], +        }; +        let caps = caps("https://macaw.chat".to_string(), info).unwrap(); +        let stdout = tokio::io::stdout(); +        let mut writer = Writer::new(stdout); +        writer.write(&caps).await; +    } +} diff --git a/filamento/src/error.rs b/filamento/src/error.rs index 1dd4f47..b785f26 100644 --- a/filamento/src/error.rs +++ b/filamento/src/error.rs @@ -249,6 +249,12 @@ pub enum CapsDecodeError {  }  #[derive(Debug, Error, Clone)] +pub enum CapsEncodeError { +    #[error("invalid data form in disco extensions")] +    InvalidDataForm, +} + +#[derive(Debug, Error, Clone)]  pub enum HashNodeConversionError {      #[error("no prefix")]      NoPrefix, | 
