From 899ef71e80d2e3c128df612272a510907eff9fb1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 9 Mar 2020 10:10:41 +0100 Subject: [PATCH] Implement a bit of XEP-0060 (PubSub) (#119) This squashed series of commits implements basic PubSub functionality like requesting data or subscribing to a PubSub node. --- xmpp.go | 107 ++++++++++++++++++++++++++++++++++----- xmpp_pubsub.go | 133 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 xmpp_pubsub.go diff --git a/xmpp.go b/xmpp.go index 372aaf6..f801e29 100644 --- a/xmpp.go +++ b/xmpp.go @@ -640,6 +640,11 @@ func (c *Client) Recv() (stanza interface{}, err error) { } switch v := val.(type) { case *clientMessage: + if v.Event.XMLNS == XMPPNS_PUBSUB_EVENT { + // Handle Pubsub notifications + return pubsubClientToReturn(v.Event), nil + } + stamp, _ := time.Parse( "2006-01-02T15:04:05Z", v.Delay.Stamp, @@ -664,19 +669,91 @@ func (c *Client) Recv() (stanza interface{}, err error) { case *clientPresence: return Presence{v.From, v.To, v.Type, v.Show, v.Status}, nil case *clientIQ: - // TODO check more strictly - if v.Query.XMLName.Space == "urn:xmpp:ping" { + switch { + case v.Query.XMLName.Space == "urn:xmpp:ping": + // TODO check more strictly err := c.SendResultPing(v.ID, v.From) if err != nil { return Chat{}, err } - } - if v.Query.XMLName.Local == "" { + fallthrough + case v.Type == "error": + switch v.ID { + case "sub1": + // Pubsub subscription failed + var errs []clientPubsubError + err := xml.Unmarshal([]byte(v.Error.InnerXML), &errs) + if err != nil { + return PubsubSubscription{}, err + } + + var errsStr []string + for _, e := range errs { + errsStr = append(errsStr, e.XMLName.Local) + } + + return PubsubSubscription{ + Errors: errsStr, + }, nil + } + case v.Type == "result" && v.ID == "unsub1": + // Unsubscribing MAY contain a pubsub element. But it does + // not have to + return PubsubUnsubscription{ + SubID: "", + JID: v.From, + Node: "", + Errors: nil, + }, nil + case v.Query.XMLName.Local == "pubsub": + switch v.ID { + case "sub1": + // Subscription or unsubscription was successful + var sub clientPubsubSubscription + err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub) + if err != nil { + return PubsubSubscription{}, err + } + + return PubsubSubscription{ + SubID: sub.SubID, + JID: sub.JID, + Node: sub.Node, + Errors: nil, + }, nil + case "unsub1": + var sub clientPubsubSubscription + err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub) + if err != nil { + return PubsubUnsubscription{}, err + } + + return PubsubUnsubscription{ + SubID: sub.SubID, + JID: v.From, + Node: sub.Node, + Errors: nil, + }, nil + case "items1", "items3": + var p clientPubsubItems + err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) + if err != nil { + return PubsubItems{}, err + } + + return PubsubItems{ + p.Node, + pubsubItemsToReturn(p.Items), + }, nil + } + case v.Query.XMLName.Local == "": return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type}, nil - } else if res, err := xml.Marshal(v.Query); err != nil { - // should never occur - return Chat{}, err - } else { + default: + res, err := xml.Marshal(v.Query) + if err != nil { + return Chat{}, err + } + return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type, Query: res}, nil } @@ -832,6 +909,9 @@ type clientMessage struct { Body string `xml:"body"` Thread string `xml:"thread"` + // Pubsub + Event clientPubsubEvent `xml:"event"` + // Any hasn't matched element Other []XMLElement `xml:",any"` @@ -909,11 +989,12 @@ type clientIQ struct { } type clientError struct { - XMLName xml.Name `xml:"jabber:client error"` - Code string `xml:",attr"` - Type string `xml:",attr"` - Any xml.Name - Text string + XMLName xml.Name `xml:"jabber:client error"` + Code string `xml:",attr"` + Type string `xml:"type,attr"` + Any xml.Name + InnerXML []byte `xml:",innerxml"` + Text string } type clientQuery struct { diff --git a/xmpp_pubsub.go b/xmpp_pubsub.go new file mode 100644 index 0000000..74e60d8 --- /dev/null +++ b/xmpp_pubsub.go @@ -0,0 +1,133 @@ +package xmpp + +import ( + "encoding/xml" + "fmt" +) + +const ( + XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub" + XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event" +) + +type clientPubsubItem struct { + XMLName xml.Name `xml:"item"` + ID string `xml:"id,attr"` + Body []byte `xml:",innerxml"` +} + +type clientPubsubItems struct { + XMLName xml.Name `xml:"items"` + Node string `xml:"node,attr"` + Items []clientPubsubItem `xml:"item"` +} + +type clientPubsub struct { + XMLName xml.Name `xml:"pubsub"` + Items clientPubsubItems `xml:"items"` +} + +type clientPubsubEvent struct { + XMLName xml.Name `xml:"event"` + XMLNS string `xml:"xmlns,attr"` + Items clientPubsubItems `xml:"items"` +} + +type clientPubsubError struct { + XMLName xml.Name +} + +type clientPubsubSubscription struct { + XMLName xml.Name `xml:"subscription"` + Node string `xml:"node,attr"` + JID string `xml:"jid,attr"` + SubID string `xml:"subid,attr"` +} + +type PubsubEvent struct { + Node string + Items []PubsubItem +} + +type PubsubSubscription struct { + SubID string + JID string + Node string + Errors []string +} +type PubsubUnsubscription PubsubSubscription + +type PubsubItem struct { + ID string + InnerXML []byte +} + +type PubsubItems struct { + Node string + Items []PubsubItem +} + +// Converts []clientPubsubItem to []PubsubItem +func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem { + var tmp []PubsubItem + for _, i := range items { + tmp = append(tmp, PubsubItem{ + ID: i.ID, + InnerXML: i.Body, + }) + } + + return tmp +} + +func pubsubClientToReturn(event clientPubsubEvent) PubsubEvent { + return PubsubEvent{ + Node: event.Items.Node, + Items: pubsubItemsToReturn(event.Items.Items), + } +} + +func pubsubStanza(body string) string { + return fmt.Sprintf("%s", + XMPPNS_PUBSUB, body) +} + +func pubsubSubscriptionStanza(node, jid string) string { + body := fmt.Sprintf("", + xmlEscape(node), + xmlEscape(jid)) + return pubsubStanza(body) +} + +func pubsubUnsubscriptionStanza(node, jid string) string { + body := fmt.Sprintf("", + xmlEscape(node), + xmlEscape(jid)) + return pubsubStanza(body) +} + +func (c *Client) PubsubSubscribeNode(node, jid string) { + c.RawInformation(c.jid, + jid, + "sub1", + "set", + pubsubSubscriptionStanza(node, c.jid)) +} + +func (c *Client) PubsubUnsubscribeNode(node, jid string) { + c.RawInformation(c.jid, + jid, + "unsub1", + "set", + pubsubUnsubscriptionStanza(node, c.jid)) +} + +func (c *Client) PubsubRequestLastItems(node, jid string) { + body := fmt.Sprintf("", node) + c.RawInformation(c.jid, jid, "items1", "get", pubsubStanza(body)) +} + +func (c *Client) PubsubRequestItem(node, jid, id string) { + body := fmt.Sprintf("", node, id) + c.RawInformation(c.jid, jid, "items3", "get", pubsubStanza(body)) +}