diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go
index 1577147..473fefa 100644
--- a/_examples/delegation/delegation.go
+++ b/_examples/delegation/delegation.go
@@ -83,7 +83,7 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
return
}
- iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+ iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
switch info.Node {
case "":
@@ -192,7 +192,7 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
if pubsub.Publish.XMLName.Local == "publish" {
// Prepare pubsub IQ reply
- iqResp := xmpp.NewIQ("result", forwardedIQ.To, forwardedIQ.From, forwardedIQ.Id, "en")
+ iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
payload := xmpp.PubSub{
XMLName: xml.Name{
Space: "http://jabber.org/protocol/pubsub",
@@ -201,7 +201,7 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
}
iqResp.Payload = &payload
// Wrap the reply in delegation 'forward'
- iqForward := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+ iqForward := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
delegPayload := xmpp.Delegation{
XMLName: xml.Name{
Space: "urn:xmpp:delegation:1",
diff --git a/_examples/go.mod b/_examples/go.mod
index c9263aa..21ddbc7 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -8,3 +8,5 @@ require (
github.com/processone/soundcloud v1.0.0
gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52
)
+
+replace gosrc.io/xmpp => gosrc.io/xmpp v0.1.1-0.20190619153249-b1dde2330764
diff --git a/_examples/go.sum b/_examples/go.sum
index b442e11..889d4f7 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -8,4 +8,7 @@ golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52 h1:H5BezaFYvDL9r72ng90ICneftomo1iXx6+BxxZ9jBtg=
gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52/go.mod h1:WvSgrZF7lMvjd1SH8nVGi7ZGr6gNU7oUuBdwpFTs9nY=
+gosrc.io/xmpp v0.1.1-0.20190619153249-b1dde2330764 h1:jlYtpqdRoBC3Gke7MacXsVpSZL0g5nIBG/b9JVxpAVY=
+gosrc.io/xmpp v0.1.1-0.20190619153249-b1dde2330764/go.mod h1:WvSgrZF7lMvjd1SH8nVGi7ZGr6gNU7oUuBdwpFTs9nY=
diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go
index 9752dca..17cfc1a 100644
--- a/_examples/xmpp_component/xmpp_component.go
+++ b/_examples/xmpp_component/xmpp_component.go
@@ -59,7 +59,7 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
return
}
- iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+ iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
identity := xmpp.Identity{
Name: opts.Name,
Category: opts.Category,
@@ -95,7 +95,7 @@ func discoItems(c xmpp.Sender, p xmpp.Packet) {
return
}
- iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+ iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
var payload xmpp.DiscoItems
if discoItems.Node == "" {
@@ -116,7 +116,7 @@ func handleVersion(c xmpp.Sender, p xmpp.Packet) {
return
}
- iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+ iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
var payload xmpp.Version
payload.Name = "Fluux XMPP Component"
payload.Version = "0.0.1"
diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go
index 8aaad73..3725f08 100644
--- a/_examples/xmpp_echo/xmpp_echo.go
+++ b/_examples/xmpp_echo/xmpp_echo.go
@@ -43,7 +43,7 @@ func handleMessage(s xmpp.Sender, p xmpp.Packet) {
}
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
- reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: msg.From}, Body: msg.Body}
+ reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}
diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go
index c958d3f..2218279 100644
--- a/_examples/xmpp_jukebox/xmpp_jukebox.go
+++ b/_examples/xmpp_jukebox/xmpp_jukebox.go
@@ -34,6 +34,7 @@ func main() {
Address: *address,
Jid: *jid,
Password: *password,
+ // PacketLogger: os.Stdout,
Insecure: true,
}
@@ -91,7 +92,7 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
playSCURL(player, url)
setResponse := new(xmpp.ControlSetResponse)
// FIXME: Broken
- reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
+ reply := xmpp.IQ{Attrs: xmpp.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
_ = s.Send(reply)
// TODO add Soundclound artist / title retrieval
sendUserTune(s, "Radiohead", "Spectre")
@@ -102,7 +103,7 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
func sendUserTune(s xmpp.Sender, artist string, title string) {
tune := xmpp.Tune{Artist: artist, Title: title}
- iq := xmpp.NewIQ("set", "", "", "usertune-1", "en")
+ iq := xmpp.NewIQ(xmpp.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}}
iq.Payload = &payload
_ = s.Send(iq)
diff --git a/auth.go b/auth.go
index 90b73e5..497258d 100644
--- a/auth.go
+++ b/auth.go
@@ -33,7 +33,7 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
fmt.Fprintf(socket, "%s", nsSASL, enc)
// Next message should be either success or failure.
- val, err := next(decoder)
+ val, err := nextPacket(decoder)
if err != nil {
return err
}
diff --git a/backoff.go b/backoff.go
index caa53d1..2dfec8d 100644
--- a/backoff.go
+++ b/backoff.go
@@ -13,7 +13,7 @@ It can be used in several ways:
- Using ticker channel to trigger callback function on tick
The functions for Backoff are not threadsafe, but you can:
-- Keep the attempt counter on your end and use DurationForAttempt(int)
+- Keep the attempt counter on your end and use durationForAttempt(int)
- Use lock in your own code to protect the Backoff structure.
TODO: Implement Backoff Ticker channel
@@ -34,11 +34,11 @@ const (
defaultCap int = 180000 // 3 minutes
)
-// Backoff can provide increasing duration with the number of attempt
+// backoff provides increasing duration with the number of attempt
// performed. The structure is used to support exponential backoff on
// connection attempts to avoid hammering the server we are connecting
// to.
-type Backoff struct {
+type backoff struct {
NoJitter bool
Base int
Factor int
@@ -47,20 +47,20 @@ type Backoff struct {
attempt int
}
-// Duration returns the duration to apply to the current attempt.
-func (b *Backoff) Duration() time.Duration {
- d := b.DurationForAttempt(b.attempt)
+// duration returns the duration to apply to the current attempt.
+func (b *backoff) duration() time.Duration {
+ d := b.durationForAttempt(b.attempt)
b.attempt++
return d
}
-// Wait sleeps for backoff duration for current attempt.
-func (b *Backoff) Wait() {
- time.Sleep(b.Duration())
+// wait sleeps for backoff duration for current attempt.
+func (b *backoff) wait() {
+ time.Sleep(b.duration())
}
-// DurationForAttempt returns a duration for an attempt number, in a stateless way.
-func (b *Backoff) DurationForAttempt(attempt int) time.Duration {
+// durationForAttempt returns a duration for an attempt number, in a stateless way.
+func (b *backoff) durationForAttempt(attempt int) time.Duration {
b.setDefault()
expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt)))
d := int(math.Trunc(expBackoff))
@@ -70,13 +70,13 @@ func (b *Backoff) DurationForAttempt(attempt int) time.Duration {
return time.Duration(d) * time.Millisecond
}
-// Reset sets back the number of attempts to 0. This is to be called after a successfull operation has been performed,
+// reset sets back the number of attempts to 0. This is to be called after a successful operation has been performed,
// to reset the exponential backoff interval.
-func (b *Backoff) Reset() {
+func (b *backoff) reset() {
b.attempt = 0
}
-func (b *Backoff) setDefault() {
+func (b *backoff) setDefault() {
if b.Base == 0 {
b.Base = defaultBase
}
diff --git a/backoff_test.go b/backoff_test.go
index 9ef7ce0..9a7fde7 100644
--- a/backoff_test.go
+++ b/backoff_test.go
@@ -1,21 +1,19 @@
-package xmpp_test
+package xmpp
import (
"testing"
"time"
-
- "gosrc.io/xmpp"
)
func TestDurationForAttempt_NoJitter(t *testing.T) {
- b := xmpp.Backoff{Base: 25, NoJitter: true}
+ b := backoff{Base: 25, NoJitter: true}
bInMS := time.Duration(b.Base) * time.Millisecond
- if b.DurationForAttempt(0) != bInMS {
- t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.DurationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond)
+ if b.durationForAttempt(0) != bInMS {
+ t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.durationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond)
}
var prevDuration, d time.Duration
for i := 0; i < 10; i++ {
- d = b.DurationForAttempt(i)
+ d = b.durationForAttempt(i)
if !(d >= prevDuration) {
t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration)
}
diff --git a/check_cert.go b/check_cert.go
index 05e44e1..5addd87 100644
--- a/check_cert.go
+++ b/check_cert.go
@@ -54,14 +54,14 @@ func (c *ServerCheck) Check() error {
}
// Set xml decoder and extract streamID from reply (not used for now)
- _, err = initDecoder(decoder)
+ _, err = initStream(decoder)
if err != nil {
return err
}
// extract stream features
var f StreamFeatures
- packet, err := next(decoder)
+ packet, err := nextPacket(decoder)
if err != nil {
err = fmt.Errorf("stream open decode features: %s", err)
return err
diff --git a/client.go b/client.go
index 2fa6b03..bb62ce8 100644
--- a/client.go
+++ b/client.go
@@ -200,7 +200,7 @@ func (c *Client) SendRaw(packet string) error {
// Loop: Receive data from server
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
for {
- val, err := next(c.Session.decoder)
+ val, err := nextPacket(c.Session.decoder)
if err != nil {
close(keepaliveQuit)
c.updateState(StateDisconnected)
diff --git a/component.go b/component.go
index 8ae8c2e..5fedc9a 100644
--- a/component.go
+++ b/component.go
@@ -78,7 +78,7 @@ func (c *Component) Connect() error {
c.decoder = xml.NewDecoder(conn)
// 2. Initialize xml decoder and extract streamID from reply
- streamId, err := initDecoder(c.decoder)
+ streamId, err := initStream(c.decoder)
if err != nil {
return errors.New("cannot init decoder " + err.Error())
}
@@ -89,7 +89,7 @@ func (c *Component) Connect() error {
}
// 4. Check server response for authentication
- val, err := next(c.decoder)
+ val, err := nextPacket(c.decoder)
if err != nil {
return err
}
@@ -119,7 +119,7 @@ func (c *Component) SetHandler(handler EventHandler) {
// Receiver Go routine receiver
func (c *Component) recv() (err error) {
for {
- val, err := next(c.decoder)
+ val, err := nextPacket(c.decoder)
if err != nil {
c.updateState(StateDisconnected)
return err
diff --git a/doc.go b/doc.go
index 5e1c074..40f4f6a 100644
--- a/doc.go
+++ b/doc.go
@@ -1,13 +1,23 @@
/*
-Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
+Fluux XMPP is an modern and full-featured XMPP library that can be used to build clients or
+server components.
-The goal is to make simple to write simple adhoc XMPP clients:
+The goal is to make simple to write modern compliant XMPP software:
- For automation (like for example monitoring of an XMPP service),
- For building connected "things" by plugging them on an XMPP server,
- For writing simple chatbots to control a service or a thing.
+ - For writing XMPP servers components. Fluux XMPP supports:
+ - XEP-0114: Jabber Component Protocol
+ - XEP-0355: Namespace Delegation
+ - XEP-0356: Privileged Entity
-Fluux XMPP can be used to build XMPP clients or XMPP components.
+The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
+
+The library includes a StreamManager that provides features like autoreconnect exponential back-off.
+
+The library is implementing latest versions of the XMPP specifications (RFC 6120 and RFC 6121), and includes
+support for many extensions.
Clients
diff --git a/error.go b/error.go
new file mode 100644
index 0000000..2fe542f
--- /dev/null
+++ b/error.go
@@ -0,0 +1,118 @@
+package xmpp
+
+import (
+ "encoding/xml"
+ "strconv"
+)
+
+/*
+TODO support ability to put Raw payload inside IQ
+*/
+
+// ============================================================================
+// 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"`
+}
+
+func (x *Err) Namespace() string {
+ return x.XMLName.Space
+}
+
+// UnmarshalXML implements custom parsing for IQs
+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"}
+ if elt.XMLName == textName {
+ x.Text = string(elt.Content)
+ } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
+ 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}
+ e.EncodeToken(xml.StartElement{Name: reason})
+ e.EncodeToken(xml.EndElement{Name: reason})
+ }
+
+ // Text
+ if x.Text != "" {
+ text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
+ e.EncodeToken(xml.StartElement{Name: text})
+ e.EncodeToken(xml.CharData(x.Text))
+ e.EncodeToken(xml.EndElement{Name: text})
+ }
+
+ return e.EncodeToken(xml.EndElement{Name: start.Name})
+}
diff --git a/error_enum.go b/error_enum.go
new file mode 100644
index 0000000..b89b925
--- /dev/null
+++ b/error_enum.go
@@ -0,0 +1,13 @@
+package xmpp
+
+// 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 = "motify"
+ ErrorTypeWait ErrorType = "wait"
+)
diff --git a/iq.go b/iq.go
index ec165eb..4597609 100644
--- a/iq.go
+++ b/iq.go
@@ -3,127 +3,20 @@ package xmpp
import (
"encoding/xml"
"fmt"
- "strconv"
)
/*
TODO support ability to put Raw payload inside IQ
*/
-// ============================================================================
-// 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 string `xml:"type,attr,omitempty"`
- Reason string
- Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
-}
-
-func (x *Err) Namespace() string {
- return x.XMLName.Space
-}
-
-// UnmarshalXML implements custom parsing for IQs
-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 = 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"}
- if elt.XMLName == textName {
- x.Text = string(elt.Content)
- } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
- 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: 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}
- e.EncodeToken(xml.StartElement{Name: reason})
- e.EncodeToken(xml.EndElement{Name: reason})
- }
-
- // Text
- if x.Text != "" {
- text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
- e.EncodeToken(xml.StartElement{Name: text})
- e.EncodeToken(xml.CharData(x.Text))
- e.EncodeToken(xml.EndElement{Name: text})
- }
-
- return e.EncodeToken(xml.EndElement{Name: start.Name})
-}
-
// ============================================================================
// IQ Packet
+// IQ implements RFC 6120 - A.5 Client Namespace (a part)
type IQ struct { // Info/Query
XMLName xml.Name `xml:"iq"`
- PacketAttrs
+ // 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
@@ -133,16 +26,16 @@ type IQ struct { // Info/Query
RawXML string `xml:",innerxml"`
}
-func NewIQ(iqtype, from, to, id, lang string) IQ {
+type IQPayload interface {
+ Namespace() string
+}
+
+func NewIQ(a Attrs) IQ {
+ // TODO generate IQ ID if not set
+ // TODO ensure that type is set, as it is required
return IQ{
XMLName: xml.Name{Local: "iq"},
- PacketAttrs: PacketAttrs{
- Id: id,
- From: from,
- To: to,
- Type: iqtype,
- Lang: lang,
- },
+ Attrs: a,
}
}
@@ -182,7 +75,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.Id = attr.Value
}
if attr.Name.Local == "type" {
- iq.Type = attr.Value
+ iq.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
iq.To = attr.Value
@@ -190,9 +83,6 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if attr.Name.Local == "from" {
iq.From = attr.Value
}
- if attr.Name.Local == "lang" {
- iq.Lang = attr.Value
- }
}
// decode inner elements
@@ -223,6 +113,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.Payload = iqExt
continue
}
+ // TODO: If unknown decode as generic node
return fmt.Errorf("unexpected element in iq: %s %s", tt.Name.Space, tt.Name.Local)
case xml.EndElement:
if tt == start.End() {
@@ -233,11 +124,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
}
// ============================================================================
-// Generic IQ Payload
-
-type IQPayload interface {
- Namespace() string
-}
+// Generic / unknown content
// Node is a generic structure to represent XML data. It is used to parse
// unreferenced or custom stanza payload.
diff --git a/iq_test.go b/iq_test.go
index 790d173..0a8aae6 100644
--- a/iq_test.go
+++ b/iq_test.go
@@ -16,7 +16,7 @@ func TestUnmarshalIqs(t *testing.T) {
parsedIQ xmpp.IQ
}{
{"",
- xmpp.IQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: xmpp.PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}},
+ xmpp.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: xmpp.Attrs{Type: xmpp.IQTypeSet, To: "test@localhost", Id: "1"}}},
//{"", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
}
@@ -35,7 +35,7 @@ func TestUnmarshalIqs(t *testing.T) {
}
func TestGenerateIq(t *testing.T) {
- iq := xmpp.NewIQ("result", "admin@localhost", "test@localhost", "1", "en")
+ iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
payload := xmpp.DiscoInfo{
Identity: xmpp.Identity{
Name: "Test Gateway",
@@ -93,7 +93,7 @@ func TestErrorTag(t *testing.T) {
}
func TestDiscoItems(t *testing.T) {
- iq := xmpp.NewIQ("get", "romeo@montague.net/orchard", "catalog.shakespeare.lit", "items3", "en")
+ iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
payload := xmpp.DiscoItems{
Node: "music",
}
diff --git a/message.go b/message.go
index 906f6e3..07b3379 100644
--- a/message.go
+++ b/message.go
@@ -7,9 +7,11 @@ import (
// ============================================================================
// Message Packet
+// Message implements RFC 6120 - A.5 Client Namespace (a part)
type Message struct {
XMLName xml.Name `xml:"message"`
- PacketAttrs
+ Attrs
+
Subject string `xml:"subject,omitempty"`
Body string `xml:"body,omitempty"`
Thread string `xml:"thread,omitempty"`
@@ -21,16 +23,10 @@ func (Message) Name() string {
return "message"
}
-func NewMessage(msgtype, from, to, id, lang string) Message {
+func NewMessage(a Attrs) Message {
return Message{
XMLName: xml.Name{Local: "message"},
- PacketAttrs: PacketAttrs{
- Id: id,
- From: from,
- To: to,
- Type: msgtype,
- Lang: lang,
- },
+ Attrs: a,
}
}
@@ -63,7 +59,7 @@ func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
msg.Id = attr.Value
}
if attr.Name.Local == "type" {
- msg.Type = attr.Value
+ msg.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
msg.To = attr.Value
diff --git a/message_test.go b/message_test.go
index fb30029..b7775f4 100644
--- a/message_test.go
+++ b/message_test.go
@@ -9,7 +9,7 @@ import (
)
func TestGenerateMessage(t *testing.T) {
- message := xmpp.NewMessage("chat", "admin@localhost", "test@localhost", "1", "en")
+ message := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
message.Body = "Hi"
message.Subject = "Msg Subject"
diff --git a/packet.go b/packet.go
index 4b6f519..ce42236 100644
--- a/packet.go
+++ b/packet.go
@@ -4,13 +4,13 @@ type Packet interface {
Name() string
}
-// PacketAttrs represents the common structure for base XMPP packets.
-type PacketAttrs struct {
- Id string `xml:"id,attr,omitempty"`
- From string `xml:"from,attr,omitempty"`
- To string `xml:"to,attr,omitempty"`
- Type string `xml:"type,attr,omitempty"`
- Lang string `xml:"lang,attr,omitempty"`
+// 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 {
diff --git a/packet_enum.go b/packet_enum.go
new file mode 100644
index 0000000..9bc30e4
--- /dev/null
+++ b/packet_enum.go
@@ -0,0 +1,25 @@
+package xmpp
+
+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"
+)
diff --git a/parser.go b/parser.go
index bc17dd5..465db5d 100644
--- a/parser.go
+++ b/parser.go
@@ -14,29 +14,29 @@ import (
// 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 ( or )
-func initDecoder(p *xml.Decoder) (sessionID string, err error) {
+func initStream(p *xml.Decoder) (sessionID string, err error) {
for {
var t xml.Token
t, err = p.Token()
if err != nil {
- return
+ return sessionID, err
}
switch elem := t.(type) {
case xml.StartElement:
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space)
- return
+ return sessionID, err
}
- // Parse Stream attributes
+ // Parse XMPP stream attributes
for _, attrs := range elem.Attr {
switch attrs.Name.Local {
case "id":
sessionID = attrs.Value
}
}
- return
+ return sessionID, err
}
}
}
@@ -58,10 +58,12 @@ func nextStart(p *xml.Decoder) (xml.StartElement, error) {
}
}
-// next scans XML token stream for next element and then assign a structure to decode
-// that elements.
+// 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
-func next(p *xml.Decoder) (Packet, error) {
+// 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
se, err := nextStart(p)
if err != nil {
@@ -84,6 +86,13 @@ func next(p *xml.Decoder) (Packet, error) {
}
}
+/*
+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, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "error":
@@ -96,6 +105,7 @@ func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) {
}
}
+// decodeSASL decodes a packet related to SASL authentication.
func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "success":
@@ -108,6 +118,7 @@ func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
}
}
+// 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":
@@ -122,9 +133,10 @@ func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
}
}
+// decodeClient decodes all known packets in the component namespace.
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
- case "handshake":
+ case "handshake": // handshake is used to authenticate components
return handshake.decode(p, se)
case "message":
return message.decode(p, se)
diff --git a/presence.go b/presence.go
index 95c0a2f..be9c6f2 100644
--- a/presence.go
+++ b/presence.go
@@ -5,28 +5,24 @@ import "encoding/xml"
// ============================================================================
// Presence Packet
+// Presence implements RFC 6120 - A.5 Client Namespace (a part)
type Presence struct {
XMLName xml.Name `xml:"presence"`
- PacketAttrs
- Show string `xml:"show,omitempty"` // away, chat, dnd, xa
- Status string `xml:"status,omitempty"`
- Priority int `xml:"priority,omitempty"`
- Error Err `xml:"error,omitempty"`
+ Attrs
+ Show PresenceShow `xml:"show,omitempty"`
+ Status string `xml:"status,omitempty"`
+ Priority int8 `xml:"priority,omitempty"` // default: 0
+ Error Err `xml:"error,omitempty"`
}
func (Presence) Name() string {
return "presence"
}
-func NewPresence(from, to, id, lang string) Presence {
+func NewPresence(a Attrs) Presence {
return Presence{
XMLName: xml.Name{Local: "presence"},
- PacketAttrs: PacketAttrs{
- Id: id,
- From: from,
- To: to,
- Lang: lang,
- },
+ Attrs: a,
}
}
diff --git a/presence_enum.go b/presence_enum.go
new file mode 100644
index 0000000..c0723bd
--- /dev/null
+++ b/presence_enum.go
@@ -0,0 +1,12 @@
+package xmpp
+
+// 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"
+)
diff --git a/presence_test.go b/presence_test.go
index 9d33cc1..fb8e09a 100644
--- a/presence_test.go
+++ b/presence_test.go
@@ -10,8 +10,8 @@ import (
)
func TestGeneratePresence(t *testing.T) {
- presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en")
- presence.Show = "chat"
+ presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
+ presence.Show = xmpp.PresenceShowChat
data, err := xml.Marshal(presence)
if err != nil {
@@ -32,13 +32,13 @@ 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 string `xml:"show"`
- Status string `xml:"status"`
- Priority int `xml:"priority"`
+ Show xmpp.PresenceShow `xml:"show"`
+ Status string `xml:"status"`
+ Priority int8 `xml:"priority"`
}
- presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en")
- presence.Show = "xa"
+ presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
+ presence.Show = xmpp.PresenceShowXA
presence.Status = "Coding"
presence.Priority = 10
diff --git a/router_test.go b/router_test.go
index 4b6efe6..02ff2b1 100644
--- a/router_test.go
+++ b/router_test.go
@@ -19,8 +19,7 @@ func TestNameMatcher(t *testing.T) {
// Check that a message packet is properly matched
conn := NewSenderMock()
- // TODO: We want packet creation code to use struct to use default values
- msg := xmpp.NewMessage("chat", "", "test@localhost", "1", "")
+ msg := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, To: "test@localhost", Id: "1"})
msg.Body = "Hello"
router.Route(conn, msg)
if conn.String() != successFlag {
@@ -29,7 +28,7 @@ func TestNameMatcher(t *testing.T) {
// Check that an IQ packet is not matched
conn = NewSenderMock()
- iq := xmpp.NewIQ("get", "", "localhost", "1", "")
+ iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, To: "localhost", Id: "1"})
iq.Payload = &xmpp.DiscoInfo{}
router.Route(conn, iq)
if conn.String() == successFlag {
@@ -47,7 +46,8 @@ func TestIQNSMatcher(t *testing.T) {
// Check that an IQ with proper namespace does match
conn := NewSenderMock()
- iqDisco := xmpp.NewIQ("get", "", "localhost", "1", "")
+ iqDisco := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, To: "localhost", Id: "1"})
+ // TODO: Add a function to generate payload with proper namespace initialisation
iqDisco.Payload = &xmpp.DiscoInfo{
XMLName: xml.Name{
Space: xmpp.NSDiscoInfo,
@@ -60,7 +60,8 @@ func TestIQNSMatcher(t *testing.T) {
// Check that another namespace is not matched
conn = NewSenderMock()
- iqVersion := xmpp.NewIQ("get", "", "localhost", "1", "")
+ iqVersion := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, To: "localhost", Id: "1"})
+ // TODO: Add a function to generate payload with proper namespace initialisation
iqVersion.Payload = &xmpp.DiscoInfo{
XMLName: xml.Name{
Space: "jabber:iq:version",
@@ -240,7 +241,7 @@ func (s SenderMock) String() string {
func TestSenderMock(t *testing.T) {
conn := NewSenderMock()
- msg := xmpp.NewMessage("", "", "test@localhost", "1", "")
+ msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost", Id: "1"})
msg.Body = "Hello"
if err := conn.Send(msg); err != nil {
t.Error("Could not send message")
diff --git a/session.go b/session.go
index 48dc02f..88486c6 100644
--- a/session.go
+++ b/session.go
@@ -92,7 +92,7 @@ func (s *Session) open(domain string) (f StreamFeatures) {
}
// Set xml decoder and extract streamID from reply
- s.StreamId, s.err = initDecoder(s.decoder) // TODO refactor / rename
+ s.StreamId, s.err = initStream(s.decoder) // TODO refactor / rename
if s.err != nil {
return
}
diff --git a/stream_manager.go b/stream_manager.go
index 0b8cda7..9c7e020 100644
--- a/stream_manager.go
+++ b/stream_manager.go
@@ -104,7 +104,7 @@ func (sm *StreamManager) Stop() {
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
func (sm *StreamManager) connect() error {
- var backoff Backoff // TODO: Group backoff calculation features with connection manager?
+ var backoff backoff // TODO: Group backoff calculation features with connection manager?
for {
var err error
@@ -118,7 +118,7 @@ func (sm *StreamManager) connect() error {
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
}
}
- backoff.Wait()
+ backoff.wait()
} else { // We are connected, we can leave the retry loop
break
}