From ba5ad94525940e3e34983425961550c67afc49ae Mon Sep 17 00:00:00 2001
From: cel 🌸 <cel@bunny.garden>
Date: Fri, 28 Mar 2025 22:44:11 +0000
Subject: feat(filamento): disco info requests

---
 filamento/examples/example.rs  |  8 +++--
 filamento/src/disco.rs         | 43 ++++++++++++++++++++++
 filamento/src/error.rs         | 25 +++++++++++--
 filamento/src/lib.rs           | 24 +++++++++++--
 filamento/src/logic/offline.rs |  5 ++-
 filamento/src/logic/online.rs  | 82 ++++++++++++++++++++++++++++++++++++++++--
 6 files changed, 175 insertions(+), 12 deletions(-)

(limited to 'filamento')

diff --git a/filamento/examples/example.rs b/filamento/examples/example.rs
index b1ab6ce..10267bc 100644
--- a/filamento/examples/example.rs
+++ b/filamento/examples/example.rs
@@ -20,7 +20,7 @@ async fn main() {
     });
 
     client.connect().await.unwrap();
-    tokio::time::sleep(Duration::from_secs(15)).await;
+    tokio::time::sleep(Duration::from_secs(5)).await;
     info!("sending message");
     client
         .send_message(
@@ -31,5 +31,9 @@ async fn main() {
         )
         .await
         .unwrap();
-    println!("sent message");
+    info!("sent message");
+    info!("sending disco query");
+    let info = client.disco_info(None).await.unwrap();
+    tokio::time::sleep(Duration::from_secs(5)).await;
+    info!("got disco result: {:#?}", info);
 }
diff --git a/filamento/src/disco.rs b/filamento/src/disco.rs
index 86e339e..ccc99cb 100644
--- a/filamento/src/disco.rs
+++ b/filamento/src/disco.rs
@@ -4,6 +4,7 @@ use stanza::xep_0030::{info, items};
 pub use feature::Feature;
 pub use identity::Identity;
 
