aboutsummaryrefslogtreecommitdiffstats
path: root/stanza/src/client
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2024-12-04 18:18:37 +0000
committerLibravatar cel 🌸 <cel@bunny.garden>2024-12-04 18:18:37 +0000
commit1b91ff690488b65b552c90bd5392b9a300c8c981 (patch)
tree9c290f69b26eba0393d7bbc05ba29c28ea74a26e /stanza/src/client
parent03764f8cedb3f0a55a61be0f0a59faaa6357a83a (diff)
downloadluz-1b91ff690488b65b552c90bd5392b9a300c8c981.tar.gz
luz-1b91ff690488b65b552c90bd5392b9a300c8c981.tar.bz2
luz-1b91ff690488b65b552c90bd5392b9a300c8c981.zip
use cargo workspace
Diffstat (limited to 'stanza/src/client')
-rw-r--r--stanza/src/client/error.rs83
-rw-r--r--stanza/src/client/iq.rs122
-rw-r--r--stanza/src/client/message.rs185
-rw-r--r--stanza/src/client/mod.rs61
-rw-r--r--stanza/src/client/presence.rs225
5 files changed, 676 insertions, 0 deletions
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)
+ }
+}