diff options
author | 2025-04-01 08:27:33 +0100 | |
---|---|---|
committer | 2025-04-01 08:27:33 +0100 | |
commit | 132d932e6264774ad7d6eab9f52719848c5c91bb (patch) | |
tree | 35eb26a46e3da3e7a6451d6fe49f413c00458199 | |
parent | b18c1cc2568dc116de5944d9829303ef8cc61b00 (diff) | |
download | luz-132d932e6264774ad7d6eab9f52719848c5c91bb.tar.gz luz-132d932e6264774ad7d6eab9f52719848c5c91bb.tar.bz2 luz-132d932e6264774ad7d6eab9f52719848c5c91bb.zip |
feat(stanza): xep-0060: pubsub
-rw-r--r-- | stanza/Cargo.toml | 1 | ||||
-rw-r--r-- | stanza/src/lib.rs | 2 | ||||
-rw-r--r-- | stanza/src/stanza_error.rs | 15 | ||||
-rw-r--r-- | stanza/src/xep_0060/errors.rs | 282 | ||||
-rw-r--r-- | stanza/src/xep_0060/event.rs | 450 | ||||
-rw-r--r-- | stanza/src/xep_0060/mod.rs | 4 | ||||
-rw-r--r-- | stanza/src/xep_0060/owner.rs | 358 | ||||
-rw-r--r-- | stanza/src/xep_0060/pubsub.rs | 722 |
8 files changed, 1833 insertions, 1 deletions
diff --git a/stanza/Cargo.toml b/stanza/Cargo.toml index 352d1ce..0e6f6b5 100644 --- a/stanza/Cargo.toml +++ b/stanza/Cargo.toml @@ -13,5 +13,6 @@ chrono = { version = "0.4.40", optional = true } rfc_6121 = [] xep_0004 = [] xep_0030 = [] +xep_0060 = ["xep_0004", "dep:chrono"] xep_0199 = [] xep_0203 = ["dep:chrono"] diff --git a/stanza/src/lib.rs b/stanza/src/lib.rs index 85c5c84..f4c0899 100644 --- a/stanza/src/lib.rs +++ b/stanza/src/lib.rs @@ -13,6 +13,8 @@ pub mod stream_error; pub mod xep_0004; #[cfg(feature = "xep_0030")] pub mod xep_0030; +#[cfg(feature = "xep_0060")] +pub mod xep_0060; #[cfg(feature = "xep_0199")] pub mod xep_0199; #[cfg(feature = "xep_0203")] diff --git a/stanza/src/stanza_error.rs b/stanza/src/stanza_error.rs index a4b58bc..664a168 100644 --- a/stanza/src/stanza_error.rs +++ b/stanza/src/stanza_error.rs @@ -6,6 +6,9 @@ use peanuts::{ }; use thiserror::Error; +#[cfg(feature = "xep_0060")] +use crate::xep_0060::{self, errors::Error as PubsubError}; + pub const XMLNS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas"; #[derive(Error, Clone, Debug)] @@ -54,6 +57,10 @@ pub enum Error { UndefinedCondition, #[error("unexpected request")] UnexpectedRequest, + #[cfg(feature = "xep_0060")] + #[error("pubsub: {0}")] + PubSub(#[from] PubsubError), + // TODO: unrecognized error } impl FromElement for Error { @@ -82,9 +89,13 @@ impl FromElement for Error { (Some(XMLNS), "subscription-required") => error = Error::SubscriptionRequired, (Some(XMLNS), "undefined-condition") => error = Error::UndefinedCondition, (Some(XMLNS), "unexpected-request") => error = Error::UnexpectedRequest, + #[cfg(feature = "xep_0060")] + (Some(xep_0060::errors::XMLNS), _) => { + error = Error::PubSub(PubsubError::from_element(element)?) + } _ => return Err(peanuts::DeserializeError::UnexpectedElement(element)), } - element.no_more_content()?; + // element.no_more_content()?; return Ok(error); } } @@ -118,6 +129,8 @@ impl IntoElement for Error { Error::SubscriptionRequired => Element::builder("subscription-required", Some(XMLNS)), Error::UndefinedCondition => Element::builder("undefined-condition", Some(XMLNS)), Error::UnexpectedRequest => Element::builder("unexpected-request", Some(XMLNS)), + #[cfg(feature = "xep_0060")] + Error::PubSub(pubsub) => pubsub.builder(), } } } diff --git a/stanza/src/xep_0060/errors.rs b/stanza/src/xep_0060/errors.rs new file mode 100644 index 0000000..10d1483 --- /dev/null +++ b/stanza/src/xep_0060/errors.rs @@ -0,0 +1,282 @@ +use std::{fmt::Display, str::FromStr}; + +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, +}; +use thiserror::Error; + +pub const XMLNS: &str = "http://jabber.org/protocol/pubsub#errors"; + +#[derive(Error, Clone, Debug)] +pub enum Error { + #[error("closed node")] + ClosedNode, + #[error("configuration required")] + ConfigurationRequired, + #[error("invalid jid")] + InvalidJID, + #[error("invalid options")] + InvalidOptions, + #[error("invalid payload")] + InvalidPayload, + #[error("invalid subscription id")] + InvalidSubID, + #[error("item forbidden")] + ItemForbidden, + #[error("item required")] + ItemRequired, + #[error("jid required")] + JIDRequired, + #[error("max items exceeded")] + MaxItemsExceeded, + #[error("max nodes exceeded")] + MaxNodesExceeded, + #[error("node id required")] + NodeIDRequired, + #[error("not in roster group")] + NotInRosterGroup, + #[error("not subscribed")] + NotSubscribed, + #[error("payload too big")] + PayloadTooBig, + #[error("payload required")] + PayloadRequired, + #[error("pending subscription")] + PendingSubscription, + #[error("precondition not met")] + PreconditionNotMet, + #[error("presence subscription required")] + PresenceSubscriptionRequired, + #[error("subscription id required")] + SubIDRequired, + #[error("too many subscriptions")] + TooManySubscriptions, + #[error("unsupported feature: {0}")] + Unsupported(Feature), + #[error("unsupported access mode")] + UnsupportedAccessModel, +} + +impl FromElement for Error { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + match element.identify() { + (Some(XMLNS), "closed-node") => Ok(Self::ClosedNode), + (Some(XMLNS), "configuration-required") => Ok(Self::ConfigurationRequired), + (Some(XMLNS), "invalid-jid") => Ok(Self::InvalidJID), + (Some(XMLNS), "invalid-options") => Ok(Self::InvalidOptions), + (Some(XMLNS), "invalid-payload") => Ok(Self::InvalidPayload), + (Some(XMLNS), "invalid-subid") => Ok(Self::InvalidSubID), + (Some(XMLNS), "item-forbidden") => Ok(Self::ItemForbidden), + (Some(XMLNS), "item-required") => Ok(Self::ItemRequired), + (Some(XMLNS), "jid-required") => Ok(Self::JIDRequired), + (Some(XMLNS), "max-items-exceeded") => Ok(Self::MaxItemsExceeded), + (Some(XMLNS), "max-nodes-exceeded") => Ok(Self::MaxNodesExceeded), + (Some(XMLNS), "nodeid-required") => Ok(Self::NodeIDRequired), + (Some(XMLNS), "not-in-roster-group") => Ok(Self::NotInRosterGroup), + (Some(XMLNS), "not-subscribed") => Ok(Self::NotSubscribed), + (Some(XMLNS), "payload-too-big") => Ok(Self::PayloadTooBig), + (Some(XMLNS), "payload-required") => Ok(Self::PayloadRequired), + (Some(XMLNS), "pending-subscription") => Ok(Self::PendingSubscription), + (Some(XMLNS), "precondition-not-met") => Ok(Self::PreconditionNotMet), + (Some(XMLNS), "presence-subscription-required") => { + Ok(Self::PresenceSubscriptionRequired) + } + (Some(XMLNS), "subid-required") => Ok(Self::SubIDRequired), + (Some(XMLNS), "too-many-subscription") => Ok(Self::TooManySubscriptions), + (Some(XMLNS), "unsupported") => Ok(Self::Unsupported(element.attribute("feature")?)), + (Some(XMLNS), "unsupported-access-model") => Ok(Self::UnsupportedAccessModel), + _ => return Err(peanuts::DeserializeError::UnexpectedElement(element)), + } + } +} + +impl IntoElement for Error { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + Error::ClosedNode => Element::builder("closed-node", Some(XMLNS)), + Error::ConfigurationRequired => Element::builder("configuration-required", Some(XMLNS)), + Error::InvalidJID => Element::builder("invalid-jid", Some(XMLNS)), + Error::InvalidOptions => Element::builder("invalid-options", Some(XMLNS)), + Error::InvalidPayload => Element::builder("invalid-payload", Some(XMLNS)), + Error::InvalidSubID => Element::builder("invalid-subid", Some(XMLNS)), + Error::ItemForbidden => Element::builder("item-forbidden", Some(XMLNS)), + Error::ItemRequired => Element::builder("item-required", Some(XMLNS)), + Error::JIDRequired => Element::builder("jid-required", Some(XMLNS)), + Error::MaxItemsExceeded => Element::builder("max-items-exceeded", Some(XMLNS)), + Error::MaxNodesExceeded => Element::builder("max-nodes-exceeded", Some(XMLNS)), + Error::NodeIDRequired => Element::builder("node-id-required", Some(XMLNS)), + Error::NotInRosterGroup => Element::builder("not-in-roster-group", Some(XMLNS)), + Error::NotSubscribed => Element::builder("not-subscribed", Some(XMLNS)), + Error::PayloadTooBig => Element::builder("payload-too-big", Some(XMLNS)), + Error::PayloadRequired => Element::builder("payload-required", Some(XMLNS)), + Error::PendingSubscription => Element::builder("pending-subscription", Some(XMLNS)), + Error::PreconditionNotMet => Element::builder("precondition-not-met", Some(XMLNS)), + Error::PresenceSubscriptionRequired => { + Element::builder("presence-subscription-required", Some(XMLNS)) + } + Error::SubIDRequired => Element::builder("subid-required", Some(XMLNS)), + Error::TooManySubscriptions => Element::builder("too-many-subscriptions", Some(XMLNS)), + Error::Unsupported(feature) => { + Element::builder("unsupported", Some(XMLNS)).push_attribute("feature", feature) + } + Error::UnsupportedAccessModel => { + Element::builder("unsupported-access-model", Some(XMLNS)) + } + } + } +} + +#[derive(Debug, Clone)] +pub enum Feature { + AccessAuthorize, + AccessOpen, + AccessPresence, + AccessRoster, + AccessWhitelist, + AutoCreate, + AutoSubscribe, + Collections, + ConfigNode, + CreateAndConfigure, + CreateNodes, + DeleteItems, + DeleteNodes, + FilteredNotifications, + GetPending, + InstantNodes, + ItemIDs, + LastPublished, + LeasedSubscription, + ManageSubscriptions, + MemberAffiliation, + MetaData, + ModifyAffiliations, + MultiCollection, + MultiItems, + MultiSubscribe, + OutcastAffiliation, + PersistentItems, + PresenceNotifications, + PresenceSubscribe, + Publish, + PublishOptions, + PublishOnlyAffiliation, + PublisherAffiliation, + PurgeNodes, + RetractItems, + RetrieveAffiliations, + RetrieveDefault, + RetrieveDefaultSub, + RetrieveItems, + RetrieveSubscriptions, + Subscribe, + SubscriptionOptions, + SubscriptionNotifications, +} + +impl Display for Feature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Feature::AccessAuthorize => "access-authorize", + Feature::AccessOpen => "access-open", + Feature::AccessPresence => "access-presence", + Feature::AccessRoster => "access-roster", + Feature::AccessWhitelist => "access-whitelist", + Feature::AutoCreate => "auto-create", + Feature::AutoSubscribe => "auto-subscribe", + Feature::Collections => "collections", + Feature::ConfigNode => "config-node", + Feature::CreateAndConfigure => "create-and-configure", + Feature::CreateNodes => "create-nodes", + Feature::DeleteItems => "delete-items", + Feature::DeleteNodes => "delete-nodes", + Feature::FilteredNotifications => "filtered-notifications", + Feature::GetPending => "get-pending", + Feature::InstantNodes => "instant-nodes", + Feature::ItemIDs => "item-ids", + Feature::LastPublished => "last-published", + Feature::LeasedSubscription => "leased-subscription", + Feature::ManageSubscriptions => "manage-subscriptions", + Feature::MemberAffiliation => "member-affiliation", + Feature::MetaData => "meta-data", + Feature::ModifyAffiliations => "modify-affiliations", + Feature::MultiCollection => "multi-collection", + Feature::MultiItems => "multi-items", + Feature::MultiSubscribe => "multi-subscribe", + Feature::OutcastAffiliation => "outcast-affiliation", + Feature::PersistentItems => "persistent-items", + Feature::PresenceNotifications => "presence-notifications", + Feature::PresenceSubscribe => "presence-subscribe", + Feature::Publish => "publish", + Feature::PublishOptions => "publish-options", + Feature::PublishOnlyAffiliation => "publish-only-affiliation", + Feature::PublisherAffiliation => "publisher-affiliation", + Feature::PurgeNodes => "purge-nodes", + Feature::RetractItems => "retract-items", + Feature::RetrieveAffiliations => "retrieve-affiliations", + Feature::RetrieveDefault => "retrieve-default", + Feature::RetrieveDefaultSub => "retrieve-default-sub", + Feature::RetrieveItems => "retrieve-items", + Feature::RetrieveSubscriptions => "retrieve-subscriptions", + Feature::Subscribe => "subscribe", + Feature::SubscriptionOptions => "subscription-options", + Feature::SubscriptionNotifications => "subscription-notifications", + }; + f.write_str(str) + } +} + +impl FromStr for Feature { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s { + "access-authorize" => Feature::AccessAuthorize, + "access-open" => Feature::AccessOpen, + "access-presence" => Feature::AccessPresence, + "access-roster" => Feature::AccessRoster, + "access-whitelist" => Feature::AccessWhitelist, + "auto-create" => Feature::AutoCreate, + "auto-subscribe" => Feature::AutoSubscribe, + "collections" => Feature::Collections, + "config-node" => Feature::ConfigNode, + "create-and-configure" => Feature::CreateAndConfigure, + "create-nodes" => Feature::CreateNodes, + "delete-items" => Feature::DeleteItems, + "delete-nodes" => Feature::DeleteNodes, + "filtered-notifications" => Feature::FilteredNotifications, + "get-pending" => Feature::GetPending, + "instant-nodes" => Feature::InstantNodes, + "item-ids" => Feature::ItemIDs, + "last-published" => Feature::LastPublished, + "leased-subscription" => Feature::LeasedSubscription, + "manage-subscriptions" => Feature::ManageSubscriptions, + "member-affiliation" => Feature::MemberAffiliation, + "meta-data" => Feature::MetaData, + "modify-affiliations" => Feature::ModifyAffiliations, + "multi-collection" => Feature::MultiCollection, + "multi-items" => Feature::MultiItems, + "multi-subscribe" => Feature::MultiSubscribe, + "outcast-affiliation" => Feature::OutcastAffiliation, + "persistent-items" => Feature::PersistentItems, + "presence-notifications" => Feature::PresenceNotifications, + "presence-subscribe" => Feature::PresenceSubscribe, + "publish" => Feature::Publish, + "publish-options" => Feature::PublishOptions, + "publish-only-affiliation" => Feature::PublishOnlyAffiliation, + "publisher-affiliation" => Feature::PublisherAffiliation, + "purge-nodes" => Feature::PurgeNodes, + "retract-items" => Feature::RetractItems, + "retrieve-affiliations" => Feature::RetrieveAffiliations, + "retrieve-default" => Feature::RetrieveDefault, + "retrieve-default-sub" => Feature::RetrieveDefaultSub, + "retrieve-items" => Feature::RetrieveItems, + "retrieve-subscriptions" => Feature::RetrieveSubscriptions, + "subscribe" => Feature::Subscribe, + "subscription-options" => Feature::SubscriptionOptions, + "subscription-notifications" => Feature::SubscriptionNotifications, + s => return Err(DeserializeError::FromStr(s.to_owned())), + }) + } +} diff --git a/stanza/src/xep_0060/event.rs b/stanza/src/xep_0060/event.rs new file mode 100644 index 0000000..bdd8b53 --- /dev/null +++ b/stanza/src/xep_0060/event.rs @@ -0,0 +1,450 @@ +use std::str::FromStr; + +use chrono::{DateTime, Utc}; +use jid::JID; +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, +}; + +use crate::xep_0004::X; + +pub const XMLNS: &str = "http://jabber.org/protocol/pubsub#event"; + +#[derive(Clone, Debug)] +pub enum Event { + Collection(Collection), + Configuration(Configuration), + Delete(Delete), + Items(Items), + Purge(Purge), + Subscription(Subscription), +} + +impl FromElement for Event { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("event")?; + element.check_namespace(XMLNS)?; + + let child = element.pop_child_one::<Element>()?; + + match child.identify() { + (Some(XMLNS), "collection") => Ok(Self::Collection(Collection::from_element(child)?)), + (Some(XMLNS), "configuration") => { + Ok(Self::Configuration(Configuration::from_element(child)?)) + } + (Some(XMLNS), "delete") => Ok(Self::Delete(Delete::from_element(child)?)), + (Some(XMLNS), "items") => Ok(Self::Items(Items::from_element(child)?)), + (Some(XMLNS), "purge") => Ok(Self::Purge(Purge::from_element(child)?)), + (Some(XMLNS), "subscription") => { + Ok(Self::Subscription(Subscription::from_element(child)?)) + } + _ => Err(peanuts::DeserializeError::UnexpectedElement(child)), + } + } +} + +impl IntoElement for Event { + fn builder(&self) -> peanuts::element::ElementBuilder { + let builder = Element::builder("event", Some(XMLNS)); + + match self { + Event::Collection(collection) => builder.push_child(collection.clone()), + Event::Configuration(configuration) => builder.push_child(configuration.clone()), + Event::Delete(delete) => builder.push_child(delete.clone()), + Event::Items(items) => builder.push_child(items.clone()), + Event::Purge(purge) => builder.push_child(purge.clone()), + Event::Subscription(subscription) => builder.push_child(subscription.clone()), + } + } +} + +#[derive(Clone, Debug)] +pub struct Collection { + node: Option<String>, + r#type: CollectionType, +} + +impl FromElement for Collection { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("node")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute_opt("node")?; + let r#type = element.pop_child_one()?; + + Ok(Self { node, r#type }) + } +} + +impl IntoElement for Collection { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("collection", Some(XMLNS)) + .push_attribute_opt("node", self.node.clone()) + .push_child(self.r#type.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum CollectionType { + Associate(Associate), + Disassociate(Disassociate), +} + +impl FromElement for CollectionType { + fn from_element(element: Element) -> peanuts::element::DeserializeResult<Self> { + match element.identify() { + (Some(XMLNS), "associate") => { + Ok(CollectionType::Associate(Associate::from_element(element)?)) + } + (Some(XMLNS), "disassociate") => Ok(CollectionType::Disassociate( + Disassociate::from_element(element)?, + )), + _ => Err(DeserializeError::UnexpectedElement(element)), + } + } +} + +impl IntoElement for CollectionType { + fn builder(&self) -> peanuts::element::ElementBuilder { + match self { + CollectionType::Associate(associate) => associate.builder(), + CollectionType::Disassociate(disassociate) => disassociate.builder(), + } + } +} + +#[derive(Clone, Debug)] +pub struct Associate { + node: String, +} + +impl FromElement for Associate { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("associate")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + Ok(Self { node }) + } +} + +impl IntoElement for Associate { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("associate", Some(XMLNS)).push_attribute("node", self.node.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Disassociate { + node: String, +} + +impl FromElement for Disassociate { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("disassociate")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + Ok(Self { node }) + } +} + +impl IntoElement for Disassociate { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("disassociate", Some(XMLNS)).push_attribute("node", self.node.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Configuration { + node: Option<String>, + configuration: Option<X>, +} + +impl FromElement for Configuration { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("configuration")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute_opt("node")?; + + let configuration = element.pop_child_opt()?; + + Ok(Self { + node, + configuration, + }) + } +} + +impl IntoElement for Configuration { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("configuration", Some(XMLNS)) + .push_attribute_opt("node", self.node.clone()) + .push_child_opt(self.configuration.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Delete { + node: String, + redirect: Option<Redirect>, +} + +impl FromElement for Delete { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("delete")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + let redirect = element.pop_child_opt()?; + + Ok(Self { node, redirect }) + } +} + +impl IntoElement for Delete { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("delete", Some(XMLNS)) + .push_attribute("node", self.node.clone()) + .push_child_opt(self.redirect.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Items { + node: String, + items: ItemsType, +} + +impl FromElement for Items { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("items")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + let items = if let Ok(items) = element.pop_children() { + ItemsType::Item(items) + } else { + ItemsType::Retract(element.pop_children()?) + }; + + Ok(Self { node, items }) + } +} + +impl IntoElement for Items { + fn builder(&self) -> peanuts::element::ElementBuilder { + let builder = + Element::builder("items", Some(XMLNS)).push_attribute("node", self.node.clone()); + + match &self.items { + ItemsType::Item(items) => builder.push_children(items.clone()), + ItemsType::Retract(retracts) => builder.push_children(retracts.clone()), + } + } +} + +#[derive(Clone, Debug)] +pub enum ItemsType { + Item(Vec<Item>), + Retract(Vec<Retract>), +} + +#[derive(Clone, Debug)] +pub struct Item { + id: Option<String>, + publisher: Option<String>, + item: Option<Content>, +} + +impl FromElement for Item { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("item")?; + element.check_namespace(XMLNS)?; + + let id = element.attribute_opt("id")?; + let publisher = element.attribute_opt("publisher")?; + + let item = element.pop_child_opt()?; + + Ok(Self { + id, + publisher, + item, + }) + } +} + +impl IntoElement for Item { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("item", Some(XMLNS)) + .push_attribute_opt("id", self.id.clone()) + .push_attribute_opt("publisher", self.publisher.clone()) + .push_child_opt(self.item.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum Content { + Unknown, +} + +impl FromElement for Content { + fn from_element(_element: Element) -> peanuts::element::DeserializeResult<Self> { + // TODO: types + return Ok(Self::Unknown); + } +} + +impl IntoElement for Content { + fn builder(&self) -> peanuts::element::ElementBuilder { + panic!("unknown content cannot be serialized") + } +} + +#[derive(Clone, Debug)] +pub struct Purge { + node: String, +} + +impl FromElement for Purge { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("purge")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + Ok(Self { node }) + } +} + +impl IntoElement for Purge { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("purge", Some(XMLNS)).push_attribute("node", self.node.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Redirect { + uri: String, +} + +impl FromElement for Redirect { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("redirect")?; + element.check_namespace(XMLNS)?; + + let uri = element.attribute("uri")?; + + Ok(Self { uri }) + } +} + +impl IntoElement for Redirect { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("redirect", Some(XMLNS)).push_attribute("uri", self.uri.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Retract { + id: String, +} + +impl FromElement for Retract { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("retract")?; + element.check_namespace(XMLNS)?; + + let id = element.attribute("id")?; + + Ok(Self { id }) + } +} + +impl IntoElement for Retract { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("retract", Some(XMLNS)).push_attribute("id", self.id.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Subscription { + expiry: Option<DateTime<Utc>>, + jid: JID, + node: Option<String>, + subid: Option<String>, + subscription: Option<SubscriptionState>, +} + +impl FromElement for Subscription { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subscription")?; + element.check_namespace(XMLNS)?; + + let expiry = element.attribute_opt("expiry")?; + let jid = element.attribute("jid")?; + let node = element.attribute_opt("node")?; + let subid = element.attribute_opt("subid")?; + let subscription = element.attribute_opt("subscription")?; + + Ok(Self { + expiry, + jid, + node, + subid, + subscription, + }) + } +} + +impl IntoElement for Subscription { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subscription", Some(XMLNS)) + .push_attribute_opt("expiry", self.expiry) + .push_attribute("jid", self.jid.clone()) + .push_attribute_opt("node", self.node.clone()) + .push_attribute_opt("subid", self.subid.clone()) + .push_attribute_opt("subscription", self.subscription.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum SubscriptionState { + None, + Pending, + Subscribed, + Unconfigured, +} + +impl FromStr for SubscriptionState { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "none" => Ok(Self::None), + "pending" => Ok(Self::Pending), + "subscribed" => Ok(Self::Subscribed), + "unconfigured" => Ok(Self::Unconfigured), + s => Err(DeserializeError::FromStr(s.to_owned())), + } + } +} + +impl ToString for SubscriptionState { + fn to_string(&self) -> String { + match self { + SubscriptionState::None => "none", + SubscriptionState::Pending => "pending", + SubscriptionState::Subscribed => "subscribed", + SubscriptionState::Unconfigured => "unconfigured", + } + .to_owned() + } +} diff --git a/stanza/src/xep_0060/mod.rs b/stanza/src/xep_0060/mod.rs new file mode 100644 index 0000000..566310f --- /dev/null +++ b/stanza/src/xep_0060/mod.rs @@ -0,0 +1,4 @@ +pub mod errors; +pub mod event; +pub mod owner; +pub mod pubsub; diff --git a/stanza/src/xep_0060/owner.rs b/stanza/src/xep_0060/owner.rs new file mode 100644 index 0000000..1fedc60 --- /dev/null +++ b/stanza/src/xep_0060/owner.rs @@ -0,0 +1,358 @@ +use std::str::FromStr; + +use jid::JID; +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, +}; + +use crate::xep_0004::X; + +pub const XMLNS: &str = "http://jabber.org/protocol/pubsub#owner"; + +#[derive(Clone, Debug)] +pub enum Pubsub { + Affiliations(Affiliations), + Configure(Configure), + Default(Default), + Delete(Delete), + Purge(Purge), + Subscriptions(Subscriptions), +} + +impl FromElement for Pubsub { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("pubsub")?; + element.check_namespace(XMLNS)?; + + let child = element.pop_child_one::<Element>()?; + + match child.identify() { + (Some(XMLNS), "affiliations") => { + Ok(Self::Affiliations(Affiliations::from_element(child)?)) + } + (Some(XMLNS), "configure") => Ok(Self::Configure(Configure::from_element(child)?)), + (Some(XMLNS), "default") => Ok(Self::Default(Default::from_element(child)?)), + (Some(XMLNS), "delete") => Ok(Self::Delete(Delete::from_element(child)?)), + (Some(XMLNS), "purge") => Ok(Self::Purge(Purge::from_element(child)?)), + (Some(XMLNS), "subscriptions") => { + Ok(Self::Subscriptions(Subscriptions::from_element(child)?)) + } + _ => Err(DeserializeError::UnexpectedElement(child)), + } + } +} + +impl IntoElement for Pubsub { + fn builder(&self) -> peanuts::element::ElementBuilder { + let builder = Element::builder("pubsub", Some(XMLNS)); + + match self { + Pubsub::Affiliations(affiliations) => builder.push_child(affiliations.clone()), + Pubsub::Configure(configure) => builder.push_child(configure.clone()), + Pubsub::Default(default) => builder.push_child(default.clone()), + Pubsub::Delete(delete) => builder.push_child(delete.clone()), + Pubsub::Purge(purge) => builder.push_child(purge.clone()), + Pubsub::Subscriptions(subscriptions) => builder.push_child(subscriptions.clone()), + } + } +} + +#[derive(Clone, Debug)] +pub struct Affiliations { + node: String, + affiliations: Vec<Affiliation>, +} + +impl FromElement for Affiliations { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("affiliations")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + let affiliations = element.pop_children()?; + + Ok(Self { node, affiliations }) + } +} + +impl IntoElement for Affiliations { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("affiliations", Some(XMLNS)) + .push_attribute("node", self.node.clone()) + .push_children(self.affiliations.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Affiliation { + affiliation: AffiliationType, + jid: JID, +} + +impl FromElement for Affiliation { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("affiliation")?; + element.check_namespace(XMLNS)?; + + let affiliation = element.attribute("affiliation")?; + let jid = element.attribute("jid")?; + + Ok(Self { affiliation, jid }) + } +} + +impl IntoElement for Affiliation { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("affiliation", Some(XMLNS)) + .push_attribute("affiliation", self.affiliation.clone()) + .push_attribute("jid", self.jid.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum AffiliationType { + Member, + None, + Outcast, + Owner, + Publisher, + PublishOnly, +} + +impl FromStr for AffiliationType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "member" => Ok(Self::Member), + "none" => Ok(Self::None), + "outcast" => Ok(Self::Outcast), + "owner" => Ok(Self::Owner), + "publisher" => Ok(Self::Publisher), + "publish-only" => Ok(Self::PublishOnly), + s => Err(DeserializeError::FromStr(s.to_owned())), + } + } +} + +impl ToString for AffiliationType { + fn to_string(&self) -> String { + match self { + AffiliationType::Member => "member", + AffiliationType::None => "none", + AffiliationType::Outcast => "outcast", + AffiliationType::Owner => "owner", + AffiliationType::Publisher => "publisher", + AffiliationType::PublishOnly => "publish-only", + } + .to_owned() + } +} + +#[derive(Clone, Debug)] +pub struct Configure { + node: Option<String>, + configure: Option<X>, +} + +impl FromElement for Configure { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("configure")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute_opt("node")?; + + let configure = element.pop_child_opt()?; + + Ok(Self { node, configure }) + } +} + +impl IntoElement for Configure { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("configure", Some(XMLNS)) + .push_attribute_opt("node", self.node.clone()) + .push_child_opt(self.configure.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Default(Option<X>); + +impl FromElement for Default { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("default")?; + element.check_namespace(XMLNS)?; + + Ok(Self(element.pop_child_opt()?)) + } +} + +impl IntoElement for Default { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("default", Some(XMLNS)).push_child_opt(self.0.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Delete { + node: String, + redirect: Option<Redirect>, +} + +impl FromElement for Delete { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("delete")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + let redirect = element.pop_child_opt()?; + + Ok(Self { node, redirect }) + } +} + +impl IntoElement for Delete { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("delete", Some(XMLNS)) + .push_attribute("node", self.node.clone()) + .push_child_opt(self.redirect.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Purge { + node: String, +} + +impl FromElement for Purge { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("purge")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + Ok(Self { node }) + } +} + +impl IntoElement for Purge { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("purge", Some(XMLNS)).push_attribute("node", self.node.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Redirect { + uri: String, +} + +impl FromElement for Redirect { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("redirect")?; + element.check_namespace(XMLNS)?; + + let uri = element.attribute("uri")?; + + Ok(Self { uri }) + } +} + +impl IntoElement for Redirect { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("redirect", Some(XMLNS)).push_attribute("uri", self.uri.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Subscriptions { + node: String, + subscriptions: Vec<Subscription>, +} + +impl FromElement for Subscriptions { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subscriptions")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + let subscriptions = element.pop_children()?; + + Ok(Self { + node, + subscriptions, + }) + } +} + +impl IntoElement for Subscriptions { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subscriptions", Some(XMLNS)) + .push_attribute("node", self.node.clone()) + .push_children(self.subscriptions.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Subscription { + subscription: SubscriptionState, + jid: JID, +} + +impl FromElement for Subscription { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subscription")?; + element.check_namespace(XMLNS)?; + + let subscription = element.attribute("subscription")?; + let jid = element.attribute("jid")?; + + Ok(Self { subscription, jid }) + } +} + +impl IntoElement for Subscription { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subscription", Some(XMLNS)) + .push_attribute("subscription", self.subscription.clone()) + .push_attribute("jid", self.jid.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum SubscriptionState { + None, + Pending, + Subscribed, + Unconfigured, +} + +impl FromStr for SubscriptionState { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "none" => Ok(Self::None), + "pending" => Ok(Self::Pending), + "subscribed" => Ok(Self::Subscribed), + "unconfigured" => Ok(Self::Unconfigured), + s => Err(DeserializeError::FromStr(s.to_owned())), + } + } +} + +impl ToString for SubscriptionState { + fn to_string(&self) -> String { + match self { + SubscriptionState::None => "none", + SubscriptionState::Pending => "pending", + SubscriptionState::Subscribed => "subscribed", + SubscriptionState::Unconfigured => "unconfigured", + } + .to_owned() + } +} diff --git a/stanza/src/xep_0060/pubsub.rs b/stanza/src/xep_0060/pubsub.rs new file mode 100644 index 0000000..3a15a59 --- /dev/null +++ b/stanza/src/xep_0060/pubsub.rs @@ -0,0 +1,722 @@ +use std::str::FromStr; + +use jid::JID; +use peanuts::{ + element::{FromElement, IntoElement}, + DeserializeError, Element, +}; + +use crate::xep_0004::X; + +pub const XMLNS: &str = "http://jabber.org/protocol/pubsub"; + +#[derive(Clone, Debug)] +pub enum Pubsub { + Create(Create, Option<Configure>), + Subscribe(Option<Subscribe>, Option<Options>), + Publish(Publish, Option<PublishOptions>), + Affiliations(Affiliations), + Default(Default), + Items(Items), + Retract(Retract), + Subscription(Subscription), + Subscriptions(Subscriptions), + Unsubscribe(Unsubscribe), + Pubsub, +} + +impl FromElement for Pubsub { + fn from_element(mut element: peanuts::Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("pubsub")?; + element.check_namespace(XMLNS)?; + + let first_child = element.pop_child_opt::<Element>()?; + + if let Some(first_child) = first_child { + match first_child.identify() { + (Some(XMLNS), "create") => { + let create = Create::from_element(first_child)?; + let configure = element.pop_child_opt()?; + Ok(Self::Create(create, configure)) + } + (Some(XMLNS), "subscribe") => { + let subscribe = Subscribe::from_element(first_child)?; + let options = element.pop_child_opt()?; + Ok(Self::Subscribe(Some(subscribe), options)) + } + (Some(XMLNS), "options") => { + let options = Options::from_element(first_child)?; + Ok(Self::Subscribe(None, Some(options))) + } + (Some(XMLNS), "publish") => { + let publish = Publish::from_element(first_child)?; + let publish_options = element.pop_child_opt()?; + Ok(Self::Publish(publish, publish_options)) + } + (Some(XMLNS), "affiliations") => { + Ok(Self::Affiliations(Affiliations::from_element(first_child)?)) + } + (Some(XMLNS), "default") => Ok(Self::Default(Default::from_element(first_child)?)), + (Some(XMLNS), "items") => Ok(Self::Items(Items::from_element(first_child)?)), + (Some(XMLNS), "retract") => Ok(Self::Retract(Retract::from_element(first_child)?)), + (Some(XMLNS), "subscription") => { + Ok(Self::Subscription(Subscription::from_element(first_child)?)) + } + (Some(XMLNS), "subscriptions") => Ok(Self::Subscriptions( + Subscriptions::from_element(first_child)?, + )), + (Some(XMLNS), "unsubscribe") => { + Ok(Self::Unsubscribe(Unsubscribe::from_element(first_child)?)) + } + _ => Err(peanuts::DeserializeError::UnexpectedElement(first_child)), + } + } else { + Ok(Pubsub::Pubsub) + } + } +} + +impl IntoElement for Pubsub { + fn builder(&self) -> peanuts::element::ElementBuilder { + let element = Element::builder("pubsub", Some(XMLNS)); + + match self { + Pubsub::Create(create, configure) => element + .push_child(create.clone()) + .push_child_opt(configure.clone()), + Pubsub::Subscribe(subscribe, options) => element + .push_child_opt(subscribe.clone()) + .push_child_opt(options.clone()), + Pubsub::Publish(publish, publish_options) => element + .push_child(publish.clone()) + .push_child_opt(publish_options.clone()), + Pubsub::Affiliations(affiliations) => element.push_child(affiliations.clone()), + Pubsub::Default(default) => element.push_child(default.clone()), + Pubsub::Items(items) => element.push_child(items.clone()), + Pubsub::Retract(retract) => element.push_child(retract.clone()), + Pubsub::Subscription(subscription) => element.push_child(subscription.clone()), + Pubsub::Subscriptions(subscriptions) => element.push_child(subscriptions.clone()), + Pubsub::Unsubscribe(unsubscribe) => element.push_child(unsubscribe.clone()), + Pubsub::Pubsub => element, + } + } +} + +#[derive(Clone, Debug)] +pub struct Affiliations { + node: Option<String>, + affiliations: Vec<Affiliation>, +} + +impl FromElement for Affiliations { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("affiliations")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute_opt("node")?; + + let affiliations = element.pop_children()?; + + Ok(Self { node, affiliations }) + } +} + +impl IntoElement for Affiliations { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("affilations", Some(XMLNS)) + .push_attribute_opt("node", self.node.clone()) + .push_children(self.affiliations.clone()) + } +} + +// empty +#[derive(Clone, Debug)] +pub struct Affiliation { + affiliation: AffiliationType, + node: String, +} + +impl FromElement for Affiliation { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("affiliation")?; + element.check_namespace(XMLNS)?; + + let affiliation = element.attribute("affiliation")?; + let node = element.attribute("node")?; + + Ok(Self { affiliation, node }) + } +} + +impl IntoElement for Affiliation { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("affiliation", Some(XMLNS)) + .push_attribute("affiliation", self.affiliation.clone()) + .push_attribute("node", self.node.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum AffiliationType { + Member, + None, + Outcast, + Owner, + Publisher, + PublishOnly, +} + +impl FromStr for AffiliationType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "member" => Ok(AffiliationType::Member), + "none" => Ok(AffiliationType::None), + "outcast" => Ok(AffiliationType::Outcast), + "owner" => Ok(AffiliationType::Owner), + "publisher" => Ok(AffiliationType::Publisher), + "publish-only" => Ok(AffiliationType::PublishOnly), + s => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for AffiliationType { + fn to_string(&self) -> String { + match self { + AffiliationType::Member => "member", + AffiliationType::None => "none", + AffiliationType::Outcast => "outcast", + AffiliationType::Owner => "owner", + AffiliationType::Publisher => "publisher", + AffiliationType::PublishOnly => "publish-only", + } + .to_owned() + } +} + +#[derive(Clone, Debug)] +pub struct Configure(Option<X>); + +impl FromElement for Configure { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("configure")?; + element.check_namespace(XMLNS)?; + + Ok(Configure(element.pop_child_opt()?)) + } +} + +impl IntoElement for Configure { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("configure", Some(XMLNS)).push_child_opt(self.0.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Create { + node: Option<String>, +} + +impl FromElement for Create { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("create")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute_opt("node")?; + + Ok(Self { node }) + } +} + +impl IntoElement for Create { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("create", Some(XMLNS)).push_attribute_opt("node", self.node.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Default { + node: Option<String>, + // default leaf + r#type: Option<DefaultType>, + default: Option<X>, +} + +impl FromElement for Default { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("default")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute_opt("node")?; + let r#type = element.attribute_opt("type")?; + + let default = element.pop_child_opt()?; + + Ok(Self { + node, + r#type, + default, + }) + } +} + +impl IntoElement for Default { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("default", Some(XMLNS)) + .push_attribute_opt("node", self.node.clone()) + .push_attribute_opt("type", self.r#type.clone()) + .push_child_opt(self.default.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum DefaultType { + Collection, + Leaf, +} + +impl FromStr for DefaultType { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "collection" => Ok(Self::Collection), + "leaf" => Ok(Self::Leaf), + s => Err(DeserializeError::FromStr(s.to_string())), + } + } +} + +impl ToString for DefaultType { + fn to_string(&self) -> String { + match self { + DefaultType::Collection => "collection", + DefaultType::Leaf => "leaf", + } + .to_owned() + } +} + +#[derive(Clone, Debug)] +pub struct Items { + max_items: Option<usize>, + node: String, + subid: Option<String>, + items: Vec<Item>, +} + +impl FromElement for Items { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("items")?; + element.check_namespace(XMLNS)?; + + let max_items = element.attribute_opt("max_items")?; + let node = element.attribute("node")?; + let subid = element.attribute_opt("subid")?; + + let items = element.pop_children()?; + + Ok(Self { + max_items, + node, + subid, + items, + }) + } +} + +impl IntoElement for Items { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("items", Some(XMLNS)) + .push_attribute_opt("max_items", self.max_items) + .push_attribute("node", self.node.clone()) + .push_attribute_opt("subid", self.subid.clone()) + .push_children(self.items.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Item { + id: Option<String>, + publisher: Option<String>, + item: Option<Content>, +} + +impl FromElement for Item { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("item")?; + element.check_namespace(XMLNS)?; + + let id = element.attribute_opt("id")?; + let publisher = element.attribute_opt("publisher")?; + + let item = element.pop_child_opt()?; + + Ok(Self { + id, + publisher, + item, + }) + } +} + +impl IntoElement for Item { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("item", Some(XMLNS)) + .push_attribute_opt("id", self.id.clone()) + .push_attribute_opt("publisher", self.publisher.clone()) + .push_child_opt(self.item.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum Content { + Unknown, +} + +impl FromElement for Content { + fn from_element(_element: Element) -> peanuts::element::DeserializeResult<Self> { + // TODO: types + return Ok(Self::Unknown); + } +} + +impl IntoElement for Content { + fn builder(&self) -> peanuts::element::ElementBuilder { + panic!("unknown content cannot be serialized") + } +} + +#[derive(Clone, Debug)] +pub struct Options { + jid: JID, + node: Option<String>, + subid: Option<String>, + options: Option<X>, +} + +impl FromElement for Options { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("options")?; + element.check_namespace(XMLNS)?; + + let jid = element.attribute("jid")?; + let node = element.attribute_opt("node")?; + let subid = element.attribute_opt("subid")?; + + let options = element.pop_child_opt()?; + + Ok(Self { + jid, + node, + subid, + options, + }) + } +} + +impl IntoElement for Options { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("options", Some(XMLNS)) + .push_attribute("jid", self.jid.clone()) + .push_attribute_opt("node", self.node.clone()) + .push_attribute_opt("subid", self.subid.clone()) + .push_child_opt(self.options.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Publish { + node: String, + items: Vec<Item>, +} + +impl FromElement for Publish { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("publish")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + + let items = element.pop_children()?; + + Ok(Self { node, items }) + } +} + +impl IntoElement for Publish { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("publish", Some(XMLNS)) + .push_attribute("node", self.node.clone()) + .push_children(self.items.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct PublishOptions(pub Option<X>); + +impl FromElement for PublishOptions { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("publish-options")?; + element.check_namespace(XMLNS)?; + + Ok(Self(element.pop_child_opt()?)) + } +} + +impl IntoElement for PublishOptions { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("publish-options", Some(XMLNS)).push_child_opt(self.0.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Retract { + node: String, + notify: Option<bool>, + /// minOccurs = 1 + items: Vec<Item>, +} + +impl FromElement for Retract { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("retract")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute("node")?; + let notify = element.attribute_opt("notify")?; + + let items = element.pop_children()?; + + Ok(Self { + node, + notify, + items, + }) + } +} + +impl IntoElement for Retract { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("retract", Some(XMLNS)) + .push_attribute("node", self.node.clone()) + .push_attribute_opt("notify", self.notify) + .push_children(self.items.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct SubscribeOptions { + // <required/> element + required: bool, +} + +impl FromElement for SubscribeOptions { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subscribe-options")?; + element.check_namespace(XMLNS)?; + + let required; + if let Some(_) = element.pop_child_opt::<Required>()? { + required = true; + } else { + required = false; + } + + Ok(Self { required }) + } +} + +impl IntoElement for SubscribeOptions { + fn builder(&self) -> peanuts::element::ElementBuilder { + let builder = Element::builder("subscribe-options", Some(XMLNS)); + + if self.required { + builder.push_child(Required) + } else { + builder + } + } +} + +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(Self) + } +} + +impl IntoElement for Required { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("required", Some(XMLNS)) + } +} + +#[derive(Clone, Debug)] +pub struct Subscribe { + jid: JID, + node: Option<String>, +} + +impl FromElement for Subscribe { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subscribe")?; + element.check_namespace(XMLNS)?; + + let jid = element.attribute("jid")?; + let node = element.attribute_opt("node")?; + + Ok(Self { jid, node }) + } +} + +impl IntoElement for Subscribe { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subscribe", Some(XMLNS)) + .push_attribute("jid", self.jid.clone()) + .push_attribute_opt("node", self.node.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Subscriptions { + node: Option<String>, + subscriptions: Vec<Subscription>, +} + +impl FromElement for Subscriptions { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subscriptions")?; + element.check_namespace(XMLNS)?; + + let node = element.attribute_opt("node")?; + + let subscriptions = element.pop_children()?; + + Ok(Self { + node, + subscriptions, + }) + } +} + +impl IntoElement for Subscriptions { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subscriptions", Some(XMLNS)) + .push_attribute_opt("node", self.node.clone()) + .push_children(self.subscriptions.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Subscription { + jid: JID, + node: Option<String>, + subid: Option<String>, + subscription: Option<SubscriptionState>, + subscribe_options: Option<SubscribeOptions>, +} + +impl FromElement for Subscription { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("subscription")?; + element.check_namespace(XMLNS)?; + + let jid = element.attribute("jid")?; + let node = element.attribute_opt("node")?; + let subid = element.attribute_opt("subid")?; + let subscription = element.attribute_opt("subscription")?; + + let subscribe_options = element.pop_child_opt()?; + + Ok(Self { + jid, + node, + subid, + subscription, + subscribe_options, + }) + } +} + +impl IntoElement for Subscription { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("subscription", Some(XMLNS)) + .push_attribute("jid", self.jid.clone()) + .push_attribute_opt("node", self.node.clone()) + .push_attribute_opt("subid", self.subid.clone()) + .push_attribute_opt("subscription", self.subscription.clone()) + .push_child_opt(self.subscribe_options.clone()) + } +} + +#[derive(Clone, Debug)] +pub enum SubscriptionState { + None, + Pending, + Subscribed, + Unconfigured, +} + +impl FromStr for SubscriptionState { + type Err = DeserializeError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "none" => Ok(Self::None), + "pending" => Ok(Self::Pending), + "subscribed" => Ok(Self::Subscribed), + "unconfigured" => Ok(Self::Unconfigured), + s => Err(DeserializeError::FromStr(s.to_owned())), + } + } +} + +impl ToString for SubscriptionState { + fn to_string(&self) -> String { + match self { + SubscriptionState::None => "none", + SubscriptionState::Pending => "pending", + SubscriptionState::Subscribed => "subscribed", + SubscriptionState::Unconfigured => "unconfigured", + } + .to_string() + } +} + +#[derive(Clone, Debug)] +pub struct Unsubscribe { + jid: JID, + node: Option<String>, + subid: Option<String>, +} + +impl FromElement for Unsubscribe { + fn from_element(mut element: Element) -> peanuts::element::DeserializeResult<Self> { + element.check_name("unsubscribe")?; + element.check_namespace(XMLNS)?; + + let jid = element.attribute("jid")?; + let node = element.attribute_opt("node")?; + let subid = element.attribute_opt("subid")?; + + Ok(Self { jid, node, subid }) + } +} + +impl IntoElement for Unsubscribe { + fn builder(&self) -> peanuts::element::ElementBuilder { + Element::builder("unsubscribe", Some(XMLNS)) + .push_attribute("jid", self.jid.clone()) + .push_attribute_opt("node", self.node.clone()) + .push_attribute_opt("subid", self.subid.clone()) + } +} + +// TODO: enforce 'empty' |