+#[derive(Debug, Clone)]
 pub struct Info {
     node: Option<String>,
     features: Vec<Feature>,
@@ -109,6 +110,7 @@ mod feature {
     use stanza::xep_0030::info;
 
     // https://xmpp.org/registrar/disco-features.html
+    #[derive(Debug, Clone)]
     pub enum Feature {
         DNSSRV,
         FullUnicode,
@@ -527,6 +529,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Amp {
         Errors,
         Action(AmpAction),
@@ -543,6 +546,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum AmpAction {
         Alert,
         Drop,
@@ -562,6 +566,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum AmpCondition {
         Deliver,
         ExpireAt,
@@ -581,6 +586,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Bytestreams {
         UDP,
     }
@@ -594,6 +600,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum CapsVersion {
         One(Option<CapsOne>),
         Two(Option<CapsTwo>),
@@ -614,6 +621,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum CapsOne {
         Optimize,
     }
@@ -627,6 +635,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum CapsTwo {
         Optimize,
     }
@@ -640,6 +649,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Disco {
         Info,
         Items,
@@ -655,6 +665,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum MUC {
         Admin,
         Owner,
@@ -704,6 +715,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum PubSub {
         AccessAuthorize,
         AccessOpen,
@@ -821,6 +833,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum SOAP {
         Fault,
     }
@@ -834,6 +847,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum WaitingList {
         Schemes(WaitingListSchemes),
     }
@@ -846,6 +860,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum WaitingListSchemes {
         Mailto,
         Tel,
@@ -863,6 +878,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Component {
         Accept,
         Connect,
@@ -878,6 +894,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Iq {
         Auth,
         Gateway,
@@ -911,6 +928,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum X {
         Data,
         Encrypted,
@@ -930,6 +948,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum XMPPSASL {
         C2S,
         S2S,
@@ -945,6 +964,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum XMPPTLS {
         C2S,
         S2S,
@@ -960,6 +980,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Archive {
         Auto,
         Manage,
@@ -979,6 +1000,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Avatar {
         Data,
         Metadata,
@@ -994,6 +1016,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Jingle {
         Apps(JingleApps),
     }
@@ -1006,6 +1029,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum JingleApps {
         RTP(JingleAppsRTP),
     }
@@ -1018,6 +1042,7 @@ mod feature {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum JingleAppsRTP {
         Audio,
         Video,
@@ -1037,6 +1062,7 @@ mod feature {
 mod identity {
     use stanza::xep_0030::info;
 
+    #[derive(Debug, Clone)]
     pub struct Identity {
         name: Option<String>,
         category: Category,
@@ -1065,6 +1091,7 @@ mod identity {
     // TODO: separate crate for disco registry
 
     /// categories taken from [XMPP Disco Categories](https://xmpp.org/registrar/disco-categories.html)
+    #[derive(Debug, Clone)]
     pub enum Category {
         Account(Account),
         Auth(Auth),
@@ -1165,6 +1192,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Account {
         Admin,
         Anonymous,
@@ -1195,6 +1223,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Auth {
         Cert,
         Generic,
@@ -1234,6 +1263,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Authz {
         Ephemeral,
         Other(String),
@@ -1258,6 +1288,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Automation {
         CommandList,
         CommandNode,
@@ -1294,6 +1325,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Client {
         Bot,
         Console,
@@ -1342,6 +1374,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Collaboration {
         Whiteboard,
         Other(String),
@@ -1366,6 +1399,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Component {
         Archive,
         C2S,
@@ -1417,6 +1451,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Conference {
         IRC,
         Text,
@@ -1444,6 +1479,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Directory {
         Chatroom,
         Group,
@@ -1477,6 +1513,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Gateway {
         Aim,
         Discord,
@@ -1579,6 +1616,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Headline {
         NewMail,
         RSS,
@@ -1609,6 +1647,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Hierarchy {
         Branch,
         Leaf,
@@ -1636,6 +1675,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Proxy {
         Bytestreams,
         Other(String),
@@ -1660,6 +1700,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum PubSub {
         Collection,
         Leaf,
@@ -1694,6 +1735,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Server {
         IM,
         Other(String),
@@ -1718,6 +1760,7 @@ mod identity {
         }
     }
 
+    #[derive(Debug, Clone)]
     pub enum Store {
         Berkeley,
         File,
diff --git a/filamento/src/error.rs b/filamento/src/error.rs
index ccb4406..8e2e4be 100644
--- a/filamento/src/error.rs
+++ b/filamento/src/error.rs
@@ -1,7 +1,8 @@
 use std::sync::Arc;
 
+use jid::JID;
 use lampada::error::{ConnectionError, ReadError, WriteError};
-use stanza::client::Stanza;
+use stanza::client::{Stanza, iq::Query};
 use thiserror::Error;
 
 pub use lampada::error::CommandError;
@@ -80,14 +81,32 @@ pub enum RosterError {
     // TODO: display for stanza, to show as xml, same for read error types.
     #[error("unexpected reply: {0:?}")]
     UnexpectedStanza(Stanza),
-    #[error("stream read: {0}")]
-    Read(#[from] ReadError),
     #[error("stanza error: {0}")]
     StanzaError(#[from] stanza::client::error::Error),
     #[error("could not reply to roster push: {0}")]
     PushReply(WriteError),
 }
 
+#[derive(Debug, Error, Clone)]
+pub enum DiscoError {
+    #[error("write error: {0}")]
+    Write(#[from] WriteError),
+    #[error("iq response: {0}")]
+    IqResponse(#[from] RequestError),
+    #[error("reply from incorrect entity: {0}")]
+    IncorrectEntity(JID),
+    #[error("unexpected reply: {0:?}")]
+    UnexpectedStanza(Stanza),
+    #[error("stanza error: {0}")]
+    StanzaError(#[from] stanza::client::error::Error),
+    #[error("disco result missing query item")]
+    MissingQuery,
+    #[error("disco error missing error")]
+    MissingError,
+    #[error("received mismatched query")]
+    MismatchedQuery(Query),
+}
+
 #[derive(Debug, Error, Clone)]
 pub enum RequestError {
     #[error("sending request: {0}")]
diff --git a/filamento/src/lib.rs b/filamento/src/lib.rs
index bc946ae..ed33e99 100644
--- a/filamento/src/lib.rs
+++ b/filamento/src/lib.rs
@@ -9,8 +9,9 @@ use std::{
 use chat::{Body, Chat, Message};
 use chrono::Utc;
 use db::Db;
+use disco::Info;
 use error::{
-    ConnectionJobError, DatabaseError, Error, IqError, MessageRecvError, PresenceError,
+    ConnectionJobError, DatabaseError, DiscoError, Error, IqError, MessageRecvError, PresenceError,
     RosterError, StatusError,
 };
 use futures::FutureExt;
@@ -98,8 +99,13 @@ pub enum Command {
     /// send a message to a jid (any kind of jid that can receive a message, e.g. a user or a
     /// chatroom). if disconnected, will be cached so when client connects, message will be sent.
     SendMessage(JID, Body, oneshot::Sender<Result<(), WriteError>>),
-    /// disco info request
-    DiscoInfo(JID, oneshot::Sender<Result>),
+    /// disco info query
+    DiscoInfo(
+        Option<JID>,
+        oneshot::Sender<Result<disco::Info, DiscoError>>,
+    ),
+    // /// disco items query
+    // DiscoItems(JID, oneshot::Sender<Result<disco::Info, DiscoError>>),
 }
 
 #[derive(Debug, Clone)]
@@ -473,6 +479,18 @@ impl Client {
             .map_err(|e| CommandError::Actor(Into::<ActorError>::into(e)))??;
         Ok(result)
     }
+
+    pub async fn disco_info(&self, jid: Option<JID>) -> Result<Info, CommandError<DiscoError>> {
+        let (send, recv) = oneshot::channel();
+        self.send(CoreClientCommand::Command(Command::DiscoInfo(jid, send)))
+            .await
+            .map_err(|e| CommandError::Actor(Into::<ActorError>::into(e)))?;
+        let result = timeout(self.timeout, recv)
+            .await
+            .map_err(|e| CommandError::Actor(Into::<ActorError>::into(e)))?
+            .map_err(|e| CommandError::Actor(Into::<ActorError>::into(e)))??;
+        Ok(result)
+    }
 }
 
 impl From<Command> for CoreClientCommand<Command> {
diff --git a/filamento/src/logic/offline.rs b/filamento/src/logic/offline.rs
index e864f22..7dfb394 100644
--- a/filamento/src/logic/offline.rs
+++ b/filamento/src/logic/offline.rs
@@ -2,7 +2,7 @@ use lampada::error::WriteError;
 
 use crate::{
     Command,
-    error::{DatabaseError, Error, RosterError, StatusError},
+    error::{DatabaseError, DiscoError, Error, RosterError, StatusError},
     presence::Online,
     roster::Contact,
 };
@@ -113,6 +113,9 @@ pub async fn handle_offline_result(logic: &ClientLogic, command: Command) -> Res
         Command::SendPresence(_jid, _presence, sender) => {
             sender.send(Err(WriteError::Disconnected));
         }
+        Command::DiscoInfo(_jid, sender) => {
+            sender.send(Err(DiscoError::Write(WriteError::Disconnected)));
+        }
     }
     Ok(())
 }
diff --git a/filamento/src/logic/online.rs b/filamento/src/logic/online.rs
index 05d3f2b..c76907b 100644
--- a/filamento/src/logic/online.rs
+++ b/filamento/src/logic/online.rs
@@ -4,18 +4,20 @@ use lampada::{Connected, WriteMessage, error::WriteError};
 use stanza::{
     client::{
         Stanza,
-        iq::{self, Iq, IqType},
+        iq::{self, Iq, IqType, Query},
     },
+    xep_0030::info,
     xep_0203::Delay,
 };
 use tokio::sync::oneshot;
-use tracing::{debug, info};
+use tracing::{debug, error, info};
 use uuid::Uuid;
 
 use crate::{
     Command, UpdateMessage,
     chat::{Body, Message},
-    error::{DatabaseError, Error, MessageSendError, RosterError, StatusError},
+    disco::Info,
+    error::{DatabaseError, DiscoError, Error, MessageSendError, RosterError, StatusError},
     presence::{Online, Presence, PresenceType},
     roster::{Contact, ContactUpdate},
 };
@@ -532,6 +534,76 @@ pub async fn handle_send_presence(
     Ok(())
 }
 
+// TODO: cache disco infos
+pub async fn handle_disco_info(
+    logic: &ClientLogic,
+    connection: Connected,
+    jid: Option<JID>,
+) -> Result<Info, DiscoError> {
+    let id = Uuid::new_v4().to_string();
+    let request = Iq {
+        from: Some(connection.jid().clone()),
+        id: id.clone(),
+        to: jid.clone(),
+        r#type: IqType::Get,
+        lang: None,
+        query: Some(Query::DiscoInfo(info::Query {
+            node: None,
+            features: Vec::new(),
+            identities: Vec::new(),
+        })),
+        errors: Vec::new(),
+    };
+    match logic
+        .pending()
+        .request(&connection, Stanza::Iq(request), id)
+        .await?
+    {
+        Stanza::Iq(Iq {
+            from,
+            r#type,
+            query,
+            mut errors,
+            ..
+            // TODO: maybe abstract a bunch of these different errors related to iqs into an iq error thing? as in like call iq.result(), get the query from inside, error otherwise.
+        }) if r#type == IqType::Result || r#type == IqType::Error => {
+            if from == jid || {
+                if jid == None {
+                    from == Some(connection.jid().as_bare())
+                } else {
+                    false
+                }
+            } {
+                match r#type {
+                    IqType::Result => {
+                        if let Some(query) = query {
+                            match query {
+                                Query::DiscoInfo(info) => Ok(info.into()),
+                                q => Err(DiscoError::MismatchedQuery(q)),
+                            }
+                        } else {
+                            Err(DiscoError::MissingQuery)
+                        }
+                    }
+                    IqType::Error => {
+                        if let Some(error) = errors.pop() {
+                            Err(error.into())
+                        } else {
+                            Err(DiscoError::MissingError)
+                        }
+                    }
+                    _ => unreachable!(),
+                }
+            } else {
+                Err(DiscoError::IncorrectEntity(
+                    from.unwrap_or_else(|| connection.jid().as_bare()),
+                ))
+            }
+        }
+        s => Err(DiscoError::UnexpectedStanza(s)),
+    }
+}
+
 // TODO: could probably macro-ise?
 pub async fn handle_online_result(
     logic: &ClientLogic,
@@ -629,6 +701,10 @@ pub async fn handle_online_result(
             let result = handle_send_presence(connection, jid, presence).await;
             let _ = sender.send(result);
         }
+        Command::DiscoInfo(jid, sender) => {
+            let result = handle_disco_info(logic, connection, jid).await;
+            let _ = sender.send(result);
+        }
     }
     Ok(())
 }
-- 
cgit