~samwhited/xmpp

741e8a5cde1c826639c4e8464d0037830d2883af — Sam Whited 4 months ago 68530b0
roster: support roster versioning

Though the payloads had version strings on them and a roster versioning
feature existed, it wasn't possible to fetch a roster using the roster
version so support wasn't really done. This makes it possible to
actually use roster versioning.

Signed-off-by: Sam Whited <sam@samwhited.com>
3 files changed, 52 insertions(+), 22 deletions(-)

M CHANGELOG.md
M roster/roster.go
M roster/roster_test.go
M CHANGELOG.md => CHANGELOG.md +7 -0
@@ 7,6 7,9 @@ All notable changes to this project will be documented in this file.
### Breaking

- roster: rename `version` attribute to `ver`
- roster: the `Push` callback now takes the roster version
- roster: `FetchIQ` now takes a `roster.IQ` instead of a `stanza.IQ` so that the
  version can be set
- styling: decoding tokens now uses an iterator pattern
- xmpp: the `WebSocket` option on `StreamConfig` has been removed in favor of
  `websocket.Negotiator`


@@ 25,6 28,8 @@ All notable changes to this project will be documented in this file.
- carbons: new package implementing [XEP-0280: Message Carbons]
- commands: new package implementing [XEP-0050: Ad-Hoc Commands]
- muc: new package implementing [XEP-0045: Multi-User Chat] and [XEP-0249: Direct MUC Invitations]
- roster: the roster `Iter` now returns the roster version being iterated over
  from the `Version` method
- stanza: implement [XEP-0203: Delayed Delivery]
- stanza: more general `UnmarshalError` function that doesn't focus on IQs
- stanza: add `Error` method to `Presence` and `Message`


@@ 38,6 43,8 @@ All notable changes to this project will be documented in this file.
  panics
- form: unmarshaling into an existing form now resets the stored values to
  prevent data leaks across forms
- roster: the roster version is now always included, even if empty, to signal
  that we support roster versioning
- stanza: unmarshaling error IQs now works even if the error is not the first
  child in the payload
- styling: pre-block start tokens with no newline had nonsensical formatting

M roster/roster.go => roster/roster.go +34 -14
@@ 30,7 30,7 @@ func Handle(h Handler) mux.Option {

// Handler responds to roster pushes.
type Handler struct {
	Push func(Item) error
	Push func(ver string, item Item) error
}

// HandleIQ responds to roster push IQs.


@@ 40,7 40,14 @@ func (h Handler) HandleIQ(iq stanza.IQ, t xmlstream.TokenReadEncoder, start *xml
	if err != nil {
		return err
	}
	return h.Push(item)
	var ver string
	for _, attr := range start.Attr {
		if attr.Name.Local == "ver" {
			ver = attr.Value
			break
		}
	}
	return h.Push(ver, item)
}

// Iter is an iterator over roster items.


@@ 48,6 55,7 @@ type Iter struct {
	iter    *xmlstream.Iter
	current Item
	err     error
	ver     string
}

// Next returns true if there are more items to decode.


@@ 72,6 80,12 @@ func (i *Iter) Next() bool {
	return true
}

// Version returns the roster version being iterated over or the empty string if
// roster versioning is not enabled.
func (i *Iter) Version() string {
	return i.ver
}

// Err returns the last error encountered by the iterator (if any).
func (i *Iter) Err() error {
	if i.err != nil {


@@ 104,24 118,33 @@ func (i *Iter) Close() error {
// Any errors encountered while creating the iter are deferred until the iter is
// used.
func Fetch(ctx context.Context, s *xmpp.Session) *Iter {
	return FetchIQ(ctx, stanza.IQ{}, s)
	return FetchIQ(ctx, IQ{}, s)
}

// FetchIQ is like Fetch but it allows you to customize the IQ.
// Changing the type of the provided IQ has no effect.
func FetchIQ(ctx context.Context, iq stanza.IQ, s *xmpp.Session) *Iter {
	if iq.Type != stanza.GetIQ {
		iq.Type = stanza.GetIQ
	}
	rosterIQ := IQ{IQ: iq}
	iter, err := s.IterIQ(ctx, rosterIQ.TokenReader())
// Changing the type of the provided IQ or adding items has no effect.
func FetchIQ(ctx context.Context, iq IQ, s *xmpp.Session) *Iter {
	iq.Query.Item = nil
	iq.Type = stanza.GetIQ
	iter, start, err := s.IterIQ(ctx, iq.TokenReader())
	if err != nil {
		return &Iter{err: err}
	}
	var ver string
	for _, attr := range start.Attr {
		if attr.Name.Local == "ver" {
			ver = attr.Value
			break
		}
	}
	if ver == "" {
		ver = iq.Query.Ver
	}

	// Return the iterator which will parse the rest of the payload incrementally.
	return &Iter{
		iter: iter,
		ver:  ver,
	}
}



@@ 173,10 196,7 @@ func (iq IQ) TokenReader() xml.TokenReader {
// Payload returns a stream of XML tokekns that match the roster query payload
// without the IQ wrapper.
func (iq IQ) payload() xml.TokenReader {
	attrs := []xml.Attr{}
	if iq.Query.Ver != "" {
		attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "ver"}, Value: iq.Query.Ver})
	}
	attrs := []xml.Attr{{Name: xml.Name{Local: "ver"}, Value: iq.Query.Ver}}

	return xmlstream.Wrap(
		&itemMarshaler{items: iq.Query.Item[:]},

M roster/roster_test.go => roster/roster_test.go +11 -8
@@ 59,13 59,13 @@ func TestFetch(t *testing.T) {
					return e.Encode(sendIQ)
				}),
			)
			iter := roster.FetchIQ(context.Background(), IQ, cs.Client)
			iter := roster.FetchIQ(context.Background(), roster.IQ{IQ: IQ}, cs.Client)
			items := make([]roster.Item, 0, len(tc.items))
			for iter.Next() {
				items = append(items, iter.Item())
			}
			if err := iter.Err(); err != tc.err {
				t.Errorf("Wrong error after iter: want=%q, got=%q", tc.err, err)
				t.Errorf("wrong error after iter: want=%q, got=%q", tc.err, err)
			}
			iter.Close()



@@ 75,7 75,7 @@ func TestFetch(t *testing.T) {
			}

			if !reflect.DeepEqual(items, tc.items) {
				t.Errorf("Wrong items:\nwant=\n%+v,\ngot=\n%+v", tc.items, items)
				t.Errorf("wrong items:\nwant=\n%+v,\ngot=\n%+v", tc.items, items)
			}
		})
	}


