From fd9abe8288e91462f51c726817de2b6a5c6447d4 Mon Sep 17 00:00:00 2001 From: ValdikSS Date: Tue, 29 May 2018 20:44:44 +0300 Subject: [PATCH 1/3] Implement XMPP message ID and message correction functionality --- xmpp.go | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/xmpp.go b/xmpp.go index 2b180e2..f8a924c 100644 --- a/xmpp.go +++ b/xmpp.go @@ -608,6 +608,8 @@ type Chat struct { Thread string Ooburl string Oobdesc string + ID string + ReplaceID string Roster Roster Other []string OtherElem []XMLElement @@ -681,6 +683,8 @@ func (c *Client) Recv() (stanza interface{}, err error) { Text: v.Body, Subject: v.Subject, Thread: v.Thread, + ID: v.ID, + ReplaceID: v.ReplaceID.ID, Other: v.OtherStrings(), OtherElem: v.Other, Stamp: stamp, @@ -828,7 +832,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { // Send sends the message wrapped inside an XMPP message stanza body. func (c *Client) Send(chat Chat) (n int, err error) { - var subtext, thdtext, oobtext string + var subtext, thdtext, oobtext, msgidtext, msgcorrecttext string if chat.Subject != `` { subtext = `` + xmlEscape(chat.Subject) + `` } @@ -843,10 +847,19 @@ func (c *Client) Send(chat Chat) (n int, err error) { oobtext += `` } - stanza := "" + subtext + "%s" + oobtext + thdtext + "" + if chat.ID != `` { + msgidtext = `id='` + xmlEscape(chat.ID) + `'` + } else { + msgidtext = `id='` + cnonce() + `'` + } + + if chat.ReplaceID != `` { + msgcorrecttext = `` + } - return fmt.Fprintf(c.conn, stanza, - xmlEscape(chat.Remote), xmlEscape(chat.Type), cnonce(), xmlEscape(chat.Text)) + stanza := "" + subtext + "%s" + msgcorrecttext + oobtext + thdtext + "" + + return fmt.Fprintf(c.conn, stanza, xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text)) } // SendOOB sends OOB data wrapped inside an XMPP message stanza, without actual body. @@ -961,6 +974,11 @@ type bindBind struct { Jid string `xml:"jid"` } +type clientMessageCorrect struct { + XMLName xml.Name `xml:"urn:xmpp:message-correct:0 replace"` + ID string `xml:"id,attr"` +} + // RFC 3921 B.1 jabber:client type clientMessage struct { XMLName xml.Name `xml:"jabber:client message"` @@ -970,9 +988,10 @@ type clientMessage struct { Type string `xml:"type,attr"` // chat, error, groupchat, headline, or normal // These should technically be []clientText, but string is much more convenient. - Subject string `xml:"subject"` - Body string `xml:"body"` - Thread string `xml:"thread"` + Subject string `xml:"subject"` + Body string `xml:"body"` + Thread string `xml:"thread"` + ReplaceID clientMessageCorrect // Pubsub Event clientPubsubEvent `xml:"event"` From e81e8cd7cc20b4aab231a5b16b8215fd8a258e59 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 18 Apr 2020 18:26:26 +0200 Subject: [PATCH 2/3] Improve support for XEP-0030 (#6) This commit allows the user to query information about the server or a node belonging to the server as per XEP-0030. --- xmpp.go | 49 +++++++++++++++---- xmpp_disco.go | 99 +++++++++++++++++++++++++++++++++++++++ xmpp_information_query.go | 20 +++++++- 3 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 xmpp_disco.go diff --git a/xmpp.go b/xmpp.go index f8a924c..1fa1705 100644 --- a/xmpp.go +++ b/xmpp.go @@ -726,15 +726,44 @@ func (c *Client) Recv() (stanza interface{}, err error) { 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.Type == "result": + switch v.ID { + case "unsub1": + // Unsubscribing MAY contain a pubsub element. But it does + // not have to + return PubsubUnsubscription{ + SubID: "", + JID: v.From, + Node: "", + Errors: nil, + }, nil + case "info1": + if v.Query.XMLName.Space == XMPPNS_DISCO_ITEMS { + var itemsQuery clientDiscoItemsQuery + err := xml.Unmarshal(v.InnerXML, &itemsQuery) + if err != nil { + return []DiscoItem{}, err + } + + return DiscoItems{ + Jid: v.From, + Items: clientDiscoItemsToReturn(itemsQuery.Items), + }, nil + } + case "info3": + if v.Query.XMLName.Space == XMPPNS_DISCO_INFO { + var disco clientDiscoQuery + err := xml.Unmarshal(v.InnerXML, &disco) + if err != nil { + return DiscoResult{}, err + } + + return DiscoResult{ + Features: clientFeaturesToReturn(disco.Features), + Identities: clientIdentitiesToReturn(disco.Identities), + }, nil + } + } case v.Query.XMLName.Local == "pubsub": switch v.ID { case "sub1": @@ -1070,6 +1099,8 @@ type clientIQ struct { Query XMLElement `xml:",any"` Error clientError Bind bindBind + + InnerXML []byte `xml:",innerxml"` } type clientError struct { diff --git a/xmpp_disco.go b/xmpp_disco.go new file mode 100644 index 0000000..0bca664 --- /dev/null +++ b/xmpp_disco.go @@ -0,0 +1,99 @@ +package xmpp + +import ( + "encoding/xml" +) + +const ( + XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items" + XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info" +) + +type clientDiscoFeature struct { + XMLName xml.Name `xml:"feature"` + Var string `xml:"var,attr"` +} + +type clientDiscoIdentity struct { + XMLName xml.Name `xml:"identity"` + Category string `xml:"category,attr"` + Type string `xml:"type,attr"` + Name string `xml:"name,attr"` +} + +type clientDiscoQuery struct { + XMLName xml.Name `xml:"query"` + Features []clientDiscoFeature `xml:"feature"` + Identities []clientDiscoIdentity `xml:"identity"` +} + +type clientDiscoItem struct { + XMLName xml.Name `xml:"item"` + Jid string `xml:"jid,attr"` + Node string `xml:"node,attr"` + Name string `xml:"name,attr"` +} + +type clientDiscoItemsQuery struct { + XMLName xml.Name `xml:"query"` + Items []clientDiscoItem `xml:"item"` +} + +type DiscoIdentity struct { + Category string + Type string + Name string +} + +type DiscoItem struct { + Jid string + Name string + Node string +} + +type DiscoResult struct { + Features []string + Identities []DiscoIdentity +} + +type DiscoItems struct { + Jid string + Items []DiscoItem +} + +func clientFeaturesToReturn(features []clientDiscoFeature) []string { + var ret []string + + for _, feature := range features { + ret = append(ret, feature.Var) + } + + return ret +} + +func clientIdentitiesToReturn(identities []clientDiscoIdentity) []DiscoIdentity { + var ret []DiscoIdentity + + for _, id := range identities { + ret = append(ret, DiscoIdentity{ + Category: id.Category, + Type: id.Type, + Name: id.Name, + }) + } + + return ret +} + +func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem { + var ret []DiscoItem + for _, item := range items { + ret = append(ret, DiscoItem{ + Jid: item.Jid, + Name: item.Name, + Node: item.Node, + }) + } + + return ret +} diff --git a/xmpp_information_query.go b/xmpp_information_query.go index 7f6d9c1..90dee95 100644 --- a/xmpp_information_query.go +++ b/xmpp_information_query.go @@ -10,10 +10,26 @@ const IQTypeSet = "set" const IQTypeResult = "result" func (c *Client) Discovery() (string, error) { - const namespace = "http://jabber.org/protocol/disco#items" // use getCookie for a pseudo random id. reqID := strconv.FormatUint(uint64(getCookie()), 10) - return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, namespace, "") + return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "") +} + +// Discover information about a node +func (c *Client) DiscoverNodeInfo(node string) (string, error) { + query := fmt.Sprintf("", XMPPNS_DISCO_INFO, node) + return c.RawInformation(c.jid, c.domain, "info3", IQTypeGet, query) +} + +// Discover items that the server exposes +func (c *Client) DiscoverServerItems() (string, error) { + return c.DiscoverEntityItems(c.domain) +} + +// Discover items that an entity exposes +func (c *Client) DiscoverEntityItems(jid string) (string, error) { + query := fmt.Sprintf("", XMPPNS_DISCO_ITEMS) + return c.RawInformation(c.jid, jid, "info1", IQTypeGet, query) } // RawInformationQuery sends an information query request to the server. From 847598adf6a51c64d14de477aa94d3e77df99a44 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 19 Apr 2020 00:50:40 +0200 Subject: [PATCH 3/3] Fix broken PubSub functionality (#7) --- xmpp.go | 166 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 86 insertions(+), 80 deletions(-) diff --git a/xmpp.go b/xmpp.go index 1fa1705..30a7ff7 100644 --- a/xmpp.go +++ b/xmpp.go @@ -335,7 +335,6 @@ func cnonce() string { } func (c *Client) init(o *Options) error { - var domain string var user string a := strings.SplitN(o.User, "@", 2) @@ -501,7 +500,7 @@ func (c *Client) init(o *Options) error { c.domain = domain if o.Session { - //if server support session, open it + // if server support session, open it fmt.Fprintf(c.conn, "", xmlEscape(domain), cookie, nsSession) } @@ -537,7 +536,7 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string tc := o.TLSConfig if tc == nil { tc = DefaultConfig.Clone() - //TODO(scott): we should consider using the server's address or reverse lookup + // TODO(scott): we should consider using the server's address or reverse lookup tc.ServerName = domain } t := tls.Client(c.conn, tc) @@ -728,15 +727,46 @@ func (c *Client) Recv() (stanza interface{}, err error) { } case v.Type == "result": switch v.ID { + case "sub1": + if v.Query.XMLName.Local == "pubsub" { + // 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": - // Unsubscribing MAY contain a pubsub element. But it does - // not have to - return PubsubUnsubscription{ - SubID: "", - JID: v.From, - Node: "", - Errors: nil, - }, nil + if v.Query.XMLName.Local == "pubsub" { + 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 + } else { + // Unsubscribing MAY contain a pubsub element. But it does + // not have to + return PubsubUnsubscription{ + SubID: "", + JID: v.From, + Node: "", + Errors: nil, + }, nil + } case "info1": if v.Query.XMLName.Space == XMPPNS_DISCO_ITEMS { var itemsQuery clientDiscoItemsQuery @@ -763,64 +793,36 @@ func (c *Client) Recv() (stanza interface{}, err error) { Identities: clientIdentitiesToReturn(disco.Identities), }, 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 - } - - switch p.Node { - case XMPPNS_AVATAR_PEP_DATA: - if len(p.Items) == 0 { - return AvatarData{}, errors.New("No avatar data items available") + if v.Query.XMLName.Local == "pubsub" { + var p clientPubsubItems + err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) + if err != nil { + return PubsubItems{}, err } - return handleAvatarData(p.Items[0].Body, - v.From, - p.Items[0].ID) - case XMPPNS_AVATAR_PEP_METADATA: - if len(p.Items) == 0 { - return AvatarMetadata{}, errors.New("No avatar metadata items available") + switch p.Node { + case XMPPNS_AVATAR_PEP_DATA: + if len(p.Items) == 0 { + return AvatarData{}, errors.New("No avatar data items available") + } + + return handleAvatarData(p.Items[0].Body, + v.From, + p.Items[0].ID) + case XMPPNS_AVATAR_PEP_METADATA: + if len(p.Items) == 0 { + return AvatarMetadata{}, errors.New("No avatar metadata items available") + } + + return handleAvatarMetadata(p.Items[0].Body, + v.From) + default: + return PubsubItems{ + p.Node, + pubsubItemsToReturn(p.Items), + }, nil } - - return handleAvatarMetadata(p.Items[0].Body, - v.From) - default: - return PubsubItems{ - p.Node, - pubsubItemsToReturn(p.Items), - }, nil } // Note: XEP-0084 states that metadata and data // should be fetched with an id of retrieve1. @@ -828,20 +830,22 @@ func (c *Client) Recv() (stanza interface{}, err error) { // can just use items1 and items3 to do the same // as an Avatar node is just a PEP (PubSub) node. /*case "retrieve1": - var p clientPubsubItems - err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) - if err != nil { - return PubsubItems{}, err - } + if v.Query.XMLName.Local == "pubsub" { + var p clientPubsubItems + err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) + if err != nil { + return PubsubItems{}, err + } - switch p.Node { - case XMPPNS_AVATAR_PEP_DATA: - return handleAvatarData(p.Items[0].Body, - v.From, - p.Items[0].ID) - case XMPPNS_AVATAR_PEP_METADATA: - return handleAvatarMetadata(p.Items[0].Body, - v + switch p.Node { + case XMPPNS_AVATAR_PEP_DATA: + return handleAvatarData(p.Items[0].Body, + v.From, + p.Items[0].ID) + case XMPPNS_AVATAR_PEP_METADATA: + return handleAvatarMetadata(p.Items[0].Body, + v.From) + } }*/ } case v.Query.XMLName.Local == "": @@ -852,8 +856,10 @@ func (c *Client) Recv() (stanza interface{}, err error) { return Chat{}, err } - return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type, - Query: res}, nil + return IQ{ + ID: v.ID, From: v.From, To: v.To, Type: v.Type, + Query: res, + }, nil } } }