diff --git a/xmpp.go b/xmpp.go index 2b180e2..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) @@ -608,6 +607,8 @@ type Chat struct { Thread string Ooburl string Oobdesc string + ID string + ReplaceID string Roster Roster Other []string OtherElem []XMLElement @@ -681,6 +682,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, @@ -722,93 +725,127 @@ 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.Query.XMLName.Local == "pubsub": + case v.Type == "result": 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 - } + 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": - var sub clientPubsubSubscription - err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub) - if err != nil { - return PubsubUnsubscription{}, err + return PubsubSubscription{ + SubID: sub.SubID, + JID: sub.JID, + Node: sub.Node, + Errors: nil, + }, nil } + case "unsub1": + 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 - case "items1", "items3": - var p clientPubsubItems - err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) - if err != nil { - return PubsubItems{}, 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 } - - switch p.Node { - case XMPPNS_AVATAR_PEP_DATA: - if len(p.Items) == 0 { - return AvatarData{}, errors.New("No avatar data items available") + 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 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 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 handleAvatarMetadata(p.Items[0].Body, - v.From) - default: - return PubsubItems{ - p.Node, - pubsubItemsToReturn(p.Items), + return DiscoResult{ + Features: clientFeaturesToReturn(disco.Features), + Identities: clientIdentitiesToReturn(disco.Identities), }, nil } + case "items1", "items3": + 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: + 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 + } + } // Note: XEP-0084 states that metadata and data // should be fetched with an id of retrieve1. // Since we already have PubSub implemented, we // 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 == "": @@ -819,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 } } } @@ -828,7 +867,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 +882,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 = `` + } + + stanza := "" + subtext + "%s" + msgcorrecttext + oobtext + thdtext + "" - return fmt.Fprintf(c.conn, stanza, - xmlEscape(chat.Remote), xmlEscape(chat.Type), cnonce(), xmlEscape(chat.Text)) + 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 +1009,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 +1023,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"` @@ -1051,6 +1105,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.