diff options
author | 2024-12-04 18:18:37 +0000 | |
---|---|---|
committer | 2024-12-04 18:18:37 +0000 | |
commit | 1b91ff690488b65b552c90bd5392b9a300c8c981 (patch) | |
tree | 9c290f69b26eba0393d7bbc05ba29c28ea74a26e /stanza | |
parent | 03764f8cedb3f0a55a61be0f0a59faaa6357a83a (diff) | |
download | luz-1b91ff690488b65b552c90bd5392b9a300c8c981.tar.gz luz-1b91ff690488b65b552c90bd5392b9a300c8c981.tar.bz2 luz-1b91ff690488b65b552c90bd5392b9a300c8c981.zip |
use cargo workspace
Diffstat (limited to 'stanza')
-rw-r--r-- | stanza/Cargo.toml | 8 | ||||
-rw-r--r-- | stanza/src/bind.rs | 98 | ||||
-rw-r--r-- | stanza/src/client/error.rs | 83 | ||||
-rw-r--r-- | stanza/src/client/iq.rs | 122 | ||||
-rw-r--r-- | stanza/src/client/message.rs | 185 | ||||
-rw-r--r-- | stanza/src/client/mod.rs | 61 | ||||
-rw-r--r-- | stanza/src/client/presence.rs | 225 | ||||
-rw-r--r-- | stanza/src/lib.rs | 11 | ||||
-rw-r--r-- | stanza/src/sasl.rs | 240 | ||||
-rw-r--r-- | stanza/src/stanza_error.rs | 126 | ||||
-rw-r--r-- | stanza/src/starttls.rs | 94 | ||||
-rw-r--r-- | stanza/src/stream.rs | 205 | ||||
-rw-r--r-- | stanza/src/stream_error.rs | 137 |
13 files changed, 1595 insertions, 0 deletions
diff --git a/stanza/Cargo.toml b/stanza/Cargo.toml new file mode 100644 index 0000000..a1ba85f --- /dev/null +++ b/stanza/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "stanza" +version = "0.1.0" +edition = "2021" + +[dependencies] +peanuts = { version = "0.1.0", path = "../../peanuts" } +jid = { version = "0.1.0", path = "../jid" } diff --git a/stanza/src/bind.rs b/stanza/src/bind.rs new file mode 100644 index 0000000..155fd1b --- /dev/null +++ b/stanza/src/bind.rs @@ -0,0 +1,98 @@ +use jid::JID; +use peanuts::{ + element::{FromElement, IntoElement}, + Element, +}; + +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/stanza/src/client/error.rs b/stanza/src/client/error.rs new file mode 100644 index 0000000..689953a --- /dev/null +++ b/stanza/src/client/error.rs @@ -0,0 +1,83 @@ +use std::str::FromStr; + +use peanuts::element::{FromElement, IntoElement}; +use peanuts::{DeserializeError, Element}; + +use crate::stanza_error::Error as StanzaError; +use crate::stanza_error::Text; + +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/stanza/src/client/iq.rs b/stanza/src/client/iq.rs new file mode 100644 index 0000000..388979e --- /dev/null +++ b/stanza/src/client/iq.rs @@ -0,0 +1,122 @@ +use std::str::FromStr; + +use jid::JID; +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; + +use crate::{ + bind::{self, Bind}, + client::error::Error, +}; + +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/stanza/src/client/message.rs b/stanza/src/client/message.rs new file mode 100644 index 0000000..b9d995f --- /dev/null +++ b/stanza/src/client/message.rs @@ -0,0 +1,185 @@ +use std::str::FromStr; + +use jid::JID; +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; + +use super::XMLNS; + +pub struct Message { + from: Option<JID>, + id: Option<String>, + to: Option<JID>, + // can be omitted, if so default to normal + r#type: MessageType, + lang: Option<String>, + // children + subject: Option<Subject>, + body: Option<Body>, + thread: Option<Thread>, +} + +impl FromElement for Message { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("message")?; + element.check_namespace(XMLNS)?; + + let from = element.attribute_opt("from")?; + let id = element.attribute_opt("id")?; + let to = element.attribute_opt("to")?; + let r#type = element.attribute_opt("type")?.unwrap_or_default(); + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + + let subject = element.child_opt()?; + let body = element.child_opt()?; + let thread = element.child_opt()?; + + Ok(Message { + from, + id, + to, + r#type, + lang, + subject, + body, + thread, + }) + } +} + +impl IntoElement for Message { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("message", Some(XMLNS)) + .push_attribute_opt("from", self.from.clone()) + .push_attribute_opt("id", self.id.clone()) + .push_attribute_opt("to", self.to.clone()) + .push_attribute_opt("type", { + if self.r#type == MessageType::Normal { + None + } else { + Some(self.r#type) + } + }) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_child_opt(self.subject.clone()) + .push_child_opt(self.body.clone()) + .push_child_opt(self.thread.clone()) + } +} + +#[derive(Default, PartialEq, Eq, Copy, Clone)] +pub enum MessageType { + Chat, + Error, + Groupchat, + Headline, + #[default] + Normal, +} + +impl FromStr for MessageType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "chat" => Ok(MessageType::Chat), + "error" => Ok(MessageType::Error), + "groupchat" => Ok(MessageType::Groupchat), + "headline" => Ok(MessageType::Headline), + "normal" => Ok(MessageType::Normal), + _ => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for MessageType { + fn to_string(&self) -> String { + match self { + MessageType::Chat => "chat".to_string(), + MessageType::Error => "error".to_string(), + MessageType::Groupchat => "groupchat".to_string(), + MessageType::Headline => "headline".to_string(), + MessageType::Normal => "normal".to_string(), + } + } +} + +#[derive(Clone)] +pub struct Body { + lang: Option<String>, + body: Option<String>, +} + +impl FromElement for Body { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("body")?; + element.check_namespace(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let body = element.pop_value_opt()?; + + Ok(Body { lang, body }) + } +} + +impl IntoElement for Body { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("body", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text_opt(self.body.clone()) + } +} + +#[derive(Clone)] +pub struct Subject { + lang: Option<String>, + subject: Option<String>, +} + +impl FromElement for Subject { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subject")?; + element.check_namespace(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let subject = element.pop_value_opt()?; + + Ok(Subject { lang, subject }) + } +} + +impl IntoElement for Subject { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subject", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text_opt(self.subject.clone()) + } +} + +#[derive(Clone)] +pub struct Thread { + parent: Option<String>, + thread: Option<String>, +} + +impl FromElement for Thread { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("thread")?; + element.check_namespace(XMLNS)?; + + let parent = element.attribute_opt("parent")?; + let thread = element.pop_value_opt()?; + + Ok(Thread { parent, thread }) + } +} + +impl IntoElement for Thread { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("thread", Some(XMLNS)) + .push_attribute_opt("parent", self.parent.clone()) + .push_text_opt(self.thread.clone()) + } +} diff --git a/stanza/src/client/mod.rs b/stanza/src/client/mod.rs new file mode 100644 index 0000000..2b063d6 --- /dev/null +++ b/stanza/src/client/mod.rs @@ -0,0 +1,61 @@ +use iq::Iq; +use message::Message; +use peanuts::{ + element::{Content, ContentBuilder, FromContent, FromElement, IntoContent, IntoElement}, + DeserializeError, +}; +use presence::Presence; + +use super::stream::{self, Error as StreamError}; + +pub mod error; +pub mod iq; +pub mod message; +pub mod presence; + +pub const XMLNS: &str = "jabber:client"; + +pub enum Stanza { + Message(Message), + Presence(Presence), + Iq(Iq), + Error(StreamError), + OtherContent(Content), +} + +impl FromContent for Stanza { + fn from_content(content: Content) -> peanuts::element::DeserializeResult<Self> { + match content { + Content::Element(element) => Ok(Stanza::from_element(element)?), + Content::Text(_) => Ok(Stanza::OtherContent(content)), + Content::PI => Ok(Stanza::OtherContent(content)), + Content::Comment(_) => Ok(Stanza::OtherContent(content)), + } + } +} + +impl FromElement for Stanza { + fn from_element(element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + match element.identify() { + (Some(XMLNS), "message") => Ok(Stanza::Message(Message::from_element(element)?)), + (Some(XMLNS), "presence") => Ok(Stanza::Presence(Presence::from_element(element)?)), + (Some(XMLNS), "iq") => Ok(Stanza::Iq(Iq::from_element(element)?)), + (Some(stream::XMLNS), "error") => { + Ok(Stanza::Error(StreamError::from_element(element)?)) + } + _ => Err(DeserializeError::UnexpectedElement(element)), + } + } +} + +impl IntoContent for Stanza { + fn builder(&self) -> peanuts::element::ContentBuilder { + match self { + Stanza::Message(message) => <Message as IntoContent>::builder(message), + Stanza::Presence(presence) => <Presence as IntoContent>::builder(presence), + Stanza::Iq(iq) => <Iq as IntoContent>::builder(iq), + Stanza::Error(error) => <StreamError as IntoContent>::builder(error), + Stanza::OtherContent(_content) => ContentBuilder::Comment("other-content".to_string()), + } + } +} diff --git a/stanza/src/client/presence.rs b/stanza/src/client/presence.rs new file mode 100644 index 0000000..dd14bff --- /dev/null +++ b/stanza/src/client/presence.rs @@ -0,0 +1,225 @@ +use std::str::FromStr; + +use jid::JID; +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; + +use super::{error::Error, XMLNS}; + +pub struct Presence { + from: Option<JID>, + id: Option<String>, + to: Option<JID>, + r#type: Option<PresenceType>, + lang: Option<String>, + // children + show: Option<Show>, + status: Option<Status>, + priority: Option<Priority>, + // TODO: ##other + // other: Vec<Other>, + errors: Vec<Error>, +} + +impl FromElement for Presence { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("presence")?; + element.check_namespace(XMLNS)?; + + let from = element.attribute_opt("from")?; + let id = element.attribute_opt("id")?; + let to = element.attribute_opt("to")?; + let r#type = element.attribute_opt("type")?; + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + + let show = element.child_opt()?; + let status = element.child_opt()?; + let priority = element.child_opt()?; + let errors = element.children()?; + + Ok(Presence { + from, + id, + to, + r#type, + lang, + show, + status, + priority, + errors, + }) + } +} + +impl IntoElement for Presence { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("presence", Some(XMLNS)) + .push_attribute_opt("from", self.from.clone()) + .push_attribute_opt("id", self.id.clone()) + .push_attribute_opt("to", self.to.clone()) + .push_attribute_opt("type", self.r#type) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_child_opt(self.show) + .push_child_opt(self.status.clone()) + .push_child_opt(self.priority) + .push_children(self.errors.clone()) + } +} + +pub enum Other {} + +#[derive(Copy, Clone)] +pub enum PresenceType { + Error, + Probe, + Subscribe, + Subscribed, + Unavailable, + Unsubscribe, + Unsubscribed, +} + +impl FromStr for PresenceType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "error" => Ok(PresenceType::Error), + "probe" => Ok(PresenceType::Probe), + "subscribe" => Ok(PresenceType::Subscribe), + "subscribed" => Ok(PresenceType::Subscribed), + "unavailable" => Ok(PresenceType::Unavailable), + "unsubscribe" => Ok(PresenceType::Unsubscribe), + "unsubscribed" => Ok(PresenceType::Unsubscribed), + s => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for PresenceType { + fn to_string(&self) -> String { + match self { + PresenceType::Error => "error".to_string(), + PresenceType::Probe => "probe".to_string(), + PresenceType::Subscribe => "subscribe".to_string(), + PresenceType::Subscribed => "subscribed".to_string(), + PresenceType::Unavailable => "unavailable".to_string(), + PresenceType::Unsubscribe => "unsubscribe".to_string(), + PresenceType::Unsubscribed => "unsubscribed".to_string(), + } + } +} + +#[derive(Copy, Clone)] +pub enum Show { + Away, + Chat, + Dnd, + Xa, +} + +impl FromElement for Show { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("show")?; + element.check_namespace(XMLNS)?; + + Ok(element.pop_value()?) + } +} + +impl FromStr for Show { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "away" => Ok(Show::Away), + "chat" => Ok(Show::Chat), + "dnd" => Ok(Show::Dnd), + "xa" => Ok(Show::Xa), + s => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl IntoElement for Show { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("show", Some(XMLNS)).push_text(*self) + } +} + +impl ToString for Show { + fn to_string(&self) -> String { + match self { + Show::Away => "away".to_string(), + Show::Chat => "chat".to_string(), + Show::Dnd => "dnd".to_string(), + Show::Xa => "xa".to_string(), + } + } +} + +#[derive(Clone)] +pub struct Status { + lang: Option<String>, + status: String1024, +} + +impl FromElement for Status { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("status")?; + element.check_namespace(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", XML_NS)?; + let status = element.pop_value()?; + + Ok(Status { lang, status }) + } +} + +impl IntoElement for Status { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("status", Some(XMLNS)) + .push_attribute_opt_namespaced(XML_NS, "lang", self.lang.clone()) + .push_text(self.status.clone()) + } +} + +// TODO: enforce? +/// minLength 1 maxLength 1024 +#[derive(Clone)] +pub struct String1024(pub String); + +impl FromStr for String1024 { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(String1024(s.to_string())) + } +} + +impl ToString for String1024 { + fn to_string(&self) -> String { + self.0.clone() + } +} + +// xs:byte +#[derive(Clone, Copy)] +pub struct Priority(pub i8); + +impl FromElement for Priority { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("priority")?; + element.check_namespace(XMLNS)?; + + Ok(Priority(element.pop_value()?)) + } +} + +impl IntoElement for Priority { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("priority", Some(XMLNS)).push_text(self.0) + } +} diff --git a/stanza/src/lib.rs b/stanza/src/lib.rs new file mode 100644 index 0000000..32716d3 --- /dev/null +++ b/stanza/src/lib.rs @@ -0,0 +1,11 @@ +use peanuts::declaration::VersionInfo; + +pub mod bind; +pub mod client; +pub mod sasl; +pub mod stanza_error; +pub mod starttls; +pub mod stream; +pub mod stream_error; + +pub static XML_VERSION: VersionInfo = VersionInfo::One; diff --git a/stanza/src/sasl.rs b/stanza/src/sasl.rs new file mode 100644 index 0000000..0a3f06f --- /dev/null +++ b/stanza/src/sasl.rs @@ -0,0 +1,240 @@ +use std::ops::Deref; + +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, +}; + +pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-sasl"; + +#[derive(Debug, Clone)] +pub struct Mechanisms { + pub mechanisms: Vec<String>, +} + +impl FromElement for Mechanisms { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("mechanisms")?; + element.check_namespace(XMLNS)?; + let mechanisms: Vec<Mechanism> = element.pop_children()?; + let mechanisms = mechanisms + .into_iter() + .map(|Mechanism(mechanism)| mechanism) + .collect(); + Ok(Mechanisms { mechanisms }) + } +} + +impl IntoElement for Mechanisms { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("mechanisms", Some(XMLNS)).push_children( + self.mechanisms + .iter() + .map(|mechanism| Mechanism(mechanism.to_string())) + .collect(), + ) + } +} + +pub struct Mechanism(String); + +impl FromElement for Mechanism { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("mechanism")?; + element.check_namespace(XMLNS)?; + + let mechanism = element.pop_value()?; + + Ok(Mechanism(mechanism)) + } +} + +impl IntoElement for Mechanism { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("mechanism", Some(XMLNS)).push_text(self.0.clone()) + } +} + +impl Deref for Mechanism { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug)] +pub struct Auth { + pub mechanism: String, + pub sasl_data: String, +} + +impl IntoElement for Auth { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("auth", Some(XMLNS)) + .push_attribute("mechanism", self.mechanism.clone()) + .push_text(self.sasl_data.clone()) + } +} + +#[derive(Debug)] +pub struct Challenge(String); + +impl Deref for Challenge { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromElement for Challenge { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("challenge")?; + element.check_namespace(XMLNS)?; + + let sasl_data = element.value()?; + + Ok(Challenge(sasl_data)) + } +} + +#[derive(Debug)] +pub struct Success(Option<String>); + +impl Deref for Success { + type Target = Option<String>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromElement for Success { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("success")?; + element.check_namespace(XMLNS)?; + + let sasl_data = element.value_opt()?; + + Ok(Success(sasl_data)) + } +} + +#[derive(Debug)] +pub enum ServerResponse { + Challenge(Challenge), + Success(Success), + Failure(Failure), +} + +impl FromElement for ServerResponse { + fn from_element(element: Element) -> peanuts::element::DeserializeResult<Self> { + match element.identify() { + (Some(XMLNS), "challenge") => { + Ok(ServerResponse::Challenge(Challenge::from_element(element)?)) + } + (Some(XMLNS), "success") => { + Ok(ServerResponse::Success(Success::from_element(element)?)) + } + (Some(XMLNS), "failure") => { + Ok(ServerResponse::Failure(Failure::from_element(element)?)) + } + _ => Err(DeserializeError::UnexpectedElement(element)), + } + } +} + +#[derive(Debug)] +pub struct Response(String); + +impl Response { + pub fn new(response: String) -> Self { + Self(response) + } +} + +impl Deref for Response { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl IntoElement for Response { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("response", Some(XMLNS)).push_text(self.0.clone()) + } +} + +#[derive(Debug)] +pub struct Failure { + r#type: Option<FailureType>, + text: Option<Text>, +} + +impl FromElement for Failure { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("failure")?; + element.check_namespace(XMLNS)?; + + let r#type = element.pop_child_opt()?; + let text = element.pop_child_opt()?; + + Ok(Failure { r#type, text }) + } +} + +#[derive(Debug)] +pub enum FailureType { + Aborted, + AccountDisabled, + CredentialsExpired, + EncryptionRequired, + IncorrectEncoding, + InvalidAuthzid, + InvalidMechanism, + MalformedRequest, + MechanismTooWeak, + NotAuthorized, + TemporaryAuthFailure, +} + +impl FromElement for FailureType { + fn from_element(element: Element) -> peanuts::element::DeserializeResult<Self> { + match element.identify() { + (Some(XMLNS), "aborted") => Ok(FailureType::Aborted), + (Some(XMLNS), "account-disabled") => Ok(FailureType::AccountDisabled), + (Some(XMLNS), "credentials-expired") => Ok(FailureType::CredentialsExpired), + (Some(XMLNS), "encryption-required") => Ok(FailureType::EncryptionRequired), + (Some(XMLNS), "incorrect-encoding") => Ok(FailureType::IncorrectEncoding), + (Some(XMLNS), "invalid-authzid") => Ok(FailureType::InvalidAuthzid), + (Some(XMLNS), "invalid-mechanism") => Ok(FailureType::InvalidMechanism), + (Some(XMLNS), "malformed-request") => Ok(FailureType::MalformedRequest), + (Some(XMLNS), "mechanism-too-weak") => Ok(FailureType::MechanismTooWeak), + (Some(XMLNS), "not-authorized") => Ok(FailureType::NotAuthorized), + (Some(XMLNS), "temporary-auth-failure") => Ok(FailureType::TemporaryAuthFailure), + _ => Err(DeserializeError::UnexpectedElement(element)), + } + } +} + +#[derive(Debug)] +pub struct Text { + lang: Option<String>, + text: Option<String>, +} + +impl FromElement for Text { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("text")?; + element.check_namespace(XMLNS)?; + + let lang = element.attribute_opt_namespaced("lang", peanuts::XML_NS)?; + + let text = element.pop_value_opt()?; + + Ok(Text { lang, text }) + } +} diff --git a/stanza/src/stanza_error.rs b/stanza/src/stanza_error.rs new file mode 100644 index 0000000..99c1f15 --- /dev/null +++ b/stanza/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/stanza/src/starttls.rs b/stanza/src/starttls.rs new file mode 100644 index 0000000..fb66711 --- /dev/null +++ b/stanza/src/starttls.rs @@ -0,0 +1,94 @@ +use std::collections::{HashMap, HashSet}; + +use peanuts::{ + element::{Content, FromElement, IntoElement, Name, NamespaceDeclaration}, + Element, +}; + +pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-tls"; + +#[derive(Debug, Clone)] +pub struct StartTls { + pub required: bool, +} + +impl IntoElement for StartTls { + fn builder(&self) -> peanuts::element::ElementBuilder { + let mut builder = Element::builder("starttls", Some(XMLNS)); + + if self.required { + builder = builder.push_child(Required) + } + + builder + } +} + +impl FromElement for StartTls { + fn from_element( + mut element: peanuts::Element, + ) -> std::result::Result<StartTls, peanuts::DeserializeError> { + element.check_name("starttls")?; + element.check_namespace(XMLNS)?; + + let mut required = false; + if let Some(_) = element.child_opt::<Required>()? { + required = true; + } + + Ok(StartTls { required }) + } +} + +#[derive(Debug)] +pub struct Required; + +impl FromElement for Required { + fn from_element(element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("required")?; + element.check_namespace(XMLNS)?; + + Ok(Required) + } +} + +impl IntoElement for Required { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("required", Some(XMLNS)) + } +} + +#[derive(Debug)] +pub struct Proceed; + +impl IntoElement for Proceed { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("proceed", Some(XMLNS)) + } +} + +impl FromElement for Proceed { + fn from_element(element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("proceed")?; + element.check_namespace(XMLNS)?; + + Ok(Proceed) + } +} + +pub struct Failure; + +impl IntoElement for Failure { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("failure", Some(XMLNS)) + } +} + +impl FromElement for Failure { + fn from_element(element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("failure")?; + element.check_namespace(XMLNS)?; + + Ok(Failure) + } +} diff --git a/stanza/src/stream.rs b/stanza/src/stream.rs new file mode 100644 index 0000000..89b03d8 --- /dev/null +++ b/stanza/src/stream.rs @@ -0,0 +1,205 @@ +use std::collections::{HashMap, HashSet}; + +use jid::JID; +use peanuts::element::{Content, ElementBuilder, FromElement, IntoElement, NamespaceDeclaration}; +use peanuts::{element::Name, Element}; + +use crate::bind; + +use super::sasl::{self, Mechanisms}; +use super::starttls::{self, StartTls}; +use super::stream_error::{Error as StreamError, Text}; +use super::{client, stream_error}; + +pub const XMLNS: &str = "http://etherx.jabber.org/streams"; + +// MUST be qualified by stream namespace +// #[derive(XmlSerialize, XmlDeserialize)] +// #[peanuts(xmlns = XMLNS)] +#[derive(Debug)] +pub struct Stream { + pub from: Option<JID>, + to: Option<JID>, + id: Option<String>, + version: Option<String>, + // TODO: lang enum + lang: Option<String>, + // #[peanuts(content)] + // content: Message, +} + +impl FromElement for Stream { + fn from_element(mut element: Element) -> std::result::Result<Self, peanuts::DeserializeError> { + element.check_namespace(XMLNS)?; + element.check_name("stream")?; + + let from = element.attribute_opt("from")?; + let to = element.attribute_opt("to")?; + let id = element.attribute_opt("id")?; + let version = element.attribute_opt("version")?; + let lang = element.attribute_opt_namespaced("lang", peanuts::XML_NS)?; + + Ok(Stream { + from, + to, + id, + version, + lang, + }) + } +} + +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>, client::XMLNS) + .push_attribute_opt("to", self.to.clone()) + .push_attribute_opt("from", self.from.clone()) + .push_attribute_opt("id", self.id.clone()) + .push_attribute_opt("version", self.version.clone()) + .push_attribute_opt_namespaced(peanuts::XML_NS, "to", self.lang.clone()) + } +} + +impl<'s> Stream { + pub fn new( + from: Option<JID>, + to: Option<JID>, + id: Option<String>, + version: Option<String>, + lang: Option<String>, + ) -> Self { + Self { + from, + to, + id, + version, + lang, + } + } + + /// For initial stream headers, the initiating entity SHOULD include the 'xml:lang' attribute. + /// For privacy, it is better to not set `from` when sending a client stanza over an unencrypted connection. + pub fn new_client(from: Option<JID>, to: JID, id: Option<String>, lang: String) -> Self { + Self { + from, + to: Some(to), + id, + version: Some("1.0".to_string()), + lang: Some(lang), + } + } +} + +#[derive(Debug)] +pub struct Features { + pub features: Vec<Feature>, +} + +impl Features { + pub fn negotiate(self) -> Option<Feature> { + if let Some(Feature::StartTls(s)) = self + .features + .iter() + .find(|feature| matches!(feature, Feature::StartTls(_s))) + { + // TODO: avoid clone + return Some(Feature::StartTls(s.clone())); + } else if let Some(Feature::Sasl(mechanisms)) = self + .features + .iter() + .find(|feature| matches!(feature, Feature::Sasl(_))) + { + // TODO: avoid clone + return Some(Feature::Sasl(mechanisms.clone())); + } else if let Some(Feature::Bind) = self + .features + .into_iter() + .find(|feature| matches!(feature, Feature::Bind)) + { + Some(Feature::Bind) + } else { + return None; + } + } +} + +impl IntoElement for Features { + fn builder(&self) -> ElementBuilder { + Element::builder("features", Some(XMLNS)).push_children(self.features.clone()) + } +} + +impl FromElement for Features { + fn from_element( + mut element: Element, + ) -> std::result::Result<Features, peanuts::DeserializeError> { + element.check_namespace(XMLNS)?; + element.check_name("features")?; + + let features = element.children()?; + + Ok(Features { features }) + } +} + +#[derive(Debug, Clone)] +pub enum Feature { + StartTls(StartTls), + Sasl(Mechanisms), + Bind, + Unknown, +} + +impl IntoElement for Feature { + fn builder(&self) -> ElementBuilder { + match self { + Feature::StartTls(start_tls) => start_tls.builder(), + Feature::Sasl(mechanisms) => mechanisms.builder(), + Feature::Bind => todo!(), + Feature::Unknown => todo!(), + } + } +} + +impl FromElement for Feature { + fn from_element(element: Element) -> peanuts::element::DeserializeResult<Self> { + match element.identify() { + (Some(starttls::XMLNS), "starttls") => { + Ok(Feature::StartTls(StartTls::from_element(element)?)) + } + (Some(sasl::XMLNS), "mechanisms") => { + Ok(Feature::Sasl(Mechanisms::from_element(element)?)) + } + (Some(bind::XMLNS), "bind") => Ok(Feature::Bind), + _ => Ok(Feature::Unknown), + } + } +} + +#[derive(Debug)] +pub struct Error { + error: StreamError, + text: Option<Text>, +} + +impl FromElement for Error { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("error")?; + element.check_namespace(XMLNS)?; + + let error = element.pop_child_one()?; + let text = element.pop_child_opt()?; + + Ok(Error { error, text }) + } +} + +impl IntoElement for Error { + fn builder(&self) -> ElementBuilder { + Element::builder("error", Some(XMLNS)) + .push_child(self.error.clone()) + .push_child_opt(self.text.clone()) + } +} diff --git a/stanza/src/stream_error.rs b/stanza/src/stream_error.rs new file mode 100644 index 0000000..5ae04a6 --- /dev/null +++ b/stanza/src/stream_error.rs @@ -0,0 +1,137 @@ +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, XML_NS, +}; + +pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-streams"; + +#[derive(Clone, Debug)] +pub enum Error { + BadFormat, + BadNamespacePrefix, + Conflict, + ConnectionTimeout, + HostGone, + HostUnknown, + ImproperAddressing, + InternalServerError, + InvalidFrom, + InvalidId, + InvalidNamespace, + InvalidXml, + NotAuthorized, + NotWellFormed, + PolicyViolation, + RemoteConnectionFailed, + Reset, + ResourceConstraint, + RestrictedXml, + SeeOtherHost(Option<String>), + SystemShutdown, + UndefinedCondition, + UnsupportedEncoding, + UnsupportedStanzaType, + UnsupportedVersion, +} + +impl FromElement for Error { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + let error; + match element.identify() { + (Some(XMLNS), "bad-format") => error = Error::BadFormat, + (Some(XMLNS), "bad-namespace-prefix") => error = Error::BadNamespacePrefix, + (Some(XMLNS), "conflict") => error = Error::Conflict, + (Some(XMLNS), "connection-timeout") => error = Error::ConnectionTimeout, + (Some(XMLNS), "host-gone") => error = Error::HostGone, + (Some(XMLNS), "host-unknown") => error = Error::HostUnknown, + (Some(XMLNS), "improper-addressing") => error = Error::ImproperAddressing, + (Some(XMLNS), "internal-server-error") => error = Error::InternalServerError, + (Some(XMLNS), "invalid-from") => error = Error::InvalidFrom, + (Some(XMLNS), "invalid-id") => error = Error::InvalidId, + (Some(XMLNS), "invalid-namespace") => error = Error::InvalidNamespace, + (Some(XMLNS), "invalid-xml") => error = Error::InvalidXml, + (Some(XMLNS), "not-authorized") => error = Error::NotAuthorized, + (Some(XMLNS), "not-well-formed") => error = Error::NotWellFormed, + (Some(XMLNS), "policy-violation") => error = Error::PolicyViolation, + (Some(XMLNS), "remote-connection-failed") => error = Error::RemoteConnectionFailed, + (Some(XMLNS), "reset") => error = Error::Reset, + (Some(XMLNS), "resource-constraint") => error = Error::ResourceConstraint, + (Some(XMLNS), "restricted-xml") => error = Error::RestrictedXml, + (Some(XMLNS), "see-other-host") => { + return Ok(Error::SeeOtherHost(element.pop_value_opt()?)) + } + (Some(XMLNS), "system-shutdown") => error = Error::SystemShutdown, + (Some(XMLNS), "undefined-condition") => error = Error::UndefinedCondition, + (Some(XMLNS), "unsupported-encoding") => error = Error::UnsupportedEncoding, + (Some(XMLNS), "unsupported-stanza-type") => error = Error::UnsupportedStanzaType, + (Some(XMLNS), "unsupported-version") => error = Error::UnsupportedVersion, + _ => return Err(DeserializeError::UnexpectedElement(element)), + } + element.no_more_content()?; + return Ok(error); + } +} + +impl IntoElement for Error { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + Error::BadFormat => Element::builder("bad-format", Some(XMLNS)), + Error::BadNamespacePrefix => Element::builder("bad-namespace-prefix", Some(XMLNS)), + Error::Conflict => Element::builder("conflict", Some(XMLNS)), + Error::ConnectionTimeout => Element::builder("connection-timeout", Some(XMLNS)), + Error::HostGone => Element::builder("host-gone", Some(XMLNS)), + Error::HostUnknown => Element::builder("host-unknown", Some(XMLNS)), + Error::ImproperAddressing => Element::builder("improper-addressing", Some(XMLNS)), + Error::InternalServerError => Element::builder("internal-server-error", Some(XMLNS)), + Error::InvalidFrom => Element::builder("invalid-from", Some(XMLNS)), + Error::InvalidId => Element::builder("invalid-id", Some(XMLNS)), + Error::InvalidNamespace => Element::builder("invalid-namespace", Some(XMLNS)), + Error::InvalidXml => Element::builder("invalid-xml", Some(XMLNS)), + Error::NotAuthorized => Element::builder("not-authorized", Some(XMLNS)), + Error::NotWellFormed => Element::builder("not-well-formed", Some(XMLNS)), + Error::PolicyViolation => Element::builder("policy-violation", Some(XMLNS)), + Error::RemoteConnectionFailed => { + Element::builder("remote-connection-failed", Some(XMLNS)) + } + Error::Reset => Element::builder("reset", Some(XMLNS)), + Error::ResourceConstraint => Element::builder("resource-constraint", Some(XMLNS)), + Error::RestrictedXml => Element::builder("restricted-xml", Some(XMLNS)), + Error::SeeOtherHost(h) => { + Element::builder("see-other-host", Some(XMLNS)).push_text_opt(h.clone()) + } + Error::SystemShutdown => Element::builder("system-shutdown", Some(XMLNS)), + Error::UndefinedCondition => Element::builder("undefined-condition", Some(XMLNS)), + Error::UnsupportedEncoding => Element::builder("unsupported-encoding", Some(XMLNS)), + Error::UnsupportedStanzaType => { + Element::builder("unsupported-stanza-type", Some(XMLNS)) + } + Error::UnsupportedVersion => Element::builder("unsupported-version", Some(XMLNS)), + } + } +} + +#[derive(Clone, Debug)] +pub struct Text { + text: Option<String>, + lang: 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()) + } +} |