diff --git a/xmpp.go b/xmpp.go index f801e29..4e86787 100644 --- a/xmpp.go +++ b/xmpp.go @@ -642,7 +642,20 @@ func (c *Client) Recv() (stanza interface{}, err error) { case *clientMessage: if v.Event.XMLNS == XMPPNS_PUBSUB_EVENT { // Handle Pubsub notifications - return pubsubClientToReturn(v.Event), nil + switch v.Event.Items.Node { + case XMPPNS_AVATAR_PEP_METADATA: + return handleAvatarMetadata(v.Event.Items.Items[0].Body, + v.From) + // I am not sure whether this can even happen. + // XEP-0084 only specifies a subscription to + // the metadata node. + /*case XMPPNS_AVATAR_PEP_DATA: + return handleAvatarData(v.Event.Items.Items[0].Body, + v.From, + v.Event.Items.Items[0].ID)*/ + default: + return pubsubClientToReturn(v.Event), nil + } } stamp, _ := time.Parse( @@ -741,10 +754,41 @@ func (c *Client) Recv() (stanza interface{}, err error) { return PubsubItems{}, err } - return PubsubItems{ - p.Node, - pubsubItemsToReturn(p.Items), - }, nil + 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) + 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 + } + + 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 + }*/ } case v.Query.XMLName.Local == "": return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type}, nil diff --git a/xmpp_avatar.go b/xmpp_avatar.go new file mode 100644 index 0000000..7303dba --- /dev/null +++ b/xmpp_avatar.go @@ -0,0 +1,125 @@ +package xmpp + +import ( + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "errors" + "strconv" +) + +const ( + XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data" + XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata" +) + +type clientAvatarData struct { + XMLName xml.Name `xml:"data"` + Data []byte `xml:",innerxml"` +} + +type clientAvatarInfo struct { + XMLName xml.Name `xml:"info"` + Bytes string `xml:"bytes,attr"` + Width string `xml:"width,attr"` + Height string `xml:"height,attr"` + ID string `xml:"id,attr"` + Type string `xml:"type,attr"` + URL string `xml:"url,attr"` +} + +type clientAvatarMetadata struct { + XMLName xml.Name `xml:"metadata"` + XMLNS string `xml:"xmlns,attr"` + Info clientAvatarInfo `xml:"info"` +} + +type AvatarData struct { + Data []byte + From string +} + +type AvatarMetadata struct { + From string + Bytes int + Width int + Height int + ID string + Type string + URL string +} + +func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) { + var data clientAvatarData + err := xml.Unmarshal(itemsBody, &data) + if err != nil { + return AvatarData{}, err + } + + // Base64-decode the avatar data to check its SHA1 hash + dataRaw, err := base64.StdEncoding.DecodeString( + string(data.Data)) + if err != nil { + return AvatarData{}, err + } + + hash := sha1.Sum(dataRaw) + hashStr := hex.EncodeToString(hash[:]) + if hashStr != id { + return AvatarData{}, errors.New("SHA1 hashes do not match") + } + + return AvatarData{ + Data: dataRaw, + From: from, + }, nil +} + +func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) { + var meta clientAvatarMetadata + err := xml.Unmarshal(body, &meta) + if err != nil { + return AvatarMetadata{}, err + } + + return AvatarMetadata{ + From: from, + Bytes: atoiw(meta.Info.Bytes), + Width: atoiw(meta.Info.Width), + Height: atoiw(meta.Info.Height), + ID: meta.Info.ID, + Type: meta.Info.Type, + URL: meta.Info.URL, + }, nil +} + +// A wrapper for atoi which just returns -1 if an error occurs +func atoiw(str string) int { + i, err := strconv.Atoi(str) + if err != nil { + return -1 + } + + return i +} + +func (c *Client) AvatarSubscribeMetadata(jid string) { + c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid) +} + +func (c *Client) AvatarUnsubscribeMetadata(jid string) { + c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid) +} + +func (c *Client) AvatarRequestData(jid string) { + c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid) +} + +func (c *Client) AvatarRequestDataByID(jid, id string) { + c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id) +} + +func (c *Client) AvatarRequestMetadata(jid string) { + c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid) +}