From be198ca15bbaf633c1535db5bae7091520546aed Mon Sep 17 00:00:00 2001
From: cel 🌸 <cel@bunny.garden>
Date: Mon, 2 Dec 2024 21:50:15 +0000
Subject: implement bind

---
 src/error.rs                  |   5 +-
 src/jabber.rs                 | 111 ++++++++++++++++++++++++++++++++++++-
 src/stanza/bind.rs            |  98 ++++++++++++++++++++++++++++++++
 src/stanza/client/error.rs    |  83 ++++++++++++++++++++++++++++
 src/stanza/client/iq.rs       | 124 +++++++++++++++++++++++++++++++++++++++++
 src/stanza/client/message.rs  |  37 +++++++++++++
 src/stanza/client/mod.rs      |   6 ++
 src/stanza/client/presence.rs |  48 ++++++++++++++++
 src/stanza/error.rs           | 126 ++++++++++++++++++++++++++++++++++++++++++
 src/stanza/iq.rs              |   1 -
 src/stanza/message.rs         |   1 -
 src/stanza/mod.rs             |   7 ++-
 src/stanza/presence.rs        |   1 -
 src/stanza/starttls.rs        |   8 ++-
 src/stanza/stream.rs          |   9 ++-
 15 files changed, 654 insertions(+), 11 deletions(-)
 create mode 100644 src/stanza/client/error.rs
 create mode 100644 src/stanza/client/iq.rs
 create mode 100644 src/stanza/client/message.rs
 create mode 100644 src/stanza/client/mod.rs
 create mode 100644 src/stanza/client/presence.rs
 create mode 100644 src/stanza/error.rs
 delete mode 100644 src/stanza/iq.rs
 delete mode 100644 src/stanza/message.rs
 delete mode 100644 src/stanza/presence.rs

diff --git a/src/error.rs b/src/error.rs
index a1f853b..b5cf446 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -2,6 +2,7 @@ use std::str::Utf8Error;
 
 use rsasl::mechname::MechanismNameError;
 
+use crate::stanza::client::error::Error as ClientError;
 use crate::{jid::ParseError, stanza::sasl::Failure};
 
 #[derive(Debug)]
@@ -22,12 +23,14 @@ pub enum Error {
     Negotiation,
     TlsRequired,
     UnexpectedEnd,
-    UnexpectedElement,
+    UnexpectedElement(peanuts::Element),
     UnexpectedText,
     XML(peanuts::Error),
     SASL(SASLError),
     JID(ParseError),
     Authentication(Failure),
+    ClientError(ClientError),
+    MissingError,
 }
 
 #[derive(Debug)]
diff --git a/src/jabber.rs b/src/jabber.rs
index 599879d..96cd73a 100644
--- a/src/jabber.rs
+++ b/src/jabber.rs
@@ -13,6 +13,9 @@ use trust_dns_resolver::proto::rr::domain::IntoLabel;
 
 use crate::connection::{Tls, Unencrypted};
 use crate::error::Error;
+use crate::stanza::bind::{Bind, BindType, FullJidType, ResourceType};
+use crate::stanza::client::error::Error as ClientError;
+use crate::stanza::client::iq::{Iq, IqType, Query};
 use crate::stanza::sasl::{Auth, Challenge, Mechanisms, Response, ServerResponse};
 use crate::stanza::starttls::{Proceed, StartTls};
 use crate::stanza::stream::{Feature, Features, Stream};
