~samwhited/xmpp

196304307d9d915570ed0c5c4734b8247a75cc9d — Sam Whited 3 months ago 0e39877
history: add initial implementation of MAM

Signed-off-by: Sam Whited <sam@samwhited.com>
4 files changed, 189 insertions(+), 0 deletions(-)

M CHANGELOG.md
A history/doc.go
A history/history.go
A history/query.go
M CHANGELOG.md => CHANGELOG.md +2 -0
@@ 27,6 27,7 @@ All notable changes to this project will be documented in this file.
- blocklist: new package implementing [XEP-0191: Blocking Command]
- carbons: new package implementing [XEP-0280: Message Carbons]
- commands: new package implementing [XEP-0050: Ad-Hoc Commands]
- history: implement [XEP-0313: Message Archive Management]
- muc: new package implementing [XEP-0045: Multi-User Chat] and [XEP-0249: Direct MUC Invitations]
- mux: `mux.ServeMux` now implements `info.FeatureIter`
- roster: the roster `Iter` now returns the roster version being iterated over


@@ 69,6 70,7 @@ All notable changes to this project will be documented in this file.
[XEP-0203: Delayed Delivery]: https://xmpp.org/extensions/xep-0203.html
[XEP-0249: Direct MUC Invitations]: https://xmpp.org/extensions/xep-0249.html
[XEP-0280: Message Carbons]: https://xmpp.org/extensions/xep-0280.html
[XEP-0313: Message Archive Management]: https://xmpp.org/extensions/xep-0313.html


## v0.19.0 — 2021-05-02

A history/doc.go => history/doc.go +12 -0
@@ 0,0 1,12 @@
// Copyright 2021 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

// Package history implements fetching messages from an archive.
package history // import "mellium.im/xmpp/history"

// The namespaces used by this package, provided as a convenience.
const (
	NS    = `urn:xmpp:mam:2`
	NSExt = `urn:xmpp:mam:2#extended`
)

A history/history.go => history/history.go +51 -0
@@ 0,0 1,51 @@
// Copyright 2021 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package history

import (
	"context"
	"encoding/xml"

	"mellium.im/xmpp"
	"mellium.im/xmpp/internal/attr"
	"mellium.im/xmpp/jid"
	"mellium.im/xmpp/mux"
	"mellium.im/xmpp/paging"
	"mellium.im/xmpp/stanza"
)

// Handle returns an option that registers a Handler for incoming history query
// results.
func Handle(h mux.MessageHandler) mux.Option {
	return mux.Message("", xml.Name{Space: NS, Local: "result"}, h)
}

// Fetch requests messages from the archive.
// The messages must be handled separately and Fetch blocks until they have all
// been prosessed.
func Fetch(ctx context.Context, filter Query, to jid.JID, s *xmpp.Session) (paging.Set, error) {
	return FetchIQ(ctx, filter, stanza.IQ{
		To: to,
	}, s)
}

// FetchIQ is like fetch but it allows modifying the underlying IQ.
// Changing the type of the IQ has no effect.
func FetchIQ(ctx context.Context, filter Query, iq stanza.IQ, s *xmpp.Session) (paging.Set, error) {
	if filter.ID == "" {
		filter.ID = attr.RandomID()
	}
	iq.Type = stanza.SetIQ
	var result struct {
		XMLName xml.Name `xml:"urn:xmpp:mam:2 fin"`
		Set     paging.Set
	}
	err := s.UnmarshalIQ(
		ctx,
		iq.Wrap(filter.TokenReader()),
		&result,
	)
	return result.Set, err
}

A history/query.go => history/query.go +124 -0
@@ 0,0 1,124 @@
// Copyright 2021 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package history

import (
	"encoding/xml"
	"time"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/form"
	"mellium.im/xmpp/jid"
	"mellium.im/xmpp/paging"
)

// Query is a request to the archive for data.
// An empty query indicates all messages should be fetched without a filter and
// with a random ID.
type Query struct {
	// Query parameters
	ID string

	// Filters
	With     jid.JID
	Start    time.Time
	End      time.Time
	BeforeID string
	AfterID  string
	IDs      []string

	// Limit limits the total number of messages returned.
	Limit uint64

	// Last starts fetching from the last page.
	Last bool

	// Reverse flips messages returned within a page.
	Reverse bool
}

const (
	fieldWith   = "with"
	fieldStart  = "start"
	fieldEnd    = "end"
	fieldAfter  = "after-id"
	fieldBefore = "before-id"
	fieldIDs    = "ids"
)

// TokenReader implements xmlstream.Marshaler.
func (f Query) TokenReader() xml.TokenReader {
	dataForm := form.New(
		form.Hidden("FORM_TYPE", form.Value(NS)),
		form.JID(fieldWith),
		form.Text(fieldStart),
		form.Text(fieldEnd),
		form.Text(fieldAfter),
		form.Text(fieldBefore),
		form.ListMulti(fieldIDs),
	)
	if !f.With.Equal(jid.JID{}) {
		/* #nosec */
		dataForm.Set(fieldWith, f.With)
	}
	if !f.Start.IsZero() {
		/* #nosec */
		dataForm.Set(fieldStart, f.Start.UTC().Format(time.RFC3339))
	}
	if !f.End.IsZero() {
		/* #nosec */
		dataForm.Set(fieldEnd, f.End.UTC().Format(time.RFC3339))
	}
	if f.AfterID != "" {
		/* #nosec */
		dataForm.Set(fieldAfter, f.AfterID)
	}
	if f.BeforeID != "" {
		/* #nosec */
		dataForm.Set(fieldBefore, f.BeforeID)
	}
	if len(f.IDs) > 0 {
		/* #nosec */
		dataForm.Set(fieldIDs, f.IDs)
	}
	filter, _ := dataForm.Submit()

	inner := []xml.TokenReader{
		filter,
	}
	if f.Last {
		inner = append(inner, (&paging.RequestPrev{
			Max: f.Limit,
		}).TokenReader())
	} else {
		inner = append(inner, (&paging.RequestNext{
			Max: f.Limit,
		}).TokenReader())
	}
	if f.Reverse {
		inner = append(inner, xmlstream.Wrap(
			nil,
			xml.StartElement{Name: xml.Name{Local: "flip-page"}},
		))
	}
	return xmlstream.Wrap(
		xmlstream.MultiReader(inner...),
		xml.StartElement{
			Name: xml.Name{Space: NS, Local: "query"},
			Attr: []xml.Attr{{Name: xml.Name{Local: "queryid"}, Value: f.ID}},
		},
	)
}

// WriteXML implements xmlstream.WriterTo.
func (f *Query) WriteXML(w xmlstream.TokenWriter) (int, error) {
	return xmlstream.Copy(w, f.TokenReader())
}

// MarshalXML implements xml.Marshaler.
func (f *Query) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
	_, err := f.WriteXML(e)
	return err
}