WIP : Support for XEP-313

This commit is contained in:
CORNIERE Rémi 2020-04-29 10:19:28 +02:00
parent ce71bc5c76
commit 7cea390519
8 changed files with 215 additions and 40 deletions

View File

@ -349,6 +349,25 @@ func (c *Client) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, err
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
}
// SendIQ sends an IQ set or get stanza to the server. If a result is received
// the provided handler function will automatically be called.
//
// The provided context should have a timeout to prevent the client from waiting
// forever for an IQ result. For example:
//
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
// result := <- client.SendIQ(ctx, iq)
//
func (c *Client) SendMamRequest(ctx context.Context, iq *stanza.IQ) (chan stanza.Packet, error) {
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
return nil, ErrCanOnlySendGetOrSetIq
}
if err := c.Send(iq); err != nil {
return nil, err
}
return c.router.NewMamResultRoute(ctx, iq.Id), nil
}
// SendRaw sends an XMPP stanza as a string to the server.
// It can be invalid XML or XMPP content. In that case, the server will
// disconnect the client. It is up to the user of this method to

View File

@ -30,6 +30,9 @@ type Router struct {
IQResultRoutes map[string]*IQResultRoute
IQResultRouteLock sync.RWMutex
MamResultRoutes map[string]*MamResultRoute
MamResultRoutesLock sync.RWMutex
}
// NewRouter returns a new router instance.
@ -55,15 +58,43 @@ func (r *Router) route(s Sender, p stanza.Packet) {
}
iq, isIq := p.(*stanza.IQ)
if isIq {
r.IQResultRouteLock.RLock()
route, ok := r.IQResultRoutes[iq.Id]
r.IQResultRouteLock.RUnlock()
// Mam IQs (See XEP-0313)
if iq.Payload.Namespace() == stanza.NSMam {
r.MamResultRoutesLock.RLock()
route, ok := r.MamResultRoutes[iq.Id]
r.MamResultRoutesLock.RUnlock()
if ok {
r.MamResultRoutesLock.Lock()
delete(r.MamResultRoutes, iq.Id)
r.MamResultRoutesLock.Unlock()
route.results <- iq
close(route.results)
return
}
} else { // "Classic" IQs
r.IQResultRouteLock.RLock()
route, ok := r.IQResultRoutes[iq.Id]
r.IQResultRouteLock.RUnlock()
if ok {
r.IQResultRouteLock.Lock()
delete(r.IQResultRoutes, iq.Id)
r.IQResultRouteLock.Unlock()
route.result <- *iq
close(route.result)
return
}
}
}
// If message is part of a response to a Mam query, forward it through the dedicated channel (See XEP-0313)
msg, ok := p.(stanza.Message)
if ok {
r.MamResultRoutesLock.RLock()
route, ok := r.MamResultRoutes[iq.Id]
r.MamResultRoutesLock.RUnlock()
if ok {
r.IQResultRouteLock.Lock()
delete(r.IQResultRoutes, iq.Id)
r.IQResultRouteLock.Unlock()
route.result <- *iq
close(route.result)
route.results <- msg
return
}
}
@ -147,6 +178,26 @@ func (r *Router) NewIQResultRoute(ctx context.Context, id string) chan stanza.IQ
return route.result
}
// NewIQResultRoute register a route that will catch message stanzas and the closing IQ result attached to the
// the given queryId. The route will automatically be unregistered.
func (r *Router) NewMamResultRoute(ctx context.Context, id string) chan stanza.Packet {
route := NewMamResultRoute(ctx)
r.MamResultRoutesLock.Lock()
r.MamResultRoutes[id] = route
r.MamResultRoutesLock.Unlock()
// Start a go function to make sure the route is unregistered when the context
// is done.
go func() {
<-route.context.Done()
r.MamResultRoutesLock.Lock()
delete(r.IQResultRoutes, id)
r.MamResultRoutesLock.Unlock()
}()
return route.results
}
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
for _, route := range r.routes {
if route.Match(p, match) {
@ -187,6 +238,20 @@ func NewIQResultRoute(ctx context.Context) *IQResultRoute {
}
}
// ==============================================================================
type MamResultRoute struct {
context context.Context
results chan stanza.Packet
}
// NewIQResultRoute creates a new IQResultRoute instance
func NewMamResultRoute(ctx context.Context) *MamResultRoute {
return &MamResultRoute{
context: ctx,
results: make(chan stanza.Packet),
}
}
// ============================================================================
// IQ result handler

View File

@ -53,38 +53,6 @@ func (d *Delegation) GetSet() *ResultSet {
return d.ResultSet
}
// Forwarded is used to wrapped forwarded stanzas.
// TODO: Move it in another file, as it is not limited to components.
type Forwarded struct {
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
Stanza Packet
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if packet, err := decodeClient(d, tt); err == nil {
f.Stanza = packet
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
type Delegated struct {
XMLName xml.Name `xml:"delegated"`
Namespace string `xml:"namespace,attr,omitempty"`

34
stanza/forwarded.go Normal file
View File

@ -0,0 +1,34 @@
package stanza
import "encoding/xml"
// Forwarded is used to wrapped forwarded stanzas.
type Forwarded struct {
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
Stanza Packet
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Check sub elements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if packet, err := decodeClient(d, tt); err == nil {
f.Stanza = packet
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}

52
stanza/iq_mam.go Normal file
View File

@ -0,0 +1,52 @@
package stanza
import (
"encoding/xml"
"github.com/google/uuid"
)
// ----------
// Namespaces
const (
// NSRoster is the Roster IQ namespace
NSMam = "urn:xmpp:mam:2"
)
// Roster struct represents Roster IQs
type MamQuery struct {
XMLName xml.Name `xml:"urn:xmpp:mam:2 query"`
QueryId string `xml:"queryid,attr"`
}
// Namespace defines the namespace for the RosterIQ
func (mq *MamQuery) Namespace() string {
return mq.XMLName.Space
}
func (mq *MamQuery) GetQueryId() string {
return mq.QueryId
}
// To implement IqPayload interface only
func (mq *MamQuery) GetSet() *ResultSet {
return nil
}
// ---------------
// Builder helpers
// RosterIQ builds a default Roster payload
func (iq *IQ) NewMamIQ() *MamQuery {
mq := MamQuery{
XMLName: xml.Name{
Space: NSMam,
Local: "query",
},
}
if id, err := uuid.NewRandom(); err == nil {
mq.QueryId = id.String()
}
iq.Payload = &mq
return &mq
}

View File

@ -16,6 +16,7 @@ type Message struct {
Subject string `xml:"subject,omitempty"`
Body string `xml:"body,omitempty"`
Thread string `xml:"thread,omitempty"`
StanzaId *StanzaId `xml:"stanza-id"`
Error Err `xml:"error,omitempty"`
Extensions []MsgExtension `xml:",omitempty"`
}

16
stanza/msg_id.go Normal file
View File

@ -0,0 +1,16 @@
package stanza
import "encoding/xml"
/*
Support for:
- XEP-0313 - Message Archive Management (MAM): https://xmpp.org/extensions/xep-0313.html
This MUST NOT be interpreted as an archive ID unless the server has previously advertised support for 'urn:xmpp:mam:2'
See : https://xmpp.org/extensions/xep-0313.html#archives_id
*/
type StanzaId struct {
XMLName xml.Name `xml:"urn:xmpp:sid:0 stanza-id"`
By string `xml:"by,attr"`
Id string `xml:"id,attr"`
}

20
stanza/msg_id_test.go Normal file
View File

@ -0,0 +1,20 @@
package stanza
import (
"bytes"
"encoding/xml"
"testing"
)
const expectedMarshal = `<stanza-id xmlns="urn:xmpp:sid:0" by="jid" id="unique-id"></stanza-id>`
func TestMarshal(t *testing.T) {
d := StanzaId{
By: "jid",
Id: "unique-id",
}
data, e := xml.Marshal(d)
if e != nil || !bytes.Equal(data, []byte(expectedMarshal)) {
t.Fatalf("Marshal failed. Expected: %v, Actual: %v", expectedMarshal, string(data))
}
}