Add constants (enumlike) for stanza types and simplify packet creation (#62)

* Add constants (enumlike) for stanza types
* NewIQ, NewMessage and NewPresence are now initialized with the Attrs struct
* Update examples
* Do not export backoff code. For now, we do not need to expose backoff in the documentation
* Make presence priority an int8
This commit is contained in:
genofire 2019-06-22 11:13:33 +02:00 committed by Mickaël Rémond
parent 145fce6b3f
commit d9fdff0839
28 changed files with 299 additions and 225 deletions

View File

@ -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",

View File

@ -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

View File

@ -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=

View File

@ -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"

View File

@ -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)
}

View File

@ -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)

View File

@ -33,7 +33,7 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
// Next message should be either success or failure.
val, err := next(decoder)
val, err := nextPacket(decoder)
if err != nil {
return err
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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

16
doc.go
View File

@ -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

118
error.go Normal file
View File

@ -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})
}

13
error_enum.go Normal file
View File

@ -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"
)

141
iq.go
View File

@ -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.

View File

@ -16,7 +16,7 @@ func TestUnmarshalIqs(t *testing.T) {
parsedIQ xmpp.IQ
}{
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
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 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}},
}
@ -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",
}

View File

@ -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

View File

@ -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"

View File

@ -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 {

25
packet_enum.go Normal file
View File

@ -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"
)

View File

@ -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 ( <conflict/> or <host-unknown/> )
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 <stream> 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)

View File

@ -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,
}
}

12
presence_enum.go Normal file
View File

@ -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"
)

View File

@ -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

View File

@ -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")

View File

@ -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
}

View File

@ -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
}