@@ -147,7 +150,96 @@ where
     }
 
     pub async fn bind(&mut self) -> Result<()> {
-        todo!()
+        let iq_id = nanoid::nanoid!();
+        if let Some(resource) = self.jid.clone().unwrap().resourcepart {
+            let iq = Iq {
+                from: None,
+                id: iq_id.clone(),
+                to: None,
+                r#type: IqType::Set,
+                lang: None,
+                query: Some(Query::Bind(Bind {
+                    r#type: Some(BindType::Resource(ResourceType(resource))),
+                })),
+                errors: Vec::new(),
+            };
+            self.writer.write_full(&iq).await?;
+            let result: Iq = self.reader.read().await?;
+            match result {
+                Iq {
+                    from: _,
+                    id,
+                    to: _,
+                    r#type: IqType::Result,
+                    lang: _,
+                    query:
+                        Some(Query::Bind(Bind {
+                            r#type: Some(BindType::Jid(FullJidType(jid))),
+                        })),
+                    errors: _,
+                } if id == iq_id => {
+                    self.jid = Some(jid);
+                    return Ok(());
+                }
+                Iq {
+                    from: _,
+                    id,
+                    to: _,
+                    r#type: IqType::Error,
+                    lang: _,
+                    query: None,
+                    errors,
+                } if id == iq_id => {
+                    return Err(Error::ClientError(
+                        errors.first().ok_or(Error::MissingError)?.clone(),
+                    ))
+                }
+                _ => return Err(Error::UnexpectedElement(result.into_element())),
+            }
+        } else {
+            let iq = Iq {
+                from: None,
+                id: iq_id.clone(),
+                to: None,
+                r#type: IqType::Set,
+                lang: None,
+                query: Some(Query::Bind(Bind { r#type: None })),
+                errors: Vec::new(),
+            };
+            self.writer.write_full(&iq).await?;
+            let result: Iq = self.reader.read().await?;
+            match result {
+                Iq {
+                    from: _,
+                    id,
+                    to: _,
+                    r#type: IqType::Result,
+                    lang: _,
+                    query:
+                        Some(Query::Bind(Bind {
+                            r#type: Some(BindType::Jid(FullJidType(jid))),
+                        })),
+                    errors: _,
+                } if id == iq_id => {
+                    self.jid = Some(jid);
+                    return Ok(());
+                }
+                Iq {
+                    from: _,
+                    id,
+                    to: _,
+                    r#type: IqType::Error,
+                    lang: _,
+                    query: None,
+                    errors,
+                } if id == iq_id => {
+                    return Err(Error::ClientError(
+                        errors.first().ok_or(Error::MissingError)?.clone(),
+                    ))
+                }
+                _ => return Err(Error::UnexpectedElement(result.into_element())),
+            }
+        }
     }
 
     #[instrument]
@@ -324,9 +416,12 @@ impl std::fmt::Debug for Jabber<Unencrypted> {
 
 #[cfg(test)]
 mod tests {
+    use std::time::Duration;
+
     use super::*;
     use crate::connection::Connection;
     use test_log::test;
+    use tokio::time::sleep;
 
     #[test(tokio::test)]
     async fn start_stream() {
@@ -373,4 +468,18 @@ mod tests {
             Feature::Unknown => todo!(),
         }
     }
+
+    #[tokio::test]
+    async fn negotiate() {
+        let jabber = Connection::connect_user("test@blos.sm", "slayed".to_string())
+            .await
+            .unwrap()
+            .ensure_tls()
+            .await
+            .unwrap()
+            .negotiate()
+            .await
+            .unwrap();
+        sleep(Duration::from_secs(5)).await
+    }
 }
diff --git a/src/stanza/bind.rs b/src/stanza/bind.rs
index 8b13789..0e67a83 100644
--- a/src/stanza/bind.rs
+++ b/src/stanza/bind.rs
@@ -1 +1,99 @@
+use peanuts::{
+    element::{FromElement, IntoElement},
+    Element,
+};
 
+use crate::JID;
+
+pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-bind";
+
+#[derive(Clone)]
+pub struct Bind {
+    pub r#type: Option<BindType>,
+}
+
+impl FromElement for Bind {
+    fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        element.check_name("bind");
+        element.check_name(XMLNS);
+
+        let r#type = element.pop_child_opt()?;
+
+        Ok(Bind { r#type })
+    }
+}
+
+impl IntoElement for Bind {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        Element::builder("bind", Some(XMLNS)).push_child_opt(self.r#type.clone())
+    }
+}
+
+#[derive(Clone)]
+pub enum BindType {
+    Resource(ResourceType),
+    Jid(FullJidType),
+}
+
+impl FromElement for BindType {
+    fn from_element(element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        match element.identify() {
+            (Some(XMLNS), "resource") => {
+                Ok(BindType::Resource(ResourceType::from_element(element)?))
+            }
+            (Some(XMLNS), "jid") => Ok(BindType::Jid(FullJidType::from_element(element)?)),
+            _ => Err(peanuts::DeserializeError::UnexpectedElement(element)),
+        }
+    }
+}
+
+impl IntoElement for BindType {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        match self {
+            BindType::Resource(resource_type) => resource_type.builder(),
+            BindType::Jid(full_jid_type) => full_jid_type.builder(),
+        }
+    }
+}
+
+// minLength 8 maxLength 3071
+#[derive(Clone)]
+pub struct FullJidType(pub JID);
+
+impl FromElement for FullJidType {
+    fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        element.check_name("jid");
+        element.check_namespace(XMLNS);
+
+        let jid = element.pop_value()?;
+
+        Ok(FullJidType(jid))
+    }
+}
+
+impl IntoElement for FullJidType {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        Element::builder("jid", Some(XMLNS)).push_text(self.0.clone())
+    }
+}
+
+// minLength 1 maxLength 1023
+#[derive(Clone)]
+pub struct ResourceType(pub String);
+
+impl FromElement for ResourceType {
+    fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        element.check_name("resource")?;
+        element.check_namespace(XMLNS)?;
+
+        let resource = element.pop_value()?;
+
+        Ok(ResourceType(resource))
+    }
+}
+
+impl IntoElement for ResourceType {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        Element::builder("resource", Some(XMLNS)).push_text(self.0.clone())
+    }
+}
diff --git a/src/stanza/client/error.rs b/src/stanza/client/error.rs
new file mode 100644
index 0000000..fc5ed21
--- /dev/null
+++ b/src/stanza/client/error.rs
@@ -0,0 +1,83 @@
+use std::str::FromStr;
+
+use peanuts::element::{FromElement, IntoElement};
+use peanuts::{DeserializeError, Element};
+
+use crate::stanza::error::Text;
+use crate::stanza::Error as StanzaError;
+
+use super::XMLNS;
+
+#[derive(Clone, Debug)]
+pub struct Error {
+    by: Option<String>,
+    r#type: ErrorType,
+    // children (sequence)
+    error: StanzaError,
+    text: Option<Text>,
+}
+
+impl FromElement for Error {
+    fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        element.check_name("error")?;
+        element.check_name(XMLNS)?;
+
+        let by = element.attribute_opt("by")?;
+        let r#type = element.attribute("type")?;
+        let error = element.pop_child_one()?;
+        let text = element.pop_child_opt()?;
+
+        Ok(Error {
+            by,
+            r#type,
+            error,
+            text,
+        })
+    }
+}
+
+impl IntoElement for Error {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        Element::builder("error", Some(XMLNS))
+            .push_attribute_opt("by", self.by.clone())
+            .push_attribute("type", self.r#type)
+            .push_child(self.error.clone())
+            .push_child_opt(self.text.clone())
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum ErrorType {
+    Auth,
+    Cancel,
+    Continue,
+    Modify,
+    Wait,
+}
+
+impl FromStr for ErrorType {
+    type Err = DeserializeError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "auth" => Ok(ErrorType::Auth),
+            "cancel" => Ok(ErrorType::Cancel),
+            "continue" => Ok(ErrorType::Continue),
+            "modify" => Ok(ErrorType::Modify),
+            "wait" => Ok(ErrorType::Wait),
+            _ => Err(DeserializeError::FromStr(s.to_string())),
+        }
+    }
+}
+
+impl ToString for ErrorType {
+    fn to_string(&self) -> String {
+        match self {
+            ErrorType::Auth => "auth".to_string(),
+            ErrorType::Cancel => "cancel".to_string(),
+            ErrorType::Continue => "continue".to_string(),
+            ErrorType::Modify => "modify".to_string(),
+            ErrorType::Wait => "wait".to_string(),
+        }
+    }
+}
diff --git a/src/stanza/client/iq.rs b/src/stanza/client/iq.rs
new file mode 100644
index 0000000..b23f8b7
--- /dev/null
+++ b/src/stanza/client/iq.rs
@@ -0,0 +1,124 @@
+use std::str::FromStr;
+
+use peanuts::{
+    element::{FromElement, IntoElement},
+    DeserializeError, Element, XML_NS,
+};
+
+use crate::{
+    stanza::{
+        bind::{self, Bind},
+        client::error::Error,
+    },
+    JID,
+};
+
+use super::XMLNS;
+
+pub struct Iq {
+    pub from: Option<JID>,
+    pub id: String,
+    pub to: Option<JID>,
+    pub r#type: IqType,
+    pub lang: Option<String>,
+    // children
+    // ##other
+    pub query: Option<Query>,
+    pub errors: Vec<Error>,
+}
+
+#[derive(Clone)]
+pub enum Query {
+    Bind(Bind),
+    Unsupported,
+}
+
+impl FromElement for Query {
+    fn from_element(element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        match element.identify() {
+            (Some(bind::XMLNS), "bind") => Ok(Query::Bind(Bind::from_element(element)?)),
+            _ => Ok(Query::Unsupported),
+        }
+    }
+}
+
+impl IntoElement for Query {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        match self {
+            Query::Bind(bind) => bind.builder(),
+            // TODO: consider what to do if attempt to serialize unsupported
+            Query::Unsupported => todo!(),
+        }
+    }
+}
+
+impl FromElement for Iq {
+    fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        element.check_name("iq")?;
+        element.check_namespace(XMLNS)?;
+
+        let from = element.attribute_opt("from")?;
+        let id = element.attribute("id")?;
+        let to = element.attribute_opt("to")?;
+        let r#type = element.attribute("type")?;
+        let lang = element.attribute_opt_namespaced("lang", XML_NS)?;
+        let query = element.pop_child_opt()?;
+        let errors = element.pop_children()?;
+
+        Ok(Iq {
+            from,
+            id,
+            to,
+            r#type,
+            lang,
+            query,
+            errors,
+        })
+    }
+}
+
+impl IntoElement for Iq {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        Element::builder("iq", Some(XMLNS))
+            .push_attribute_opt("from", self.from.clone())
+            .push_attribute("id", self.id.clone())
+            .push_attribute_opt("to", self.to.clone())
+            .push_attribute("type", self.r#type)
+            .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone())
+            .push_child_opt(self.query.clone())
+            .push_children(self.errors.clone())
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum IqType {
+    Error,
+    Get,
+    Result,
+    Set,
+}
+
+impl FromStr for IqType {
+    type Err = DeserializeError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "error" => Ok(IqType::Error),
+            "get" => Ok(IqType::Get),
+            "result" => Ok(IqType::Result),
+            "set" => Ok(IqType::Set),
+            _ => Err(DeserializeError::FromStr(s.to_string())),
+        }
+    }
+}
+
+impl ToString for IqType {
+    fn to_string(&self) -> String {
+        match self {
+            IqType::Error => "error".to_string(),
+            IqType::Get => "get".to_string(),
+            IqType::Result => "result".to_string(),
+            IqType::Set => "set".to_string(),
+        }
+    }
+}
diff --git a/src/stanza/client/message.rs b/src/stanza/client/message.rs
new file mode 100644
index 0000000..cdfda5d
--- /dev/null
+++ b/src/stanza/client/message.rs
@@ -0,0 +1,37 @@
+use crate::JID;
+
+pub struct Message {
+    from: Option<JID>,
+    id: Option<String>,
+    to: Option<JID>,
+    r#type: Option<MessageType>,
+    // children
+    subject: Option<Subject>,
+    body: Option<Body>,
+    thread: Option<Thread>,
+    lang: Option<String>,
+}
+
+pub enum MessageType {
+    Chat,
+    Error,
+    Groupchat,
+    Headline,
+    Normal,
+}
+
+pub struct Body {
+    lang: Option<String>,
+    body: Option<String>,
+}
+
+pub struct Subject {
+    lang: Option<String>,
+    subject: Option<String>,
+}
+
+pub struct Thread {
+    // TODO: NOT DONE
+    parent: Option<String>,
+    thread: Option<String>,
+}
diff --git a/src/stanza/client/mod.rs b/src/stanza/client/mod.rs
new file mode 100644
index 0000000..7b25b15
--- /dev/null
+++ b/src/stanza/client/mod.rs
@@ -0,0 +1,6 @@
+pub mod error;
+pub mod iq;
+pub mod message;
+pub mod presence;
+
+pub const XMLNS: &str = "jabber:client";
diff --git a/src/stanza/client/presence.rs b/src/stanza/client/presence.rs
new file mode 100644
index 0000000..46194f3
--- /dev/null
+++ b/src/stanza/client/presence.rs
@@ -0,0 +1,48 @@
+use peanuts::element::{FromElement, IntoElement};
+
+use crate::JID;
+
+use super::error::Error;
+
+pub struct Presence {
+    from: Option<JID>,
+    id: Option<String>,
+    to: Option<JID>,
+    r#type: PresenceType,
+    lang: Option<String>,
+    // children
+    show: Option<Show>,
+    status: Option<Status>,
+    priority: Option<Priority>,
+    errors: Vec<Error>,
+    // ##other
+    // content: Vec<Box<dyn AsElement>>,
+}
+
+pub enum PresenceType {
+    Error,
+    Probe,
+    Subscribe,
+    Subscribed,
+    Unavailable,
+    Unsubscribe,
+    Unsubscribed,
+}
+
+pub enum Show {
+    Away,
+    Chat,
+    Dnd,
+    Xa,
+}
+
+pub struct Status {
+    lang: Option<String>,
+    status: String1024,
+}
+
+// minLength 1 maxLength 1024
+pub struct String1024(String);
+
+// xs:byte
+pub struct Priority(u8);
diff --git a/src/stanza/error.rs b/src/stanza/error.rs
new file mode 100644
index 0000000..99c1f15
--- /dev/null
+++ b/src/stanza/error.rs
@@ -0,0 +1,126 @@
+// https://datatracker.ietf.org/doc/html/rfc6120#appendix-A.8
+
+use peanuts::{
+    element::{FromElement, IntoElement},
+    Element, XML_NS,
+};
+
+pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas";
+
+#[derive(Clone, Debug)]
+pub enum Error {
+    BadRequest,
+    Conflict,
+    FeatureNotImplemented,
+    Forbidden,
+    Gone(Option<String>),
+    InternalServerError,
+    ItemNotFound,
+    JidMalformed,
+    NotAcceptable,
+    NotAllowed,
+    NotAuthorized,
+    PolicyViolation,
+    RecipientUnavailable,
+    Redirect(Option<String>),
+    RegistrationRequired,
+    RemoteServerNotFound,
+    RemoteServerTimeout,
+    ResourceConstraint,
+    ServiceUnavailable,
+    SubscriptionRequired,
+    UndefinedCondition,
+    UnexpectedRequest,
+}
+
+impl FromElement for Error {
+    fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        let error;
+        match element.identify() {
+            (Some(XMLNS), "bad-request") => error = Error::BadRequest,
+            (Some(XMLNS), "conflict") => error = Error::Conflict,
+            (Some(XMLNS), "feature-not-implemented") => error = Error::FeatureNotImplemented,
+            (Some(XMLNS), "forbidden") => error = Error::Forbidden,
+            (Some(XMLNS), "gone") => return Ok(Error::Gone(element.pop_value_opt()?)),
+            (Some(XMLNS), "internal-server-error") => error = Error::InternalServerError,
+            (Some(XMLNS), "item-not-found") => error = Error::ItemNotFound,
+            (Some(XMLNS), "jid-malformed") => error = Error::JidMalformed,
+            (Some(XMLNS), "not-acceptable") => error = Error::NotAcceptable,
+            (Some(XMLNS), "not-allowed") => error = Error::NotAllowed,
+            (Some(XMLNS), "not-authorized") => error = Error::NotAuthorized,
+            (Some(XMLNS), "policy-violation") => error = Error::PolicyViolation,
+            (Some(XMLNS), "recipient-unavailable") => error = Error::RecipientUnavailable,
+            (Some(XMLNS), "redirect") => return Ok(Error::Redirect(element.pop_value_opt()?)),
+            (Some(XMLNS), "registration-required") => error = Error::RegistrationRequired,
+            (Some(XMLNS), "remote-server-not-found") => error = Error::RemoteServerNotFound,
+            (Some(XMLNS), "remote-server-timeout") => error = Error::RemoteServerTimeout,
+            (Some(XMLNS), "resource-constraint") => error = Error::ResourceConstraint,
+            (Some(XMLNS), "service-unavailable") => error = Error::ServiceUnavailable,
+            (Some(XMLNS), "subscription-required") => error = Error::SubscriptionRequired,
+            (Some(XMLNS), "undefined-condition") => error = Error::UndefinedCondition,
+            (Some(XMLNS), "unexpected-request") => error = Error::UnexpectedRequest,
+            _ => return Err(peanuts::DeserializeError::UnexpectedElement(element)),
+        }
+        element.no_more_content()?;
+        return Ok(error);
+    }
+}
+
+impl IntoElement for Error {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        match self {
+            Error::BadRequest => Element::builder("bad-request", Some(XMLNS)),
+            Error::Conflict => Element::builder("conflict", Some(XMLNS)),
+            Error::FeatureNotImplemented => {
+                Element::builder("feature-not-implemented", Some(XMLNS))
+            }
+            Error::Forbidden => Element::builder("forbidden", Some(XMLNS)),
+            Error::Gone(r) => Element::builder("gone", Some(XMLNS)).push_text_opt(r.clone()),
+            Error::InternalServerError => Element::builder("internal-server-error", Some(XMLNS)),
+            Error::ItemNotFound => Element::builder("item-not-found", Some(XMLNS)),
+            Error::JidMalformed => Element::builder("jid-malformed", Some(XMLNS)),
+            Error::NotAcceptable => Element::builder("not-acceptable", Some(XMLNS)),
+            Error::NotAllowed => Element::builder("not-allowed", Some(XMLNS)),
+            Error::NotAuthorized => Element::builder("not-authorized", Some(XMLNS)),
+            Error::PolicyViolation => Element::builder("policy-violation", Some(XMLNS)),
+            Error::RecipientUnavailable => Element::builder("recipient-unavailable", Some(XMLNS)),
+            Error::Redirect(r) => {
+                Element::builder("redirect", Some(XMLNS)).push_text_opt(r.clone())
+            }
+            Error::RegistrationRequired => Element::builder("registration-required", Some(XMLNS)),
+            Error::RemoteServerNotFound => Element::builder("remote-server-not-found", Some(XMLNS)),
+            Error::RemoteServerTimeout => Element::builder("remote-server-timeout", Some(XMLNS)),
+            Error::ResourceConstraint => Element::builder("resource-constraint", Some(XMLNS)),
+            Error::ServiceUnavailable => Element::builder("service-unavailable", Some(XMLNS)),
+            Error::SubscriptionRequired => Element::builder("subscription-required", Some(XMLNS)),
+            Error::UndefinedCondition => Element::builder("undefined-condition", Some(XMLNS)),
+            Error::UnexpectedRequest => Element::builder("unexpected-request", Some(XMLNS)),
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct Text {
+    lang: Option<String>,
+    text: Option<String>,
+}
+
+impl FromElement for Text {
+    fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> {
+        element.check_name("text")?;
+        element.check_name(XMLNS)?;
+
+        let lang = element.attribute_opt_namespaced("lang", XML_NS)?;
+        let text = element.pop_value_opt()?;
+
+        Ok(Text { lang, text })
+    }
+}
+
+impl IntoElement for Text {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        Element::builder("text", Some(XMLNS))
+            .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone())
+            .push_text_opt(self.text.clone())
+    }
+}
diff --git a/src/stanza/iq.rs b/src/stanza/iq.rs
deleted file mode 100644
index 8b13789..0000000
--- a/src/stanza/iq.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/stanza/message.rs b/src/stanza/message.rs
deleted file mode 100644
index 8b13789..0000000
--- a/src/stanza/message.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/stanza/mod.rs b/src/stanza/mod.rs
index 4f1ce48..84e80ab 100644
--- a/src/stanza/mod.rs
+++ b/src/stanza/mod.rs
@@ -1,11 +1,12 @@
 use peanuts::declaration::VersionInfo;
 
 pub mod bind;
