uhh whats a submodule i have never heard of that bro
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# XMPP Stanza
|
||||
|
||||
XMPP `stanza` package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
||||
|
||||
## Stanza creation
|
||||
|
||||
When creating stanzas, you can use two approaches:
|
||||
|
||||
1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the
|
||||
stanza.
|
||||
2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the
|
||||
final stanza.
|
||||
|
||||
The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of
|
||||
struct that you can build by hand.
|
||||
|
||||
### Composing stanzas manually with structs
|
||||
|
||||
Here is for example how you would generate an IQ discovery result:
|
||||
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
identity := stanza.Identity{
|
||||
Name: opts.Name,
|
||||
Category: opts.Category,
|
||||
Type: opts.Type,
|
||||
}
|
||||
payload := stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
Identity: []stanza.Identity{identity},
|
||||
Features: []stanza.Feature{
|
||||
{Var: stanza.NSDiscoInfo},
|
||||
{Var: stanza.NSDiscoItems},
|
||||
{Var: "jabber:iq:version"},
|
||||
{Var: "urn:xmpp:delegation:1"},
|
||||
},
|
||||
}
|
||||
iqResp.Payload = &payload
|
||||
|
||||
### Using helpers
|
||||
|
||||
Here is for example how you would generate an IQ discovery result using Builder:
|
||||
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||
disco := iq.DiscoInfo()
|
||||
disco.AddIdentity("Test Component", "gateway", "service")
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
|
||||
## Payload and extensions
|
||||
|
||||
### Message
|
||||
|
||||
Here is the list of implemented message extensions:
|
||||
|
||||
- `Delegation`
|
||||
|
||||
- `Markable`
|
||||
- `MarkAcknowledged`
|
||||
- `MarkDisplayed`
|
||||
- `MarkReceived`
|
||||
|
||||
- `StateActive`
|
||||
- `StateComposing`
|
||||
- `StateGone`
|
||||
- `StateInactive`
|
||||
- `StatePaused`
|
||||
|
||||
- `HTML`
|
||||
|
||||
- `OOB`
|
||||
|
||||
- `ReceiptReceived`
|
||||
- `ReceiptRequest`
|
||||
|
||||
- `Mood`
|
||||
|
||||
### Presence
|
||||
|
||||
Here is the list of implemented presence extensions:
|
||||
|
||||
- `MucPresence`
|
||||
|
||||
### IQ
|
||||
|
||||
IQ (Information Queries) contain a payload associated with the request and possibly an error. The main difference with
|
||||
Message and Presence extension is that you can only have one payload per IQ. The XMPP specification does not support
|
||||
having multiple payloads.
|
||||
|
||||
Here is the list of structs implementing IQPayloads:
|
||||
|
||||
- `ControlSet`
|
||||
- `ControlSetResponse`
|
||||
- `Delegation`
|
||||
- `DiscoInfo`
|
||||
- `DiscoItems`
|
||||
- `Pubsub`
|
||||
- `Version`
|
||||
- `Node`
|
||||
|
||||
Finally, when the payload of the parsed stanza is unknown, the parser will provide the unknown payload as a generic
|
||||
`Node` element. You can also use the Node struct to add custom information on stanza generation. However, in both cases,
|
||||
you may also consider [adding your own custom extensions on stanzas]().
|
||||
|
||||
|
||||
## Adding your own custom extensions on stanzas
|
||||
|
||||
Extensions are registered on launch using the `Registry`. It can be used to register you own custom payload. You may
|
||||
want to do so to support extensions we did not yet implement, or to add your own custom extensions to your XMPP stanzas.
|
||||
|
||||
To create an extension you need:
|
||||
1. to create a struct for that extension. It need to have XMLName for consistency and to tagged at the struct level with
|
||||
`xml` info.
|
||||
2. It need to implement one or several extensions interface: stanza.IQPayload, stanza.MsgExtension and / or
|
||||
stanza.PresExtension
|
||||
3. Add that custom extension to the stanza.TypeRegistry during the file init.
|
||||
|
||||
Here an example code showing how to create a custom IQPayload.
|
||||
|
||||
```go
|
||||
package myclient
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
type CustomPayload struct {
|
||||
XMLName xml.Name `xml:"my:custom:payload query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (c CustomPayload) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
func init() {
|
||||
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,157 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Implements the XEP-0050 extension
|
||||
|
||||
const (
|
||||
CommandActionCancel = "cancel"
|
||||
CommandActionComplete = "complete"
|
||||
CommandActionExecute = "execute"
|
||||
CommandActionNext = "next"
|
||||
CommandActionPrevious = "prev"
|
||||
|
||||
CommandStatusCancelled = "canceled"
|
||||
CommandStatusCompleted = "completed"
|
||||
CommandStatusExecuting = "executing"
|
||||
|
||||
CommandNoteTypeErr = "error"
|
||||
CommandNoteTypeInfo = "info"
|
||||
CommandNoteTypeWarn = "warn"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"`
|
||||
|
||||
CommandElements []CommandElement
|
||||
|
||||
BadAction *struct{} `xml:"bad-action,omitempty"`
|
||||
BadLocale *struct{} `xml:"bad-locale,omitempty"`
|
||||
BadPayload *struct{} `xml:"bad-payload,omitempty"`
|
||||
BadSessionId *struct{} `xml:"bad-sessionid,omitempty"`
|
||||
MalformedAction *struct{} `xml:"malformed-action,omitempty"`
|
||||
SessionExpired *struct{} `xml:"session-expired,omitempty"`
|
||||
|
||||
// Attributes
|
||||
Action string `xml:"action,attr,omitempty"`
|
||||
Node string `xml:"node,attr"`
|
||||
SessionId string `xml:"sessionid,attr,omitempty"`
|
||||
Status string `xml:"status,attr,omitempty"`
|
||||
Lang string `xml:"lang,attr,omitempty"`
|
||||
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Command) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
func (c *Command) GetSet() *ResultSet {
|
||||
return c.ResultSet
|
||||
}
|
||||
|
||||
type CommandElement interface {
|
||||
Ref() string
|
||||
}
|
||||
|
||||
type Actions struct {
|
||||
XMLName xml.Name `xml:"actions"`
|
||||
|
||||
Prev *struct{} `xml:"prev,omitempty"`
|
||||
Next *struct{} `xml:"next,omitempty"`
|
||||
Complete *struct{} `xml:"complete,omitempty"`
|
||||
|
||||
Execute string `xml:"execute,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Actions) Ref() string {
|
||||
return "actions"
|
||||
}
|
||||
|
||||
type Note struct {
|
||||
XMLName xml.Name `xml:"note"`
|
||||
|
||||
Text string `xml:",cdata"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (n *Note) Ref() string {
|
||||
return "note"
|
||||
}
|
||||
func (f *Form) Ref() string { return "form" }
|
||||
|
||||
func (n *Node) Ref() string {
|
||||
return "node"
|
||||
}
|
||||
|
||||
func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
c.XMLName = start.Name
|
||||
|
||||
// Extract packet attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "action" {
|
||||
c.Action = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "node" {
|
||||
c.Node = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "sessionid" {
|
||||
c.SessionId = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "status" {
|
||||
c.Status = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "lang" {
|
||||
c.Lang = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
// Decode sub-elements
|
||||
var err error
|
||||
switch tt.Name.Local {
|
||||
|
||||
case "actions":
|
||||
a := Actions{}
|
||||
err = d.DecodeElement(&a, &tt)
|
||||
c.CommandElements = append(c.CommandElements, &a)
|
||||
case "note":
|
||||
nt := Note{}
|
||||
err = d.DecodeElement(&nt, &tt)
|
||||
c.CommandElements = append(c.CommandElements, &nt)
|
||||
case "x":
|
||||
f := Form{}
|
||||
err = d.DecodeElement(&f, &tt)
|
||||
c.CommandElements = append(c.CommandElements, &f)
|
||||
default:
|
||||
n := Node{}
|
||||
err = d.DecodeElement(&n, &tt)
|
||||
c.CommandElements = append(c.CommandElements, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/commands", Local: "command"}, Command{})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestMarshalCommands(t *testing.T) {
|
||||
input := "<command xmlns=\"http://jabber.org/protocol/commands\" node=\"list\" " +
|
||||
"sessionid=\"list:20020923T213616Z-700\" status=\"completed\"><x xmlns=\"jabber:x:data\" " +
|
||||
"type=\"result\"><title>Available Services</title><reported xmlns=\"jabber:x:data\"><field var=\"service\" " +
|
||||
"label=\"Service\"></field><field var=\"runlevel-1\" label=\"Single-User mode\">" +
|
||||
"</field><field var=\"runlevel-2\" label=\"Non-Networked Multi-User mode\"></field><field var=\"runlevel-3\" " +
|
||||
"label=\"Full Multi-User mode\"></field><field var=\"runlevel-5\" label=\"X-Window mode\"></field></reported>" +
|
||||
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>httpd</value></field><field var=\"runlevel-1\">" +
|
||||
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
|
||||
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
|
||||
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>postgresql</value></field>" +
|
||||
"<field var=\"runlevel-1\"><value>off</value></field><field var=\"runlevel-2\"><value>off</value></field>" +
|
||||
"<field var=\"runlevel-3\"><value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
|
||||
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>jabberd</value></field><field var=\"runlevel-1\">" +
|
||||
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
|
||||
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item></x></command>"
|
||||
var c stanza.Command
|
||||
err := xml.Unmarshal([]byte(input), &c)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal initial input")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(c)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal unmarshalled input")
|
||||
}
|
||||
|
||||
if err := compareMarshal(input, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Handshake Stanza
|
||||
|
||||
// Handshake is a stanza used by XMPP components to authenticate on XMPP
|
||||
// component port.
|
||||
type Handshake struct {
|
||||
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||
// TODO Add handshake value with test for proper serialization
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (Handshake) Name() string {
|
||||
return "component:handshake"
|
||||
}
|
||||
|
||||
// Handshake decoding wrapper
|
||||
|
||||
type handshakeDecoder struct{}
|
||||
|
||||
var handshake handshakeDecoder
|
||||
|
||||
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
|
||||
var packet Handshake
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component delegation
|
||||
// XEP-0355
|
||||
|
||||
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
|
||||
// depending on the context.
|
||||
type Delegation struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
|
||||
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
|
||||
Delegated *Delegated // This is used in a message to confirm delegated namespace
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Delegation) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// We should be able to properly parse delegation confirmation messages
|
||||
func TestParsingDelegationMessage(t *testing.T) {
|
||||
packetStr := `<message to='service.localhost' from='localhost'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<delegated namespace='http://jabber.org/protocol/pubsub'/>
|
||||
</delegation>
|
||||
</message>`
|
||||
var msg Message
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &msg); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as MsgExtension
|
||||
var nsDelegated string
|
||||
for _, ext := range msg.Extensions {
|
||||
if delegation, ok := ext.(*Delegation); ok {
|
||||
nsDelegated = delegation.Delegated.Namespace
|
||||
}
|
||||
}
|
||||
if nsDelegated != "http://jabber.org/protocol/pubsub" {
|
||||
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we can parse a delegation IQ.
|
||||
// The most important thing is to be able to
|
||||
func TestParsingDelegationIQ(t *testing.T) {
|
||||
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
|
||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||
<publish node='http://jabber.org/protocol/mood'>
|
||||
<item id='current'>
|
||||
<mood xmlns='http://jabber.org/protocol/mood'>
|
||||
<excited/>
|
||||
</mood>
|
||||
</item>
|
||||
</publish>
|
||||
</pubsub>
|
||||
</iq>
|
||||
</forwarded>
|
||||
</delegation>
|
||||
</iq>`
|
||||
var iq IQ
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &iq); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as IQPayload
|
||||
var node string
|
||||
if iq.Payload != nil {
|
||||
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||
packet := delegation.Forwarded.Stanza
|
||||
forwardedIQ, ok := packet.(*IQ)
|
||||
if !ok {
|
||||
t.Errorf("Could not extract packet IQ")
|
||||
return
|
||||
}
|
||||
if forwardedIQ.Payload != nil {
|
||||
if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); ok {
|
||||
node = pubsub.Publish.Node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if node != "http://jabber.org/protocol/mood" {
|
||||
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Helper structures and functions to manage dates and timestamps as defined in
|
||||
// XEP-0082: XMPP Date and Time Profiles (https://xmpp.org/extensions/xep-0082.html)
|
||||
|
||||
const dateLayoutXEP0082 = "2006-01-02"
|
||||
const timeLayoutXEP0082 = "15:04:05+00:00"
|
||||
|
||||
var InvalidDateInput = errors.New("could not parse date. Input might not be in a supported format")
|
||||
var InvalidDateOutput = errors.New("could not format date as desired")
|
||||
|
||||
type JabberDate struct {
|
||||
value time.Time
|
||||
}
|
||||
|
||||
func (d JabberDate) DateToString() string {
|
||||
return d.value.Format(dateLayoutXEP0082)
|
||||
}
|
||||
|
||||
func (d JabberDate) DateTimeToString(nanos bool) string {
|
||||
if nanos {
|
||||
return d.value.Format(time.RFC3339Nano)
|
||||
}
|
||||
return d.value.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (d JabberDate) TimeToString(nanos bool) (string, error) {
|
||||
if nanos {
|
||||
spl := strings.Split(d.value.Format(time.RFC3339Nano), "T")
|
||||
if len(spl) != 2 {
|
||||
return "", InvalidDateOutput
|
||||
}
|
||||
return spl[1], nil
|
||||
}
|
||||
spl := strings.Split(d.value.Format(time.RFC3339), "T")
|
||||
if len(spl) != 2 {
|
||||
return "", InvalidDateOutput
|
||||
}
|
||||
return spl[1], nil
|
||||
}
|
||||
|
||||
func NewJabberDateFromString(strDate string) (JabberDate, error) {
|
||||
t, err := time.Parse(time.RFC3339, strDate)
|
||||
if err == nil {
|
||||
return JabberDate{value: t}, nil
|
||||
}
|
||||
|
||||
t, err = time.Parse(time.RFC3339Nano, strDate)
|
||||
if err == nil {
|
||||
return JabberDate{value: t}, nil
|
||||
}
|
||||
|
||||
t, err = time.Parse(dateLayoutXEP0082, strDate)
|
||||
if err == nil {
|
||||
return JabberDate{value: t}, nil
|
||||
}
|
||||
|
||||
t, err = time.Parse(timeLayoutXEP0082, strDate)
|
||||
if err == nil {
|
||||
return JabberDate{value: t}, nil
|
||||
}
|
||||
|
||||
return JabberDate{}, InvalidDateInput
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDateToString(t *testing.T) {
|
||||
t1 := JabberDate{value: time.Now()}
|
||||
t2 := JabberDate{value: time.Now().Add(24 * time.Hour)}
|
||||
|
||||
t1Str := t1.DateToString()
|
||||
t2Str := t2.DateToString()
|
||||
|
||||
if t1Str == t2Str {
|
||||
t.Fatalf("time representations should not be identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateToStringOracle(t *testing.T) {
|
||||
expected := "2009-11-10"
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||
|
||||
t1Str := t1.DateToString()
|
||||
if t1Str != expected {
|
||||
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeToString(t *testing.T) {
|
||||
t1 := JabberDate{value: time.Now()}
|
||||
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
|
||||
|
||||
t1Str := t1.DateTimeToString(false)
|
||||
t2Str := t2.DateTimeToString(false)
|
||||
|
||||
if t1Str == t2Str {
|
||||
t.Fatalf("time representations should not be identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeToStringOracle(t *testing.T) {
|
||||
expected := "2009-11-10T23:03:22+08:00"
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||
|
||||
t1Str := t1.DateTimeToString(false)
|
||||
if t1Str != expected {
|
||||
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeToStringNanos(t *testing.T) {
|
||||
t1 := JabberDate{value: time.Now()}
|
||||
time.After(10 * time.Millisecond)
|
||||
t2 := JabberDate{value: time.Now()}
|
||||
|
||||
t1Str := t1.DateTimeToString(true)
|
||||
t2Str := t2.DateTimeToString(true)
|
||||
|
||||
if t1Str == t2Str {
|
||||
t.Fatalf("time representations should not be identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeToStringNanosOracle(t *testing.T) {
|
||||
expected := "2009-11-10T23:03:22.000000089+08:00"
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||
|
||||
t1Str := t1.DateTimeToString(true)
|
||||
if t1Str != expected {
|
||||
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeToString(t *testing.T) {
|
||||
t1 := JabberDate{value: time.Now()}
|
||||
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
|
||||
|
||||
t1Str, err := t1.TimeToString(false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t2Str, err := t2.TimeToString(false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if t1Str == t2Str {
|
||||
t.Fatalf("time representations should not be identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeToStringOracle(t *testing.T) {
|
||||
expected := "23:03:22+08:00"
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||
|
||||
t1Str, err := t1.TimeToString(false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if t1Str != expected {
|
||||
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeToStringNanos(t *testing.T) {
|
||||
t1 := JabberDate{value: time.Now()}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
t2 := JabberDate{value: time.Now()}
|
||||
|
||||
t1Str, err := t1.TimeToString(true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t2Str, err := t2.TimeToString(true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if t1Str == t2Str {
|
||||
t.Fatalf("time representations should not be identical")
|
||||
}
|
||||
}
|
||||
func TestTimeToStringNanosOracle(t *testing.T) {
|
||||
expected := "23:03:22.000000089+08:00"
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
|
||||
|
||||
t1Str, err := t1.TimeToString(true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if t1Str != expected {
|
||||
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJabberDateParsing(t *testing.T) {
|
||||
date := "2009-11-10"
|
||||
_, err := NewJabberDateFromString(date)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dateTime := "2009-11-10T23:03:22+08:00"
|
||||
_, err = NewJabberDateFromString(dateTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dateTimeNanos := "2009-11-10T23:03:22.000000089+08:00"
|
||||
_, err = NewJabberDateFromString(dateTimeNanos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// TODO : fix these. Parsing a time with an offset doesn't work
|
||||
//time := "23:03:22+08:00"
|
||||
//_, err = NewJabberDateFromString(time)
|
||||
//if err != nil {
|
||||
// t.Fatalf(err.Error())
|
||||
//}
|
||||
|
||||
//timeNanos := "23:03:22.000000089+08:00"
|
||||
//_, err = NewJabberDateFromString(timeNanos)
|
||||
//if err != nil {
|
||||
// t.Fatalf(err.Error())
|
||||
//}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
XMPP stanza package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
|
||||
*/
|
||||
package stanza
|
||||
@@ -0,0 +1,136 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// XMPP Errors
|
||||
|
||||
// Err is an XMPP stanza payload that is used to report error on message,
|
||||
// presence or iq stanza.
|
||||
// It is intended to be added in the payload of the erroneous stanza.
|
||||
type Err struct {
|
||||
XMLName xml.Name `xml:"error"`
|
||||
Code int `xml:"code,attr,omitempty"`
|
||||
Type ErrorType `xml:"type,attr"` // required
|
||||
Reason string
|
||||
Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for XMPP errors
|
||||
func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
x.XMLName = start.Name
|
||||
|
||||
// Extract attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "type" {
|
||||
x.Type = ErrorType(attr.Value)
|
||||
}
|
||||
if attr.Name.Local == "code" {
|
||||
if code, err := strconv.Atoi(attr.Value); err == nil {
|
||||
x.Code = code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check subelements to extract error text and reason (from local namespace).
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
elt := new(Node)
|
||||
|
||||
err = d.DecodeElement(elt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||
// TODO : change the pubsub handling ? It kind of dilutes the information
|
||||
// Handles : 6.1.3.11 Node Has Moved for XEP-0060 (PubSubGeneric)
|
||||
goneName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "gone"}
|
||||
if elt.XMLName == textName || // Regular error text
|
||||
elt.XMLName == goneName { // Gone text for pubsub
|
||||
x.Text = elt.Content
|
||||
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" ||
|
||||
elt.XMLName.Space == "http://jabber.org/protocol/pubsub#errors" {
|
||||
if strings.TrimSpace(x.Reason) != "" {
|
||||
x.Reason = strings.Join([]string{elt.XMLName.Local}, ":")
|
||||
} else {
|
||||
x.Reason = elt.XMLName.Local
|
||||
}
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
if x.Code == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode start element and attributes
|
||||
start.Name = xml.Name{Local: "error"}
|
||||
|
||||
code := xml.Attr{
|
||||
Name: xml.Name{Local: "code"},
|
||||
Value: strconv.Itoa(x.Code),
|
||||
}
|
||||
start.Attr = append(start.Attr, code)
|
||||
|
||||
if len(x.Type) > 0 {
|
||||
typ := xml.Attr{
|
||||
Name: xml.Name{Local: "type"},
|
||||
Value: string(x.Type),
|
||||
}
|
||||
start.Attr = append(start.Attr, typ)
|
||||
}
|
||||
err = e.EncodeToken(start)
|
||||
|
||||
// SubTags
|
||||
// Reason
|
||||
if x.Reason != "" {
|
||||
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
|
||||
err = e.EncodeToken(xml.StartElement{Name: reason})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.EncodeToken(xml.EndElement{Name: reason})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Text
|
||||
if x.Text != "" {
|
||||
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||
err = e.EncodeToken(xml.StartElement{Name: text})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.EncodeToken(xml.CharData(x.Text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.EncodeToken(xml.EndElement{Name: text})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package stanza
|
||||
|
||||
// ErrorType is a Enum of error attribute type
|
||||
type ErrorType string
|
||||
|
||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
||||
const (
|
||||
ErrorTypeAuth ErrorType = "auth"
|
||||
ErrorTypeCancel ErrorType = "cancel"
|
||||
ErrorTypeContinue ErrorType = "continue"
|
||||
ErrorTypeModify ErrorType = "modify"
|
||||
ErrorTypeWait ErrorType = "wait"
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestErr_UnmarshalXML(t *testing.T) {
|
||||
packet := `
|
||||
<iq from='pubsub.example.com'
|
||||
id='kj4vz31m'
|
||||
to='romeo@example.net/foo'
|
||||
type='error'>
|
||||
<error type='wait'>
|
||||
<resource-constraint
|
||||
xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>System overloaded, please retry</text>
|
||||
</error>
|
||||
</iq>`
|
||||
|
||||
parsedIQ := IQ{}
|
||||
data := []byte(packet)
|
||||
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
xmppError := parsedIQ.Error
|
||||
if xmppError.Text != "System overloaded, please retry" {
|
||||
t.Errorf("Could not extract error text: '%s'", xmppError.Text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package stanza
|
||||
|
||||
// FIFO queue for string contents
|
||||
// Implementations have no guarantee regarding thread safety !
|
||||
type FifoQueue interface {
|
||||
// Pop returns the first inserted element still in queue and deletes it from queue. If queue is empty, returns nil
|
||||
// No guarantee regarding thread safety !
|
||||
Pop() Queueable
|
||||
|
||||
// PopN returns the N first inserted elements still in queue and deletes them from queue. If queue is empty or i<=0, returns nil
|
||||
// If number to pop is greater than queue length, returns all queue elements
|
||||
// No guarantee regarding thread safety !
|
||||
PopN(i int) []Queueable
|
||||
|
||||
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty, returns nil
|
||||
// No guarantee regarding thread safety !
|
||||
Peek() Queueable
|
||||
|
||||
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty or i<=0, returns nil.
|
||||
// If number to peek is greater than queue length, returns all queue elements
|
||||
// No guarantee regarding thread safety !
|
||||
PeekN() []Queueable
|
||||
// Push adds an element to the queue
|
||||
// No guarantee regarding thread safety !
|
||||
Push(s Queueable) error
|
||||
|
||||
// Empty returns true if queue is empty
|
||||
// No guarantee regarding thread safety !
|
||||
Empty() bool
|
||||
}
|
||||
|
||||
type Queueable interface {
|
||||
QueueableName() string
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
type FormType string
|
||||
|
||||
const (
|
||||
FormTypeCancel = "cancel"
|
||||
FormTypeForm = "form"
|
||||
FormTypeResult = "result"
|
||||
FormTypeSubmit = "submit"
|
||||
)
|
||||
|
||||
// See XEP-0004 and XEP-0068
|
||||
// Pointer semantics
|
||||
type Form struct {
|
||||
XMLName xml.Name `xml:"jabber:x:data x"`
|
||||
Instructions []string `xml:"instructions"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
Fields []*Field `xml:"field,omitempty"`
|
||||
Reported *FormItem `xml:"reported"`
|
||||
Items []FormItem `xml:"item,omitempty"`
|
||||
Type string `xml:"type,attr"`
|
||||
}
|
||||
|
||||
type FormItem struct {
|
||||
XMLName xml.Name
|
||||
Fields []Field `xml:"field,omitempty"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
XMLName xml.Name `xml:"field"`
|
||||
Description string `xml:"desc,omitempty"`
|
||||
Required *string `xml:"required"`
|
||||
ValuesList []string `xml:"value"`
|
||||
Options []Option `xml:"option,omitempty"`
|
||||
Var string `xml:"var,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Label string `xml:"label,attr,omitempty"`
|
||||
}
|
||||
|
||||
func NewForm(fields []*Field, formType string) *Form {
|
||||
return &Form{
|
||||
Type: formType,
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
type FieldType string
|
||||
|
||||
const (
|
||||
FieldTypeBool = "boolean"
|
||||
FieldTypeFixed = "fixed"
|
||||
FieldTypeHidden = "hidden"
|
||||
FieldTypeJidMulti = "jid-multi"
|
||||
FieldTypeJidSingle = "jid-single"
|
||||
FieldTypeListMulti = "list-multi"
|
||||
FieldTypeListSingle = "list-single"
|
||||
FieldTypeTextMulti = "text-multi"
|
||||
FieldTypeTextPrivate = "text-private"
|
||||
FieldTypeTextSingle = "text-Single"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
XMLName xml.Name `xml:"option"`
|
||||
Label string `xml:"label,attr,omitempty"`
|
||||
ValuesList []string `xml:"value"`
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
formSubmit = "<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\">" +
|
||||
"<configure node=\"princely_musings\">" +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\">" +
|
||||
"<field var=\"FORM_TYPE\" type=\"hidden\">" +
|
||||
"<value>http://jabber.org/protocol/pubsub#node_config</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#title\">" +
|
||||
"<value>Princely Musings (Atom)</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#deliver_notifications\">" +
|
||||
"<value>1</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#access_model\">" +
|
||||
"<value>roster</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#roster_groups_allowed\">" +
|
||||
"<value>friends</value>" +
|
||||
"<value>servants</value>" +
|
||||
"<value>courtiers</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#type\">" +
|
||||
"<value>http://www.w3.org/2005/Atom</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#notification_type\" type=\"list-single\"" +
|
||||
"label=\"Specify the delivery style for event notifications\">" +
|
||||
"<value>headline</value>" +
|
||||
"<option>" +
|
||||
"<value>normal</value>" +
|
||||
"</option>" +
|
||||
"<option>" +
|
||||
"<value>headline</value>" +
|
||||
"</option>" +
|
||||
"</field>" +
|
||||
"</x>" +
|
||||
"</configure>" +
|
||||
"</pubsub>"
|
||||
|
||||
clientJid = "hamlet@denmark.lit/elsinore"
|
||||
serviceJid = "pubsub.shakespeare.lit"
|
||||
iqId = "config1"
|
||||
serviceNode = "princely_musings"
|
||||
)
|
||||
|
||||
func TestMarshalFormSubmit(t *testing.T) {
|
||||
formIQ, err := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create formIQ: %v", err)
|
||||
}
|
||||
formIQ.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &ConfigureOwner{
|
||||
Node: serviceNode,
|
||||
Form: &Form{
|
||||
Type: FormTypeSubmit,
|
||||
Fields: []*Field{
|
||||
{Var: "FORM_TYPE", Type: FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}},
|
||||
{Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}},
|
||||
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
|
||||
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
|
||||
{Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}},
|
||||
{
|
||||
Var: "pubsub#notification_type",
|
||||
Type: "list-single",
|
||||
Label: "Specify the delivery style for event notifications",
|
||||
ValuesList: []string{"headline"},
|
||||
Options: []Option{
|
||||
{ValuesList: []string{"normal"}},
|
||||
{ValuesList: []string{"headline"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
b, err := xml.Marshal(formIQ.Payload)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not marshal formIQ : %v", err)
|
||||
}
|
||||
|
||||
if strings.ReplaceAll(string(b), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
|
||||
t.Fatalf("Expected formIQ and marshalled one are different.\nExepected : %s\nMarshalled : %s", formSubmit, string(b))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUnmarshalFormSubmit(t *testing.T) {
|
||||
var f PubSubOwner
|
||||
mErr := xml.Unmarshal([]byte(formSubmit), &f)
|
||||
if mErr != nil {
|
||||
t.Fatalf("failed to unmarshal formSubmit ! %s", mErr)
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(&f)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal formSubmit")
|
||||
}
|
||||
|
||||
if strings.ReplaceAll(string(data), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
|
||||
t.Fatalf("failed unmarshal/marshal for formSubmit : %s\n%s", string(data), formSubmit)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type ControlSet struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
|
||||
Fields []ControlField `xml:",any"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ControlSet) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
func (c *ControlSet) GetSet() *ResultSet {
|
||||
return c.ResultSet
|
||||
}
|
||||
|
||||
type ControlGetForm struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
|
||||
}
|
||||
|
||||
type ControlField struct {
|
||||
XMLName xml.Name
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Value string `xml:"value,attr,omitempty"`
|
||||
}
|
||||
|
||||
type ControlSetResponse struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
|
||||
}
|
||||
|
||||
func (c *ControlSetResponse) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
func (c *ControlSetResponse) GetSet() *ResultSet {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:iot:control", Local: "set"}, ControlSet{})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestControlSet(t *testing.T) {
|
||||
packet := `
|
||||
<iq to='test@localhost/jukebox' from='admin@localhost/mbp' type='set' id='2'>
|
||||
<set xmlns='urn:xmpp:iot:control' xml:lang='en'>
|
||||
<string name='action' value='play'/>
|
||||
<string name='url' value='https://soundcloud.com/radiohead/spectre'/>
|
||||
</set>
|
||||
</iq>`
|
||||
|
||||
parsedIQ := IQ{}
|
||||
data := []byte(packet)
|
||||
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
|
||||
t.Errorf("Payload is not an iot control set: %v", cs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO support ability to put Raw payload inside IQ
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// IQ Packet
|
||||
|
||||
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
|
||||
type IQ struct { // Info/Query
|
||||
XMLName xml.Name `xml:"iq"`
|
||||
// MUST have a ID
|
||||
Attrs
|
||||
// We can only have one payload on IQ:
|
||||
// "An IQ stanza of type "get" or "set" MUST contain exactly one
|
||||
// child element, which specifies the semantics of the particular
|
||||
// request."
|
||||
Payload IQPayload `xml:",omitempty"`
|
||||
Error *Err `xml:"error,omitempty"`
|
||||
// Any is used to decode unknown payload as a generic structure
|
||||
Any *Node `xml:",any"`
|
||||
}
|
||||
|
||||
type IQPayload interface {
|
||||
Namespace() string
|
||||
GetSet() *ResultSet
|
||||
}
|
||||
|
||||
func NewIQ(a Attrs) (*IQ, error) {
|
||||
if a.Id == "" {
|
||||
if id, err := uuid.NewRandom(); err == nil {
|
||||
a.Id = id.String()
|
||||
}
|
||||
}
|
||||
|
||||
iq := IQ{
|
||||
XMLName: xml.Name{Local: "iq"},
|
||||
Attrs: a,
|
||||
}
|
||||
|
||||
if iq.Type.IsEmpty() {
|
||||
return nil, IqTypeUnset
|
||||
}
|
||||
return &iq, nil
|
||||
}
|
||||
|
||||
func (iq *IQ) MakeError(xerror Err) *IQ {
|
||||
from := iq.From
|
||||
to := iq.To
|
||||
|
||||
iq.Type = "error"
|
||||
iq.From = to
|
||||
iq.To = from
|
||||
iq.Error = &xerror
|
||||
|
||||
return iq
|
||||
}
|
||||
|
||||
func (*IQ) Name() string {
|
||||
return "iq"
|
||||
}
|
||||
|
||||
// NoOp to implement BiDirIteratorElt
|
||||
func (*IQ) NoOp() {
|
||||
|
||||
}
|
||||
|
||||
type iqDecoder struct{}
|
||||
|
||||
var iq iqDecoder
|
||||
|
||||
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (*IQ, error) {
|
||||
var packet IQ
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return &packet, err
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for IQs
|
||||
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
iq.XMLName = start.Name
|
||||
|
||||
// Extract IQ attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "id" {
|
||||
iq.Id = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "type" {
|
||||
iq.Type = StanzaType(attr.Value)
|
||||
}
|
||||
if attr.Name.Local == "to" {
|
||||
iq.To = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "from" {
|
||||
iq.From = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
case xml.StartElement:
|
||||
if tt.Name.Local == "error" {
|
||||
var xmppError Err
|
||||
err = d.DecodeElement(&xmppError, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Error = &xmppError
|
||||
continue
|
||||
}
|
||||
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
||||
// Decode payload extension
|
||||
err = d.DecodeElement(iqExt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Payload = iqExt
|
||||
continue
|
||||
}
|
||||
// TODO: If unknown decode as generic node
|
||||
node := new(Node)
|
||||
err = d.DecodeElement(node, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Any = node
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
IqTypeUnset = errors.New("iq type is not set but is mandatory")
|
||||
IqIDUnset = errors.New("iq stanza ID is not set but is mandatory")
|
||||
IqSGetNoPl = errors.New("iq is of type get or set but has no payload")
|
||||
IqResNoPl = errors.New("iq is of type result but has no payload")
|
||||
IqErrNoErrPl = errors.New("iq is of type error but has no error payload")
|
||||
)
|
||||
|
||||
// IsValid checks if the IQ is valid. If not, return an error with the reason as a message
|
||||
// Following RFC-3920 for IQs
|
||||
func (iq *IQ) IsValid() (bool, error) {
|
||||
// ID is required
|
||||
if len(strings.TrimSpace(iq.Id)) == 0 {
|
||||
return false, IqIDUnset
|
||||
}
|
||||
|
||||
// Type is required
|
||||
if iq.Type.IsEmpty() {
|
||||
return false, IqTypeUnset
|
||||
}
|
||||
|
||||
// Type get and set must contain one and only one child element that specifies the semantics
|
||||
if iq.Type == IQTypeGet || iq.Type == IQTypeSet {
|
||||
if iq.Payload == nil && iq.Any == nil {
|
||||
return false, IqSGetNoPl
|
||||
}
|
||||
}
|
||||
|
||||
// A result must include zero or one child element
|
||||
if iq.Type == IQTypeResult {
|
||||
if iq.Payload != nil && iq.Any != nil {
|
||||
return false, IqResNoPl
|
||||
}
|
||||
}
|
||||
|
||||
//Error type must contain an "error" child element
|
||||
if iq.Type == IQTypeError {
|
||||
if iq.Error == nil {
|
||||
return false, IqErrNoErrPl
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Disco Info
|
||||
|
||||
const (
|
||||
// NSDiscoInfo defines the namespace for disco IQ stanzas
|
||||
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
||||
)
|
||||
|
||||
// ----------
|
||||
// Namespaces
|
||||
|
||||
type DiscoInfo struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Identity []Identity `xml:"identity"`
|
||||
Features []Feature `xml:"feature"`
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
// Namespace lets DiscoInfo implement the IQPayload interface
|
||||
func (d *DiscoInfo) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) GetSet() *ResultSet {
|
||||
return d.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
// DiscoInfo builds a default DiscoInfo payload
|
||||
func (iq *IQ) DiscoInfo() *DiscoInfo {
|
||||
d := DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
}
|
||||
iq.Payload = &d
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) AddIdentity(name, category, typ string) {
|
||||
identity := Identity{
|
||||
XMLName: xml.Name{Local: "identity"},
|
||||
Name: name,
|
||||
Category: category,
|
||||
Type: typ,
|
||||
}
|
||||
d.Identity = append(d.Identity, identity)
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) AddFeatures(namespace ...string) {
|
||||
for _, ns := range namespace {
|
||||
d.Features = append(d.Features, Feature{Var: ns})
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetNode(node string) *DiscoInfo {
|
||||
d.Node = node
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
|
||||
d.Identity = ident
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
|
||||
d.Features = []Feature{}
|
||||
for _, ns := range namespace {
|
||||
d.Features = append(d.Features, Feature{Var: ns})
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// -----------
|
||||
// SubElements
|
||||
|
||||
type Identity struct {
|
||||
XMLName xml.Name `xml:"identity,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Category string `xml:"category,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
XMLName xml.Name `xml:"feature"`
|
||||
Var string `xml:"var,attr"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Disco Info
|
||||
|
||||
const (
|
||||
NSDiscoItems = "http://jabber.org/protocol/disco#items"
|
||||
)
|
||||
|
||||
type DiscoItems struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Items []DiscoItem `xml:"item"`
|
||||
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (d *DiscoItems) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
func (d *DiscoItems) GetSet() *ResultSet {
|
||||
return d.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
// DiscoItems builds a default DiscoItems payload
|
||||
func (iq *IQ) DiscoItems() *DiscoItems {
|
||||
d := DiscoItems{
|
||||
XMLName: xml.Name{Space: NSDiscoItems, Local: "query"},
|
||||
}
|
||||
iq.Payload = &d
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *DiscoItems) SetNode(node string) *DiscoItems {
|
||||
d.Node = node
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DiscoItems) AddItem(jid, node, name string) *DiscoItems {
|
||||
item := DiscoItem{
|
||||
JID: jid,
|
||||
Node: node,
|
||||
Name: name,
|
||||
}
|
||||
d.Items = append(d.Items, item)
|
||||
return d
|
||||
}
|
||||
|
||||
type DiscoItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
JID string `xml:"jid,attr,omitempty"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoInfo, Local: "query"}, DiscoInfo{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoItems, Local: "query"}, DiscoItems{})
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// Test DiscoInfo Builder with several features
|
||||
func TestDiscoInfo_Builder(t *testing.T) {
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
disco := iq.DiscoInfo()
|
||||
disco.AddIdentity("Test Component", "gateway", "service")
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
|
||||
parsedIQ, err := checkMarshalling(t, iq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check result
|
||||
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
|
||||
if !ok {
|
||||
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||
}
|
||||
|
||||
// Check features
|
||||
features := []string{stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"}
|
||||
if len(pp.Features) != len(features) {
|
||||
t.Errorf("Features length mismatch: %#v", pp.Features)
|
||||
} else {
|
||||
for i, f := range pp.Features {
|
||||
if f.Var != features[i] {
|
||||
t.Errorf("Missing feature: %s", features[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check identity
|
||||
if len(pp.Identity) != 1 {
|
||||
t.Errorf("Identity length mismatch: %#v", pp.Identity)
|
||||
} else {
|
||||
if pp.Identity[0].Name != "Test Component" {
|
||||
t.Errorf("Incorrect identity name: %#v", pp.Identity[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implements XEP-0030 example 17
|
||||
// https://xmpp.org/extensions/xep-0030.html#example-17
|
||||
func TestDiscoItems_Builder(t *testing.T) {
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit",
|
||||
To: "romeo@montague.net/orchard", Id: "items-2"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iq.DiscoItems().
|
||||
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
|
||||
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
|
||||
AddItem("catalog.shakespeare.lit", "music", "Music from the time of Shakespeare")
|
||||
|
||||
parsedIQ, err := checkMarshalling(t, iq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check result
|
||||
pp, ok := parsedIQ.Payload.(*stanza.DiscoItems)
|
||||
if !ok {
|
||||
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||
}
|
||||
|
||||
// Check items
|
||||
items := []stanza.DiscoItem{{xml.Name{}, "catalog.shakespeare.lit", "books", "Books by and about Shakespeare"},
|
||||
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
|
||||
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
|
||||
if len(pp.Items) != len(items) {
|
||||
t.Errorf("List length mismatch: %#v", pp.Items)
|
||||
} else {
|
||||
for i, item := range pp.Items {
|
||||
if item.JID != items[i].JID {
|
||||
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||
}
|
||||
if item.Node != items[i].Node {
|
||||
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||
}
|
||||
if item.Name != items[i].Name {
|
||||
t.Errorf("Name Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Roster
|
||||
|
||||
const (
|
||||
// NSRoster is the Roster IQ namespace
|
||||
NSRoster = "jabber:iq:roster"
|
||||
// SubscriptionNone indicates the user does not have a subscription to
|
||||
// the contact's presence, and the contact does not have a subscription
|
||||
// to the user's presence; this is the default value, so if the subscription
|
||||
// attribute is not included then the state is to be understood as "none"
|
||||
SubscriptionNone = "none"
|
||||
|
||||
// SubscriptionTo indicates the user has a subscription to the contact's
|
||||
// presence, but the contact does not have a subscription to the user's presence.
|
||||
SubscriptionTo = "to"
|
||||
|
||||
// SubscriptionFrom indicates the contact has a subscription to the user's
|
||||
// presence, but the user does not have a subscription to the contact's presence
|
||||
SubscriptionFrom = "from"
|
||||
|
||||
// SubscriptionBoth indicates the user and the contact have subscriptions to each
|
||||
// other's presence (also called a "mutual subscription")
|
||||
SubscriptionBoth = "both"
|
||||
)
|
||||
|
||||
// ----------
|
||||
// Namespaces
|
||||
|
||||
// Roster struct represents Roster IQs
|
||||
type Roster struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:roster query"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
// Namespace defines the namespace for the RosterIQ
|
||||
func (r *Roster) Namespace() string {
|
||||
return r.XMLName.Space
|
||||
}
|
||||
func (r *Roster) GetSet() *ResultSet {
|
||||
return r.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
// RosterIQ builds a default Roster payload
|
||||
func (iq *IQ) RosterIQ() *Roster {
|
||||
r := Roster{
|
||||
XMLName: xml.Name{
|
||||
Space: NSRoster,
|
||||
Local: "query",
|
||||
},
|
||||
}
|
||||
iq.Payload = &r
|
||||
return &r
|
||||
}
|
||||
|
||||
// -----------
|
||||
// SubElements
|
||||
|
||||
// RosterItems represents the list of items in a roster IQ
|
||||
type RosterItems struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:roster query"`
|
||||
Items []RosterItem `xml:"item"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
// Namespace lets RosterItems implement the IQPayload interface
|
||||
func (r *RosterItems) Namespace() string {
|
||||
return r.XMLName.Space
|
||||
}
|
||||
|
||||
func (r *RosterItems) GetSet() *ResultSet {
|
||||
return r.ResultSet
|
||||
}
|
||||
|
||||
// RosterItem represents an item in the roster iq
|
||||
type RosterItem struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:roster item"`
|
||||
Jid string `xml:"jid,attr"`
|
||||
Ask string `xml:"ask,attr,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Subscription string `xml:"subscription,attr,omitempty"`
|
||||
Groups []string `xml:"group"`
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
// RosterItems builds a default RosterItems payload
|
||||
func (iq *IQ) RosterItems() *RosterItems {
|
||||
ri := RosterItems{
|
||||
XMLName: xml.Name{Space: "jabber:iq:roster", Local: "query"},
|
||||
}
|
||||
iq.Payload = &ri
|
||||
return &ri
|
||||
}
|
||||
|
||||
// AddItem builds an item and ads it to the roster IQ
|
||||
func (r *RosterItems) AddItem(jid, subscription, ask, name string, groups []string) *RosterItems {
|
||||
item := RosterItem{
|
||||
Jid: jid,
|
||||
Name: name,
|
||||
Groups: groups,
|
||||
Subscription: subscription,
|
||||
Ask: ask,
|
||||
}
|
||||
r.Items = append(r.Items, item)
|
||||
return r
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, Roster{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, RosterItems{})
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRosterBuilder(t *testing.T) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
var noGroup []string
|
||||
|
||||
iq.RosterItems().AddItem("xl8ceawrfu8zdneomw1h6h28d@crypho.com",
|
||||
SubscriptionBoth,
|
||||
"",
|
||||
"xl8ceaw",
|
||||
[]string{"0flucpm8i2jtrjhxw01uf1nd2",
|
||||
"bm2bajg9ex4e1swiuju9i9nu5",
|
||||
"rvjpanomi4ejpx42fpmffoac0"}).
|
||||
AddItem("9aynsym60zbu78jbdvpho7s68@crypho.com",
|
||||
SubscriptionBoth,
|
||||
"",
|
||||
"9aynsym60",
|
||||
[]string{"mzaoy73i6ra5k502182zi1t97"}).
|
||||
AddItem("admin@crypho.com",
|
||||
SubscriptionBoth,
|
||||
"",
|
||||
"admin",
|
||||
noGroup)
|
||||
|
||||
parsedIQ, err := checkMarshalling(t, iq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check result
|
||||
pp, ok := parsedIQ.Payload.(*RosterItems)
|
||||
if !ok {
|
||||
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||
}
|
||||
|
||||
// Check items
|
||||
items := []RosterItem{
|
||||
{
|
||||
XMLName: xml.Name{},
|
||||
Name: "xl8ceaw",
|
||||
Ask: "",
|
||||
Jid: "xl8ceawrfu8zdneomw1h6h28d@crypho.com",
|
||||
Subscription: SubscriptionBoth,
|
||||
Groups: []string{"0flucpm8i2jtrjhxw01uf1nd2",
|
||||
"bm2bajg9ex4e1swiuju9i9nu5",
|
||||
"rvjpanomi4ejpx42fpmffoac0"},
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{},
|
||||
Name: "9aynsym60",
|
||||
Ask: "",
|
||||
Jid: "9aynsym60zbu78jbdvpho7s68@crypho.com",
|
||||
Subscription: SubscriptionBoth,
|
||||
Groups: []string{"mzaoy73i6ra5k502182zi1t97"},
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{},
|
||||
Name: "admin",
|
||||
Ask: "",
|
||||
Jid: "admin@crypho.com",
|
||||
Subscription: SubscriptionBoth,
|
||||
Groups: noGroup,
|
||||
},
|
||||
}
|
||||
if len(pp.Items) != len(items) {
|
||||
t.Errorf("List length mismatch: %#v", pp.Items)
|
||||
} else {
|
||||
for i, item := range pp.Items {
|
||||
if item.Jid != items[i].Jid {
|
||||
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||
}
|
||||
if !reflect.DeepEqual(item.Groups, items[i].Groups) {
|
||||
t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||
}
|
||||
if item.Name != items[i].Name {
|
||||
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||
}
|
||||
if item.Ask != items[i].Ask {
|
||||
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||
}
|
||||
if item.Subscription != items[i].Subscription {
|
||||
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkMarshalling(t *testing.T, iq *IQ) (*IQ, error) {
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshall
|
||||
var parsedIQ IQ
|
||||
err = xml.Unmarshal(data, &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
|
||||
}
|
||||
return &parsedIQ, err
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestUnmarshalIqs(t *testing.T) {
|
||||
//var cs1 = new(iot.ControlSet)
|
||||
var tests = []struct {
|
||||
iqString string
|
||||
parsedIQ stanza.IQ
|
||||
}{
|
||||
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
|
||||
stanza.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: stanza.Attrs{Type: stanza.IQTypeSet, To: "test@localhost", Id: "1"}}},
|
||||
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", test.iqString)
|
||||
}
|
||||
|
||||
if !xmlEqual(parsedIQ, test.parsedIQ) {
|
||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ, test.parsedIQ))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateIqId(t *testing.T) {
|
||||
t.Parallel()
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Id: "1", Type: "dummy type"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
if iq.Id != "1" {
|
||||
t.Errorf("NewIQ replaced id with %s", iq.Id)
|
||||
}
|
||||
|
||||
iq, err = stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
if iq.Id == "" {
|
||||
t.Error("NewIQ did not generate an Id")
|
||||
}
|
||||
|
||||
otherIq, err := stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
if iq.Id == otherIq.Id {
|
||||
t.Errorf("NewIQ generated two identical ids: %s", iq.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateIq(t *testing.T) {
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
payload := stanza.DiscoInfo{
|
||||
Identity: []stanza.Identity{
|
||||
{Name: "Test Gateway",
|
||||
Category: "gateway",
|
||||
Type: "mqtt",
|
||||
}},
|
||||
Features: []stanza.Feature{
|
||||
{Var: stanza.NSDiscoInfo},
|
||||
{Var: stanza.NSDiscoItems},
|
||||
},
|
||||
}
|
||||
iq.Payload = &payload
|
||||
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
if strings.Contains(string(data), "<error ") {
|
||||
t.Error("empty error should not be serialized")
|
||||
}
|
||||
|
||||
parsedIQ := stanza.IQ{}
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
|
||||
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorTag(t *testing.T) {
|
||||
xError := stanza.Err{
|
||||
XMLName: xml.Name{Local: "error"},
|
||||
Code: 503,
|
||||
Type: "cancel",
|
||||
Reason: "service-unavailable",
|
||||
Text: "User session not found",
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(xError)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
}
|
||||
|
||||
parsedError := stanza.Err{}
|
||||
if err = xml.Unmarshal(data, &parsedError); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if !xmlEqual(parsedError, xError) {
|
||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedError, xError))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoItems(t *testing.T) {
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
payload := stanza.DiscoItems{
|
||||
Node: "music",
|
||||
}
|
||||
iq.Payload = &payload
|
||||
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
parsedIQ := stanza.IQ{}
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if !xmlEqual(parsedIQ.Payload, iq.Payload) {
|
||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalPayload(t *testing.T) {
|
||||
query := "<iq to='service.localhost' type='get' id='1'><query xmlns='jabber:iq:version'/></iq>"
|
||||
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(query), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", query)
|
||||
}
|
||||
|
||||
if parsedIQ.Payload == nil {
|
||||
t.Error("Missing payload")
|
||||
}
|
||||
|
||||
namespace := parsedIQ.Payload.Namespace()
|
||||
if namespace != "jabber:iq:version" {
|
||||
t.Errorf("incorrect namespace: %s", namespace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayloadWithError(t *testing.T) {
|
||||
iq := `<iq xml:lang='en' to='test1@localhost/resource' from='test@localhost' type='error' id='aac1a'>
|
||||
<query xmlns='jabber:iq:version'/>
|
||||
<error code='407' type='auth'>
|
||||
<subscription-required xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
<text xml:lang='en' xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Not subscribed</text>
|
||||
</error>
|
||||
</iq>`
|
||||
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(iq), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal error: %s", iq)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedIQ.Error.Reason != "subscription-required" {
|
||||
t.Errorf("incorrect error value: '%s'", parsedIQ.Error.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownPayload(t *testing.T) {
|
||||
iq := `<iq type="get" to="service.localhost" id="1" >
|
||||
<query xmlns="unknown:ns"/>
|
||||
</iq>`
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(iq), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal error: %#v (%s)", err, iq)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedIQ.Any.XMLName.Space != "unknown:ns" {
|
||||
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
type testCase struct {
|
||||
iq string
|
||||
shouldErr bool
|
||||
}
|
||||
testIQs := make(map[string]testCase)
|
||||
testIQs["Valid IQ"] = testCase{
|
||||
`<iq type="get" to="service.localhost" id="1" >
|
||||
<query xmlns="unknown:ns"/>
|
||||
</iq>`,
|
||||
false,
|
||||
}
|
||||
testIQs["Invalid IQ"] = testCase{
|
||||
`<iq type="get" to="service.localhost">
|
||||
<query xmlns="unknown:ns"/>
|
||||
</iq>`,
|
||||
true,
|
||||
}
|
||||
|
||||
for name, tcase := range testIQs {
|
||||
t.Run(name, func(st *testing.T) {
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(tcase.iq), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq)
|
||||
return
|
||||
}
|
||||
isValid, err := parsedIQ.IsValid()
|
||||
if !isValid && !tcase.shouldErr {
|
||||
t.Errorf("failed validation for iq because: %s\nin test case : %s", err, tcase.iq)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
// Software Version (XEP-0092)
|
||||
|
||||
// Version
|
||||
type Version struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:version query"`
|
||||
Name string `xml:"name,omitempty"`
|
||||
Version string `xml:"version,omitempty"`
|
||||
OS string `xml:"os,omitempty"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (v *Version) Namespace() string {
|
||||
return v.XMLName.Space
|
||||
}
|
||||
|
||||
func (v *Version) GetSet() *ResultSet {
|
||||
return v.ResultSet
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
// Version builds a default software version payload
|
||||
func (iq *IQ) Version() *Version {
|
||||
d := Version{
|
||||
XMLName: xml.Name{Space: "jabber:iq:version", Local: "query"},
|
||||
}
|
||||
iq.Payload = &d
|
||||
return &d
|
||||
}
|
||||
|
||||
// Set all software version info
|
||||
func (v *Version) SetInfo(name, version, os string) *Version {
|
||||
v.Name = name
|
||||
v.Version = version
|
||||
v.OS = os
|
||||
return v
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "jabber:iq:version", Local: "query"}, Version{})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// Build a Software Version reply
|
||||
// https://xmpp.org/extensions/xep-0092.html#example-2
|
||||
func TestVersion_Builder(t *testing.T) {
|
||||
name := "Exodus"
|
||||
version := "0.7.0.4"
|
||||
os := "Windows-XP 5.01.2600"
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
|
||||
To: "juliet@capulet.com/balcony", Id: "version_1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iq.Version().SetInfo(name, version, os)
|
||||
|
||||
parsedIQ, err := checkMarshalling(t, iq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check result
|
||||
pp, ok := parsedIQ.Payload.(*stanza.Version)
|
||||
if !ok {
|
||||
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||
}
|
||||
|
||||
// Check version info
|
||||
if pp.Name != name {
|
||||
t.Errorf("Name Mismatch (expected: %s): %s", name, pp.Name)
|
||||
}
|
||||
if pp.Version != version {
|
||||
t.Errorf("Version Mismatch (expected: %s): %s", version, pp.Version)
|
||||
}
|
||||
if pp.OS != os {
|
||||
t.Errorf("OS Mismatch (expected: %s): %s", os, pp.OS)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Jid struct {
|
||||
Node string
|
||||
Domain string
|
||||
Resource string
|
||||
}
|
||||
|
||||
func NewJid(sjid string) (*Jid, error) {
|
||||
jid := new(Jid)
|
||||
|
||||
if sjid == "" {
|
||||
return jid, fmt.Errorf("jid cannot be empty")
|
||||
}
|
||||
|
||||
s1 := strings.SplitN(sjid, "@", 2)
|
||||
if len(s1) == 1 { // This is a server or component Jid
|
||||
jid.Domain = s1[0]
|
||||
} else { // Jid has a local username part
|
||||
if s1[0] == "" {
|
||||
return jid, fmt.Errorf("invalid jid '%s", sjid)
|
||||
}
|
||||
jid.Node = s1[0]
|
||||
if s1[1] == "" {
|
||||
return jid, fmt.Errorf("domain cannot be empty")
|
||||
}
|
||||
jid.Domain = s1[1]
|
||||
}
|
||||
|
||||
// Extract resource from domain field
|
||||
s2 := strings.SplitN(jid.Domain, "/", 2)
|
||||
if len(s2) == 2 { // If len = 1, domain is already correct, and resource is already empty
|
||||
jid.Domain = s2[0]
|
||||
jid.Resource = s2[1]
|
||||
}
|
||||
|
||||
if !isUsernameValid(jid.Node) {
|
||||
return jid, fmt.Errorf("invalid Node in Jid '%s'", sjid)
|
||||
}
|
||||
if !isDomainValid(jid.Domain) {
|
||||
return jid, fmt.Errorf("invalid domain in Jid '%s'", sjid)
|
||||
}
|
||||
|
||||
return jid, nil
|
||||
}
|
||||
|
||||
func (j *Jid) Full() string {
|
||||
if j.Resource == "" {
|
||||
return j.Bare()
|
||||
} else if j.Node == "" {
|
||||
return j.Node + "/" + j.Resource
|
||||
} else {
|
||||
return j.Node + "@" + j.Domain + "/" + j.Resource
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Jid) Bare() string {
|
||||
if j.Node == "" {
|
||||
return j.Domain
|
||||
} else {
|
||||
return j.Node + "@" + j.Domain
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers, for parsing / validation
|
||||
|
||||
func isUsernameValid(username string) bool {
|
||||
invalidRunes := []rune{'@', '/', '\'', '"', ':', '<', '>'}
|
||||
return strings.IndexFunc(username, isInvalid(invalidRunes)) < 0
|
||||
}
|
||||
|
||||
func isDomainValid(domain string) bool {
|
||||
if len(domain) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
invalidRunes := []rune{'@', '/'}
|
||||
return strings.IndexFunc(domain, isInvalid(invalidRunes)) < 0
|
||||
}
|
||||
|
||||
func isInvalid(invalidRunes []rune) func(c rune) bool {
|
||||
isInvalid := func(c rune) bool {
|
||||
if unicode.IsSpace(c) {
|
||||
return true
|
||||
}
|
||||
for _, r := range invalidRunes {
|
||||
if c == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return isInvalid
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidJids(t *testing.T) {
|
||||
tests := []struct {
|
||||
jidstr string
|
||||
expected Jid
|
||||
}{
|
||||
{jidstr: "test@domain.com", expected: Jid{"test", "domain.com", ""}},
|
||||
{jidstr: "test@domain.com/resource", expected: Jid{"test", "domain.com", "resource"}},
|
||||
// resource can contain '/' or '@'
|
||||
{jidstr: "test@domain.com/a/b", expected: Jid{"test", "domain.com", "a/b"}},
|
||||
{jidstr: "test@domain.com/a@b", expected: Jid{"test", "domain.com", "a@b"}},
|
||||
{jidstr: "domain.com", expected: Jid{"", "domain.com", ""}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
jid, err := NewJid(tt.jidstr)
|
||||
if err != nil {
|
||||
t.Errorf("could not parse correct jid: %s", tt.jidstr)
|
||||
continue
|
||||
}
|
||||
|
||||
if jid == nil {
|
||||
t.Error("jid should not be nil")
|
||||
}
|
||||
|
||||
if jid.Node != tt.expected.Node {
|
||||
t.Errorf("incorrect jid Node (%s): %s", tt.expected.Node, jid.Node)
|
||||
}
|
||||
|
||||
if jid.Node != tt.expected.Node {
|
||||
t.Errorf("incorrect jid domain (%s): %s", tt.expected.Domain, jid.Domain)
|
||||
}
|
||||
|
||||
if jid.Resource != tt.expected.Resource {
|
||||
t.Errorf("incorrect jid resource (%s): %s", tt.expected.Resource, jid.Resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncorrectJids(t *testing.T) {
|
||||
badJids := []string{
|
||||
"",
|
||||
"user@",
|
||||
"@domain.com",
|
||||
"user:name@domain.com",
|
||||
"user<name@domain.com",
|
||||
"test@domain.com@otherdomain.com",
|
||||
"test@domain com/resource",
|
||||
}
|
||||
|
||||
for _, sjid := range badJids {
|
||||
if _, err := NewJid(sjid); err == nil {
|
||||
t.Error("parsing incorrect jid should return error: " + sjid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFull(t *testing.T) {
|
||||
fullJids := []string{
|
||||
"test@domain.com/my resource",
|
||||
"test@domain.com",
|
||||
"domain.com",
|
||||
}
|
||||
for _, sjid := range fullJids {
|
||||
parsedJid, err := NewJid(sjid)
|
||||
if err != nil {
|
||||
t.Errorf("could not parse jid: %v", err)
|
||||
}
|
||||
fullJid := parsedJid.Full()
|
||||
if fullJid != sjid {
|
||||
t.Errorf("incorrect full jid: %s", fullJid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBare(t *testing.T) {
|
||||
tests := []struct {
|
||||
jidstr string
|
||||
expected string
|
||||
}{
|
||||
{jidstr: "test@domain.com", expected: "test@domain.com"},
|
||||
{jidstr: "test@domain.com/resource", expected: "test@domain.com"},
|
||||
{jidstr: "domain.com", expected: "domain.com"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
parsedJid, err := NewJid(tt.jidstr)
|
||||
if err != nil {
|
||||
t.Errorf("could not parse jid: %v", err)
|
||||
}
|
||||
bareJid := parsedJid.Bare()
|
||||
if bareJid != tt.expected {
|
||||
t.Errorf("incorrect bare jid: %s", bareJid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Message Packet
|
||||
|
||||
// Message implements RFC 6120 - A.5 Client Namespace (a part)
|
||||
type Message struct {
|
||||
XMLName xml.Name `xml:"message"`
|
||||
Attrs
|
||||
|
||||
Subject string `xml:"subject,omitempty"`
|
||||
Body string `xml:"body,omitempty"`
|
||||
Thread string `xml:"thread,omitempty"`
|
||||
Error Err `xml:"error,omitempty"`
|
||||
Extensions []MsgExtension `xml:",omitempty"`
|
||||
}
|
||||
|
||||
func (Message) Name() string {
|
||||
return "message"
|
||||
}
|
||||
|
||||
func NewMessage(a Attrs) Message {
|
||||
return Message{
|
||||
XMLName: xml.Name{Local: "message"},
|
||||
Attrs: a,
|
||||
}
|
||||
}
|
||||
|
||||
// Get search and extracts a specific extension on a message.
|
||||
// It receives a pointer to an MsgExtension. 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 message.
|
||||
//
|
||||
// Example usage:
|
||||
// var oob xmpp.OOB
|
||||
// if ok := msg.Get(&oob); ok {
|
||||
// // oob extension has been found
|
||||
// }
|
||||
func (msg *Message) Get(ext MsgExtension) bool {
|
||||
target := reflect.ValueOf(ext)
|
||||
if target.Kind() != reflect.Ptr {
|
||||
panic("you must pass a pointer to the message Get method")
|
||||
}
|
||||
|
||||
for _, e := range msg.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 messageDecoder struct{}
|
||||
|
||||
var message messageDecoder
|
||||
|
||||
func (messageDecoder) decode(p *xml.Decoder, se xml.StartElement) (Message, error) {
|
||||
var packet Message
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// XMPPFormat with all Extensions
|
||||
func (msg *Message) XMPPFormat() string {
|
||||
out, err := xml.MarshalIndent(msg, "", "")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for messages
|
||||
func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
msg.XMLName = start.Name
|
||||
|
||||
// Extract packet attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "id" {
|
||||
msg.Id = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "type" {
|
||||
msg.Type = StanzaType(attr.Value)
|
||||
}
|
||||
if attr.Name.Local == "to" {
|
||||
msg.To = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "from" {
|
||||
msg.From = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "lang" {
|
||||
msg.Lang = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
if msgExt := TypeRegistry.GetMsgExtension(tt.Name); msgExt != nil {
|
||||
// Decode message extension
|
||||
err = d.DecodeElement(msgExt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Extensions = append(msg.Extensions, msgExt)
|
||||
} else {
|
||||
// Decode standard message sub-elements
|
||||
var err error
|
||||
switch tt.Name.Local {
|
||||
case "body":
|
||||
err = d.DecodeElement(&msg.Body, &tt)
|
||||
case "thread":
|
||||
err = d.DecodeElement(&msg.Thread, &tt)
|
||||
case "subject":
|
||||
err = d.DecodeElement(&msg.Subject, &tt)
|
||||
case "error":
|
||||
err = d.DecodeElement(&msg.Error, &tt)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestGenerateMessage(t *testing.T) {
|
||||
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
message.Body = "Hi"
|
||||
message.Subject = "Msg Subject"
|
||||
|
||||
data, err := xml.Marshal(message)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
parsedMessage := stanza.Message{}
|
||||
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if !xmlEqual(parsedMessage, message) {
|
||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedMessage, message))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeError(t *testing.T) {
|
||||
str := `<message from='juliet@capulet.com'
|
||||
id='msg_1'
|
||||
to='romeo@montague.lit'
|
||||
type='error'>
|
||||
<error type='cancel'>
|
||||
<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
</error>
|
||||
</message>`
|
||||
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message error stanza unmarshall error: %v", err)
|
||||
return
|
||||
}
|
||||
if parsedMessage.Error.Type != "cancel" {
|
||||
t.Errorf("incorrect error type: %s", parsedMessage.Error.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOOB(t *testing.T) {
|
||||
image := "https://localhost/image.png"
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||
ext := stanza.OOB{
|
||||
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
|
||||
URL: image,
|
||||
}
|
||||
msg.Extensions = append(msg.Extensions, &ext)
|
||||
|
||||
// OOB can properly be found
|
||||
var oob stanza.OOB
|
||||
// Try to find and
|
||||
if ok := msg.Get(&oob); !ok {
|
||||
t.Error("could not find oob extension")
|
||||
return
|
||||
}
|
||||
if oob.URL != image {
|
||||
t.Errorf("OOB URL was not properly extracted: ''%s", oob.URL)
|
||||
}
|
||||
|
||||
// Markable is not found
|
||||
var m stanza.Markable
|
||||
if ok := msg.Get(&m); ok {
|
||||
t.Error("we should not have found markable extension")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0333 - Chat Markers: https://xmpp.org/extensions/xep-0333.html
|
||||
*/
|
||||
|
||||
const NSMsgChatMarkers = "urn:xmpp:chat-markers:0"
|
||||
|
||||
type Markable struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 markable"`
|
||||
}
|
||||
|
||||
type MarkReceived struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 received"`
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
type MarkDisplayed struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 displayed"`
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
type MarkAcknowledged struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 acknowledged"`
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "markable"}, Markable{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "received"}, MarkReceived{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "displayed"}, MarkDisplayed{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "acknowledged"}, MarkAcknowledged{})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0085 - Chat State Notifications: https://xmpp.org/extensions/xep-0085.html
|
||||
*/
|
||||
|
||||
const NSMsgChatStateNotifications = "http://jabber.org/protocol/chatstates"
|
||||
|
||||
type StateActive struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates active"`
|
||||
}
|
||||
|
||||
type StateComposing struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates composing"`
|
||||
}
|
||||
|
||||
type StateGone struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates gone"`
|
||||
}
|
||||
|
||||
type StateInactive struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates inactive"`
|
||||
}
|
||||
|
||||
type StatePaused struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates paused"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "active"}, StateActive{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "composing"}, StateComposing{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "gone"}, StateGone{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "inactive"}, StateInactive{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "paused"}, StatePaused{})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0334: Message Processing Hints: https://xmpp.org/extensions/xep-0334.html
|
||||
Pointers should be used to keep consistent with unmarshal. Eg :
|
||||
msg.Extensions = append(msg.Extensions, &stanza.HintNoCopy{}, &stanza.HintStore{})
|
||||
*/
|
||||
|
||||
type HintNoPermanentStore struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:hints no-permanent-store"`
|
||||
}
|
||||
|
||||
type HintNoStore struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:hints no-store"`
|
||||
}
|
||||
|
||||
type HintNoCopy struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:hints no-copy"`
|
||||
}
|
||||
type HintStore struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:hints store"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-permanent-store"}, HintNoPermanentStore{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-store"}, HintNoStore{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-copy"}, HintNoCopy{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "store"}, HintStore{})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const msg_const = `
|
||||
<message
|
||||
from="romeo@montague.lit/laptop"
|
||||
to="juliet@capulet.lit/laptop">
|
||||
<body>V unir avtugf pybnx gb uvqr zr sebz gurve fvtug</body>
|
||||
<no-copy xmlns="urn:xmpp:hints"></no-copy>
|
||||
<no-permanent-store xmlns="urn:xmpp:hints"></no-permanent-store>
|
||||
<no-store xmlns="urn:xmpp:hints"></no-store>
|
||||
<store xmlns="urn:xmpp:hints"></store>
|
||||
</message>`
|
||||
|
||||
func TestSerializationHint(t *testing.T) {
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
|
||||
msg.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
|
||||
msg.Extensions = append(msg.Extensions, stanza.HintNoCopy{}, stanza.HintNoPermanentStore{}, stanza.HintNoStore{}, stanza.HintStore{})
|
||||
data, _ := xml.Marshal(msg)
|
||||
if strings.ReplaceAll(strings.Join(strings.Fields(msg_const), ""), "\n", "") != strings.Join(strings.Fields(string(data)), "") {
|
||||
t.Fatalf("marshalled message does not match expected message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalHints(t *testing.T) {
|
||||
// Init message as in the const value
|
||||
msgConst := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
|
||||
msgConst.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
|
||||
msgConst.Extensions = append(msgConst.Extensions, &stanza.HintNoCopy{}, &stanza.HintNoPermanentStore{}, &stanza.HintNoStore{}, &stanza.HintStore{})
|
||||
|
||||
// Compare message with the const value
|
||||
msg := stanza.Message{}
|
||||
err := xml.Unmarshal([]byte(msg_const), &msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if msgConst.XMLName.Local != msg.XMLName.Local {
|
||||
t.Fatalf("message tags do not match. Expected: %s, Actual: %s", msgConst.XMLName.Local, msg.XMLName.Local)
|
||||
}
|
||||
if msgConst.Body != msg.Body {
|
||||
t.Fatalf("message bodies do not match. Expected: %s, Actual: %s", msgConst.Body, msg.Body)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(msgConst.Attrs, msg.Attrs) {
|
||||
t.Fatalf("attributes do not match")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(msgConst.Error, msg.Error) {
|
||||
t.Fatalf("attributes do not match")
|
||||
}
|
||||
var found bool
|
||||
for _, ext := range msgConst.Extensions {
|
||||
for _, strExt := range msg.Extensions {
|
||||
if reflect.TypeOf(ext) == reflect.TypeOf(strExt) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("extensions do not match")
|
||||
}
|
||||
found = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type HTML struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/xhtml-im html"`
|
||||
Body HTMLBody
|
||||
Lang string `xml:"xml:lang,attr,omitempty"`
|
||||
}
|
||||
|
||||
type HTMLBody struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/1999/xhtml body"`
|
||||
// InnerXML MUST be valid xhtml. We do not check if it is valid when generating the XMPP stanza.
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/xhtml-im", Local: "html"}, HTML{})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestHTMLGen(t *testing.T) {
|
||||
htmlBody := "<p>Hello <b>World</b></p>"
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||
msg.Body = "Hello World"
|
||||
body := stanza.HTMLBody{
|
||||
InnerXML: htmlBody,
|
||||
}
|
||||
html := stanza.HTML{Body: body}
|
||||
msg.Extensions = append(msg.Extensions, html)
|
||||
|
||||
result := msg.XMPPFormat()
|
||||
str := `<message to="test@localhost"><body>Hello World</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>Hello <b>World</b></p></body></html></message>`
|
||||
if result != str {
|
||||
t.Errorf("incorrect serialize message:\n%s", result)
|
||||
}
|
||||
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message HTML unmarshall error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedMessage.Body != msg.Body {
|
||||
t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body)
|
||||
}
|
||||
|
||||
var h stanza.HTML
|
||||
if ok := parsedMessage.Get(&h); !ok {
|
||||
t.Error("could not extract HTML body")
|
||||
}
|
||||
|
||||
if h.Body.InnerXML != htmlBody {
|
||||
t.Errorf("could not extract html body: '%s'", h.Body.InnerXML)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0066 - Out of Band Data: https://xmpp.org/extensions/xep-0066.html
|
||||
*/
|
||||
|
||||
type OOB struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"jabber:x:oob x"`
|
||||
URL string `xml:"url"`
|
||||
Desc string `xml:"desc,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "jabber:x:oob", Local: "x"}, OOB{})
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Implementation of the http://jabber.org/protocol/pubsub#event namespace
|
||||
type PubSubEvent struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#event event"`
|
||||
MsgExtension
|
||||
EventElement EventElement
|
||||
//List ItemsEvent
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/pubsub#event", Local: "event"}, PubSubEvent{})
|
||||
}
|
||||
|
||||
type EventElement interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Collection
|
||||
// *********************
|
||||
|
||||
const PubSubCollectionEventName = "Collection"
|
||||
|
||||
type CollectionEvent struct {
|
||||
AssocDisassoc AssocDisassoc
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (c CollectionEvent) Name() string {
|
||||
return PubSubCollectionEventName
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Associate/Disassociate
|
||||
// *********************
|
||||
type AssocDisassoc interface {
|
||||
GetAssocDisassoc() string
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Associate
|
||||
// *********************
|
||||
const Assoc = "Associate"
|
||||
|
||||
type AssociateEvent struct {
|
||||
XMLName xml.Name `xml:"associate"`
|
||||
Node string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
func (a *AssociateEvent) GetAssocDisassoc() string {
|
||||
return Assoc
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Disassociate
|
||||
// *********************
|
||||
const Disassoc = "Disassociate"
|
||||
|
||||
type DisassociateEvent struct {
|
||||
XMLName xml.Name `xml:"disassociate"`
|
||||
Node string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
func (e *DisassociateEvent) GetAssocDisassoc() string {
|
||||
return Disassoc
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Configuration
|
||||
// *********************
|
||||
|
||||
const PubSubConfigEventName = "Configuration"
|
||||
|
||||
type ConfigurationEvent struct {
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Form *Form
|
||||
}
|
||||
|
||||
func (c ConfigurationEvent) Name() string {
|
||||
return PubSubConfigEventName
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Delete
|
||||
// *********************
|
||||
const PubSubDeleteEventName = "Delete"
|
||||
|
||||
type DeleteEvent struct {
|
||||
Node string `xml:"node,attr"`
|
||||
Redirect *RedirectEvent `xml:"redirect"`
|
||||
}
|
||||
|
||||
func (c DeleteEvent) Name() string {
|
||||
return PubSubConfigEventName
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Redirect
|
||||
// *********************
|
||||
type RedirectEvent struct {
|
||||
URI string `xml:"uri,attr"`
|
||||
}
|
||||
|
||||
// *********************
|
||||
// List
|
||||
// *********************
|
||||
|
||||
const PubSubItemsEventName = "List"
|
||||
|
||||
type ItemsEvent struct {
|
||||
XMLName xml.Name `xml:"items"`
|
||||
Items []ItemEvent `xml:"item,omitempty"`
|
||||
Node string `xml:"node,attr"`
|
||||
Retract *RetractEvent `xml:"retract"`
|
||||
}
|
||||
|
||||
type ItemEvent struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Id string `xml:"id,attr,omitempty"`
|
||||
Publisher string `xml:"publisher,attr,omitempty"`
|
||||
Any *Node `xml:",any"`
|
||||
}
|
||||
|
||||
func (i ItemsEvent) Name() string {
|
||||
return PubSubItemsEventName
|
||||
}
|
||||
|
||||
// *********************
|
||||
// List
|
||||
// *********************
|
||||
|
||||
type RetractEvent struct {
|
||||
XMLName xml.Name `xml:"retract"`
|
||||
ID string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Purge
|
||||
// *********************
|
||||
const PubSubPurgeEventName = "Purge"
|
||||
|
||||
type PurgeEvent struct {
|
||||
XMLName xml.Name `xml:"purge"`
|
||||
Node string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
func (p PurgeEvent) Name() string {
|
||||
return PubSubPurgeEventName
|
||||
}
|
||||
|
||||
// *********************
|
||||
// Subscription
|
||||
// *********************
|
||||
const PubSubSubscriptionEventName = "Subscription"
|
||||
|
||||
type SubscriptionEvent struct {
|
||||
SubStatus string `xml:"subscription,attr,omitempty"`
|
||||
Expiry string `xml:"expiry,attr,omitempty"`
|
||||
SubInfo `xml:",omitempty"`
|
||||
}
|
||||
|
||||
func (s SubscriptionEvent) Name() string {
|
||||
return PubSubSubscriptionEventName
|
||||
}
|
||||
|
||||
func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
pse.XMLName = start.Name
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ee EventElement
|
||||
switch tt := t.(type) {
|
||||
case xml.StartElement:
|
||||
switch tt.Name.Local {
|
||||
case "collection":
|
||||
ee = &CollectionEvent{}
|
||||
case "configuration":
|
||||
ee = &ConfigurationEvent{}
|
||||
case "delete":
|
||||
ee = &DeleteEvent{}
|
||||
case "items":
|
||||
ee = &ItemsEvent{}
|
||||
case "purge":
|
||||
ee = &PurgeEvent{}
|
||||
case "subscription":
|
||||
ee = &SubscriptionEvent{}
|
||||
default:
|
||||
ee = nil
|
||||
}
|
||||
// known child element found, decode it
|
||||
if ee != nil {
|
||||
err = d.DecodeElement(ee, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pse.EventElement = ee
|
||||
}
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeMsgEvent(t *testing.T) {
|
||||
str := `<message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo'>
|
||||
<event xmlns='http://jabber.org/protocol/pubsub#event'>
|
||||
<items node='princely_musings'>
|
||||
<item id='ae890ac52d0df67ed7cfdf51b644e901'>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom'>
|
||||
<title>Soliloquy</title>
|
||||
<summary>
|
||||
To be, or not to be: that is the question:
|
||||
Whether 'tis nobler in the mind to suffer
|
||||
The slings and arrows of outrageous fortune,
|
||||
Or to take arms against a sea of troubles,
|
||||
And by opposing end them?
|
||||
</summary>
|
||||
<link rel='alternate' type='text/html'
|
||||
href='http://denmark.lit/2003/12/13/atom03'/>
|
||||
<id>tag:denmark.lit,2003:entry-32397</id>
|
||||
<published>2003-12-13T18:30:02Z</published>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
</entry>
|
||||
</item>
|
||||
</items>
|
||||
</event>
|
||||
</message>
|
||||
`
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message receipt unmarshall error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedMessage.Body != "" {
|
||||
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
|
||||
}
|
||||
|
||||
if len(parsedMessage.Extensions) < 1 {
|
||||
t.Errorf("no extension found on parsed message")
|
||||
return
|
||||
}
|
||||
|
||||
switch ext := parsedMessage.Extensions[0].(type) {
|
||||
case *stanza.PubSubEvent:
|
||||
if ext.XMLName.Local != "event" {
|
||||
t.Fatalf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
tmp, ok := parsedMessage.Extensions[0].(*stanza.PubSubEvent)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
ie, ok := tmp.EventElement.(*stanza.ItemsEvent)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
if ie.Items[0].Any.Nodes[0].Content != "Soliloquy" {
|
||||
t.Fatalf("could not read title ! Read this : %s", ie.Items[0].Any.Nodes[0].Content)
|
||||
}
|
||||
|
||||
if len(ie.Items[0].Any.Nodes) != 6 {
|
||||
t.Fatalf("some nodes were not correctly parsed")
|
||||
}
|
||||
default:
|
||||
t.Fatalf("could not find pubsub event extension")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEncodeEvent(t *testing.T) {
|
||||
expected := "<message><event xmlns=\"http://jabber.org/protocol/pubsub#event\">" +
|
||||
"<items node=\"princely_musings\"><item id=\"ae890ac52d0df67ed7cfdf51b644e901\">" +
|
||||
"<entry xmlns=\"http://www.w3.org/2005/Atom\"><title>My pub item title</title>" +
|
||||
"<summary>My pub item content summary</summary><link rel=\"alternate\" " +
|
||||
"type=\"text/html\" href=\"http://denmark.lit/2003/12/13/atom03\">" +
|
||||
"</link><id>My pub item content ID</id><published>2003-12-13T18:30:02Z</published>" +
|
||||
"<updated>2003-12-13T18:30:02Z</updated></entry></item></items></event></message>"
|
||||
message := stanza.Message{
|
||||
Extensions: []stanza.MsgExtension{
|
||||
stanza.PubSubEvent{
|
||||
EventElement: stanza.ItemsEvent{
|
||||
Items: []stanza.ItemEvent{
|
||||
{
|
||||
Id: "ae890ac52d0df67ed7cfdf51b644e901",
|
||||
Any: &stanza.Node{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://www.w3.org/2005/Atom",
|
||||
Local: "entry",
|
||||
},
|
||||
Attrs: nil,
|
||||
Content: "",
|
||||
Nodes: []stanza.Node{
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "title"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item title",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "summary"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content summary",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "link"},
|
||||
Attrs: []xml.Attr{
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "rel"},
|
||||
Value: "alternate",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "type"},
|
||||
Value: "text/html",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "href"},
|
||||
Value: "http://denmark.lit/2003/12/13/atom03",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "id"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content ID",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "published"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "updated"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Node: "princely_musings",
|
||||
Retract: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, _ := xml.Marshal(message)
|
||||
if strings.TrimSpace(string(data)) != strings.TrimSpace(expected) {
|
||||
t.Errorf("event was not encoded properly : \nexpected:%s \ngot: %s", expected, string(data))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
||||
- XEP-0184 - Message Delivery Receipts: https://xmpp.org/extensions/xep-0184.html
|
||||
*/
|
||||
|
||||
const NSMsgReceipts = "urn:xmpp:receipts"
|
||||
|
||||
// Used on outgoing message, to tell the recipient that you are requesting a message receipt / ack.
|
||||
type ReceiptRequest struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:receipts request"`
|
||||
}
|
||||
|
||||
type ReceiptReceived struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:receipts received"`
|
||||
ID string `xml:"id,attr"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "request"}, ReceiptRequest{})
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "received"}, ReceiptReceived{})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestDecodeRequest(t *testing.T) {
|
||||
str := `<message
|
||||
from='northumberland@shakespeare.lit/westminster'
|
||||
id='richard2-4.1.247'
|
||||
to='kingrichard@royalty.england.lit/throne'>
|
||||
<body>My lord, dispatch; read o'er these articles.</body>
|
||||
<request xmlns='urn:xmpp:receipts'/>
|
||||
</message>`
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message receipt unmarshall error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedMessage.Body != "My lord, dispatch; read o'er these articles." {
|
||||
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
|
||||
}
|
||||
|
||||
if len(parsedMessage.Extensions) < 1 {
|
||||
t.Errorf("no extension found on parsed message")
|
||||
return
|
||||
}
|
||||
|
||||
switch ext := parsedMessage.Extensions[0].(type) {
|
||||
case *stanza.ReceiptRequest:
|
||||
if ext.XMLName.Local != "request" {
|
||||
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
default:
|
||||
t.Errorf("could not find receipts extension")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
// Generic / unknown content
|
||||
|
||||
// Node is a generic structure to represent XML data. It is used to parse
|
||||
// unreferenced or custom stanza payload.
|
||||
type Node struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content string `xml:",cdata"`
|
||||
Nodes []Node `xml:",any"`
|
||||
}
|
||||
|
||||
func (n *Node) Namespace() string {
|
||||
return n.XMLName.Space
|
||||
}
|
||||
|
||||
// Attr represents generic XML attributes, as used on the generic XML Node
|
||||
// representation.
|
||||
type Attr struct {
|
||||
K string
|
||||
V string
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
|
||||
for _, attr := range start.Attr {
|
||||
// Do not repeat xmlns, it is already in XMLName
|
||||
if attr.Name.Local != "xmlns" {
|
||||
n.Attrs = append(n.Attrs, attr)
|
||||
}
|
||||
}
|
||||
type node Node
|
||||
return d.DecodeElement((*node)(n), &start)
|
||||
}
|
||||
|
||||
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
|
||||
// Node structure to XML.
|
||||
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
start.Attr = n.Attrs
|
||||
start.Name = n.XMLName
|
||||
|
||||
err = e.EncodeToken(start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n.Content != "" {
|
||||
err = e.EncodeToken(xml.CharData(n.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNode_Marshal(t *testing.T) {
|
||||
jsonData := []byte("{\"key\":\"value\"}")
|
||||
|
||||
iqResp, err := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iqResp.Any = &Node{
|
||||
XMLName: xml.Name{Space: "myNS", Local: "space"},
|
||||
Content: string(jsonData),
|
||||
}
|
||||
|
||||
bytes, err := xml.Marshal(iqResp)
|
||||
if err != nil {
|
||||
t.Errorf("Could not marshal XML: %v", err)
|
||||
}
|
||||
|
||||
parsedIQ := IQ{}
|
||||
if err := xml.Unmarshal(bytes, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal returned error: %v", err)
|
||||
}
|
||||
|
||||
if parsedIQ.Any.Content != string(jsonData) {
|
||||
t.Errorf("Cannot find generic any payload in parsedIQ: '%s'", parsedIQ.Any.Content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package stanza
|
||||
|
||||
const (
|
||||
NSStream = "http://etherx.jabber.org/streams"
|
||||
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
||||
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||
NSFraming = "urn:ietf:params:xml:ns:xmpp-framing"
|
||||
NSClient = "jabber:client"
|
||||
NSComponent = "jabber:component:accept"
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Open Packet
|
||||
// Reference: WebSocket connections must start with this element
|
||||
// https://tools.ietf.org/html/rfc7395#section-3.4
|
||||
type WebsocketOpen struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-framing open"`
|
||||
From string `xml:"from,attr"`
|
||||
Id string `xml:"id,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package stanza
|
||||
|
||||
type Packet interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// Attrs represents the common structure for base XMPP packets.
|
||||
type Attrs struct {
|
||||
Type StanzaType `xml:"type,attr,omitempty"`
|
||||
Id string `xml:"id,attr,omitempty"`
|
||||
From string `xml:"from,attr,omitempty"`
|
||||
To string `xml:"to,attr,omitempty"`
|
||||
Lang string `xml:"lang,attr,omitempty"`
|
||||
}
|
||||
|
||||
type packetFormatter interface {
|
||||
XMPPFormat() string
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package stanza
|
||||
|
||||
import "strings"
|
||||
|
||||
type StanzaType string
|
||||
|
||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
||||
const (
|
||||
IQTypeError StanzaType = "error"
|
||||
IQTypeGet StanzaType = "get"
|
||||
IQTypeResult StanzaType = "result"
|
||||
IQTypeSet StanzaType = "set"
|
||||
|
||||
MessageTypeChat StanzaType = "chat"
|
||||
MessageTypeError StanzaType = "error"
|
||||
MessageTypeGroupchat StanzaType = "groupchat"
|
||||
MessageTypeHeadline StanzaType = "headline"
|
||||
MessageTypeNormal StanzaType = "normal" // Default
|
||||
|
||||
PresenceTypeError StanzaType = "error"
|
||||
PresenceTypeProbe StanzaType = "probe"
|
||||
PresenceTypeSubscribe StanzaType = "subscribe"
|
||||
PresenceTypeSubscribed StanzaType = "subscribed"
|
||||
PresenceTypeUnavailable StanzaType = "unavailable"
|
||||
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
|
||||
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
|
||||
)
|
||||
|
||||
func (s StanzaType) IsEmpty() bool {
|
||||
return len(strings.TrimSpace(string(s))) == 0
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Reads and checks the opening XMPP stream element.
|
||||
// TODO It returns a stream structure containing:
|
||||
// - Host: You can check the host against the host you were expecting to connect to
|
||||
// - Id: the Stream ID is a temporary shared secret used for some hash calculation. It is also used by ProcessOne
|
||||
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
|
||||
// getting through the authentication process.
|
||||
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
|
||||
func InitStream(p *xml.Decoder) (sessionID string, err error) {
|
||||
for {
|
||||
var t xml.Token
|
||||
t, err = p.Token()
|
||||
if err != nil {
|
||||
return sessionID, err
|
||||
}
|
||||
|
||||
switch elem := t.(type) {
|
||||
case xml.StartElement:
|
||||
isStreamOpen := elem.Name.Space == NSStream && elem.Name.Local == "stream"
|
||||
isFrameOpen := elem.Name.Space == NSFraming && elem.Name.Local == "open"
|
||||
if !isStreamOpen && !isFrameOpen {
|
||||
err = errors.New("xmpp: expected <stream> or <open> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||
return sessionID, err
|
||||
}
|
||||
|
||||
// Parse XMPP stream attributes
|
||||
for _, attrs := range elem.Attr {
|
||||
switch attrs.Name.Local {
|
||||
case "id":
|
||||
sessionID = attrs.Value
|
||||
}
|
||||
}
|
||||
return sessionID, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NextPacket scans XML token stream for next complete XMPP stanza.
|
||||
// Once the type of stanza has been identified, a structure is created to decode
|
||||
// that stanza and returned.
|
||||
// TODO Use an interface to return packets interface xmppDecoder
|
||||
// TODO make auth and bind use NextPacket instead of directly NextStart
|
||||
func NextPacket(p *xml.Decoder) (Packet, error) {
|
||||
// Read start element to find out how we want to parse the XMPP packet
|
||||
t, err := NextXmppToken(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ee, ok := t.(xml.EndElement); ok {
|
||||
return decodeStream(p, ee)
|
||||
}
|
||||
|
||||
// If not an end element, then must be a start
|
||||
se, ok := t.(xml.StartElement)
|
||||
if !ok {
|
||||
return nil, errors.New("unknown token ")
|
||||
}
|
||||
// Decode one of the top level XMPP namespace
|
||||
switch se.Name.Space {
|
||||
case NSStream:
|
||||
return decodeStream(p, se)
|
||||
case NSSASL:
|
||||
return decodeSASL(p, se)
|
||||
case NSClient:
|
||||
return decodeClient(p, se)
|
||||
case NSComponent:
|
||||
return decodeComponent(p, se)
|
||||
case NSStreamManagement:
|
||||
return sm.decode(p, se)
|
||||
default:
|
||||
return nil, errors.New("unknown namespace " +
|
||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||
}
|
||||
}
|
||||
|
||||
// NextXmppToken scans XML token stream to find next StartElement or stream EndElement.
|
||||
// We need the EndElement scan, because we must register stream close tags
|
||||
func NextXmppToken(p *xml.Decoder) (xml.Token, error) {
|
||||
for {
|
||||
t, err := p.Token()
|
||||
if err == io.EOF {
|
||||
return xml.StartElement{}, errors.New("connection closed")
|
||||
}
|
||||
if err != nil {
|
||||
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
|
||||
}
|
||||
switch t := t.(type) {
|
||||
case xml.StartElement:
|
||||
return t, nil
|
||||
case xml.EndElement:
|
||||
if t.Name.Space == NSStream && t.Name.Local == "stream" {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NextStart scans XML token stream to find next StartElement.
|
||||
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||
for {
|
||||
t, err := p.Token()
|
||||
if err == io.EOF {
|
||||
return xml.StartElement{}, errors.New("connection closed")
|
||||
}
|
||||
if err != nil {
|
||||
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
|
||||
}
|
||||
switch t := t.(type) {
|
||||
case xml.StartElement:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that
|
||||
type.
|
||||
That way, we have a consistent way to do type assertion, always matching against pointers.
|
||||
*/
|
||||
|
||||
// decodeStream will fully decode a stream packet
|
||||
func decodeStream(p *xml.Decoder, t xml.Token) (Packet, error) {
|
||||
if se, ok := t.(xml.StartElement); ok {
|
||||
switch se.Name.Local {
|
||||
case "error":
|
||||
return streamError.decode(p, se)
|
||||
case "features":
|
||||
return streamFeatures.decode(p, se)
|
||||
default:
|
||||
return nil, errors.New("unexpected XMPP packet " +
|
||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||
}
|
||||
}
|
||||
|
||||
if ee, ok := t.(xml.EndElement); ok {
|
||||
if ee.Name.Local == "stream" {
|
||||
return streamClose.decode(ee), nil
|
||||
}
|
||||
return nil, errors.New("unexpected XMPP packet " +
|
||||
ee.Name.Space + " <" + ee.Name.Local + "/>")
|
||||
}
|
||||
|
||||
// Should not happen
|
||||
return nil, errors.New("unexpected XML token ")
|
||||
}
|
||||
|
||||
// decodeSASL decodes a packet related to SASL authentication.
|
||||
func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
switch se.Name.Local {
|
||||
case "success":
|
||||
return saslSuccess.decode(p, se)
|
||||
case "failure":
|
||||
return saslFailure.decode(p, se)
|
||||
default:
|
||||
return nil, errors.New("unexpected XMPP packet " +
|
||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||
}
|
||||
}
|
||||
|
||||
// decodeClient decodes all known packets in the client namespace.
|
||||
func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
switch se.Name.Local {
|
||||
case "message":
|
||||
return message.decode(p, se)
|
||||
case "presence":
|
||||
return presence.decode(p, se)
|
||||
case "iq":
|
||||
return iq.decode(p, se)
|
||||
default:
|
||||
return nil, errors.New("unexpected XMPP packet " +
|
||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||
}
|
||||
}
|
||||
|
||||
// decodeComponent decodes all known packets in the component namespace.
|
||||
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
switch se.Name.Local {
|
||||
case "handshake": // handshake is used to authenticate components
|
||||
return handshake.decode(p, se)
|
||||
case "message":
|
||||
return message.decode(p, se)
|
||||
case "presence":
|
||||
return presence.decode(p, se)
|
||||
case "iq":
|
||||
return iq.decode(p, se)
|
||||
default:
|
||||
return nil, errors.New("unexpected XMPP packet " +
|
||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type Tune struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/tune tune"`
|
||||
Artist string `xml:"artist,omitempty"`
|
||||
Length int `xml:"length,omitempty"`
|
||||
Rating int `xml:"rating,omitempty"`
|
||||
Source string `xml:"source,omitempty"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
Track string `xml:"track,omitempty"`
|
||||
Uri string `xml:"uri,omitempty"`
|
||||
}
|
||||
|
||||
// Mood defines data model for XEP-0107 - User Mood
|
||||
// See: https://xmpp.org/extensions/xep-0107.html
|
||||
type Mood struct {
|
||||
MsgExtension // Mood can be added as a message extension
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/mood mood"`
|
||||
// TODO: Custom parsing to extract mood type from tag name.
|
||||
// Note: the list is predefined.
|
||||
// Mood type
|
||||
Text string `xml:"text,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
"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"`
|
||||
}
|
||||
|
||||
const timeLayout = "2006-01-02T15:04:05Z"
|
||||
|
||||
// History implements XEP-0045: Multi-User Chat - 19.1
|
||||
type History struct {
|
||||
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() {
|
||||
TypeRegistry.MapExtension(PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc", Local: "x"}, MucPresence{})
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// https://xmpp.org/extensions/xep-0045.html#example-27
|
||||
func TestMucPassword(t *testing.T) {
|
||||
str := `<presence
|
||||
from='hag66@shakespeare.lit/pda'
|
||||
id='djn4714'
|
||||
to='coven@chat.shakespeare.lit/thirdwitch'>
|
||||
<x xmlns='http://jabber.org/protocol/muc'>
|
||||
<password>cauldronburn</password>
|
||||
</x>
|
||||
</presence>`
|
||||
|
||||
var parsedPresence stanza.Presence
|
||||
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", str)
|
||||
}
|
||||
|
||||
var muc stanza.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 := `<presence
|
||||
from='hag66@shakespeare.lit/pda'
|
||||
id='n13mt3l'
|
||||
to='coven@chat.shakespeare.lit/thirdwitch'>
|
||||
<x xmlns='http://jabber.org/protocol/muc'>
|
||||
<history maxstanzas='20'/>
|
||||
</x>
|
||||
</presence>`
|
||||
|
||||
var parsedPresence stanza.Presence
|
||||
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
|
||||
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 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 := "<presence" +
|
||||
" id=\"n13mt3l\"" +
|
||||
" from=\"hag66@shakespeare.lit/pda\"" +
|
||||
" to=\"coven@chat.shakespeare.lit/thirdwitch\">" +
|
||||
"<x xmlns=\"http://jabber.org/protocol/muc\">" +
|
||||
"<history maxstanzas=\"0\"></history>" +
|
||||
"</x>" +
|
||||
"</presence>"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Presence Packet
|
||||
|
||||
// Presence implements RFC 6120 - A.5 Client Namespace (a part)
|
||||
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"`
|
||||
Extensions []PresExtension `xml:",omitempty"`
|
||||
}
|
||||
|
||||
func (Presence) Name() string {
|
||||
return "presence"
|
||||
}
|
||||
|
||||
func NewPresence(a Attrs) Presence {
|
||||
return Presence{
|
||||
XMLName: xml.Name{Local: "presence"},
|
||||
Attrs: a,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
func (presenceDecoder) decode(p *xml.Decoder, se xml.StartElement) (Presence, error) {
|
||||
var packet Presence
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package stanza
|
||||
|
||||
// PresenceShow is a Enum of presence element show
|
||||
type PresenceShow string
|
||||
|
||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
||||
const (
|
||||
PresenceShowAway PresenceShow = "away"
|
||||
PresenceShowChat PresenceShow = "chat"
|
||||
PresenceShowDND PresenceShow = "dnd"
|
||||
PresenceShowXA PresenceShow = "xa"
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestGeneratePresence(t *testing.T) {
|
||||
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
presence.Show = stanza.PresenceShowChat
|
||||
|
||||
data, err := xml.Marshal(presence)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
var parsedPresence stanza.Presence
|
||||
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if !xmlEqual(parsedPresence, presence) {
|
||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedPresence, presence))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresenceSubElt(t *testing.T) {
|
||||
// Test structure to ensure that show, status and priority are correctly defined as presence
|
||||
// package sub-elements
|
||||
type pres struct {
|
||||
Show stanza.PresenceShow `xml:"show"`
|
||||
Status string `xml:"status"`
|
||||
Priority int8 `xml:"priority"`
|
||||
}
|
||||
|
||||
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
presence.Show = stanza.PresenceShowXA
|
||||
presence.Status = "Coding"
|
||||
presence.Priority = 10
|
||||
|
||||
data, err := xml.Marshal(presence)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
var parsedPresence pres
|
||||
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if parsedPresence.Show != presence.Show {
|
||||
t.Errorf("cannot read 'show' as presence subelement (%s)", parsedPresence.Show)
|
||||
}
|
||||
if parsedPresence.Status != presence.Status {
|
||||
t.Errorf("cannot read 'status' as presence subelement (%s)", parsedPresence.Status)
|
||||
}
|
||||
if parsedPresence.Priority != presence.Priority {
|
||||
t.Errorf("cannot read 'priority' as presence subelement (%d)", parsedPresence.Priority)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PubSubGeneric struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
|
||||
|
||||
Create *Create `xml:"create,omitempty"`
|
||||
Configure *Configure `xml:"configure,omitempty"`
|
||||
|
||||
Subscribe *SubInfo `xml:"subscribe,omitempty"`
|
||||
SubOptions *SubOptions `xml:"options,omitempty"`
|
||||
|
||||
Publish *Publish `xml:"publish,omitempty"`
|
||||
PublishOptions *PublishOptions `xml:"publish-options"`
|
||||
|
||||
Affiliations *Affiliations `xml:"affiliations,omitempty"`
|
||||
Default *Default `xml:"default,omitempty"`
|
||||
|
||||
Items *Items `xml:"items,omitempty"`
|
||||
Retract *Retract `xml:"retract,omitempty"`
|
||||
Subscription *Subscription `xml:"subscription,omitempty"`
|
||||
|
||||
Subscriptions *Subscriptions `xml:"subscriptions,omitempty"`
|
||||
// To use in responses to sub/unsub for instance
|
||||
// Subscription options
|
||||
Unsubscribe *SubInfo `xml:"unsubscribe,omitempty"`
|
||||
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PubSubGeneric) Namespace() string {
|
||||
return p.XMLName.Space
|
||||
}
|
||||
|
||||
func (p *PubSubGeneric) GetSet() *ResultSet {
|
||||
return p.ResultSet
|
||||
}
|
||||
|
||||
type Affiliations struct {
|
||||
List []Affiliation `xml:"affiliation"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Affiliation struct {
|
||||
AffiliationStatus string `xml:"affiliation"`
|
||||
Node string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
type Create struct {
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
type SubOptions struct {
|
||||
SubInfo
|
||||
Form *Form `xml:"x"`
|
||||
}
|
||||
|
||||
type Configure struct {
|
||||
Form *Form `xml:"x"`
|
||||
}
|
||||
type Default struct {
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Form *Form `xml:"x"`
|
||||
}
|
||||
|
||||
type Subscribe struct {
|
||||
XMLName xml.Name `xml:"subscribe"`
|
||||
SubInfo
|
||||
}
|
||||
type Unsubscribe struct {
|
||||
XMLName xml.Name `xml:"unsubscribe"`
|
||||
SubInfo
|
||||
}
|
||||
|
||||
// SubInfo represents information about a subscription
|
||||
// Node is the node related to the subscription
|
||||
// Jid is the subscription JID of the subscribed entity
|
||||
// SubID is the subscription ID
|
||||
type SubInfo struct {
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Jid string `xml:"jid,attr,omitempty"`
|
||||
// Sub ID is optional
|
||||
SubId *string `xml:"subid,attr,omitempty"`
|
||||
}
|
||||
|
||||
// validate checks if a node and a jid are present in the sub info, and if this jid is valid.
|
||||
func (si *SubInfo) validate() error {
|
||||
// Requests MUST contain a valid JID
|
||||
if _, err := NewJid(si.Jid); err != nil {
|
||||
return err
|
||||
}
|
||||
// SubInfo must contain both a valid JID and a node. See XEP-0060
|
||||
if strings.TrimSpace(si.Node) == "" {
|
||||
return errors.New("SubInfo must contain the node AND the subscriber JID in subscription config options requests")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handles the "5.6 Retrieve Subscriptions" of XEP-0060
|
||||
type Subscriptions struct {
|
||||
XMLName xml.Name `xml:"subscriptions"`
|
||||
List []Subscription `xml:"subscription,omitempty"`
|
||||
}
|
||||
|
||||
// Handles the "5.6 Retrieve Subscriptions" and the 6.1 Subscribe to a Node and so on of XEP-0060
|
||||
type Subscription struct {
|
||||
SubStatus string `xml:"subscription,attr,omitempty"`
|
||||
SubInfo `xml:",omitempty"`
|
||||
// Seems like we can't marshal a self-closing tag for now : https://github.com/golang/go/issues/21399
|
||||
// subscribe-options should be like this as per XEP-0060:
|
||||
// <subscribe-options>
|
||||
// <required/>
|
||||
// </subscribe-options>
|
||||
// Used to indicate if configuration options is required.
|
||||
Required *struct{}
|
||||
}
|
||||
|
||||
type PublishOptions struct {
|
||||
XMLName xml.Name `xml:"publish-options"`
|
||||
Form *Form
|
||||
}
|
||||
|
||||
type Publish struct {
|
||||
XMLName xml.Name `xml:"publish"`
|
||||
Node string `xml:"node,attr"`
|
||||
Items []Item `xml:"item,omitempty"` // xsd says there can be many. See also 12.10 Batch Processing of XEP-0060
|
||||
}
|
||||
|
||||
type Items struct {
|
||||
List []Item `xml:"item,omitempty"`
|
||||
MaxItems int `xml:"max_items,attr,omitempty"`
|
||||
Node string `xml:"node,attr"`
|
||||
SubId string `xml:"subid,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Id string `xml:"id,attr,omitempty"`
|
||||
Publisher string `xml:"publisher,attr,omitempty"`
|
||||
Any *Node `xml:",any"`
|
||||
}
|
||||
|
||||
type Retract struct {
|
||||
XMLName xml.Name `xml:"retract"`
|
||||
Node string `xml:"node,attr"`
|
||||
Notify *bool `xml:"notify,attr,omitempty"`
|
||||
Items []Item `xml:"item"`
|
||||
}
|
||||
|
||||
type PubSubOption struct {
|
||||
XMLName xml.Name `xml:"jabber:x:data options"`
|
||||
Form `xml:"x"`
|
||||
}
|
||||
|
||||
// NewSubRq builds a subscription request to a node at the given service.
|
||||
// It's a Set type IQ.
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.1 Subscribe to a Node
|
||||
func NewSubRq(serviceId string, subInfo SubInfo) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Subscribe: &subInfo,
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewUnsubRq builds an unsub request to a node at the given service.
|
||||
// It's a Set type IQ
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.2 Unsubscribe from a Node
|
||||
func NewUnsubRq(serviceId string, subInfo SubInfo) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Unsubscribe: &subInfo,
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewSubOptsRq builds a request for the subscription options.
|
||||
// It's a Get type IQ
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.3 Configure Subscription Options
|
||||
func NewSubOptsRq(serviceId string, subInfo SubInfo) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
SubOptions: &SubOptions{
|
||||
SubInfo: subInfo,
|
||||
},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewFormSubmission builds a form submission pubsub IQ
|
||||
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
|
||||
// 6.3.5 Form Submission
|
||||
func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if form.Type != FormTypeSubmit {
|
||||
return nil, errors.New("form type was expected to be submit but was : " + form.Type)
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
SubOptions: &SubOptions{
|
||||
SubInfo: subInfo,
|
||||
Form: form,
|
||||
},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewSubAndConfig builds a subscribe request that contains configuration options for the service
|
||||
// From XEP-0060 : The <options/> element MUST follow the <subscribe/> element and
|
||||
// MUST NOT possess a 'node' attribute or 'jid' attribute,
|
||||
// since the value of the <subscribe/> element's 'node' attribute specifies the desired NodeID and
|
||||
// the value of the <subscribe/> element's 'jid' attribute specifies the subscriber's JID
|
||||
// 6.3.7 Subscribe and Configure
|
||||
func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if form.Type != FormTypeSubmit {
|
||||
return nil, errors.New("form type was expected to be submit but was : " + form.Type)
|
||||
}
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Subscribe: &subInfo,
|
||||
SubOptions: &SubOptions{
|
||||
SubInfo: SubInfo{SubId: subInfo.SubId},
|
||||
Form: form,
|
||||
},
|
||||
}
|
||||
return iq, nil
|
||||
|
||||
}
|
||||
|
||||
// NewItemsRequest creates a request to query existing items from a node.
|
||||
// Specify a "maxItems" value to request only the last maxItems items. If 0, requests all items.
|
||||
// 6.5.2 Requesting All List AND 6.5.7 Requesting the Most Recent List
|
||||
func NewItemsRequest(serviceId string, node string, maxItems int) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Items: &Items{Node: node},
|
||||
}
|
||||
|
||||
if maxItems != 0 {
|
||||
ps, _ := iq.Payload.(*PubSubGeneric)
|
||||
ps.Items.MaxItems = maxItems
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewItemsRequest creates a request to get a specific item from a node.
|
||||
// 6.5.8 Requesting a Particular Item
|
||||
func NewSpecificItemRequest(serviceId, node, itemId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Items: &Items{Node: node,
|
||||
List: []Item{
|
||||
{
|
||||
Id: itemId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewPublishItemRq creates a request to publish a single item to a node identified by its provided ID
|
||||
func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (*IQ, error) {
|
||||
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return nil, errors.New("cannot publish without a target node ID")
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Publish: &Publish{Node: nodeID, Items: []Item{item}},
|
||||
}
|
||||
|
||||
// "The <item/> element provided by the publisher MAY possess an 'id' attribute,
|
||||
// specifying a unique ItemID for the item.
|
||||
// If an ItemID is not provided in the publish request,
|
||||
// the pubsub service MUST generate one and MUST ensure that it is unique for that node."
|
||||
if strings.TrimSpace(pubItemID) != "" {
|
||||
ps, _ := iq.Payload.(*PubSubGeneric)
|
||||
ps.Publish.Items[0].Id = pubItemID
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewPublishItemOptsRq creates a request to publish items to a node identified by its provided ID, along with configuration options
|
||||
// A pubsub service MAY support the ability to specify options along with a publish request
|
||||
//(if so, it MUST advertise support for the "http://jabber.org/protocol/pubsub#publish-options" feature).
|
||||
func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (*IQ, error) {
|
||||
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return nil, errors.New("cannot publish without a target node ID")
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Publish: &Publish{Node: nodeID, Items: items},
|
||||
PublishOptions: options,
|
||||
}
|
||||
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewDelItemFromNode creates a request to delete and item from a node, given its id.
|
||||
// To delete an item, the publisher sends a retract request.
|
||||
// This helper function follows 7.2 Delete an Item from a Node
|
||||
func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (*IQ, error) {
|
||||
// "The <retract/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return nil, errors.New("cannot delete item without a target node ID")
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Retract: &Retract{Node: nodeID, Items: []Item{{Id: itemId}}, Notify: notify},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewCreateAndConfigNode makes a request for node creation that has the desired node configuration.
|
||||
// See 8.1.3 Create and Configure a Node
|
||||
func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Create: &Create{Node: nodeID},
|
||||
Configure: &Configure{Form: confForm},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewCreateNode builds a request to create a node on the service referenced by "serviceId"
|
||||
// See 8.1 Create a Node
|
||||
func NewCreateNode(serviceId, nodeName string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Create: &Create{Node: nodeName},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewRetrieveAllSubsRequest builds a request to retrieve all subscriptions from all nodes
|
||||
// In order to make the request, the requesting entity MUST send an IQ-get whose <pubsub/>
|
||||
// child contains an empty <subscriptions/> element with no attributes.
|
||||
func NewRetrieveAllSubsRequest(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Subscriptions: &Subscriptions{},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewRetrieveAllAffilsRequest builds a request to retrieve all affiliations from all nodes
|
||||
// In order to make the request of the service, the requesting entity includes an empty <affiliations/> element with no attributes.
|
||||
func NewRetrieveAllAffilsRequest(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubGeneric{
|
||||
Affiliations: &Affiliations{},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub", Local: "pubsub"}, PubSubGeneric{})
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PubSubOwner struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"`
|
||||
OwnerUseCase OwnerUseCase
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (pso *PubSubOwner) Namespace() string {
|
||||
return pso.XMLName.Space
|
||||
}
|
||||
|
||||
func (pso *PubSubOwner) GetSet() *ResultSet {
|
||||
return pso.ResultSet
|
||||
}
|
||||
|
||||
type OwnerUseCase interface {
|
||||
UseCase() string
|
||||
}
|
||||
|
||||
type AffiliationsOwner struct {
|
||||
XMLName xml.Name `xml:"affiliations"`
|
||||
Affiliations []AffiliationOwner `xml:"affiliation,omitempty"`
|
||||
Node string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
func (AffiliationsOwner) UseCase() string {
|
||||
return "affiliations"
|
||||
}
|
||||
|
||||
type AffiliationOwner struct {
|
||||
XMLName xml.Name `xml:"affiliation"`
|
||||
AffiliationStatus string `xml:"affiliation,attr"`
|
||||
Jid string `xml:"jid,attr"`
|
||||
}
|
||||
|
||||
const (
|
||||
AffiliationStatusMember = "member"
|
||||
AffiliationStatusNone = "none"
|
||||
AffiliationStatusOutcast = "outcast"
|
||||
AffiliationStatusOwner = "owner"
|
||||
AffiliationStatusPublisher = "publisher"
|
||||
AffiliationStatusPublishOnly = "publish-only"
|
||||
)
|
||||
|
||||
type ConfigureOwner struct {
|
||||
XMLName xml.Name `xml:"configure"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Form *Form `xml:"x,omitempty"`
|
||||
}
|
||||
|
||||
func (*ConfigureOwner) UseCase() string {
|
||||
return "configure"
|
||||
}
|
||||
|
||||
type DefaultOwner struct {
|
||||
XMLName xml.Name `xml:"default"`
|
||||
Form *Form `xml:"x,omitempty"`
|
||||
}
|
||||
|
||||
func (*DefaultOwner) UseCase() string {
|
||||
return "default"
|
||||
}
|
||||
|
||||
type DeleteOwner struct {
|
||||
XMLName xml.Name `xml:"delete"`
|
||||
RedirectOwner *RedirectOwner `xml:"redirect,omitempty"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (*DeleteOwner) UseCase() string {
|
||||
return "delete"
|
||||
}
|
||||
|
||||
type RedirectOwner struct {
|
||||
XMLName xml.Name `xml:"redirect"`
|
||||
URI string `xml:"uri,attr"`
|
||||
}
|
||||
|
||||
type PurgeOwner struct {
|
||||
XMLName xml.Name `xml:"purge"`
|
||||
Node string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
func (*PurgeOwner) UseCase() string {
|
||||
return "purge"
|
||||
}
|
||||
|
||||
type SubscriptionsOwner struct {
|
||||
XMLName xml.Name `xml:"subscriptions"`
|
||||
Subscriptions []SubscriptionOwner `xml:"subscription"`
|
||||
Node string `xml:"node,attr"`
|
||||
}
|
||||
|
||||
func (*SubscriptionsOwner) UseCase() string {
|
||||
return "subscriptions"
|
||||
}
|
||||
|
||||
type SubscriptionOwner struct {
|
||||
SubscriptionStatus string `xml:"subscription"`
|
||||
Jid string `xml:"jid,attr"`
|
||||
}
|
||||
|
||||
const (
|
||||
SubscriptionStatusNone = "none"
|
||||
SubscriptionStatusPending = "pending"
|
||||
SubscriptionStatusSubscribed = "subscribed"
|
||||
SubscriptionStatusUnconfigured = "unconfigured"
|
||||
)
|
||||
|
||||
// NewConfigureNode creates a request to configure a node on the given service.
|
||||
// A form will be returned by the service, to which the user must respond using for instance the NewFormSubmission function.
|
||||
// See 8.2 Configure a Node
|
||||
func NewConfigureNode(serviceId, nodeName string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &ConfigureOwner{Node: nodeName},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewDelNode creates a request to delete node "nodeID" from the "serviceId" service
|
||||
// See 8.4 Delete a Node
|
||||
func NewDelNode(serviceId, nodeID string) (*IQ, error) {
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return nil, errors.New("cannot delete a node without a target node ID")
|
||||
}
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &DeleteOwner{Node: nodeID},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewPurgeAllItems creates a new purge request for the "nodeId" node, at "serviceId" service
|
||||
// See 8.5 Purge All Node Items
|
||||
func NewPurgeAllItems(serviceId, nodeId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &PurgeOwner{Node: nodeId},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewRequestDefaultConfig build a request to ask the service for the default config of its nodes
|
||||
// See 8.3 Request Default Node Configuration Options
|
||||
func NewRequestDefaultConfig(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &DefaultOwner{},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewApproveSubRequest creates a new sub approval response to a request from the service to the owner of the node
|
||||
// In order to approve the request, the owner shall submit the form and set the "pubsub#allow" field to a value of "1" or "true"
|
||||
// For tracking purposes the message MUST reflect the 'id' attribute originally provided in the request.
|
||||
// See 8.6 Manage Subscription Requests
|
||||
func NewApproveSubRequest(serviceId, reqID string, apprForm *Form) (Message, error) {
|
||||
if serviceId == "" {
|
||||
return Message{}, errors.New("need a target service serviceId send approval serviceId")
|
||||
}
|
||||
if reqID == "" {
|
||||
return Message{}, errors.New("the request ID is empty but must be used for the approval")
|
||||
}
|
||||
if apprForm == nil {
|
||||
return Message{}, errors.New("approval form is nil")
|
||||
}
|
||||
apprMess := NewMessage(Attrs{To: serviceId})
|
||||
apprMess.Extensions = []MsgExtension{apprForm}
|
||||
apprMess.Id = reqID
|
||||
|
||||
return apprMess, nil
|
||||
}
|
||||
|
||||
// NewGetPendingSubRequests creates a new request for all pending subscriptions to all their nodes at a service
|
||||
// This feature MUST be implemented using the Ad-Hoc Commands (XEP-0050) protocol
|
||||
// 8.7 Process Pending Subscription Requests
|
||||
func NewGetPendingSubRequests(serviceId string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &Command{
|
||||
// the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending"
|
||||
Node: "http://jabber.org/protocol/pubsub#get-pending",
|
||||
Action: CommandActionExecute,
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewGetPendingSubRequests creates a new request for all pending subscriptions to be approved on a given node
|
||||
// Upon receiving the data form for managing subscription requests, the owner then MAY request pending subscription
|
||||
// approval requests for a given node.
|
||||
// See 8.7.4 Per-Node Request
|
||||
func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (*IQ, error) {
|
||||
if sessionId == "" {
|
||||
return nil, errors.New("the sessionId must be maintained for the command")
|
||||
}
|
||||
|
||||
form := &Form{
|
||||
Type: FormTypeSubmit,
|
||||
Fields: []*Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}},
|
||||
}
|
||||
data, err := xml.Marshal(form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var n Node
|
||||
err = xml.Unmarshal(data, &n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &Command{
|
||||
// the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending"
|
||||
Node: "http://jabber.org/protocol/pubsub#get-pending",
|
||||
Action: CommandActionExecute,
|
||||
SessionId: sessionId,
|
||||
CommandElements: []CommandElement{&n},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewSubListRequest creates a request to list subscriptions of the client, for all nodes at the service.
|
||||
// It's a Get type IQ
|
||||
// 8.8.1 Retrieve Subscriptions
|
||||
func NewSubListRqPl(serviceId, nodeID string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &SubscriptionsOwner{Node: nodeID},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &SubscriptionsOwner{Node: nodeID, Subscriptions: subs},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewModifAffiliationRequest creates a request to either modify one or more affiliations, or delete one or more affiliations
|
||||
// 8.9.2 Modify Affiliation & 8.9.2.4 Multiple Simultaneous Modifications & 8.9.3 Delete an Entity (just set the status to "none")
|
||||
func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &AffiliationsOwner{
|
||||
Node: nodeID,
|
||||
Affiliations: newAffils,
|
||||
},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewAffiliationListRequest creates a request to list all affiliated entities
|
||||
// See 8.9.1 Retrieve List List
|
||||
func NewAffiliationListRequest(serviceId, nodeID string) (*IQ, error) {
|
||||
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iq.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &AffiliationsOwner{
|
||||
Node: nodeID,
|
||||
},
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewFormSubmission builds a form submission pubsub IQ, in the Owner namespace
|
||||
// This is typically used to respond to a form issued by the server when configuring a node.
|
||||
// See 8.2.4 Form Submission
|
||||
func NewFormSubmissionOwner(serviceId, nodeName string, fields []*Field) (*IQ, error) {
|
||||
if serviceId == "" || nodeName == "" {
|
||||
return nil, errors.New("serviceId and nodeName must be filled for this request to be valid")
|
||||
}
|
||||
|
||||
submitConf, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitConf.Payload = &PubSubOwner{
|
||||
OwnerUseCase: &ConfigureOwner{
|
||||
Node: nodeName,
|
||||
Form: NewForm(fields,
|
||||
FormTypeSubmit)},
|
||||
}
|
||||
|
||||
return submitConf, nil
|
||||
}
|
||||
|
||||
// GetFormFields gets the fields from a form in a IQ stanza of type result, as a map.
|
||||
// Key is the "var" attribute of the field, and field is the value.
|
||||
// The user can then select and modify the fields they want to alter, and submit a new form to the service using the
|
||||
// NewFormSubmission function to build the IQ.
|
||||
// TODO : remove restriction on IQ type ?
|
||||
func (iq *IQ) GetFormFields() (map[string]*Field, error) {
|
||||
if iq.Type != IQTypeResult {
|
||||
return nil, errors.New("this IQ is not a result type IQ. Cannot extract the form from it")
|
||||
}
|
||||
switch payload := iq.Payload.(type) {
|
||||
// We support IOT Control IQ
|
||||
case *PubSubGeneric:
|
||||
fieldMap := make(map[string]*Field)
|
||||
for _, elt := range payload.Configure.Form.Fields {
|
||||
fieldMap[elt.Var] = elt
|
||||
}
|
||||
return fieldMap, nil
|
||||
case *PubSubOwner:
|
||||
fieldMap := make(map[string]*Field)
|
||||
co, ok := payload.OwnerUseCase.(*ConfigureOwner)
|
||||
if !ok {
|
||||
return nil, errors.New("this IQ does not contain a PubSub payload with a configure tag for the owner namespace")
|
||||
}
|
||||
for _, elt := range co.Form.Fields {
|
||||
fieldMap[elt.Var] = elt
|
||||
}
|
||||
return fieldMap, nil
|
||||
|
||||
case *Command:
|
||||
fieldMap := make(map[string]*Field)
|
||||
var form *Form
|
||||
for _, ce := range payload.CommandElements {
|
||||
fo, ok := ce.(*Form)
|
||||
if ok {
|
||||
form = fo
|
||||
break
|
||||
}
|
||||
}
|
||||
if form == nil {
|
||||
return nil, errors.New("this IQ does not contain a command payload with a form")
|
||||
}
|
||||
for _, elt := range form.Fields {
|
||||
fieldMap[elt.Var] = elt
|
||||
}
|
||||
return fieldMap, nil
|
||||
default:
|
||||
if iq.Any != nil {
|
||||
fieldMap := make(map[string]*Field)
|
||||
if iq.Any.XMLName.Local != "command" {
|
||||
return nil, errors.New("this IQ does not contain a form")
|
||||
}
|
||||
|
||||
for _, nde := range iq.Any.Nodes {
|
||||
if nde.XMLName.Local == "x" {
|
||||
for _, n := range nde.Nodes {
|
||||
if n.XMLName.Local == "field" {
|
||||
f := Field{}
|
||||
data, err := xml.Marshal(n)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
err = xml.Unmarshal(data, &f)
|
||||
if err == nil {
|
||||
fieldMap[f.Var] = &f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fieldMap, nil
|
||||
}
|
||||
return nil, errors.New("this IQ does not contain a form")
|
||||
}
|
||||
}
|
||||
|
||||
func (pso *PubSubOwner) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
pso.XMLName = start.Name
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
// Decode sub-elements
|
||||
var err error
|
||||
switch tt.Name.Local {
|
||||
|
||||
case "affiliations":
|
||||
aff := AffiliationsOwner{}
|
||||
err = d.DecodeElement(&aff, &tt)
|
||||
pso.OwnerUseCase = &aff
|
||||
case "configure":
|
||||
co := ConfigureOwner{}
|
||||
err = d.DecodeElement(&co, &tt)
|
||||
pso.OwnerUseCase = &co
|
||||
case "default":
|
||||
def := DefaultOwner{}
|
||||
err = d.DecodeElement(&def, &tt)
|
||||
pso.OwnerUseCase = &def
|
||||
case "delete":
|
||||
del := DeleteOwner{}
|
||||
err = d.DecodeElement(&del, &tt)
|
||||
pso.OwnerUseCase = &del
|
||||
case "purge":
|
||||
pu := PurgeOwner{}
|
||||
err = d.DecodeElement(&pu, &tt)
|
||||
pso.OwnerUseCase = &pu
|
||||
case "subscriptions":
|
||||
subs := SubscriptionsOwner{}
|
||||
err = d.DecodeElement(&subs, &tt)
|
||||
pso.OwnerUseCase = &subs
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub#owner", Local: "pubsub"}, PubSubOwner{})
|
||||
}
|
||||
@@ -0,0 +1,886 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// ******************************
|
||||
// * 8.2 Configure a Node
|
||||
// ******************************
|
||||
func TestNewConfigureNode(t *testing.T) {
|
||||
expectedReq := "<iq type=\"get\" id=\"config1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <configure node=\"princely_musings\"></configure> " +
|
||||
"</pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a configure node request: %v", err)
|
||||
}
|
||||
subR.Id = "config1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
|
||||
if pubsub.OwnerUseCase == nil {
|
||||
t.Fatalf("owner use case is nil")
|
||||
}
|
||||
|
||||
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||
if !ok {
|
||||
t.Fatalf("owner use case is not a configure tag")
|
||||
}
|
||||
|
||||
if ownrUsecase.Node == "" {
|
||||
t.Fatalf("could not parse node from config tag")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigureNodeResp(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<configure node="princely_musings">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||
</field>
|
||||
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||
<value>1028</value>
|
||||
</field>
|
||||
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||
<option label="Never">
|
||||
<value>never</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed">
|
||||
<value>on_sub</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||
<value>on_sub_and_presence</value>
|
||||
</option>
|
||||
<value>never</value>
|
||||
</field>
|
||||
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||
<option>
|
||||
<value>normal</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>headline</value>
|
||||
</option>
|
||||
<value>headline</value>
|
||||
</field>
|
||||
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||
<value>http://www.w3.org/2005/Atom</value>
|
||||
</field>
|
||||
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubOwnerPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pubsub.OwnerUseCase == nil {
|
||||
t.Fatalf("owner use case is nil")
|
||||
}
|
||||
|
||||
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||
if !ok {
|
||||
t.Fatalf("owner use case is not a configure tag")
|
||||
}
|
||||
|
||||
if ownrUsecase.Form == nil {
|
||||
t.Fatalf("form is nil in the parsed config tag")
|
||||
}
|
||||
|
||||
if len(ownrUsecase.Form.Fields) != 8 {
|
||||
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// *************************************************
|
||||
// * 8.3 Request Default Node Configuration Options
|
||||
// *************************************************
|
||||
|
||||
func TestNewRequestDefaultConfig(t *testing.T) {
|
||||
expectedReq := "<iq type=\"get\" id=\"def1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <default></default> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a default config request: %v", err)
|
||||
}
|
||||
subR.Id = "def1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
|
||||
if pubsub.OwnerUseCase == nil {
|
||||
t.Fatalf("owner use case is nil")
|
||||
}
|
||||
|
||||
_, ok = pubsub.OwnerUseCase.(*stanza.DefaultOwner)
|
||||
if !ok {
|
||||
t.Fatalf("owner use case is not a default tag")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRequestDefaultConfigResp(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<configure node="princely_musings">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||
</field>
|
||||
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||
<value>1028</value>
|
||||
</field>
|
||||
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||
<option label="Never">
|
||||
<value>never</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed">
|
||||
<value>on_sub</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||
<value>on_sub_and_presence</value>
|
||||
</option>
|
||||
<value>never</value>
|
||||
</field>
|
||||
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||
<option>
|
||||
<value>normal</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>headline</value>
|
||||
</option>
|
||||
<value>headline</value>
|
||||
</field>
|
||||
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||
<value>http://www.w3.org/2005/Atom</value>
|
||||
</field>
|
||||
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubOwnerPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pubsub.OwnerUseCase == nil {
|
||||
t.Fatalf("owner use case is nil")
|
||||
}
|
||||
|
||||
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||
if !ok {
|
||||
t.Fatalf("owner use case is not a configure tag")
|
||||
}
|
||||
|
||||
if ownrUsecase.Form == nil {
|
||||
t.Fatalf("form is nil in the parsed config tag")
|
||||
}
|
||||
|
||||
if len(ownrUsecase.Form.Fields) != 8 {
|
||||
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// ***********************
|
||||
// * 8.4 Delete a Node
|
||||
// ***********************
|
||||
|
||||
func TestNewDelNode(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"delete1\" to=\"pubsub.shakespeare.lit\" >" +
|
||||
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<delete node=\"princely_musings\"></delete> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a node delete request: %v", err)
|
||||
}
|
||||
subR.Id = "delete1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
|
||||
if pubsub.OwnerUseCase == nil {
|
||||
t.Fatalf("owner use case is nil")
|
||||
}
|
||||
|
||||
_, ok = pubsub.OwnerUseCase.(*stanza.DeleteOwner)
|
||||
if !ok {
|
||||
t.Fatalf("owner use case is not a delete tag")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDelNodeResp(t *testing.T) {
|
||||
response := `
|
||||
<iq id="delete1" to="pubsub.shakespeare.lit" type="set">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<delete node="princely_musings">
|
||||
<redirect uri="xmpp:hamlet@denmark.lit"/>
|
||||
</delete>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubOwnerPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pubsub.OwnerUseCase == nil {
|
||||
t.Fatalf("owner use case is nil")
|
||||
}
|
||||
|
||||
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.DeleteOwner)
|
||||
if !ok {
|
||||
t.Fatalf("owner use case is not a configure tag")
|
||||
}
|
||||
|
||||
if ownrUsecase.RedirectOwner == nil {
|
||||
t.Fatalf("redirect is nil in the delete tag")
|
||||
}
|
||||
|
||||
if ownrUsecase.RedirectOwner.URI == "" {
|
||||
t.Fatalf("could not parse redirect uri")
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * 8.5 Purge All Node Items
|
||||
// ****************************
|
||||
|
||||
func TestNewPurgeAllItems(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"purge1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<purge node=\"princely_musings\"></purge> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a purge all items request: %v", err)
|
||||
}
|
||||
subR.Id = "purge1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
|
||||
if pubsub.OwnerUseCase == nil {
|
||||
t.Fatalf("owner use case is nil")
|
||||
}
|
||||
|
||||
purge, ok := pubsub.OwnerUseCase.(*stanza.PurgeOwner)
|
||||
if !ok {
|
||||
t.Fatalf("owner use case is not a delete tag")
|
||||
}
|
||||
|
||||
if purge.Node == "" {
|
||||
t.Fatalf("could not parse purge targer node")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ************************************
|
||||
// * 8.6 Manage Subscription Requests
|
||||
// ************************************
|
||||
func TestNewApproveSubRequest(t *testing.T) {
|
||||
expectedReq := "<message id=\"approve1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||
"<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value> </field> <field var=\"pubsub#subid\">" +
|
||||
" <value>123-abc</value> </field> <field var=\"pubsub#node\"> <value>princely_musings</value> </field> " +
|
||||
"<field var=\"pubsub#subscriber_jid\"> <value>horatio@denmark.lit</value> </field> <field var=\"pubsub#allow\"> " +
|
||||
"<value>true</value> </field> </x> </message>"
|
||||
|
||||
apprForm := &stanza.Form{
|
||||
Type: stanza.FormTypeSubmit,
|
||||
Fields: []*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#subscribe_authorization"}},
|
||||
{Var: "pubsub#subid", ValuesList: []string{"123-abc"}},
|
||||
{Var: "pubsub#node", ValuesList: []string{"princely_musings"}},
|
||||
{Var: "pubsub#subscriber_jid", ValuesList: []string{"horatio@denmark.lit"}},
|
||||
{Var: "pubsub#allow", ValuesList: []string{"true"}},
|
||||
},
|
||||
}
|
||||
|
||||
subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a sub approval request: %v", err)
|
||||
}
|
||||
subR.Id = "approve1"
|
||||
|
||||
frm, ok := subR.Extensions[0].(*stanza.Form)
|
||||
if !ok {
|
||||
t.Fatalf("extension is not a from !")
|
||||
}
|
||||
|
||||
var allowField *stanza.Field
|
||||
|
||||
for _, f := range frm.Fields {
|
||||
if f.Var == "pubsub#allow" {
|
||||
allowField = f
|
||||
}
|
||||
}
|
||||
if allowField == nil || allowField.ValuesList[0] != "true" {
|
||||
t.Fatalf("could not correctly parse the allow field in the response from")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************
|
||||
// * 8.7 Process Pending Subscription Requests
|
||||
// ********************************************
|
||||
|
||||
func TestNewGetPendingSubRequests(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"pending1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\" node=\"http://jabber.org/protocol/pubsub#get-pending\" >" +
|
||||
"</command> </iq>"
|
||||
|
||||
subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a get pending subs request: %v", err)
|
||||
}
|
||||
subR.Id = "pending1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
command, ok := subR.Payload.(*stanza.Command)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a command !")
|
||||
}
|
||||
|
||||
if command.Action != stanza.CommandActionExecute {
|
||||
t.Fatalf("command should be execute !")
|
||||
}
|
||||
|
||||
if command.Node != "http://jabber.org/protocol/pubsub#get-pending" {
|
||||
t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGetPendingSubRequestsResp(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
|
||||
</field>
|
||||
<field type="list-single" var="pubsub#node">
|
||||
<option>
|
||||
<value>princely_musings</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>news_from_elsinore</value>
|
||||
</option>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`
|
||||
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse iq")
|
||||
}
|
||||
|
||||
_, ok := respIQ.Payload.(*stanza.Command)
|
||||
if !ok {
|
||||
t.Fatal("this iq payload is not a command")
|
||||
}
|
||||
|
||||
fMap, err := respIQ.GetFormFields()
|
||||
if err != nil || len(fMap) != 2 {
|
||||
t.Fatal("could not parse command form fields")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ********************************************
|
||||
// * 8.7 Process Pending Subscription Requests
|
||||
// ********************************************
|
||||
|
||||
func TestNewApprovePendingSubRequest(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"pending2\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\"" +
|
||||
"node=\"http://jabber.org/protocol/pubsub#get-pending\"sessionid=\"pubsub-get-pending:20031021T150901Z-600\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field xmlns=\"jabber:x:data\" var=\"pubsub#node\"> " +
|
||||
"<value xmlns=\"jabber:x:data\">princely_musings</value> </field> </x> </command> </iq>"
|
||||
|
||||
subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit",
|
||||
"pubsub-get-pending:20031021T150901Z-600",
|
||||
"princely_musings")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a approve pending sub request: %v", err)
|
||||
}
|
||||
subR.Id = "pending2"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
command, ok := subR.Payload.(*stanza.Command)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a command !")
|
||||
}
|
||||
|
||||
if command.Action != stanza.CommandActionExecute {
|
||||
t.Fatalf("command should be execute !")
|
||||
}
|
||||
|
||||
//if command.Node != "http://jabber.org/protocol/pubsub#get-pending"{
|
||||
// t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !")
|
||||
//}
|
||||
//
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************
|
||||
// * 8.8.1 Retrieve Subscriptions List
|
||||
// ********************************************
|
||||
|
||||
func TestNewSubListRqPl(t *testing.T) {
|
||||
expectedReq := "<iq type=\"get\" id=\"subman1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<subscriptions node=\"princely_musings\"></subscriptions> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a sub list request: %v", err)
|
||||
}
|
||||
subR.Id = "subman1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||
}
|
||||
|
||||
subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner)
|
||||
if !ok {
|
||||
t.Fatalf("pubsub doesn not contain a subscriptions node !")
|
||||
}
|
||||
|
||||
if subs.Node != "princely_musings" {
|
||||
t.Fatalf("subs node attribute should be princely_musings. Found %s", subs.Node)
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSubListRqPlResp(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="subman1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<subscriptions node="princely_musings">
|
||||
<subscription jid="hamlet@denmark.lit" subscription="subscribed"></subscription>
|
||||
<subscription jid="polonius@denmark.lit" subscription="unconfigured"></subscription>
|
||||
<subscription jid="bernardo@denmark.lit" subid="123-abc" subscription="subscribed"></subscription>
|
||||
<subscription jid="bernardo@denmark.lit" subid="004-yyy" subscription="subscribed"></subscription>
|
||||
</subscriptions>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse iq")
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatal("this iq payload is not a command")
|
||||
}
|
||||
|
||||
subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner)
|
||||
if !ok {
|
||||
t.Fatalf("pubsub doesn not contain a subscriptions node !")
|
||||
}
|
||||
|
||||
if len(subs.Subscriptions) != 4 {
|
||||
t.Fatalf("expected to find 4 subscriptions but got %d", len(subs.Subscriptions))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ********************************************
|
||||
// * 8.9.1 Retrieve Affiliations List
|
||||
// ********************************************
|
||||
|
||||
func TestNewAffiliationListRequest(t *testing.T) {
|
||||
expectedReq := "<iq type=\"get\" id=\"ent1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<affiliations node=\"princely_musings\"></affiliations> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create an affiliations list request: %v", err)
|
||||
}
|
||||
subR.Id = "ent1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||
}
|
||||
|
||||
affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
|
||||
if !ok {
|
||||
t.Fatalf("pubsub doesn not contain an affiliations node !")
|
||||
}
|
||||
|
||||
if affils.Node != "princely_musings" {
|
||||
t.Fatalf("affils node attribute should be princely_musings. Found %s", affils.Node)
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAffiliationListRequestResp(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="ent1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<affiliations node="princely_musings">
|
||||
<affiliation affiliation="owner" jid="hamlet@denmark.lit"/>
|
||||
<affiliation affiliation="outcast" jid="polonius@denmark.lit"/>
|
||||
</affiliations>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse iq")
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatal("this iq payload is not a command")
|
||||
}
|
||||
|
||||
affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
|
||||
if !ok {
|
||||
t.Fatalf("pubsub doesn not contain an affiliations node !")
|
||||
}
|
||||
|
||||
if len(affils.Affiliations) != 2 {
|
||||
t.Fatalf("expected to find 2 subscriptions but got %d", len(affils.Affiliations))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ********************************************
|
||||
// * 8.9.2 Modify Affiliation
|
||||
// ********************************************
|
||||
|
||||
func TestNewModifAffiliationRequest(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"ent3\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <affiliations node=\"princely_musings\"> " +
|
||||
"<affiliation affiliation=\"none\" jid=\"hamlet@denmark.lit\"></affiliation> " +
|
||||
"<affiliation affiliation=\"none\" jid=\"polonius@denmark.lit\"></affiliation> " +
|
||||
"<affiliation affiliation=\"publisher\" jid=\"bard@shakespeare.lit\"></affiliation> </affiliations> </pubsub> " +
|
||||
"</iq>"
|
||||
|
||||
affils := []stanza.AffiliationOwner{
|
||||
{
|
||||
AffiliationStatus: stanza.AffiliationStatusNone,
|
||||
Jid: "hamlet@denmark.lit",
|
||||
},
|
||||
{
|
||||
AffiliationStatus: stanza.AffiliationStatusNone,
|
||||
Jid: "polonius@denmark.lit",
|
||||
},
|
||||
{
|
||||
AffiliationStatus: stanza.AffiliationStatusPublisher,
|
||||
Jid: "bard@shakespeare.lit",
|
||||
},
|
||||
}
|
||||
|
||||
subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a modif affiliation request: %v", err)
|
||||
}
|
||||
subR.Id = "ent3"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||
}
|
||||
|
||||
as, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
|
||||
if !ok {
|
||||
t.Fatalf("pubsub doesn not contain an affiliations node !")
|
||||
}
|
||||
|
||||
if as.Node != "princely_musings" {
|
||||
t.Fatalf("affils node attribute should be princely_musings. Found %s", as.Node)
|
||||
}
|
||||
if len(as.Affiliations) != 3 {
|
||||
t.Fatalf("expected 3 affiliations, found %d", len(as.Affiliations))
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFormFields(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<configure node="princely_musings">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||
</field>
|
||||
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||
<value>1028</value>
|
||||
</field>
|
||||
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||
<option label="Never">
|
||||
<value>never</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed">
|
||||
<value>on_sub</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||
<value>on_sub_and_presence</value>
|
||||
</option>
|
||||
<value>never</value>
|
||||
</field>
|
||||
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||
<option>
|
||||
<value>normal</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>headline</value>
|
||||
</option>
|
||||
<value>headline</value>
|
||||
</field>
|
||||
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||
<value>http://www.w3.org/2005/Atom</value>
|
||||
</field>
|
||||
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
var iq stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &iq)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse IQ")
|
||||
}
|
||||
|
||||
fields, err := iq.GetFormFields()
|
||||
if len(fields) != 8 {
|
||||
t.Fatalf("could not correctly parse fields. Expected 8, found : %v", len(fields))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetFormFieldsCmd(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
|
||||
</field>
|
||||
<field type="list-single" var="pubsub#node">
|
||||
<option>
|
||||
<value>princely_musings</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>news_from_elsinore</value>
|
||||
</option>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`
|
||||
var iq stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &iq)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse IQ")
|
||||
}
|
||||
|
||||
fields, err := iq.GetFormFields()
|
||||
if len(fields) != 2 {
|
||||
t.Fatalf("could not correctly parse fields. Expected 2, found : %v", len(fields))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNewFormSubmissionOwner(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"config2\" to=\"pubsub.shakespeare.lit\">" +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <configure node=\"princely_musings\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\" > <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||
"<value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#item_expire\"> " +
|
||||
"<value>604800</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
|
||||
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value> " +
|
||||
"<value>courtiers</value> </field> </x> </configure> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewFormSubmissionOwner("pubsub.shakespeare.lit",
|
||||
"princely_musings",
|
||||
[]*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#item_expire", ValuesList: []string{"604800"}},
|
||||
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
|
||||
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a form submission request: %v", err)
|
||||
}
|
||||
subR.Id = "config2"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub in namespace owner !")
|
||||
}
|
||||
|
||||
conf, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
|
||||
if !ok {
|
||||
t.Fatalf("pubsub does not contain a configure node !")
|
||||
}
|
||||
|
||||
if conf.Form == nil {
|
||||
t.Fatalf("the form is absent from the configuration submission !")
|
||||
}
|
||||
if len(conf.Form.Fields) != 4 {
|
||||
t.Fatalf("expected 4 fields, found %d", len(conf.Form.Fields))
|
||||
}
|
||||
if len(conf.Form.Fields[3].ValuesList) != 3 {
|
||||
t.Fatalf("expected 3 values in fourth field, found %d", len(conf.Form.Fields[3].ValuesList))
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) {
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
|
||||
if err != nil {
|
||||
return &stanza.PubSubOwner{}, err
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
return nil, errors.New("this iq payload is not a pubsub of the owner namespace")
|
||||
}
|
||||
|
||||
return pubsub, nil
|
||||
}
|
||||
@@ -0,0 +1,923 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
var submitFormExample = stanza.NewForm([]*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}},
|
||||
{Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}},
|
||||
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
|
||||
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
|
||||
{Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}},
|
||||
{
|
||||
Var: "pubsub#notification_type",
|
||||
Type: "list-single",
|
||||
Label: "Specify the delivery style for event notifications",
|
||||
ValuesList: []string{"headline"},
|
||||
Options: []stanza.Option{
|
||||
{ValuesList: []string{"normal"}},
|
||||
{ValuesList: []string{"headline"}},
|
||||
},
|
||||
},
|
||||
}, stanza.FormTypeSubmit)
|
||||
|
||||
// ***********************************
|
||||
// * 6.1 Subscribe to a Node
|
||||
// ***********************************
|
||||
|
||||
func TestNewSubRequest(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></subscribe>" +
|
||||
" </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a sub request: %v", err)
|
||||
}
|
||||
subR.Id = "sub1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNewSubResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscription node="princely_musings" jid="francisco@denmark.lit"
|
||||
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pubsub.Subscription == nil {
|
||||
t.Fatalf("subscription node is nil")
|
||||
}
|
||||
if pubsub.Subscription.Node == "" ||
|
||||
pubsub.Subscription.Jid == "" ||
|
||||
pubsub.Subscription.SubId == nil ||
|
||||
pubsub.Subscription.SubStatus == "" {
|
||||
t.Fatalf("one or more of the subscription attributes was not successfully decoded")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ***********************************
|
||||
// * 6.2 Unsubscribe from a Node
|
||||
// ***********************************
|
||||
|
||||
func TestNewUnSubRequest(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"unsub1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||
"<unsubscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></unsubscribe> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create an unsub request: %v", err)
|
||||
}
|
||||
subR.Id = "unsub1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Unsubscribe == nil {
|
||||
t.Fatalf("Unsubscribe tag should be present in sub config options request")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUnsubResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="unsub1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscription node="princely_musings" jid="francisco@denmark.lit" subscription="none"
|
||||
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3"/>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pubsub.Subscription == nil {
|
||||
t.Fatalf("subscription node is nil")
|
||||
}
|
||||
if pubsub.Subscription.Node == "" ||
|
||||
pubsub.Subscription.Jid == "" ||
|
||||
pubsub.Subscription.SubId == nil ||
|
||||
pubsub.Subscription.SubStatus == "" {
|
||||
t.Fatalf("one or more of the subscription attributes was not successfully decoded")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 6.3 Configure Subscription Options
|
||||
// ***************************************
|
||||
func TestNewSubOptsRq(t *testing.T) {
|
||||
expectedReq := "<iq type=\"get\"id=\"options1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||
"<options node=\"princely_musings\" jid=\"francisco@denmark.lit\"></options> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a sub options request: %v", err)
|
||||
}
|
||||
subR.Id = "options1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.SubOptions == nil {
|
||||
t.Fatalf("Options tag should be present in sub config options request")
|
||||
}
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNewConfOptsRsp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="options1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<options node="princely_musings" jid="francisco@denmark.lit">
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="FORM_TYPE" type="hidden">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
|
||||
</field>
|
||||
<field var="pubsub#deliver" type="boolean" label="Enable delivery?">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#digest" type="boolean"
|
||||
label="Receive digest notifications (approx. one per day)?">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field var="pubsub#include_body" type="boolean"
|
||||
label="Receive message body in addition to payload?">
|
||||
<value>false</value>
|
||||
</field>
|
||||
<field var="pubsub#show-values" type="list-multi"
|
||||
label="Select the presence types which are
|
||||
allowed to receive event notifications">
|
||||
<option label="Want to Chat">
|
||||
<value>chat</value>
|
||||
</option>
|
||||
<option label="Available">
|
||||
<value>online</value>
|
||||
</option>
|
||||
<option label="Away">
|
||||
<value>away</value>
|
||||
</option>
|
||||
<option label="Extended Away">
|
||||
<value>xa</value>
|
||||
</option>
|
||||
<option label="Do Not Disturb">
|
||||
<value>dnd</value>
|
||||
</option>
|
||||
<value>chat</value>
|
||||
<value>online</value>
|
||||
</field>
|
||||
</x>
|
||||
</options>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pubsub.SubOptions == nil {
|
||||
t.Fatalf("sub options node is nil")
|
||||
}
|
||||
if pubsub.SubOptions.Form == nil {
|
||||
t.Fatalf("the response form is nil")
|
||||
}
|
||||
|
||||
if len(pubsub.SubOptions.Form.Fields) != 5 {
|
||||
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 6.3.5 Form Submission
|
||||
// ***************************************
|
||||
func TestNewFormSubmission(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"options2\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <options node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\">" +
|
||||
" <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#title\"> " +
|
||||
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
|
||||
"<value>1</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
|
||||
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value>" +
|
||||
" <value>courtiers</value> </field> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
|
||||
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
|
||||
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
|
||||
"</field> </x> </options> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
|
||||
subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a form submission request: %v", err)
|
||||
}
|
||||
subR.Id = "options2"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.SubOptions == nil {
|
||||
t.Fatalf("Options tag should be present in sub config options request")
|
||||
}
|
||||
if pubsub.SubOptions.Form == nil {
|
||||
t.Fatalf("No form in form submit request !")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 6.3.7 Subscribe and Configure
|
||||
// ***************************************
|
||||
|
||||
func TestNewSubAndConfig(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\">" +
|
||||
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
|
||||
"</subscribe>" +
|
||||
"<options> <x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\">" +
|
||||
" <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#title\"> " +
|
||||
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
|
||||
"<value>1</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
|
||||
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value>" +
|
||||
" <value>courtiers</value> </field> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
|
||||
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
|
||||
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
|
||||
"</field> </x> </options> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
|
||||
subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a sub and config request: %v", err)
|
||||
}
|
||||
subR.Id = "sub1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.SubOptions == nil {
|
||||
t.Fatalf("Options tag should be present in sub config options request")
|
||||
}
|
||||
if pubsub.SubOptions.Form == nil {
|
||||
t.Fatalf("No form in form submit request !")
|
||||
}
|
||||
|
||||
// The <options/> element MUST NOT possess a 'node' attribute or 'jid' attribute
|
||||
// See XEP-0060
|
||||
if pubsub.SubOptions.SubInfo.Node != "" || pubsub.SubOptions.SubInfo.Jid != "" {
|
||||
t.Fatalf("SubInfo node and jid should be empty for the options tag !")
|
||||
}
|
||||
if pubsub.Subscribe.Node == "" || pubsub.Subscribe.Jid == "" {
|
||||
t.Fatalf("SubInfo node and jid should NOT be empty for the subscribe tag !")
|
||||
}
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSubAndConfigResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscription node="princely_musings" jid="francisco@denmark.lit"
|
||||
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
|
||||
<options>
|
||||
<x xmlns="jabber:x:data" type="result">
|
||||
<field var="FORM_TYPE" type="hidden">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
|
||||
</field>
|
||||
<field var="pubsub#deliver">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#digest">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field var="pubsub#include_body">
|
||||
<value>false</value>
|
||||
</field>
|
||||
<field var="pubsub#show-values">
|
||||
<value>chat</value>
|
||||
<value>online</value>
|
||||
<value>away</value>
|
||||
</field>
|
||||
</x>
|
||||
</options>
|
||||
</pubsub>
|
||||
</iq>
|
||||
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pubsub.Subscription == nil {
|
||||
t.Fatalf("sub node is nil")
|
||||
}
|
||||
|
||||
if pubsub.SubOptions == nil {
|
||||
t.Fatalf("sub options node is nil")
|
||||
}
|
||||
if pubsub.SubOptions.Form == nil {
|
||||
t.Fatalf("the response form is nil")
|
||||
}
|
||||
|
||||
if len(pubsub.SubOptions.Form.Fields) != 5 {
|
||||
t.Fatalf("one or more fields in the response form could not be parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 6.5.2 Requesting All List
|
||||
// ***************************************
|
||||
func TestNewItemsRequest(t *testing.T) {
|
||||
subR, err := stanza.NewItemsRequest("pubsub.shakespeare.lit", "princely_musings", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create an items request : %s", err)
|
||||
}
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Items == nil {
|
||||
t.Fatalf("List tag should be present to request items from a service")
|
||||
}
|
||||
if len(pubsub.Items.List) != 0 {
|
||||
t.Fatalf("There should be no items in the <items> tag to request all items from a service")
|
||||
}
|
||||
}
|
||||
func TestNewItemsResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="items2">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<items node="princely_musings">
|
||||
<item id="4e30f35051b7b8b42abe083742187228">
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Alone</title>
|
||||
<summary> Now I am alone. O, what a rogue and peasant slave am I! </summary>
|
||||
<link rel="alternate" type="text/html"
|
||||
href="http://denmark.lit/2003/12/13/atom03"/>
|
||||
<id>tag:denmark.lit,2003:entry-32396</id>
|
||||
<published>2003-12-13T11:09:53Z</published>
|
||||
<updated>2003-12-13T11:09:53Z</updated>
|
||||
</entry>
|
||||
</item>
|
||||
<item id="ae890ac52d0df67ed7cfdf51b644e901">
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Soliloquy</title>
|
||||
<summary> To be, or not to be: that is the question: Whether 'tis nobler in the
|
||||
mind to suffer The slings and arrows of outrageous fortune, Or to take arms
|
||||
against a sea of troubles, And by opposing end them? </summary>
|
||||
<link rel="alternate" type="text/html"
|
||||
href="http://denmark.lit/2003/12/13/atom03"/>
|
||||
<id>tag:denmark.lit,2003:entry-32397</id>
|
||||
<published>2003-12-13T18:30:02Z</published>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
</entry>
|
||||
</item>
|
||||
</items>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pubsub.Items == nil {
|
||||
t.Fatalf("sub options node is nil")
|
||||
}
|
||||
if pubsub.Items.List == nil {
|
||||
t.Fatalf("the response form is nil")
|
||||
}
|
||||
|
||||
if len(pubsub.Items.List) != 2 {
|
||||
t.Fatalf("one or more items in the response could not be parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 6.5.8 Requesting a Particular Item
|
||||
// ***************************************
|
||||
func TestNewSpecificItemRequest(t *testing.T) {
|
||||
expectedReq := "<iq type=\"get\" id=\"items3\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <items node=\"princely_musings\"> " +
|
||||
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </items> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a specific item request: %v", err)
|
||||
}
|
||||
subR.Id = "items3"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Items == nil {
|
||||
t.Fatalf("List tag should be present to request items from a service")
|
||||
}
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 7.1 Publish an Item to a Node
|
||||
// ***************************************
|
||||
func TestNewPublishItemRq(t *testing.T) {
|
||||
item := stanza.Item{
|
||||
XMLName: xml.Name{},
|
||||
Id: "",
|
||||
Publisher: "",
|
||||
Any: &stanza.Node{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://www.w3.org/2005/Atom",
|
||||
Local: "entry",
|
||||
},
|
||||
Attrs: nil,
|
||||
Content: "",
|
||||
Nodes: []stanza.Node{
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "title"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item title",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "summary"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content summary",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "link"},
|
||||
Attrs: []xml.Attr{
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "rel"},
|
||||
Value: "alternate",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "type"},
|
||||
Value: "text/html",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "href"},
|
||||
Value: "http://denmark.lit/2003/12/13/atom03",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "id"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content ID",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "published"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "updated"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
subR, err := stanza.NewPublishItemRq("pubsub.shakespeare.lit", "princely_musings", "bnd81g37d61f49fgn581", item)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create an item pub request : %s", err)
|
||||
}
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(pubsub.Publish.Node) == "" {
|
||||
t.Fatalf("the <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node.")
|
||||
}
|
||||
if pubsub.Publish.Items[0].Id == "" {
|
||||
t.Fatalf("an id was provided for the item and it should be used")
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 7.1.5 Publishing Options
|
||||
// ***************************************
|
||||
|
||||
func TestNewPublishItemOptsRq(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"pub1\"to=\"pubsub.shakespeare.lit\"> <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||
"<publish node=\"princely_musings\"> <item id=\"ae890ac52d0df67ed7cfdf51b644e901\"> " +
|
||||
"<entry xmlns=\"http://www.w3.org/2005/Atom\"> <title>Soliloquy</title> " +
|
||||
"<summary> To be, or not to be: that is the question: Whether \"tis nobler in the mind to suffer The " +
|
||||
"slings and arrows of outrageous fortune, Or to take arms against a sea of troubles, And by opposing end them? " +
|
||||
"</summary> <link rel=\"alternate\" type=\"text/html\"href=\"http://denmark.lit/2003/12/13/atom03\"></link> " +
|
||||
"<id>tag:denmark.lit,2003:entry-32397</id> <published>2003-12-13T18:30:02Z</published> " +
|
||||
"<updated>2003-12-13T18:30:02Z</updated> </entry> </item> </publish> <publish-options> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||
"<value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var=\"pubsub#access_model\"> " +
|
||||
"<value>presence</value> </field> </x> </publish-options> </pubsub> </iq>"
|
||||
|
||||
var iq stanza.IQ
|
||||
err := xml.Unmarshal([]byte(expectedReq), &iq)
|
||||
if err != nil {
|
||||
t.Fatalf("could not unmarshal example request : %s", err)
|
||||
}
|
||||
|
||||
pubsub, ok := iq.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Publish == nil {
|
||||
t.Fatalf("Publish tag is empty")
|
||||
}
|
||||
if len(pubsub.Publish.Items) != 1 {
|
||||
t.Fatalf("could not parse item properly")
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 7.2 Delete an Item from a Node
|
||||
// ***************************************
|
||||
|
||||
func TestNewDelItemFromNode(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"retract1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <retract node=\"princely_musings\"> " +
|
||||
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </retract> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a delete item from node request: %v", err)
|
||||
}
|
||||
subR.Id = "retract1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Retract == nil {
|
||||
t.Fatalf("Retract tag should be present to del an item from a service")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(pubsub.Retract.Items[0].Id) == "" {
|
||||
t.Fatalf("Item id, for the item to delete, should be non empty")
|
||||
}
|
||||
if pubsub.Retract.Items[0].Any != nil {
|
||||
t.Fatalf("Item node must be empty")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 8.1 Create a Node
|
||||
// ***************************************
|
||||
|
||||
func TestNewCreateNode(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"create1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a create node request: %v", err)
|
||||
}
|
||||
subR.Id = "create1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Create == nil {
|
||||
t.Fatalf("Create tag should be present to create a node on a service")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(pubsub.Create.Node) == "" {
|
||||
t.Fatalf("Expected node name to be present")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCreateNodeResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="hamlet@denmark.lit/elsinore" id="create2">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<create node="25e3d37dabbab9541f7523321421edc5bfeb2dae"/>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pubsub.Create == nil {
|
||||
t.Fatalf("create segment is nil")
|
||||
}
|
||||
if pubsub.Create.Node == "" {
|
||||
t.Fatalf("could not parse generated nodeId")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 8.1.3 Create and Configure a Node
|
||||
// ***************************************
|
||||
|
||||
func TestNewCreateAndConfigNode(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\" id=\"create1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> " +
|
||||
"<configure> <x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\" > " +
|
||||
"<value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#notify_retract\"> " +
|
||||
"<value>0</value> </field> <field var=\"pubsub#notify_sub\"> <value>0</value> </field> " +
|
||||
"<field var=\"pubsub#max_payload_size\"> <value>1028</value> </field> </x> </configure> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewCreateAndConfigNode("pubsub.shakespeare.lit",
|
||||
"princely_musings",
|
||||
&stanza.Form{
|
||||
Type: stanza.FormTypeSubmit,
|
||||
Fields: []*stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#notify_retract", ValuesList: []string{"0"}},
|
||||
{Var: "pubsub#notify_sub", ValuesList: []string{"0"}},
|
||||
{Var: "pubsub#max_payload_size", ValuesList: []string{"1028"}},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a create and config node request: %v", err)
|
||||
}
|
||||
subR.Id = "create1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
}
|
||||
|
||||
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Create == nil {
|
||||
t.Fatalf("Create tag should be present to create a node on a service")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(pubsub.Create.Node) == "" {
|
||||
t.Fatalf("Expected node name to be present")
|
||||
}
|
||||
|
||||
if pubsub.Configure == nil {
|
||||
t.Fatalf("Configure tag should be present to configure a node during its creation on a service")
|
||||
}
|
||||
|
||||
if pubsub.Configure.Form == nil {
|
||||
t.Fatalf("Expected a form to be present, to configure the node")
|
||||
}
|
||||
if len(pubsub.Configure.Form.Fields) != 4 {
|
||||
t.Fatalf("Expected 4 elements to be present in the config form but got : %v", len(pubsub.Configure.Form.Fields))
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ********************************
|
||||
// * 5.7 Retrieve Subscriptions
|
||||
// ********************************
|
||||
|
||||
func TestNewRetrieveAllSubsRequest(t *testing.T) {
|
||||
expected := "<iq type=\"get\" id=\"subscriptions1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscriptions></subscriptions> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a get all subs request: %v", err)
|
||||
}
|
||||
subR.Id = "subscriptions1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expected, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllSubsResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="subscriptions1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscriptions>
|
||||
<subscription node="node1" jid="francisco@denmark.lit" subscription="subscribed"/>
|
||||
<subscription node="node2" jid="francisco@denmark.lit" subscription="subscribed"/>
|
||||
<subscription node="node5" jid="francisco@denmark.lit" subscription="unconfigured"/>
|
||||
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
|
||||
subid="123-abc"/>
|
||||
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
|
||||
subid="004-yyy"/>
|
||||
</subscriptions>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("could not unmarshal response: %s", err)
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("umarshalled payload is not a pubsub")
|
||||
}
|
||||
|
||||
if pubsub.Subscriptions == nil {
|
||||
t.Fatalf("subscriptions node is nil")
|
||||
}
|
||||
if len(pubsub.Subscriptions.List) != 5 {
|
||||
t.Fatalf("incorrect number of decoded subscriptions")
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************
|
||||
// * 5.7 Retrieve Affiliations
|
||||
// ********************************
|
||||
|
||||
func TestNewRetrieveAllAffilsRequest(t *testing.T) {
|
||||
expected := "<iq type=\"get\"id=\"affil1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <affiliations></affiliations> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a get all affiliations request: %v", err)
|
||||
}
|
||||
subR.Id = "affil1"
|
||||
|
||||
if _, e := checkMarshalling(t, subR); e != nil {
|
||||
t.Fatalf("Failed to check marshalling for generated retreive all affiliations request : %s", e)
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expected, string(data)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAllAffilsResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="affil1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<affiliations>
|
||||
<affiliation node="node1" affiliation="owner"/>
|
||||
<affiliation node="node2" affiliation="publisher"/>
|
||||
<affiliation node="node5" affiliation="outcast"/>
|
||||
<affiliation node="node6" affiliation="owner"/>
|
||||
</affiliations>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("could not unmarshal response: %s", err)
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("umarshalled payload is not a pubsub")
|
||||
}
|
||||
|
||||
if pubsub.Affiliations == nil {
|
||||
t.Fatalf("subscriptions node is nil")
|
||||
}
|
||||
if len(pubsub.Affiliations.List) != 4 {
|
||||
t.Fatalf("incorrect number of decoded subscriptions")
|
||||
}
|
||||
}
|
||||
|
||||
func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) {
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
|
||||
if err != nil {
|
||||
return &stanza.PubSubGeneric{}, err
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
return nil, errors.New("this iq payload is not a pubsub")
|
||||
}
|
||||
|
||||
return pubsub, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
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.
|
||||
// That should make it possible to be able to share the decoder.
|
||||
// TODO: Ensure that a client can add its own custom namespace to the registry (or overload existing ones).
|
||||
|
||||
type PacketType uint8
|
||||
|
||||
const (
|
||||
PKTPresence PacketType = iota
|
||||
PKTMessage
|
||||
PKTIQ
|
||||
)
|
||||
|
||||
var TypeRegistry = newRegistry()
|
||||
|
||||
// We store different registries per packet type and namespace.
|
||||
type registryKey struct {
|
||||
packetType PacketType
|
||||
namespace string
|
||||
}
|
||||
|
||||
type registryForNamespace map[string]reflect.Type
|
||||
|
||||
type registry struct {
|
||||
// We store different registries per packet type and namespace.
|
||||
msgTypes map[registryKey]registryForNamespace
|
||||
// Handle concurrent access
|
||||
msgTypesLock *sync.RWMutex
|
||||
}
|
||||
|
||||
func newRegistry() *registry {
|
||||
return ®istry{
|
||||
msgTypes: make(map[registryKey]registryForNamespace),
|
||||
msgTypesLock: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// MapExtension stores extension type for packet payload.
|
||||
// The match is done per PacketType (iq, message, or presence) and XML tag name.
|
||||
// You can use the alias "*" as local XML name to be able to match all unknown tag name for that
|
||||
// packet type and namespace.
|
||||
func (r *registry) MapExtension(pktType PacketType, name xml.Name, extension MsgExtension) {
|
||||
key := registryKey{pktType, name.Space}
|
||||
r.msgTypesLock.RLock()
|
||||
store := r.msgTypes[key]
|
||||
r.msgTypesLock.RUnlock()
|
||||
|
||||
r.msgTypesLock.Lock()
|
||||
defer r.msgTypesLock.Unlock()
|
||||
if store == nil {
|
||||
store = make(map[string]reflect.Type)
|
||||
}
|
||||
store[name.Local] = reflect.TypeOf(extension)
|
||||
r.msgTypes[key] = store
|
||||
}
|
||||
|
||||
// GetExtensionType returns extension type for packet payload, based on packet type and tag name.
|
||||
func (r *registry) GetExtensionType(pktType PacketType, name xml.Name) reflect.Type {
|
||||
key := registryKey{pktType, name.Space}
|
||||
|
||||
r.msgTypesLock.RLock()
|
||||
defer r.msgTypesLock.RUnlock()
|
||||
store := r.msgTypes[key]
|
||||
result := store[name.Local]
|
||||
if result == nil && name.Local != "*" {
|
||||
return store["*"]
|
||||
}
|
||||
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 {
|
||||
if extensionType := r.GetExtensionType(PKTMessage, name); extensionType != nil {
|
||||
val := reflect.New(extensionType)
|
||||
elt := val.Interface()
|
||||
if msgExt, ok := elt.(MsgExtension); ok {
|
||||
return msgExt
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIQExtension returns an instance of IQPayload, by matching packet type and XML
|
||||
// tag name against the registry.
|
||||
func (r *registry) GetIQExtension(name xml.Name) IQPayload {
|
||||
if extensionType := r.GetExtensionType(PKTIQ, name); extensionType != nil {
|
||||
val := reflect.New(extensionType)
|
||||
elt := val.Interface()
|
||||
if iqExt, ok := elt.(IQPayload); ok {
|
||||
return iqExt
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegistry_RegisterMsgExt(t *testing.T) {
|
||||
// Setup registry
|
||||
typeRegistry := newRegistry()
|
||||
|
||||
// Register an element
|
||||
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
|
||||
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
|
||||
|
||||
// Match that element
|
||||
receipt := typeRegistry.GetMsgExtension(name)
|
||||
if receipt == nil {
|
||||
t.Error("cannot read element type from registry")
|
||||
return
|
||||
}
|
||||
|
||||
switch r := receipt.(type) {
|
||||
case *ReceiptRequest:
|
||||
default:
|
||||
t.Errorf("Registry did not return expected type ReceiptRequest: %v", reflect.TypeOf(r))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRegistryGet(b *testing.B) {
|
||||
// Setup registry
|
||||
typeRegistry := newRegistry()
|
||||
|
||||
// Register an element
|
||||
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
|
||||
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Match that element
|
||||
receipt := typeRegistry.GetExtensionType(PKTMessage, name)
|
||||
if receipt == nil {
|
||||
b.Error("cannot read element type from registry")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Support for XEP-0059
|
||||
// See https://xmpp.org/extensions/xep-0059
|
||||
const (
|
||||
// Common but not only possible namespace for query blocks in a result set context
|
||||
NSQuerySet = "jabber:iq:search"
|
||||
)
|
||||
|
||||
type ResultSet struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
|
||||
After *string `xml:"after,omitempty"`
|
||||
Before *string `xml:"before,omitempty"`
|
||||
Count *int `xml:"count,omitempty"`
|
||||
First *First `xml:"first,omitempty"`
|
||||
Index *int `xml:"index,omitempty"`
|
||||
Last *string `xml:"last,omitempty"`
|
||||
Max *int `xml:"max,omitempty"`
|
||||
}
|
||||
|
||||
type First struct {
|
||||
XMLName xml.Name `xml:"first"`
|
||||
Content string `xml:",chardata"`
|
||||
Index *int `xml:"index,attr,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Limiting the number of items
|
||||
func TestNewResultSetReq(t *testing.T) {
|
||||
expectedRq := "<iq id=\"q29302\" type=\"set\"> <query xmlns=\"urn:xmpp:mam:2\"> " +
|
||||
"<x type=\"submit\" xmlns=\"jabber:x:data\"> <field type=\"hidden\" var=\"FORM_TYPE\"> " +
|
||||
"<value>urn:xmpp:mam:2</value> </field> <field var=\"start\"> <value>2010-08-07T00:00:00Z</value> </field> </x> " +
|
||||
"<set xmlns=\"http://jabber.org/protocol/rsm\"> <max>10</max> </set> </query> </iq>"
|
||||
|
||||
maxVal := 10
|
||||
rs := &stanza.ResultSet{
|
||||
Max: &maxVal,
|
||||
}
|
||||
|
||||
// TODO when Mam is implemented
|
||||
_ = expectedRq
|
||||
_ = rs
|
||||
}
|
||||
|
||||
func TestUnmarshalResultSeqReq(t *testing.T) {
|
||||
// TODO when Mam is implemented
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// SASLAuth implements SASL Authentication initiation.
|
||||
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.2
|
||||
type SASLAuth struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
||||
Mechanism string `xml:"mechanism,attr"`
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// SASLSuccess implements SASL Success nonza, sent by server as a result of the
|
||||
// SASL auth negotiation.
|
||||
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.6
|
||||
type SASLSuccess struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
||||
}
|
||||
|
||||
func (SASLSuccess) Name() string {
|
||||
return "sasl:success"
|
||||
}
|
||||
|
||||
// SASLSuccess decoding
|
||||
type saslSuccessDecoder struct{}
|
||||
|
||||
var saslSuccess saslSuccessDecoder
|
||||
|
||||
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
|
||||
var packet SASLSuccess
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// SASLFailure
|
||||
type SASLFailure struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
||||
Any xml.Name // error reason is a subelement
|
||||
}
|
||||
|
||||
func (SASLFailure) Name() string {
|
||||
return "sasl:failure"
|
||||
}
|
||||
|
||||
// SASLFailure decoding
|
||||
type saslFailureDecoder struct{}
|
||||
|
||||
var saslFailure saslFailureDecoder
|
||||
|
||||
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
|
||||
var packet SASLFailure
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Resource binding
|
||||
|
||||
// Bind is an IQ payload used during session negotiation to bind user resource
|
||||
// to the current XMPP stream.
|
||||
// Reference: https://tools.ietf.org/html/rfc6120#section-7
|
||||
type Bind struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
||||
Resource string `xml:"resource,omitempty"`
|
||||
Jid string `xml:"jid,omitempty"`
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (b *Bind) Namespace() string {
|
||||
return b.XMLName.Space
|
||||
}
|
||||
|
||||
func (b *Bind) GetSet() *ResultSet {
|
||||
return b.ResultSet
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session (Obsolete)
|
||||
|
||||
// Session is both a stream feature and an obsolete IQ Payload, used to bind a
|
||||
// resource to the current XMPP stream on RFC 3121 only XMPP servers.
|
||||
// Session is obsolete in RFC 6121. It is added to Fluux XMPP for compliance
|
||||
// with RFC 3121.
|
||||
// Reference: https://xmpp.org/rfcs/rfc3921.html#session
|
||||
//
|
||||
// This is the draft defining how to handle the transition:
|
||||
// https://tools.ietf.org/html/draft-cridland-xmpp-session-01
|
||||
type StreamSession struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
||||
Optional *struct{} // If element does exist, it mean we are not required to open session
|
||||
// Result sets
|
||||
ResultSet *ResultSet `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StreamSession) Namespace() string {
|
||||
return s.XMLName.Space
|
||||
}
|
||||
|
||||
func (s *StreamSession) GetSet() *ResultSet {
|
||||
return s.ResultSet
|
||||
}
|
||||
|
||||
func (s *StreamSession) IsOptional() bool {
|
||||
if s.XMLName.Local == "session" {
|
||||
return s.Optional != nil
|
||||
}
|
||||
// If session element is missing, then we should not use session
|
||||
return true
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-bind", Local: "bind"}, Bind{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-session", Local: "session"}, StreamSession{})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// Check that we can detect optional session from advertised stream features
|
||||
func TestSessionFeatures(t *testing.T) {
|
||||
streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: &struct{}{}}}
|
||||
|
||||
data, err := xml.Marshal(streamFeatures)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
}
|
||||
|
||||
parsedStream := stanza.StreamFeatures{}
|
||||
if err = xml.Unmarshal(data, &parsedStream); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
|
||||
}
|
||||
|
||||
if !parsedStream.Session.IsOptional() {
|
||||
t.Error("Session should be optional")
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the Session tag can be used in IQ decoding
|
||||
func TestSessionIQ(t *testing.T) {
|
||||
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create IQ: %v", err)
|
||||
}
|
||||
iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: &struct{}{}}
|
||||
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
parsedIQ := stanza.IQ{}
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
|
||||
return
|
||||
}
|
||||
|
||||
session, ok := parsedIQ.Payload.(*stanza.StreamSession)
|
||||
if !ok {
|
||||
t.Error("Missing session payload")
|
||||
return
|
||||
}
|
||||
|
||||
if !session.IsOptional() {
|
||||
t.Error("Session should be optional")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Test Sasl mechanism
|
||||
@@ -0,0 +1,171 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type StanzaErrorGroup interface {
|
||||
GroupErrorName() string
|
||||
}
|
||||
|
||||
type BadFormat struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-format"`
|
||||
}
|
||||
|
||||
func (e *BadFormat) GroupErrorName() string { return "bad-format" }
|
||||
|
||||
type BadNamespacePrefix struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-namespace-prefix"`
|
||||
}
|
||||
|
||||
func (e *BadNamespacePrefix) GroupErrorName() string { return "bad-namespace-prefix" }
|
||||
|
||||
type Conflict struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas conflict"`
|
||||
}
|
||||
|
||||
func (e *Conflict) GroupErrorName() string { return "conflict" }
|
||||
|
||||
type ConnectionTimeout struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas connection-timeout"`
|
||||
}
|
||||
|
||||
func (e *ConnectionTimeout) GroupErrorName() string { return "connection-timeout" }
|
||||
|
||||
type HostGone struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-gone"`
|
||||
}
|
||||
|
||||
func (e *HostGone) GroupErrorName() string { return "host-gone" }
|
||||
|
||||
type HostUnknown struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-unknown"`
|
||||
}
|
||||
|
||||
func (e *HostUnknown) GroupErrorName() string { return "host-unknown" }
|
||||
|
||||
type ImproperAddressing struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas improper-addressing"`
|
||||
}
|
||||
|
||||
func (e *ImproperAddressing) GroupErrorName() string { return "improper-addressing" }
|
||||
|
||||
type InternalServerError struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error"`
|
||||
}
|
||||
|
||||
func (e *InternalServerError) GroupErrorName() string { return "internal-server-error" }
|
||||
|
||||
type InvalidForm struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-from"`
|
||||
}
|
||||
|
||||
func (e *InvalidForm) GroupErrorName() string { return "invalid-from" }
|
||||
|
||||
type InvalidId struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-id"`
|
||||
}
|
||||
|
||||
func (e *InvalidId) GroupErrorName() string { return "invalid-id" }
|
||||
|
||||
type InvalidNamespace struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-namespace"`
|
||||
}
|
||||
|
||||
func (e *InvalidNamespace) GroupErrorName() string { return "invalid-namespace" }
|
||||
|
||||
type InvalidXML struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-xml"`
|
||||
}
|
||||
|
||||
func (e *InvalidXML) GroupErrorName() string { return "invalid-xml" }
|
||||
|
||||
type NotAuthorized struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-authorized"`
|
||||
}
|
||||
|
||||
func (e *NotAuthorized) GroupErrorName() string { return "not-authorized" }
|
||||
|
||||
type NotWellFormed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-well-formed"`
|
||||
}
|
||||
|
||||
func (e *NotWellFormed) GroupErrorName() string { return "not-well-formed" }
|
||||
|
||||
type PolicyViolation struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas policy-violation"`
|
||||
}
|
||||
|
||||
func (e *PolicyViolation) GroupErrorName() string { return "policy-violation" }
|
||||
|
||||
type RemoteConnectionFailed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas remote-connection-failed"`
|
||||
}
|
||||
|
||||
func (e *RemoteConnectionFailed) GroupErrorName() string { return "remote-connection-failed" }
|
||||
|
||||
type Reset struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas reset"`
|
||||
}
|
||||
|
||||
func (e *Reset) GroupErrorName() string { return "reset" }
|
||||
|
||||
type ResourceConstraint struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint"`
|
||||
}
|
||||
|
||||
func (e *ResourceConstraint) GroupErrorName() string { return "resource-constraint" }
|
||||
|
||||
type RestrictedXML struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas restricted-xml"`
|
||||
}
|
||||
|
||||
func (e *RestrictedXML) GroupErrorName() string { return "restricted-xml" }
|
||||
|
||||
type SeeOtherHost struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas see-other-host"`
|
||||
}
|
||||
|
||||
func (e *SeeOtherHost) GroupErrorName() string { return "see-other-host" }
|
||||
|
||||
type SystemShutdown struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas system-shutdown"`
|
||||
}
|
||||
|
||||
func (e *SystemShutdown) GroupErrorName() string { return "system-shutdown" }
|
||||
|
||||
type UndefinedCondition struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition"`
|
||||
}
|
||||
|
||||
func (e *UndefinedCondition) GroupErrorName() string { return "undefined-condition" }
|
||||
|
||||
type UnsupportedEncoding struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-encoding"`
|
||||
}
|
||||
|
||||
type UnexpectedRequest struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request"`
|
||||
}
|
||||
|
||||
func (e *UnexpectedRequest) GroupErrorName() string { return "unexpected-request" }
|
||||
|
||||
func (e *UnsupportedEncoding) GroupErrorName() string { return "unsupported-encoding" }
|
||||
|
||||
type UnsupportedStanzaType struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-stanza-type"`
|
||||
}
|
||||
|
||||
func (e *UnsupportedStanzaType) GroupErrorName() string { return "unsupported-stanza-type" }
|
||||
|
||||
type UnsupportedVersion struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-version"`
|
||||
}
|
||||
|
||||
func (e *UnsupportedVersion) GroupErrorName() string { return "unsupported-version" }
|
||||
|
||||
type XMLNotWellFormed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas xml-not-well-formed"`
|
||||
}
|
||||
|
||||
func (e *XMLNotWellFormed) GroupErrorName() string { return "xml-not-well-formed" }
|
||||
@@ -0,0 +1,14 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Used during stream initiation / session establishment
|
||||
type TLSProceed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
||||
}
|
||||
|
||||
type tlsFailure struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Start of stream
|
||||
// Reference: XMPP Core stream open
|
||||
// https://tools.ietf.org/html/rfc6120#section-4.2
|
||||
type Stream struct {
|
||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"`
|
||||
From string `xml:"from,attr"`
|
||||
To string `xml:"to,attr"`
|
||||
Id string `xml:"id,attr"`
|
||||
Version string `xml:"version,attr"`
|
||||
}
|
||||
|
||||
const StreamClose = "</stream:stream>"
|
||||
@@ -0,0 +1,189 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// StreamFeatures Packet
|
||||
// Reference: The active stream features are published on
|
||||
// https://xmpp.org/registrar/stream-features.html
|
||||
// Note: That page misses draft and experimental XEP (i.e CSI, etc)
|
||||
|
||||
type StreamFeatures struct {
|
||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
||||
// Server capabilities hash
|
||||
Caps Caps
|
||||
// Stream features
|
||||
StartTLS TlsStartTLS
|
||||
Mechanisms saslMechanisms
|
||||
Bind Bind
|
||||
StreamManagement streamManagement
|
||||
// Obsolete
|
||||
Session StreamSession
|
||||
// ProcessOne Stream Features
|
||||
P1Push p1Push
|
||||
P1Rebind p1Rebind
|
||||
p1Ack p1Ack
|
||||
Any []xml.Name `xml:",any"`
|
||||
}
|
||||
|
||||
func (StreamFeatures) Name() string {
|
||||
return "stream:features"
|
||||
}
|
||||
|
||||
type streamFeatureDecoder struct{}
|
||||
|
||||
var streamFeatures streamFeatureDecoder
|
||||
|
||||
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
|
||||
var packet StreamFeatures
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// Capabilities
|
||||
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
|
||||
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
|
||||
// and peer servers do not need to send service discovery requests each time they connect."
|
||||
// This is not a stream feature but a way to let client cache server disco info.
|
||||
type Caps struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
|
||||
Hash string `xml:"hash,attr"`
|
||||
Node string `xml:"node,attr"`
|
||||
Ver string `xml:"ver,attr"`
|
||||
Ext string `xml:"ext,attr,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Supported Stream Features
|
||||
|
||||
// StartTLS feature
|
||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
|
||||
type TlsStartTLS struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
||||
Required bool
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing startTLS required flag
|
||||
func (stls *TlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
stls.XMLName = start.Name
|
||||
|
||||
// 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:
|
||||
elt := new(Node)
|
||||
|
||||
err = d.DecodeElement(elt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if elt.XMLName.Local == "required" {
|
||||
stls.Required = true
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sf *StreamFeatures) DoesStartTLS() (feature TlsStartTLS, isSupported bool) {
|
||||
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
||||
return sf.StartTLS, true
|
||||
}
|
||||
return feature, false
|
||||
}
|
||||
|
||||
// Mechanisms
|
||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
|
||||
type saslMechanisms struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
||||
Mechanism []string `xml:"mechanism"`
|
||||
}
|
||||
|
||||
// StreamManagement
|
||||
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
|
||||
type streamManagement struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
||||
}
|
||||
|
||||
func (streamManagement) Name() string {
|
||||
return "streamManagement"
|
||||
}
|
||||
|
||||
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
||||
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// P1 extensions
|
||||
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
|
||||
|
||||
// p1:push support
|
||||
type p1Push struct {
|
||||
XMLName xml.Name `xml:"p1:push push"`
|
||||
}
|
||||
|
||||
// p1:rebind suppor
|
||||
type p1Rebind struct {
|
||||
XMLName xml.Name `xml:"p1:rebind rebind"`
|
||||
}
|
||||
|
||||
// p1:ack support
|
||||
type p1Ack struct {
|
||||
XMLName xml.Name `xml:"p1:ack ack"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// StreamError Packet
|
||||
|
||||
type StreamError struct {
|
||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
|
||||
Error xml.Name `xml:",any"`
|
||||
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
|
||||
}
|
||||
|
||||
func (StreamError) Name() string {
|
||||
return "stream:error"
|
||||
}
|
||||
|
||||
type streamErrorDecoder struct{}
|
||||
|
||||
var streamError streamErrorDecoder
|
||||
|
||||
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
|
||||
var packet StreamError
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// StreamClose "Packet"
|
||||
|
||||
// This is just a closing tag and hold no information
|
||||
type StreamClosePacket struct{}
|
||||
|
||||
func (StreamClosePacket) Name() string {
|
||||
return "stream:stream"
|
||||
}
|
||||
|
||||
type streamCloseDecoder struct{}
|
||||
|
||||
var streamClose streamCloseDecoder
|
||||
|
||||
func (streamCloseDecoder) decode(_ xml.EndElement) StreamClosePacket {
|
||||
return StreamClosePacket{}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestNoStartTLS(t *testing.T) {
|
||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
|
||||
startTLS, ok := parsedSF.DoesStartTLS()
|
||||
if ok {
|
||||
t.Error("StartTLS feature should not be enabled")
|
||||
}
|
||||
if startTLS.Required {
|
||||
t.Error("StartTLS cannot be required as default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartTLS(t *testing.T) {
|
||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
||||
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
|
||||
<required/>
|
||||
</starttls>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
|
||||
startTLS, ok := parsedSF.DoesStartTLS()
|
||||
if !ok {
|
||||
t.Error("StartTLS feature should be enabled")
|
||||
}
|
||||
if !startTLS.Required {
|
||||
t.Error("StartTLS feature should be required")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Ability to support / detect previous version of stream management feature
|
||||
func TestStreamManagement(t *testing.T) {
|
||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
||||
<sm xmlns='urn:xmpp:sm:3'/>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
|
||||
ok := parsedSF.DoesStreamManagement()
|
||||
if !ok {
|
||||
t.Error("Stream Management feature should have been detected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
NSStreamManagement = "urn:xmpp:sm:3"
|
||||
)
|
||||
|
||||
type SMEnable struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 enable"`
|
||||
Max *uint `xml:"max,attr,omitempty"`
|
||||
Resume *bool `xml:"resume,attr,omitempty"`
|
||||
}
|
||||
|
||||
// Enabled as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#enable
|
||||
type SMEnabled struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 enabled"`
|
||||
Id string `xml:"id,attr,omitempty"`
|
||||
Location string `xml:"location,attr,omitempty"`
|
||||
Resume string `xml:"resume,attr,omitempty"`
|
||||
Max uint `xml:"max,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (SMEnabled) Name() string {
|
||||
return "Stream Management: enabled"
|
||||
}
|
||||
|
||||
type UnAckQueue struct {
|
||||
Uslice []*UnAckedStz
|
||||
sync.RWMutex
|
||||
}
|
||||
type UnAckedStz struct {
|
||||
Id int
|
||||
Stz string
|
||||
}
|
||||
|
||||
func NewUnAckQueue() *UnAckQueue {
|
||||
return &UnAckQueue{
|
||||
Uslice: make([]*UnAckedStz, 0, 10), // Capacity is 0 to comply with "Push" implementation (so that no reachable element is nil)
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UnAckedStz) QueueableName() string {
|
||||
return "Un-acknowledged stanza"
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) PeekN(n int) []Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
if len(uaq.Uslice) < n {
|
||||
n = len(uaq.Uslice)
|
||||
}
|
||||
|
||||
if len(uaq.Uslice) == 0 {
|
||||
return nil
|
||||
}
|
||||
var r []Queueable
|
||||
for i := 0; i < n; i++ {
|
||||
r = append(r, uaq.Uslice[i])
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// No guarantee regarding thread safety !
|
||||
func (uaq *UnAckQueue) Pop() Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
r := uaq.Peek()
|
||||
if r != nil {
|
||||
uaq.Uslice = uaq.Uslice[1:]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// No guarantee regarding thread safety !
|
||||
func (uaq *UnAckQueue) PopN(n int) []Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
r := uaq.PeekN(n)
|
||||
uaq.Uslice = uaq.Uslice[len(r):]
|
||||
return r
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) Peek() Queueable {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
if len(uaq.Uslice) == 0 {
|
||||
return nil
|
||||
}
|
||||
r := uaq.Uslice[0]
|
||||
return r
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) Push(s Queueable) error {
|
||||
if uaq == nil {
|
||||
return nil
|
||||
}
|
||||
pushIdx := 1
|
||||
if len(uaq.Uslice) != 0 {
|
||||
pushIdx = uaq.Uslice[len(uaq.Uslice)-1].Id + 1
|
||||
}
|
||||
|
||||
sStz, ok := s.(*UnAckedStz)
|
||||
if !ok {
|
||||
return errors.New("element in not compatible with this queue. expected an UnAckedStz")
|
||||
}
|
||||
|
||||
e := UnAckedStz{
|
||||
Id: pushIdx,
|
||||
Stz: sStz.Stz,
|
||||
}
|
||||
|
||||
uaq.Uslice = append(uaq.Uslice, &e)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uaq *UnAckQueue) Empty() bool {
|
||||
if uaq == nil {
|
||||
return true
|
||||
}
|
||||
r := len(uaq.Uslice)
|
||||
return r == 0
|
||||
}
|
||||
|
||||
// Request as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||
type SMRequest struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 r"`
|
||||
}
|
||||
|
||||
func (SMRequest) Name() string {
|
||||
return "Stream Management: request"
|
||||
}
|
||||
|
||||
// Answer as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||
type SMAnswer struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 a"`
|
||||
H uint `xml:"h,attr"`
|
||||
}
|
||||
|
||||
func (SMAnswer) Name() string {
|
||||
return "Stream Management: answer"
|
||||
}
|
||||
|
||||
// Resumed as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||
type SMResumed struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"`
|
||||
PrevId string `xml:"previd,attr,omitempty"`
|
||||
H *uint `xml:"h,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (SMResumed) Name() string {
|
||||
return "Stream Management: resumed"
|
||||
}
|
||||
|
||||
// Resume as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||
type SMResume struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 resume"`
|
||||
PrevId string `xml:"previd,attr,omitempty"`
|
||||
H *uint `xml:"h,attr,omitempty"`
|
||||
}
|
||||
|
||||
func (SMResume) Name() string {
|
||||
return "Stream Management: resume"
|
||||
}
|
||||
|
||||
// Failed as defined in Stream Management spec
|
||||
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||
type SMFailed struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 failed"`
|
||||
H *uint `xml:"h,attr,omitempty"`
|
||||
|
||||
StreamErrorGroup StanzaErrorGroup
|
||||
}
|
||||
|
||||
func (SMFailed) Name() string {
|
||||
return "Stream Management: failed"
|
||||
}
|
||||
|
||||
func (smf *SMFailed) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
smf.XMLName = start.Name
|
||||
|
||||
// According to https://xmpp.org/rfcs/rfc3920.html#def we should have no attributes aside from the namespace
|
||||
// which we don't use internally
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
// Decode sub-elements
|
||||
var err error
|
||||
switch tt.Name.Local {
|
||||
case "bad-format":
|
||||
bf := BadFormat{}
|
||||
err = d.DecodeElement(&bf, &tt)
|
||||
smf.StreamErrorGroup = &bf
|
||||
case "bad-namespace-prefix":
|
||||
bnp := BadNamespacePrefix{}
|
||||
err = d.DecodeElement(&bnp, &tt)
|
||||
smf.StreamErrorGroup = &bnp
|
||||
case "conflict":
|
||||
c := Conflict{}
|
||||
err = d.DecodeElement(&c, &tt)
|
||||
smf.StreamErrorGroup = &c
|
||||
case "connection-timeout":
|
||||
ct := ConnectionTimeout{}
|
||||
err = d.DecodeElement(&ct, &tt)
|
||||
smf.StreamErrorGroup = &ct
|
||||
case "host-gone":
|
||||
hg := HostGone{}
|
||||
err = d.DecodeElement(&hg, &tt)
|
||||
smf.StreamErrorGroup = &hg
|
||||
case "host-unknown":
|
||||
hu := HostUnknown{}
|
||||
err = d.DecodeElement(&hu, &tt)
|
||||
smf.StreamErrorGroup = &hu
|
||||
case "improper-addressing":
|
||||
ia := ImproperAddressing{}
|
||||
err = d.DecodeElement(&ia, &tt)
|
||||
smf.StreamErrorGroup = &ia
|
||||
case "internal-server-error":
|
||||
ise := InternalServerError{}
|
||||
err = d.DecodeElement(&ise, &tt)
|
||||
smf.StreamErrorGroup = &ise
|
||||
case "invalid-from":
|
||||
ifrm := InvalidForm{}
|
||||
err = d.DecodeElement(&ifrm, &tt)
|
||||
smf.StreamErrorGroup = &ifrm
|
||||
case "invalid-id":
|
||||
id := InvalidId{}
|
||||
err = d.DecodeElement(&id, &tt)
|
||||
smf.StreamErrorGroup = &id
|
||||
case "invalid-namespace":
|
||||
ins := InvalidNamespace{}
|
||||
err = d.DecodeElement(&ins, &tt)
|
||||
smf.StreamErrorGroup = &ins
|
||||
case "invalid-xml":
|
||||
ix := InvalidXML{}
|
||||
err = d.DecodeElement(&ix, &tt)
|
||||
smf.StreamErrorGroup = &ix
|
||||
case "not-authorized":
|
||||
na := NotAuthorized{}
|
||||
err = d.DecodeElement(&na, &tt)
|
||||
smf.StreamErrorGroup = &na
|
||||
case "not-well-formed":
|
||||
nwf := NotWellFormed{}
|
||||
err = d.DecodeElement(&nwf, &tt)
|
||||
smf.StreamErrorGroup = &nwf
|
||||
case "policy-violation":
|
||||
pv := PolicyViolation{}
|
||||
err = d.DecodeElement(&pv, &tt)
|
||||
smf.StreamErrorGroup = &pv
|
||||
case "remote-connection-failed":
|
||||
rcf := RemoteConnectionFailed{}
|
||||
err = d.DecodeElement(&rcf, &tt)
|
||||
smf.StreamErrorGroup = &rcf
|
||||
case "resource-constraint":
|
||||
rc := ResourceConstraint{}
|
||||
err = d.DecodeElement(&rc, &tt)
|
||||
smf.StreamErrorGroup = &rc
|
||||
case "restricted-xml":
|
||||
rx := RestrictedXML{}
|
||||
err = d.DecodeElement(&rx, &tt)
|
||||
smf.StreamErrorGroup = &rx
|
||||
case "see-other-host":
|
||||
soh := SeeOtherHost{}
|
||||
err = d.DecodeElement(&soh, &tt)
|
||||
smf.StreamErrorGroup = &soh
|
||||
case "system-shutdown":
|
||||
ss := SystemShutdown{}
|
||||
err = d.DecodeElement(&ss, &tt)
|
||||
smf.StreamErrorGroup = &ss
|
||||
case "undefined-condition":
|
||||
uc := UndefinedCondition{}
|
||||
err = d.DecodeElement(&uc, &tt)
|
||||
smf.StreamErrorGroup = &uc
|
||||
case "unexpected-request":
|
||||
ur := UnexpectedRequest{}
|
||||
err = d.DecodeElement(&ur, &tt)
|
||||
smf.StreamErrorGroup = &ur
|
||||
case "unsupported-encoding":
|
||||
ue := UnsupportedEncoding{}
|
||||
err = d.DecodeElement(&ue, &tt)
|
||||
smf.StreamErrorGroup = &ue
|
||||
case "unsupported-stanza-type":
|
||||
ust := UnsupportedStanzaType{}
|
||||
err = d.DecodeElement(&ust, &tt)
|
||||
smf.StreamErrorGroup = &ust
|
||||
case "unsupported-version":
|
||||
uv := UnsupportedVersion{}
|
||||
err = d.DecodeElement(&uv, &tt)
|
||||
smf.StreamErrorGroup = &uv
|
||||
case "xml-not-well-formed":
|
||||
xnwf := XMLNotWellFormed{}
|
||||
err = d.DecodeElement(&xnwf, &tt)
|
||||
smf.StreamErrorGroup = &xnwf
|
||||
default:
|
||||
return errors.New("error is unknown")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type smDecoder struct{}
|
||||
|
||||
var sm smDecoder
|
||||
|
||||
// decode decodes all known nonza in the stream management namespace.
|
||||
func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||
switch se.Name.Local {
|
||||
case "enabled":
|
||||
return s.decodeEnabled(p, se)
|
||||
case "resumed":
|
||||
return s.decodeResumed(p, se)
|
||||
case "resume":
|
||||
return s.decodeResume(p, se)
|
||||
case "r":
|
||||
return s.decodeRequest(p, se)
|
||||
case "a":
|
||||
return s.decodeAnswer(p, se)
|
||||
case "failed":
|
||||
return s.decodeFailed(p, se)
|
||||
default:
|
||||
return nil, errors.New("unexpected XMPP packet " +
|
||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||
}
|
||||
}
|
||||
|
||||
func (smDecoder) decodeEnabled(p *xml.Decoder, se xml.StartElement) (SMEnabled, error) {
|
||||
var packet SMEnabled
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed, error) {
|
||||
var packet SMResumed
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func (smDecoder) decodeResume(p *xml.Decoder, se xml.StartElement) (SMResume, error) {
|
||||
var packet SMResume
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
func (smDecoder) decodeRequest(p *xml.Decoder, se xml.StartElement) (SMRequest, error) {
|
||||
var packet SMRequest
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func (smDecoder) decodeAnswer(p *xml.Decoder, se xml.StartElement) (SMAnswer, error) {
|
||||
var packet SMAnswer
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func (smDecoder) decodeFailed(p *xml.Decoder, se xml.StartElement) (SMFailed, error) {
|
||||
var packet SMFailed
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPopEmptyQueue(t *testing.T) {
|
||||
var uaq stanza.UnAckQueue
|
||||
popped := uaq.Pop()
|
||||
if popped != nil {
|
||||
t.Fatalf("queue is empty but something was popped !")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
toPush := stanza.UnAckedStz{
|
||||
Id: 3,
|
||||
Stz: `<iq type='submit'
|
||||
from='confucius@scholars.lit/home'
|
||||
to='registrar.scholars.lit'
|
||||
id='kj3b157n'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:register'>
|
||||
<username>confucius</username>
|
||||
<first>Qui</first>
|
||||
<last>Kong</last>
|
||||
</query>
|
||||
</iq>`,
|
||||
}
|
||||
|
||||
err := uaq.Push(&toPush)
|
||||
if err != nil {
|
||||
t.Fatalf("could not push element to the queue : %v", err)
|
||||
}
|
||||
|
||||
if len(uaq.Uslice) != 4 {
|
||||
t.Fatalf("push to the non-acked queue failed")
|
||||
}
|
||||
for i := 0; i < 4; i++ {
|
||||
if uaq.Uslice[i].Id != i+1 {
|
||||
t.Fatalf("indexes were not updated correctly. Expected %d got %d", i, uaq.Uslice[i].Id)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the queue is a fifo : popped element should not be the one we just pushed.
|
||||
popped := uaq.Pop()
|
||||
poppedElt, ok := popped.(*stanza.UnAckedStz)
|
||||
if !ok {
|
||||
t.Fatalf("popped element is not a *stanza.UnAckedStz")
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(*poppedElt, toPush) {
|
||||
t.Fatalf("pushed element is at the top of the fifo queue when it should be at the bottom")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeekUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
|
||||
expectedPeek := stanza.UnAckedStz{
|
||||
Id: 1,
|
||||
Stz: `<iq type='set'
|
||||
from='romeo@montague.net/home'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search2'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'>
|
||||
<last>Capulet</last>
|
||||
</query>
|
||||
</iq>`,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedPeek, *uaq.Uslice[0]) {
|
||||
t.Fatalf("peek failed to return the correct stanza")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeekNUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
randPop := rand.Int31n(int32(initLen))
|
||||
|
||||
peeked := uaq.PeekN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice) != initLen {
|
||||
t.Fatalf("queue length changed whith peek n operation : had %d found %d after peek", initLen, len(uaq.Uslice))
|
||||
}
|
||||
|
||||
if len(peeked) != int(randPop) {
|
||||
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", randPop, len(peeked))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeekNUnackTooLong(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
|
||||
// Have a random number of elements to peek that's greater than the queue size
|
||||
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
|
||||
|
||||
peeked := uaq.PeekN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice) != initLen {
|
||||
t.Fatalf("total length changed whith peek n operation : had %d found %d after pop", initLen, len(uaq.Uslice))
|
||||
}
|
||||
|
||||
if len(peeked) != initLen {
|
||||
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", initLen, len(peeked))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPopNUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
randPop := rand.Int31n(int32(initLen))
|
||||
|
||||
popped := uaq.PopN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice)+len(popped) != initLen {
|
||||
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
|
||||
}
|
||||
|
||||
for _, elt := range popped {
|
||||
for _, oldElt := range uaq.Uslice {
|
||||
if reflect.DeepEqual(elt, oldElt) {
|
||||
t.Fatalf("pop n operation duplicated some elements")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopNUnackTooLong(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
|
||||
// Have a random number of elements to pop that's greater than the queue size
|
||||
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
|
||||
|
||||
popped := uaq.PopN(int(randPop))
|
||||
|
||||
if len(uaq.Uslice)+len(popped) != initLen {
|
||||
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
|
||||
}
|
||||
|
||||
for _, elt := range popped {
|
||||
for _, oldElt := range uaq.Uslice {
|
||||
if reflect.DeepEqual(elt, oldElt) {
|
||||
t.Fatalf("pop n operation duplicated some elements")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopUnack(t *testing.T) {
|
||||
uaq := initUnAckQueue()
|
||||
initLen := len(uaq.Uslice)
|
||||
|
||||
popped := uaq.Pop()
|
||||
|
||||
if len(uaq.Uslice)+1 != initLen {
|
||||
t.Fatalf("total length changed whith pop operation : had %d found %d after pop", initLen, len(uaq.Uslice)+1)
|
||||
}
|
||||
for _, oldElt := range uaq.Uslice {
|
||||
if reflect.DeepEqual(popped, oldElt) {
|
||||
t.Fatalf("pop n operation duplicated some elements")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func initUnAckQueue() stanza.UnAckQueue {
|
||||
q := []*stanza.UnAckedStz{
|
||||
{
|
||||
Id: 1,
|
||||
Stz: `<iq type='set'
|
||||
from='romeo@montague.net/home'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search2'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'>
|
||||
<last>Capulet</last>
|
||||
</query>
|
||||
</iq>`,
|
||||
},
|
||||
{Id: 2,
|
||||
Stz: `<iq type='get'
|
||||
from='juliet@capulet.com/balcony'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search3'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'/>
|
||||
</iq>`},
|
||||
{Id: 3,
|
||||
Stz: `<iq type='set'
|
||||
from='juliet@capulet.com/balcony'
|
||||
to='characters.shakespeare.lit'
|
||||
id='search4'
|
||||
xml:lang='en'>
|
||||
<query xmlns='jabber:iq:search'>
|
||||
<x xmlns='jabber:x:data' type='submit'>
|
||||
<field type='hidden' var='FORM_TYPE'>
|
||||
<value>jabber:iq:search</value>
|
||||
</field>
|
||||
<field var='x-gender'>
|
||||
<value>male</value>
|
||||
</field>
|
||||
</x>
|
||||
</query>
|
||||
</iq>`},
|
||||
}
|
||||
|
||||
return stanza.UnAckQueue{Uslice: q}
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
var reLeadcloseWhtsp = regexp.MustCompile(`^[\s\p{Zs}]+|[\s\p{Zs}]+$`)
|
||||
var reInsideWhtsp = regexp.MustCompile(`[\s\p{Zs}]`)
|
||||
|
||||
// ============================================================================
|
||||
// Marshaller / unmarshaller test
|
||||
|
||||
func checkMarshalling(t *testing.T, iq *stanza.IQ) (*stanza.IQ, error) {
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshall
|
||||
var parsedIQ stanza.IQ
|
||||
err = xml.Unmarshal(data, &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
|
||||
}
|
||||
return &parsedIQ, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// XML structs comparison
|
||||
|
||||
// Compare iq structure but ignore empty namespace as they are set properly on
|
||||
// marshal / unmarshal. There is no need to manage them on the manually
|
||||
// crafted structure.
|
||||
func xmlEqual(x, y interface{}) bool {
|
||||
return cmp.Equal(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
// xmlDiff compares xml structures ignoring namespace preferences
|
||||
func xmlDiff(x, y interface{}) string {
|
||||
return cmp.Diff(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
func xmlOpts() cmp.Options {
|
||||
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
|
||||
opts := cmp.Options{
|
||||
cmp.FilterValues(func(x, y interface{}) bool {
|
||||
xx, xok := x.(xml.Name)
|
||||
yy, yok := y.(xml.Name)
|
||||
if xok && yok {
|
||||
zero := xml.Name{}
|
||||
if xx == zero || yy == zero {
|
||||
return true
|
||||
}
|
||||
if xx.Space == "" || yy.Space == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, alwaysEqual),
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func delSpaces(s string) string {
|
||||
return reInsideWhtsp.ReplaceAllString(reLeadcloseWhtsp.ReplaceAllString(s, ""), "")
|
||||
}
|
||||
|
||||
func compareMarshal(expected, data string) error {
|
||||
if delSpaces(expected) != delSpaces(data) {
|
||||
return errors.New("failed to verify unmarshal->marshal. Expected :" + expected + "\ngot: " + data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user