aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-04-03 13:20:29 +0100
committerLibravatar cel 🌸 <cel@bunny.garden>2025-04-03 13:20:29 +0100
commitf48642bbd5a210b68e60715b59b1f24cf2d77fea (patch)
tree4316bbb8355cc1dfd66a22dde480a49e9158ec59
parentbf00184a09418750caeb488d8d71f9dc7afd7aff (diff)
downloadluz-f48642bbd5a210b68e60715b59b1f24cf2d77fea.tar.gz
luz-f48642bbd5a210b68e60715b59b1f24cf2d77fea.tar.bz2
luz-f48642bbd5a210b68e60715b59b1f24cf2d77fea.zip
feat(filamento): caps 1.0
-rw-r--r--filamento/migrations/20240113011930_luz.sql4
-rw-r--r--filamento/src/caps.rs117
-rw-r--r--filamento/src/error.rs5
-rw-r--r--filamento/src/logic/process_stanza.rs70
-rw-r--r--filamento/src/presence.rs4
-rw-r--r--stanza/src/client/presence.rs12
-rw-r--r--stanza/src/xep_0115.rs9
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 {