aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-04-03 11:06:00 +0100
committerLibravatar cel 🌸 <cel@bunny.garden>2025-04-03 11:06:10 +0100
commitbf00184a09418750caeb488d8d71f9dc7afd7aff (patch)
tree0f0b7f118c86e7306db50a38494320b5a8316f0e
parent236c8a4ba8ee285decf951f73a8cb1e389414e8e (diff)
downloadluz-bf00184a09418750caeb488d8d71f9dc7afd7aff.tar.gz
luz-bf00184a09418750caeb488d8d71f9dc7afd7aff.tar.bz2
luz-bf00184a09418750caeb488d8d71f9dc7afd7aff.zip
feat(filamento): caps 1.0 helper functions & tests
-rw-r--r--filamento/Cargo.toml4
-rw-r--r--filamento/src/caps.rs222
-rw-r--r--filamento/src/error.rs6
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,