diff options
| author | 2025-04-03 13:20:29 +0100 | |
|---|---|---|
| committer | 2025-04-03 13:20:29 +0100 | |
| commit | f48642bbd5a210b68e60715b59b1f24cf2d77fea (patch) | |
| tree | 4316bbb8355cc1dfd66a22dde480a49e9158ec59 | |
| parent | bf00184a09418750caeb488d8d71f9dc7afd7aff (diff) | |
| download | luz-f48642bbd5a210b68e60715b59b1f24cf2d77fea.tar.gz luz-f48642bbd5a210b68e60715b59b1f24cf2d77fea.tar.bz2 luz-f48642bbd5a210b68e60715b59b1f24cf2d77fea.zip | |
feat(filamento): caps 1.0
Diffstat (limited to '')
| -rw-r--r-- | filamento/migrations/20240113011930_luz.sql | 4 | ||||
| -rw-r--r-- | filamento/src/caps.rs | 117 | ||||
| -rw-r--r-- | filamento/src/error.rs | 5 | ||||
| -rw-r--r-- | filamento/src/logic/process_stanza.rs | 70 | ||||
| -rw-r--r-- | filamento/src/presence.rs | 4 | ||||
| -rw-r--r-- | stanza/src/client/presence.rs | 12 | ||||
| -rw-r--r-- | stanza/src/xep_0115.rs | 9 | 
7 files changed, 187 insertions, 34 deletions
| diff --git a/filamento/migrations/20240113011930_luz.sql b/filamento/migrations/20240113011930_luz.sql index c2b5a97..8c1b01c 100644 --- a/filamento/migrations/20240113011930_luz.sql +++ b/filamento/migrations/20240113011930_luz.sql @@ -132,7 +132,9 @@ insert into cached_status (id) values (0);  create table capability_hash_nodes (      node text primary key not null, -    timestamp text not null, +    timestamp text,      -- TODO: normalization      capabilities text not null  ); + +insert into capability_hash_nodes ( node, capabilities ) values ('https://bunny.garden/filamento#mSavc/SLnHm8zazs5RlcbD/iXoc=', 'aHR0cDovL2phYmJlci5vcmcvcHJvdG9jb2wvY2Fwcx9odHRwOi8vamFiYmVyLm9yZy9wcm90b2NvbC9kaXNjbyNpbmZvH2h0dHA6Ly9qYWJiZXIub3JnL3Byb3RvY29sL2Rpc2NvI2l0ZW1zH2h0dHA6Ly9qYWJiZXIub3JnL3Byb3RvY29sL25pY2sfaHR0cDovL2phYmJlci5vcmcvcHJvdG9jb2wvbmljaytub3RpZnkfHGNsaWVudB9wYx8fZmlsYW1lbnRvIDAuMS4wHx4cHA=='); diff --git a/filamento/src/caps.rs b/filamento/src/caps.rs index a0709eb..49d05ba 100644 --- a/filamento/src/caps.rs +++ b/filamento/src/caps.rs @@ -13,10 +13,41 @@ use stanza::{  use tracing::trace;  use crate::{ -    disco::{Identity, Info, identity::Category}, -    error::{CapsDecodeError, CapsEncodeError, HashNodeConversionError}, +    disco::{ +        Identity, Info, +        identity::{self, Category}, +    }, +    error::{CapsDecodeError, CapsEncodeError, CapsNodeConversionError, HashNodeConversionError},  }; +pub const CLIENT_URI: &str = "https://bunny.garden/filamento"; + +// <c xmlns="http://jabber.org/protocol/caps" hash="sha-1" ver="mSavc/SLnHm8zazs5RlcbD/iXoc=" node="https://bunny.garden/filamento"/> +pub fn c() -> C { +    caps(CLIENT_URI.to_string(), client_info().into()).unwrap() +} + +pub fn caps_node() -> String { +    caps_to_node(c()) +} + +pub fn client_info() -> Info { +    Info { +        node: None, +        features: vec![ +            "http://jabber.org/protocol/disco#items".to_string(), +            "http://jabber.org/protocol/disco#info".to_string(), +            "http://jabber.org/protocol/caps".to_string(), +            "http://jabber.org/protocol/nick".to_string(), +            "http://jabber.org/protocol/nick+notify".to_string(), +        ], +        identities: vec![Identity { +            name: Some("filamento 0.1.0".to_string()), +            category: Category::Client(identity::Client::PC), +        }], +    } +} +  pub fn caps(node: String, query: info::Query) -> Result<xep_0115::C, CapsEncodeError> {      let mut string = String::new(); @@ -113,7 +144,7 @@ pub fn caps(node: String, query: info::Query) -> Result<xep_0115::C, CapsEncodeE      })  } -pub fn caps2(query: info::Query) -> xep_0390::C { +pub fn encode_caps2(query: info::Query) -> String {      let mut string = String::new();      // features string @@ -181,6 +212,16 @@ pub fn caps2(query: info::Query) -> xep_0390::C {      let extensions_string = extensions.concat();      string.push_str(&extensions_string);      string.push('\x1c'); +    string +} + +pub fn encode_info_base64(info: info::Query) -> String { +    let string = encode_caps2(info); +    BASE64_STANDARD.encode(string) +} + +pub fn caps2(query: info::Query) -> xep_0390::C { +    let string = encode_caps2(query);      let mut sha256 = Sha256::new(); @@ -209,7 +250,7 @@ pub fn caps2(query: info::Query) -> xep_0390::C {  }  /// takes a base64 encoded cached caps string and converts it into a disco info result -pub fn info(info: String) -> Result<Info, CapsDecodeError> { +pub fn decode_info_base64(info: String) -> Result<Info, CapsDecodeError> {      let info = String::from_utf8(BASE64_STANDARD.decode(info)?)?;      let mut strings = info.split_terminator('\x1c'); @@ -255,6 +296,22 @@ pub fn info(info: String) -> Result<Info, CapsDecodeError> {      })  } +pub fn caps_to_node(caps: C) -> String { +    caps.node + "#" + caps.ver.as_str() +} + +pub fn node_to_caps(node: String) -> Result<C, CapsNodeConversionError> { +    let (node, ver) = node +        .rsplit_once('#') +        .ok_or(CapsNodeConversionError::MissingHashtag)?; +    Ok(C { +        ext: None, +        hash: "sha-1".to_string(), +        node: node.to_string(), +        ver: ver.to_string(), +    }) +} +  pub fn hash_to_node(hash: xep_0300::Hash) -> String {      let mut string = String::from("urn:xmpp:caps#");      string.push_str(&hash.algo.to_string()); @@ -276,12 +333,6 @@ pub fn node_to_hash(node: String) -> Result<Hash, HashNodeConversionError> {      })  } -static CLIENT_INFO: Info = Info { -    node: None, -    features: vec![], -    identities: vec![], -}; -  #[cfg(test)]  mod tests {      use peanuts::{Writer, element::IntoElement}; @@ -390,9 +441,51 @@ mod tests {                  items: Vec::new(),              }],          }; -        let caps = caps("https://macaw.chat".to_string(), info).unwrap(); +        let test_caps = caps("https://macaw.chat".to_string(), info).unwrap(); +        let stdout = tokio::io::stdout(); +        let mut writer = Writer::new(stdout); +        writer.write(&test_caps).await.unwrap(); +    } + +    #[tokio::test] +    pub async fn test_gen_client_caps() {          let stdout = tokio::io::stdout();          let mut writer = Writer::new(stdout); -        writer.write(&caps).await; +        // let info = info::Query { +        //     node: Some("https://bunny.garden/filamento".to_string()), +        //     features: vec![ +        //         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/caps".to_string(), +        //         }, +        //         Feature { +        //             var: "http://jabber.org/protocol/nick".to_string(), +        //         }, +        //         Feature { +        //             var: "http://jabber.org/protocol/nick+notify".to_string(), +        //         }, +        //     ], +        //     identities: vec![Identity { +        //         category: "client".to_string(), +        //         name: Some("filamento 0.1.0".to_string()), +        //         r#type: "pc".to_string(), +        //         lang: None, +        //     }], +        //     extensions: vec![], +        // }; +        let info: info::Query = client_info().into(); +        let client_caps = caps(CLIENT_URI.to_string(), info.clone()).unwrap(); +        writer.write(&client_caps).await.unwrap(); +        let node = caps_to_node(client_caps); +        println!("client caps node: `{}`", node); +        let client_caps_base64 = encode_info_base64(info); +        println!("client caps: `{}`", client_caps_base64); +        let client_caps = decode_info_base64(client_caps_base64).unwrap(); +        println!("client caps: `{:?}`", client_caps);      }  } diff --git a/filamento/src/error.rs b/filamento/src/error.rs index b785f26..5111413 100644 --- a/filamento/src/error.rs +++ b/filamento/src/error.rs @@ -262,5 +262,10 @@ pub enum HashNodeConversionError {      MissingPeriod,  } +#[derive(Debug, Error, Clone)] +pub enum CapsNodeConversionError { +    #[error("missing hashtag")] +    MissingHashtag, +}  // #[derive(Debug, Error, Clone)]  // pub enum CapsError {} diff --git a/filamento/src/logic/process_stanza.rs b/filamento/src/logic/process_stanza.rs index e9787a9..182fb43 100644 --- a/filamento/src/logic/process_stanza.rs +++ b/filamento/src/logic/process_stanza.rs @@ -8,13 +8,13 @@ use stanza::{          iq::{self, Iq, IqType},      },      stanza_error::Error as StanzaError, -    xep_0030, +    xep_0030::{self, info},  };  use tracing::{debug, error, info, warn};  use uuid::Uuid;  use crate::{ -    UpdateMessage, +    UpdateMessage, caps,      chat::{Body, Message},      error::{DatabaseError, Error, IqError, MessageRecvError, PresenceError, RosterError},      presence::{Offline, Online, Presence, PresenceType, Show}, @@ -220,22 +220,58 @@ pub async fn recv_iq(                  .unwrap_or_else(|| connection.server().clone());              if let Some(query) = iq.query {                  match query { -                    stanza::client::iq::Query::DiscoInfo(_query) => { -                        // TODO: should this only be replied to server? +                    stanza::client::iq::Query::DiscoInfo(query) => {                          info!("received disco#info request from {}", from); -                        let disco = xep_0030::info::Query { -                            node: None, -                            features: vec![xep_0030::info::Feature { -                                var: "http://jabber.org/protocol/disco#info".to_string(), -                            }], -                            identities: vec![xep_0030::info::Identity { -                                category: "client".to_string(), -                                name: Some("filamento".to_string()), -                                r#type: "pc".to_string(), -                                lang: None, -                            }], -                            extensions: Vec::new(), -                        }; +                        let current_caps_node = caps::caps_node(); +                        let disco: info::Query = +                            if query.node.is_none() || query.node == Some(current_caps_node) { +                                let mut info = caps::client_info(); +                                info.node = query.node; +                                info.into() +                            } else { +                                match logic +                                    .db() +                                    .read_capabilities(&query.node.clone().unwrap()) +                                    .await +                                { +                                    Ok(c) => match caps::decode_info_base64(c) { +                                        Ok(mut i) => { +                                            i.node = query.node; +                                            i.into() +                                        } +                                        Err(_e) => { +                                            let iq = Iq { +                                                from: Some(connection.jid().clone()), +                                                id: iq.id, +                                                to: iq.from, +                                                r#type: IqType::Error, +                                                lang: None, +                                                query: Some(iq::Query::DiscoInfo(query)), +                                                errors: vec![StanzaError::ItemNotFound.into()], +                                            }; +                                            // TODO: log error +                                            connection.write_handle().write(Stanza::Iq(iq)).await?; +                                            info!("replied to disco#info request from {}", from); +                                            return Ok(None); +                                        } +                                    }, +                                    Err(_e) => { +                                        let iq = Iq { +                                            from: Some(connection.jid().clone()), +                                            id: iq.id, +                                            to: iq.from, +                                            r#type: IqType::Error, +                                            lang: None, +                                            query: Some(iq::Query::DiscoInfo(query)), +                                            errors: vec![StanzaError::ItemNotFound.into()], +                                        }; +                                        // TODO: log error +                                        connection.write_handle().write(Stanza::Iq(iq)).await?; +                                        info!("replied to disco#info request from {}", from); +                                        return Ok(None); +                                    } +                                } +                            };                          let iq = Iq {                              from: Some(connection.jid().clone()),                              id: iq.id, diff --git a/filamento/src/presence.rs b/filamento/src/presence.rs index bae8793..a7a8965 100644 --- a/filamento/src/presence.rs +++ b/filamento/src/presence.rs @@ -2,6 +2,8 @@ use chrono::{DateTime, Utc};  use sqlx::Sqlite;  use stanza::{client::presence::String1024, xep_0203::Delay}; +use crate::caps; +  #[derive(Debug, Default, sqlx::FromRow, Clone)]  pub struct Online {      pub show: Option<Show>, @@ -96,6 +98,7 @@ impl Online {                  from: None,                  stamp: timestamp,              }), +            c: Some(caps::c()),              ..Default::default()          }      } @@ -116,6 +119,7 @@ impl Offline {                  from: None,                  stamp: timestamp,              }), +            c: Some(caps::c()),              ..Default::default()          }      } diff --git a/stanza/src/client/presence.rs b/stanza/src/client/presence.rs index bffb0d0..a8c35d0 100644 --- a/stanza/src/client/presence.rs +++ b/stanza/src/client/presence.rs @@ -6,6 +6,8 @@ use peanuts::{      DeserializeError, Element, XML_NS,  }; +#[cfg(feature = "xep_0115")] +use crate::xep_0115::C;  #[cfg(feature = "xep_0131")]  use crate::xep_0131::Headers;  #[cfg(feature = "xep_0172")] @@ -32,6 +34,8 @@ pub struct Presence {      pub headers: Option<Headers>,      #[cfg(feature = "xep_0172")]      pub nick: Option<Nick>, +    #[cfg(feature = "xep_0115")] +    pub c: Option<C>,      // ##other      pub errors: Vec<Error>,  } @@ -61,6 +65,9 @@ impl FromElement for Presence {          #[cfg(feature = "xep_0172")]          let nick = element.child_opt()?; +        #[cfg(feature = "xep_0115")] +        let c = element.child_opt()?; +          Ok(Presence {              from,              id, @@ -77,6 +84,8 @@ impl FromElement for Presence {              headers,              #[cfg(feature = "xep_0172")]              nick, +            #[cfg(feature = "xep_0115")] +            c,          })      }  } @@ -103,6 +112,9 @@ impl IntoElement for Presence {          #[cfg(feature = "xep_0172")]          let builder = builder.push_child_opt(self.nick.clone()); +        #[cfg(feature = "xep_0115")] +        let builder = builder.push_child_opt(self.c.clone()); +          builder      }  } diff --git a/stanza/src/xep_0115.rs b/stanza/src/xep_0115.rs index c4eee54..1c2ef69 100644 --- a/stanza/src/xep_0115.rs +++ b/stanza/src/xep_0115.rs @@ -5,11 +5,12 @@ use peanuts::{  pub const XMLNS: &str = "http://jabber.org/protocol/caps"; +#[derive(Debug, Clone)]  pub struct C { -    ext: Option<String>, -    hash: String, -    node: String, -    ver: String, +    pub ext: Option<String>, +    pub hash: String, +    pub node: String, +    pub ver: String,  }  impl FromElement for C { | 
