From 8f7b4ba8a46f956f2342622ff5bd962ca9d2559e Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sun, 23 Jun 2019 12:15:47 +0200 Subject: [PATCH] Implement MUC Presence Extension See #67 --- message.go | 2 +- pres_muc.go | 29 +++++++++++++ pres_muc_test.go | 60 +++++++++++++++++++++++++ presence.go | 111 ++++++++++++++++++++++++++++++++++++++++++++--- registry.go | 14 ++++++ 5 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 pres_muc.go create mode 100644 pres_muc_test.go diff --git a/message.go b/message.go index 2426f21..6b57f65 100644 --- a/message.go +++ b/message.go @@ -81,7 +81,7 @@ func (msg *Message) XMPPFormat() string { return string(out) } -// UnmarshalXML implements custom parsing for IQs +// UnmarshalXML implements custom parsing for messages func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { msg.XMLName = start.Name diff --git a/pres_muc.go b/pres_muc.go new file mode 100644 index 0000000..10d947e --- /dev/null +++ b/pres_muc.go @@ -0,0 +1,29 @@ +package xmpp + +import ( + "encoding/xml" + "time" +) + +// ============================================================================ +// MUC Presence extension + +// MucPresence implements XEP-0045: Multi-User Chat - 19.1 +type MucPresence struct { + PresExtension + XMLName xml.Name `xml:"http://jabber.org/protocol/muc x"` + Password string `xml:"password,omitempty"` + History History `xml:"history,omitempty"` +} + +// History implements XEP-0045: Multi-User Chat - 19.1 +type History struct { + MaxChars int `xml:"maxchars,attr,omitempty"` + MaxStanzas int `xml:"maxstanzas,attr,omitempty"` + Seconds int `xml:"seconds,attr,omitempty"` + Since time.Time `xml:"since,attr,omitempty"` +} + +func init() { + TypeRegistry.MapExtension(PKTPresence, xml.Name{"http://jabber.org/protocol/muc", "x"}, MucPresence{}) +} diff --git a/pres_muc_test.go b/pres_muc_test.go new file mode 100644 index 0000000..b2120e3 --- /dev/null +++ b/pres_muc_test.go @@ -0,0 +1,60 @@ +package xmpp_test + +import ( + "encoding/xml" + "testing" + + "gosrc.io/xmpp" +) + +// https://xmpp.org/extensions/xep-0045.html#example-27 +func TestMucPassword(t *testing.T) { + str := ` + + cauldronburn + +` + + var parsedPresence xmpp.Presence + if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { + t.Errorf("Unmarshal(%s) returned error", str) + } + + var muc xmpp.MucPresence + if ok := parsedPresence.Get(&muc); !ok { + t.Error("muc presence extension was not found") + } + + if muc.Password != "cauldronburn" { + t.Errorf("incorrect password: '%s'", muc.Password) + } +} + +// https://xmpp.org/extensions/xep-0045.html#example-37 +func TestMucHistory(t *testing.T) { + str := ` + + + +` + + var parsedPresence xmpp.Presence + if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { + t.Errorf("Unmarshal(%s) returned error", str) + } + + var muc xmpp.MucPresence + if ok := parsedPresence.Get(&muc); !ok { + t.Error("muc presence extension was not found") + } + + if muc.History.MaxStanzas != 20 { + t.Errorf("incorrect max stanza: '%d'", muc.History.MaxStanzas) + } +} diff --git a/presence.go b/presence.go index be9c6f2..466b9e1 100644 --- a/presence.go +++ b/presence.go @@ -1,6 +1,9 @@ package xmpp -import "encoding/xml" +import ( + "encoding/xml" + "reflect" +) // ============================================================================ // Presence Packet @@ -9,10 +12,11 @@ import "encoding/xml" type Presence struct { XMLName xml.Name `xml:"presence"` Attrs - Show PresenceShow `xml:"show,omitempty"` - Status string `xml:"status,omitempty"` - Priority int8 `xml:"priority,omitempty"` // default: 0 - Error Err `xml:"error,omitempty"` + Show PresenceShow `xml:"show,omitempty"` + Status string `xml:"status,omitempty"` + Priority int8 `xml:"priority,omitempty"` // default: 0 + Error Err `xml:"error,omitempty"` + Extensions []PresExtension `xml:",omitempty"` } func (Presence) Name() string { @@ -26,6 +30,37 @@ func NewPresence(a Attrs) Presence { } } +// Get search and extracts a specific extension on a presence stanza. +// It receives a pointer to an PresExtension. It will panic if the caller +// does not pass a pointer. +// It will return true if the passed extension is found and set the pointer +// to the extension passed as parameter to the found extension. +// It will return false if the extension is not found on the presence. +// +// Example usage: +// var muc xmpp.MucPresence +// if ok := msg.Get(&muc); ok { +// // muc presence extension has been found +// } +func (pres *Presence) Get(ext PresExtension) bool { + target := reflect.ValueOf(ext) + if target.Kind() != reflect.Ptr { + panic("you must pass a pointer to the message Get method") + } + + for _, e := range pres.Extensions { + if reflect.TypeOf(e) == target.Type() { + source := reflect.ValueOf(e) + if source.Kind() != reflect.Ptr { + source = source.Elem() + } + target.Elem().Set(source.Elem()) + return true + } + } + return false +} + type presenceDecoder struct{} var presence presenceDecoder @@ -36,3 +71,69 @@ func (presenceDecoder) decode(p *xml.Decoder, se xml.StartElement) (Presence, er // TODO Add default presence type (when omitted) return packet, err } + +// UnmarshalXML implements custom parsing for presence stanza +func (pres *Presence) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + pres.XMLName = start.Name + + // Extract packet attributes + for _, attr := range start.Attr { + if attr.Name.Local == "id" { + pres.Id = attr.Value + } + if attr.Name.Local == "type" { + pres.Type = StanzaType(attr.Value) + } + if attr.Name.Local == "to" { + pres.To = attr.Value + } + if attr.Name.Local == "from" { + pres.From = attr.Value + } + if attr.Name.Local == "lang" { + pres.Lang = attr.Value + } + } + + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + if presExt := TypeRegistry.GetPresExtension(tt.Name); presExt != nil { + // Decode message extension + err = d.DecodeElement(presExt, &tt) + if err != nil { + return err + } + pres.Extensions = append(pres.Extensions, presExt) + } else { + // Decode standard message sub-elements + var err error + switch tt.Name.Local { + case "show": + err = d.DecodeElement(&pres.Show, &tt) + case "status": + err = d.DecodeElement(&pres.Status, &tt) + case "priority": + err = d.DecodeElement(&pres.Priority, &tt) + case "error": + err = d.DecodeElement(&pres.Error, &tt) + } + if err != nil { + return err + } + } + + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} diff --git a/registry.go b/registry.go index e5afcf9..8edacb4 100644 --- a/registry.go +++ b/registry.go @@ -7,6 +7,7 @@ import ( ) type MsgExtension interface{} +type PresExtension interface{} // The Registry for msg and IQ types is a global variable. // TODO: Move to the client init process to remove the dependency on a global variable. @@ -78,6 +79,19 @@ func (r *registry) GetExtensionType(pktType PacketType, name xml.Name) reflect.T return result } +// GetPresExtension returns an instance of PresExtension, by matching packet type and XML +// tag name against the registry. +func (r *registry) GetPresExtension(name xml.Name) PresExtension { + if extensionType := r.GetExtensionType(PKTPresence, name); extensionType != nil { + val := reflect.New(extensionType) + elt := val.Interface() + if presExt, ok := elt.(PresExtension); ok { + return presExt + } + } + return nil +} + // GetMsgExtension returns an instance of MsgExtension, by matching packet type and XML // tag name against the registry. func (r *registry) GetMsgExtension(name xml.Name) MsgExtension {