-pub mod iq;
-pub mod message;
-pub mod presence;
+pub mod client;
+pub mod error;
 pub mod sasl;
 pub mod starttls;
 pub mod stream;
 
 pub static XML_VERSION: VersionInfo = VersionInfo::One;
+
+pub use error::Error;
diff --git a/src/stanza/presence.rs b/src/stanza/presence.rs
deleted file mode 100644
index 8b13789..0000000
--- a/src/stanza/presence.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/stanza/starttls.rs b/src/stanza/starttls.rs
index 33721ab..fb66711 100644
--- a/src/stanza/starttls.rs
+++ b/src/stanza/starttls.rs
@@ -17,7 +17,7 @@ impl IntoElement for StartTls {
         let mut builder = Element::builder("starttls", Some(XMLNS));
 
         if self.required {
-            builder = builder.push_child(Element::builder("required", Some(XMLNS)))
+            builder = builder.push_child(Required)
         }
 
         builder
@@ -52,6 +52,12 @@ impl FromElement for Required {
     }
 }
 
+impl IntoElement for Required {
+    fn builder(&self) -> peanuts::element::ElementBuilder {
+        Element::builder("required", Some(XMLNS))
+    }
+}
+
 #[derive(Debug)]
 pub struct Proceed;
 
diff --git a/src/stanza/stream.rs b/src/stanza/stream.rs
index fecace5..c49a2bc 100644
--- a/src/stanza/stream.rs
+++ b/src/stanza/stream.rs
@@ -5,13 +5,14 @@ use peanuts::XML_NS;
 use peanuts::{element::Name, Element};
 use tracing::debug;
 