@@ 89,7 89,7 @@ func TestFetchNoStart(t *testing.T) {
			return err
		}),
	)
	iter := roster.FetchIQ(context.Background(), stanza.IQ{ID: "123"}, cs.Client)
	iter := roster.FetchIQ(context.Background(), roster.IQ{IQ: stanza.IQ{ID: "123"}}, cs.Client)
	for iter.Next() {
		t.Fatalf("iterator should never have any items!")
	}


@@ 107,7 107,7 @@ func TestEmptyIQ(t *testing.T) {
			return err
		}),
	)
	iter := roster.FetchIQ(context.Background(), stanza.IQ{ID: "123"}, cs.Client)
	iter := roster.FetchIQ(context.Background(), roster.IQ{IQ: stanza.IQ{ID: "123"}}, cs.Client)
	for iter.Next() {
		t.Fatalf("iterator should never have any items!")
	}


@@ 119,7 119,7 @@ func TestEmptyIQ(t *testing.T) {

func TestReceivePush(t *testing.T) {
	const itemJID = "nurse@example.com"
	const x = `<iq xmlns='jabber:client' id='a78b4q6ha463' to='juliet@example.com/chamber' type='set'><query xmlns='jabber:iq:roster'><item jid='` + itemJID + `'/></query></iq>`
	const x = `<iq xmlns='jabber:client' id='a78b4q6ha463' to='juliet@example.com/chamber' type='set'><query xmlns='jabber:iq:roster' ver='testver'><item jid='` + itemJID + `'/></query></iq>`

	d := xml.NewDecoder(strings.NewReader(x))
	var b strings.Builder


@@ 127,7 127,10 @@ func TestReceivePush(t *testing.T) {

	called := false
	h := roster.Handler{
		Push: func(item roster.Item) error {
		Push: func(ver string, item roster.Item) error {
			if ver != "testver" {
				t.Errorf("wrong version: want=%q, got=%q", "testver", ver)
			}
			if item.JID.String() != itemJID {
				t.Errorf("unexpected JID: want=%q, got=%q", itemJID, item.JID.String())
			}


@@ 194,7 197,7 @@ var marshalTests = [...]struct {
}{
	0: {
		in:  roster.IQ{},
		out: `<iq type=""><query xmlns="jabber:iq:roster"></query></iq>`,
		out: `<iq type=""><query xmlns="jabber:iq:roster" ver=""></query></iq>`,
	},
	1: {
		in: roster.IQ{