diff --git a/stanza/pres_muc.go b/stanza/pres_muc.go index e944fb6..bc0e75e 100644 --- a/stanza/pres_muc.go +++ b/stanza/pres_muc.go @@ -2,6 +2,7 @@ package stanza import ( "encoding/xml" + "strconv" "time" ) @@ -16,12 +17,130 @@ type MucPresence struct { History History `xml:"history,omitempty"` } +const timeLayout = "2006-01-02T15:04:05Z" + // 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"` + XMLName xml.Name + MaxChars NullableInt `xml:"maxchars,attr,omitempty"` + MaxStanzas NullableInt `xml:"maxstanzas,attr,omitempty"` + Seconds NullableInt `xml:"seconds,attr,omitempty"` + Since time.Time `xml:"since,attr,omitempty"` +} + +type NullableInt struct { + Value int + isSet bool +} + +func NewNullableInt(val int) NullableInt { + return NullableInt{val, true} +} + +func (n NullableInt) Get() (v int, ok bool) { + return n.Value, n.isSet +} + +// UnmarshalXML implements custom parsing for history element +func (h *History) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + h.XMLName = start.Name + + // Extract attributes + for _, attr := range start.Attr { + switch attr.Name.Local { + case "maxchars": + v, err := strconv.Atoi(attr.Value) + if err != nil { + return err + } + h.MaxChars = NewNullableInt(v) + case "maxstanzas": + v, err := strconv.Atoi(attr.Value) + if err != nil { + return err + } + h.MaxStanzas = NewNullableInt(v) + case "seconds": + v, err := strconv.Atoi(attr.Value) + if err != nil { + return err + } + h.Seconds = NewNullableInt(v) + case "since": + t, err := time.Parse(timeLayout, attr.Value) + if err != nil { + return err + } + h.Since = t + } + } + + // Consume remaining data until element end + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + +func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { + mc, isMcSet := h.MaxChars.Get() + ms, isMsSet := h.MaxStanzas.Get() + s, isSSet := h.Seconds.Get() + + // We do not have any value, ignore history element + if h.Since.IsZero() && !isMcSet && !isMsSet && !isSSet { + return nil + } + + // Encode start element and attributes + start.Name = xml.Name{Local: "history"} + + if isMcSet { + attr := xml.Attr{ + Name: xml.Name{Local: "maxchars"}, + Value: strconv.Itoa(mc), + } + start.Attr = append(start.Attr, attr) + } + + if isMsSet { + attr := xml.Attr{ + Name: xml.Name{Local: "maxstanzas"}, + Value: strconv.Itoa(ms), + } + start.Attr = append(start.Attr, attr) + } + + if isSSet { + attr := xml.Attr{ + Name: xml.Name{Local: "seconds"}, + Value: strconv.Itoa(s), + } + start.Attr = append(start.Attr, attr) + } + + if !h.Since.IsZero() { + attr := xml.Attr{ + Name: xml.Name{Local: "since"}, + Value: h.Since.Format(timeLayout), + } + start.Attr = append(start.Attr, attr) + } + if err := e.EncodeToken(start); err != nil { + return err + } + + return e.EncodeToken(xml.EndElement{Name: start.Name}) + } func init() { diff --git a/stanza/pres_muc_test.go b/stanza/pres_muc_test.go index 78fe29f..209dc93 100644 --- a/stanza/pres_muc_test.go +++ b/stanza/pres_muc_test.go @@ -46,15 +46,52 @@ func TestMucHistory(t *testing.T) { var parsedPresence stanza.Presence if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { - t.Errorf("Unmarshal(%s) returned error", str) + t.Errorf("Unmarshal(%s) returned error: %s", str, err) + return } var muc stanza.MucPresence if ok := parsedPresence.Get(&muc); !ok { t.Error("muc presence extension was not found") + return } - if muc.History.MaxStanzas != 20 { - t.Errorf("incorrect max stanza: '%d'", muc.History.MaxStanzas) + if v, ok := muc.History.MaxStanzas.Get(); !ok || v != 20 { + t.Errorf("incorrect MaxStanzas: '%#v'", muc.History.MaxStanzas) + } +} + +// https://xmpp.org/extensions/xep-0045.html#example-37 +func TestMucNoHistory(t *testing.T) { + str := "" + + "" + + "" + + "" + + "" + + maxstanzas := 0 + + pres := stanza.Presence{Attrs: stanza.Attrs{ + From: "hag66@shakespeare.lit/pda", + Id: "n13mt3l", + To: "coven@chat.shakespeare.lit/thirdwitch", + }, + Extensions: []stanza.PresExtension{ + stanza.MucPresence{ + History: stanza.History{MaxStanzas: stanza.NewNullableInt(maxstanzas)}, + }, + }, + } + data, err := xml.Marshal(&pres) + if err != nil { + t.Error("error on encode:", err) + return + } + + if string(data) != str { + t.Errorf("incorrect stanza: \n%s\n%s", str, data) } }