+use crate::stanza::bind;
 use crate::{Error, JID};
 
+use super::client;
 use super::sasl::{self, Mechanisms};
 use super::starttls::{self, StartTls};
 
 pub const XMLNS: &str = "http://etherx.jabber.org/streams";
-pub const XMLNS_CLIENT: &str = "jabber:client";
 
 // MUST be qualified by stream namespace
 // #[derive(XmlSerialize, XmlDeserialize)]
@@ -53,7 +54,7 @@ impl IntoElement for Stream {
     fn builder(&self) -> ElementBuilder {
         Element::builder("stream", Some(XMLNS.to_string()))
             .push_namespace_declaration_override(Some("stream"), XMLNS)
-            .push_namespace_declaration_override(None::<&str>, XMLNS_CLIENT)
+            .push_namespace_declaration_override(None::<&str>, client::XMLNS)
             .push_attribute_opt("to", self.to.clone())
             .push_attribute_opt("from", self.from.clone())
             .push_attribute_opt("id", self.id.clone())
@@ -150,6 +151,10 @@ impl FromElement for Feature {
                 debug!("identified mechanisms");
                 Ok(Feature::Sasl(Mechanisms::from_element(element)?))
             }
+            (Some(bind::XMLNS), "bind") => {
+                debug!("identified bind");
+                Ok(Feature::Bind)
+            }
             _ => {
                 debug!("identified unknown feature");
                 Ok(Feature::Unknown)
-- 
cgit