diff --git a/README.md b/README.md
index 08b911c..62c6518 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,13 @@ config := xmpp.Config{
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
+### Extensions
+ - [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
+ Note : "6.5.4 Returning Some Items" requires support for [XEP-0059: Result Set Management](https://xmpp.org/extensions/xep-0059.html),
+ and is therefore not supported yet.
+ - [XEP-0004: Data Forms](https://xmpp.org/extensions/xep-0004.html)
+ - [XEP-0050: Ad-Hoc Commands](https://xmpp.org/extensions/xep-0050.html)
+
## Package overview
### Stanza subpackage
@@ -108,15 +115,16 @@ func main() {
Address: "localhost:5222",
},
Jid: "test@localhost",
- Credential: xmpp.Password("Test"),
+ Credential: xmpp.Password("test"),
StreamLogger: os.Stdout,
Insecure: true,
+ // TLSConfig: tls.Config{InsecureSkipVerify: true},
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
- client, err := xmpp.NewClient(config, router)
+ client, err := xmpp.NewClient(config, router, errorHandler)
if err != nil {
log.Fatalf("%+v", err)
}
@@ -138,6 +146,11 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}
+
+func errorHandler(err error) {
+ fmt.Println(err.Error())
+}
+
```
## Reference documentation
diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go
index 81642c6..d07587a 100644
--- a/_examples/delegation/delegation.go
+++ b/_examples/delegation/delegation.go
@@ -171,7 +171,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
return
}
- pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub)
+ pubsub, ok := forwardedIQ.Payload.(*stanza.PubSubGeneric)
if !ok {
// We only support pubsub delegation
return
@@ -180,7 +180,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) {
if pubsub.Publish.XMLName.Local == "publish" {
// Prepare pubsub IQ reply
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
- payload := stanza.PubSub{
+ payload := stanza.PubSubGeneric{
XMLName: xml.Name{
Space: "http://jabber.org/protocol/pubsub",
Local: "pubsub",
diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go
index 0c05edd..9f2c758 100644
--- a/_examples/xmpp_chat_client/interface.go
+++ b/_examples/xmpp_chat_client/interface.go
@@ -136,7 +136,11 @@ func writeInput(g *gocui.Gui, v *gocui.View) error {
input := strings.Join(v.ViewBufferLines(), "\n")
fmt.Fprintln(chatLogWindow, "Me : ", input)
- textChan <- input
+ if viewState.input == rawInputWindow {
+ rawTextChan <- input
+ } else {
+ textChan <- input
+ }
v.Clear()
v.EditDeleteToStartOfLine()
diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go
index 5654a2b..b6c6766 100644
--- a/_examples/xmpp_echo/xmpp_echo.go
+++ b/_examples/xmpp_echo/xmpp_echo.go
@@ -28,7 +28,7 @@ func main() {
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
- client, err := xmpp.NewClient(config, router)
+ client, err := xmpp.NewClient(config, router, errorHandler)
if err != nil {
log.Fatalf("%+v", err)
}
@@ -50,3 +50,7 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}
+
+func errorHandler(err error) {
+ fmt.Println(err.Error())
+}
diff --git a/_examples/xmpp_jukebox/README.md b/_examples/xmpp_jukebox/README.md
new file mode 100644
index 0000000..9eeb3a4
--- /dev/null
+++ b/_examples/xmpp_jukebox/README.md
@@ -0,0 +1,37 @@
+# Jukebox example
+
+## Requirements
+- You need mpg123 installed on your computer because the example runs it as a command :
+[Official MPG123 website](https://mpg123.de/)
+Most linux distributions have a package for it.
+- You need a soundcloud ID to play a music from the website through mpg123. You currently cannot play music files with this example.
+Your user ID is available in your account settings on the [soundcloud website](https://soundcloud.com/)
+**One is provided for convenience.**
+- You need a running jabber server. You can run your local instance of [ejabberd](https://www.ejabberd.im/) for example.
+- You need a registered user on the running jabber server.
+
+## Run
+You can edit the soundcloud ID in the example file with your own, or use the provided one :
+```go
+const scClientID = "dde6a0075614ac4f3bea423863076b22"
+```
+
+To run the example, build it with (while in the example directory) :
+```
+go build xmpp_jukebox.go
+```
+
+then run it with (update the command arguments accordingly):
+```
+./xmpp_jukebox -jid=MY_USERE@MY_DOMAIN/jukebox -password=MY_PASSWORD -address=MY_SERVER:MY_SERVER_PORT
+```
+Make sure to have a resource, for instance "/jukebox", on your jid.
+
+Then you can send the following stanza to "MY_USERE@MY_DOMAIN/jukebox" (with the resource) to play a song (update the soundcloud URL accordingly) :
+```xml
+
+
+
+
+
+```
diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go
index ce7ebc9..57137b8 100644
--- a/_examples/xmpp_jukebox/xmpp_jukebox.go
+++ b/_examples/xmpp_jukebox/xmpp_jukebox.go
@@ -3,6 +3,7 @@
package main
import (
+ "encoding/xml"
"flag"
"fmt"
"log"
@@ -19,7 +20,7 @@ import (
const scClientID = "dde6a0075614ac4f3bea423863076b22"
func main() {
- jid := flag.String("jid", "", "jukebok XMPP JID, resource is optional")
+ jid := flag.String("jid", "", "jukebok XMPP Jid, resource is optional")
password := flag.String("password", "", "XMPP account password")
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
flag.Parse()
@@ -48,7 +49,7 @@ func main() {
handleMessage(s, p, player)
})
router.NewRoute().
- Packet("message").
+ Packet("iq").
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
handleIQ(s, p, player)
})
@@ -108,11 +109,29 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
}
func sendUserTune(s xmpp.Sender, artist string, title string) {
- tune := stanza.Tune{Artist: artist, Title: title}
- iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "usertune-1", Lang: "en"})
- payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
- iq.Payload = &payload
- _ = s.Send(iq)
+ rq, err := stanza.NewPublishItemRq("localhost",
+ "http://jabber.org/protocol/tune",
+ "",
+ stanza.Item{
+ XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"},
+ Any: &stanza.Node{
+ Nodes: []stanza.Node{
+ {
+ XMLName: xml.Name{Local: "artist"},
+ Content: artist,
+ },
+ {
+ XMLName: xml.Name{Local: "title"},
+ Content: title,
+ },
+ },
+ },
+ })
+ if err != nil {
+ fmt.Printf("failed to build the publish request : %s", err.Error())
+ return
+ }
+ _ = s.Send(rq)
}
func playSCURL(p *mpg123.Player, rawURL string) {
@@ -120,7 +139,7 @@ func playSCURL(p *mpg123.Player, rawURL string) {
// TODO: Maybe we need to check the track itself to get the stream URL from reply ?
url := soundcloud.FormatStreamURL(songID)
- _ = p.Play(url)
+ _ = p.Play(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID))
}
// TODO
diff --git a/client.go b/client.go
index be15540..a795934 100644
--- a/client.go
+++ b/client.go
@@ -107,14 +107,14 @@ Setting up the client / Checking the parameters
*/
// NewClient generates a new XMPP client, based on Config passed as parameters.
-// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
+// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the Jid.
// Default the port to 5222.
func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) {
if config.KeepaliveInterval == 0 {
config.KeepaliveInterval = time.Second * 30
}
- // Parse JID
- if config.parsedJid, err = NewJid(config.Jid); err != nil {
+ // Parse Jid
+ if config.parsedJid, err = stanza.NewJid(config.Jid); err != nil {
err = errors.New("missing jid")
return nil, NewConnError(err, true)
}
@@ -142,6 +142,11 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e
}
}
}
+ if config.Domain == "" {
+ // Fallback to jid domain
+ config.Domain = config.parsedJid.Domain
+ }
+
c = new(Client)
c.config = config
c.router = r
diff --git a/cmd/fluuxmpp/send.go b/cmd/fluuxmpp/send.go
index 0942928..5a10c9e 100644
--- a/cmd/fluuxmpp/send.go
+++ b/cmd/fluuxmpp/send.go
@@ -2,6 +2,7 @@ package main
import (
"bufio"
+ "gosrc.io/xmpp/stanza"
"os"
"strings"
"sync"
@@ -48,7 +49,7 @@ func sendxmpp(cmd *cobra.Command, args []string) {
wg.Add(1)
// FIXME: Remove global variables
- var mucsToLeave []*xmpp.Jid
+ var mucsToLeave []*stanza.Jid
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
defer wg.Done()
@@ -57,7 +58,7 @@ func sendxmpp(cmd *cobra.Command, args []string) {
if isMUCRecipient {
for _, muc := range receiver {
- jid, err := xmpp.NewJid(muc)
+ jid, err := stanza.NewJid(muc)
if err != nil {
log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err)
continue
diff --git a/cmd/fluuxmpp/xmppmuc.go b/cmd/fluuxmpp/xmppmuc.go
index 984b014..a00fdfc 100644
--- a/cmd/fluuxmpp/xmppmuc.go
+++ b/cmd/fluuxmpp/xmppmuc.go
@@ -7,7 +7,7 @@ import (
"gosrc.io/xmpp/stanza"
)
-func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error {
+func joinMUC(c xmpp.Sender, toJID *stanza.Jid) error {
return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()},
Extensions: []stanza.PresExtension{
stanza.MucPresence{
@@ -16,7 +16,7 @@ func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error {
})
}
-func leaveMUCs(c xmpp.Sender, mucsToLeave []*xmpp.Jid) {
+func leaveMUCs(c xmpp.Sender, mucsToLeave []*stanza.Jid) {
for _, muc := range mucsToLeave {
if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{
To: muc.Full(),
diff --git a/config.go b/config.go
index da4d4ab..178da2e 100644
--- a/config.go
+++ b/config.go
@@ -1,6 +1,7 @@
package xmpp
import (
+ "gosrc.io/xmpp/stanza"
"os"
"time"
)
@@ -11,7 +12,7 @@ type Config struct {
TransportConfiguration
Jid string
- parsedJid *Jid // For easier manipulation
+ parsedJid *stanza.Jid // For easier manipulation
Credential Credential
StreamLogger *os.File // Used for debugging
Lang string // TODO: should default to 'en'
diff --git a/stanza/commands.go b/stanza/commands.go
new file mode 100644
index 0000000..5a3191f
--- /dev/null
+++ b/stanza/commands.go
@@ -0,0 +1,136 @@
+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"`
+
+ CommandElement CommandElement `xml:",any"`
+
+ 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"`
+}
+
+func (c *Command) Namespace() string {
+ return c.XMLName.Space
+}
+
+type CommandElement interface {
+ Ref() string
+}
+
+type Actions struct {
+ 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 {
+ Text string `xml:",cdata"`
+ Type string `xml:"type,attr,omitempty"`
+}
+
+func (n *Note) Ref() string {
+ return "note"
+}
+
+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 "affiliations":
+ a := Actions{}
+ d.DecodeElement(&a, &tt)
+ c.CommandElement = &a
+ case "configure":
+ nt := Note{}
+ d.DecodeElement(&nt, &tt)
+ c.CommandElement = &nt
+ default:
+ n := Node{}
+ e := d.DecodeElement(&n, &tt)
+ _ = e
+ c.CommandElement = &n
+ if err != nil {
+ return err
+ }
+ }
+
+ case xml.EndElement:
+ if tt == start.End() {
+ return nil
+ }
+ }
+ }
+}
diff --git a/stanza/commands_test.go b/stanza/commands_test.go
new file mode 100644
index 0000000..4cdee0f
--- /dev/null
+++ b/stanza/commands_test.go
@@ -0,0 +1,53 @@
+package stanza_test
+
+import (
+ "encoding/xml"
+ "gosrc.io/xmpp/stanza"
+ "testing"
+)
+
+func TestMarshalCommands(t *testing.T) {
+ input := "Available Servi" +
+ "ces- <" +
+ "field xmlns=\"jabber:x:data\" var=\"service\">httpdoffoffonon
- postgresqloffoffon" +
+ "on
- jabberdoffoffonon
"
+
+ 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.Fatalf(err.Error())
+ }
+}
diff --git a/stanza/component_test.go b/stanza/component_test.go
index d02531e..648131c 100644
--- a/stanza/component_test.go
+++ b/stanza/component_test.go
@@ -67,7 +67,7 @@ func TestParsingDelegationIQ(t *testing.T) {
return
}
if forwardedIQ.Payload != nil {
- if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
+ if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); ok {
node = pubsub.Publish.Node
}
}
diff --git a/stanza/error.go b/stanza/error.go
index 0f416e4..5f42018 100644
--- a/stanza/error.go
+++ b/stanza/error.go
@@ -3,6 +3,7 @@ package stanza
import (
"encoding/xml"
"strconv"
+ "strings"
)
// ============================================================================
@@ -53,10 +54,19 @@ func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
}
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
- if elt.XMLName == textName {
+ // 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" {
- x.Reason = elt.XMLName.Local
+ } 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:
diff --git a/stanza/form.go b/stanza/form.go
new file mode 100644
index 0000000..b9a9932
--- /dev/null
+++ b/stanza/form.go
@@ -0,0 +1,67 @@
+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
+ Type string `xml:"type,attr"`
+}
+
+type FormItem struct {
+ Fields []Field
+}
+
+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"`
+}
diff --git a/stanza/form_test.go b/stanza/form_test.go
new file mode 100644
index 0000000..a68d88e
--- /dev/null
+++ b/stanza/form_test.go
@@ -0,0 +1,107 @@
+package stanza
+
+import (
+ "encoding/xml"
+ "strings"
+ "testing"
+)
+
+const (
+ formSubmit = "" +
+ "" +
+ "" +
+ "" +
+ "http://jabber.org/protocol/pubsub#node_config" +
+ "" +
+ "" +
+ "Princely Musings (Atom)" +
+ "" +
+ "" +
+ "1" +
+ "" +
+ "" +
+ "roster" +
+ "" +
+ "" +
+ "friends" +
+ "servants" +
+ "courtiers" +
+ "" +
+ "" +
+ "http://www.w3.org/2005/Atom" +
+ "" +
+ "" +
+ "headline" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ ""
+
+ clientJid = "hamlet@denmark.lit/elsinore"
+ serviceJid = "pubsub.shakespeare.lit"
+ iqId = "config1"
+ serviceNode = "princely_musings"
+)
+
+func TestMarshalFormSubmit(t *testing.T) {
+ formIQ := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet})
+ 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)
+ }
+}
diff --git a/stanza/iq_disco_test.go b/stanza/iq_disco_test.go
index 012952e..1f8ab8b 100644
--- a/stanza/iq_disco_test.go
+++ b/stanza/iq_disco_test.go
@@ -73,11 +73,11 @@ func TestDiscoItems_Builder(t *testing.T) {
{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("Items length mismatch: %#v", pp.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)
+ 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)
diff --git a/stanza/iq_roster_test.go b/stanza/iq_roster_test.go
index 7228084..ca891df 100644
--- a/stanza/iq_roster_test.go
+++ b/stanza/iq_roster_test.go
@@ -69,11 +69,11 @@ func TestRosterBuilder(t *testing.T) {
},
}
if len(pp.Items) != len(items) {
- t.Errorf("Items length mismatch: %#v", pp.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)
+ 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)
diff --git a/jid.go b/stanza/jid.go
similarity index 87%
rename from jid.go
rename to stanza/jid.go
index cee292a..2fc9cc6 100644
--- a/jid.go
+++ b/stanza/jid.go
@@ -1,4 +1,4 @@
-package xmpp
+package stanza
import (
"fmt"
@@ -20,9 +20,9 @@ func NewJid(sjid string) (*Jid, error) {
}
s1 := strings.SplitN(sjid, "@", 2)
- if len(s1) == 1 { // This is a server or component JID
+ if len(s1) == 1 { // This is a server or component Jid
jid.Domain = s1[0]
- } else { // JID has a local username part
+ } else { // Jid has a local username part
if s1[0] == "" {
return jid, fmt.Errorf("invalid jid '%s", sjid)
}
@@ -41,10 +41,10 @@ func NewJid(sjid string) (*Jid, error) {
}
if !isUsernameValid(jid.Node) {
- return jid, fmt.Errorf("invalid Node in JID '%s'", sjid)
+ 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, fmt.Errorf("invalid domain in Jid '%s'", sjid)
}
return jid, nil
diff --git a/jid_test.go b/stanza/jid_test.go
similarity index 99%
rename from jid_test.go
rename to stanza/jid_test.go
index c6fee03..db86a9d 100644
--- a/jid_test.go
+++ b/stanza/jid_test.go
@@ -1,4 +1,4 @@
-package xmpp
+package stanza
import (
"testing"
diff --git a/stanza/msg_pubsub_event.go b/stanza/msg_pubsub_event.go
new file mode 100644
index 0000000..6ee3dfb
--- /dev/null
+++ b/stanza/msg_pubsub_event.go
@@ -0,0 +1,214 @@
+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
+ }
+ }
+
+ }
+ return nil
+}
diff --git a/stanza/msg_pubsub_event_test.go b/stanza/msg_pubsub_event_test.go
new file mode 100644
index 0000000..29a795d
--- /dev/null
+++ b/stanza/msg_pubsub_event_test.go
@@ -0,0 +1,162 @@
+package stanza_test
+
+import (
+ "encoding/xml"
+ "gosrc.io/xmpp/stanza"
+ "strings"
+ "testing"
+)
+
+func TestDecodeMsgEvent(t *testing.T) {
+ str := `
+
+
+ -
+
+ Soliloquy
+
+ 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?
+
+
+ tag:denmark.lit,2003:entry-32397
+ 2003-12-13T18:30:02Z
+ 2003-12-13T18:30:02Z
+
+
+
+
+
+ `
+ 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 := "" +
+ "- " +
+ "My pub item title" +
+ "My pub item content summary" +
+ "My pub item content ID2003-12-13T18:30:02Z" +
+ "2003-12-13T18:30:02Z
"
+ 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))
+ }
+
+}
diff --git a/stanza/pep.go b/stanza/pep.go
index 7de57ea..cfd50c1 100644
--- a/stanza/pep.go
+++ b/stanza/pep.go
@@ -15,7 +15,7 @@ type Tune struct {
Uri string `xml:"uri,omitempty"`
}
-// Mood defines deta model for XEP-0107 - User Mood
+// 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
diff --git a/stanza/pubsub.go b/stanza/pubsub.go
index 010f8b0..1cbdf16 100644
--- a/stanza/pubsub.go
+++ b/stanza/pubsub.go
@@ -2,39 +2,383 @@ package stanza
import (
"encoding/xml"
+ "errors"
+ "strings"
)
-type PubSub struct {
+type PubSubGeneric struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
- Publish *Publish
- Retract *Retract
- // TODO
+
+ 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"`
}
-func (p *PubSub) Namespace() string {
+func (p *PubSubGeneric) Namespace() string {
return p.XMLName.Space
}
+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:
+ //
+ //
+ //
+ // 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"`
- Item Item
+ 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"`
- Tune *Tune
- Mood *Mood
+ 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 string `xml:"notify,attr"`
- Item Item
+ 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 IQ{}, e
+ }
+
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 IQ{}, e
+ }
+
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 IQ{}, e
+ }
+
+ iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ 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 IQ{}, e
+ }
+ if form.Type != FormTypeSubmit {
+ return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type)
+ }
+
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 element MUST follow the element and
+// MUST NOT possess a 'node' attribute or 'jid' attribute,
+// since the value of the element's 'node' attribute specifies the desired NodeID and
+// the value of the 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 IQ{}, e
+ }
+ if form.Type != FormTypeSubmit {
+ return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type)
+ }
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ 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 element MUST possess a 'node' attribute, specifying the NodeID of the node."
+ if strings.TrimSpace(nodeID) == "" {
+ return IQ{}, errors.New("cannot publish without a target node ID")
+ }
+
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ iq.Payload = &PubSubGeneric{
+ Publish: &Publish{Node: nodeID, Items: []Item{item}},
+ }
+
+ // "The 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 element MUST possess a 'node' attribute, specifying the NodeID of the node."
+ if strings.TrimSpace(nodeID) == "" {
+ return IQ{}, errors.New("cannot publish without a target node ID")
+ }
+
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 element MUST possess a 'node' attribute, specifying the NodeID of the node."
+ if strings.TrimSpace(nodeID) == "" {
+ return IQ{}, errors.New("cannot delete item without a target node ID")
+ }
+
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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
+// child contains an empty element with no attributes.
+func NewRetrieveAllSubsRequest(serviceId string) (IQ, error) {
+ iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ 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 element with no attributes.
+func NewRetrieveAllAffilsRequest(serviceId string) (IQ, error) {
+ iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ iq.Payload = &PubSubGeneric{
+ Affiliations: &Affiliations{},
+ }
+ return iq, nil
}
func init() {
- TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{})
+ TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub", Local: "pubsub"}, PubSubGeneric{})
}
diff --git a/stanza/pubsub_owner.go b/stanza/pubsub_owner.go
new file mode 100644
index 0000000..054303f
--- /dev/null
+++ b/stanza/pubsub_owner.go
@@ -0,0 +1,377 @@
+package stanza
+
+import (
+ "encoding/xml"
+ "errors"
+ "strings"
+)
+
+type PubSubOwner struct {
+ XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"`
+ OwnerUseCase OwnerUseCase
+}
+
+func (pso *PubSubOwner) Namespace() string {
+ return pso.XMLName.Space
+}
+
+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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ 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 IQ{}, errors.New("cannot delete a node without a target node ID")
+ }
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 IQ{}, 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 IQ{}, err
+ }
+ var n Node
+ xml.Unmarshal(data, &n)
+
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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,
+ 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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ iq.Payload = &PubSubOwner{
+ OwnerUseCase: &SubscriptionsOwner{Node: nodeID},
+ }
+ return iq, nil
+}
+
+func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (IQ, error) {
+ iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
+ 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 := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
+ iq.Payload = &PubSubOwner{
+ OwnerUseCase: &AffiliationsOwner{
+ Node: nodeID,
+ },
+ }
+ return iq, 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
+ 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{}
+ d.DecodeElement(&aff, &tt)
+ pso.OwnerUseCase = &aff
+ case "configure":
+ co := ConfigureOwner{}
+ d.DecodeElement(&co, &tt)
+ pso.OwnerUseCase = &co
+ case "default":
+ def := DefaultOwner{}
+ d.DecodeElement(&def, &tt)
+ pso.OwnerUseCase = &def
+ case "delete":
+ del := DeleteOwner{}
+ d.DecodeElement(&del, &tt)
+ pso.OwnerUseCase = &del
+ case "purge":
+ pu := PurgeOwner{}
+ d.DecodeElement(&pu, &tt)
+ pso.OwnerUseCase = &pu
+ case "subscriptions":
+ subs := SubscriptionsOwner{}
+ d.DecodeElement(&subs, &tt)
+ pso.OwnerUseCase = &subs
+ 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{})
+}
diff --git a/stanza/pubsub_owner_test.go b/stanza/pubsub_owner_test.go
new file mode 100644
index 0000000..8af6194
--- /dev/null
+++ b/stanza/pubsub_owner_test.go
@@ -0,0 +1,833 @@
+package stanza_test
+
+import (
+ "encoding/xml"
+ "errors"
+ "gosrc.io/xmpp/stanza"
+ "testing"
+)
+
+// ******************************
+// * 8.2 Configure a Node
+// ******************************
+func TestNewConfigureNode(t *testing.T) {
+ expectedReq := " " +
+ " " +
+ " "
+
+ subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings")
+ subR.Id = "config1"
+ if err != nil {
+ t.Fatalf("Could not create 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestNewConfigureNodeResp(t *testing.T) {
+ response := `
+
+
+
+
+
+ http://jabber.org/protocol/pubsub#node_config
+
+
+ 0
+
+
+ 1028
+
+
+
+
+
+ never
+
+
+ 0
+
+
+
+
+ headline
+
+
+ http://www.w3.org/2005/Atom
+
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubOwnerPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ 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 := " " +
+ " "
+
+ subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit")
+ subR.Id = "def1"
+ if err != nil {
+ t.Fatalf("Could not create 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestNewRequestDefaultConfigResp(t *testing.T) {
+ response := `
+
+
+
+
+
+ http://jabber.org/protocol/pubsub#node_config
+
+
+ 0
+
+
+ 1028
+
+
+
+
+
+ never
+
+
+ 0
+
+
+
+
+ headline
+
+
+ http://www.w3.org/2005/Atom
+
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubOwnerPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ 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 := "" +
+ " " +
+ " "
+
+ subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings")
+ subR.Id = "delete1"
+ if err != nil {
+ t.Fatalf("Could not create 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestNewDelNodeResp(t *testing.T) {
+ response := `
+
+
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubOwnerPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ 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 := " " +
+ " " +
+ " "
+
+ subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings")
+ subR.Id = "purge1"
+ if err != nil {
+ t.Fatalf("Could not create 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.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.Fatalf(err.Error())
+ }
+}
+
+// ************************************
+// * 8.6 Manage Subscription Requests
+// ************************************
+func TestNewApproveSubRequest(t *testing.T) {
+ expectedReq := " " +
+ " " +
+ "http://jabber.org/protocol/pubsub#subscribe_authorization " +
+ " 123-abc princely_musings " +
+ " horatio@denmark.lit " +
+ "true "
+
+ 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)
+ subR.Id = "approve1"
+ if err != nil {
+ t.Fatalf("Could not create request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+}
+
+// ********************************************
+// * 8.7 Process Pending Subscription Requests
+// ********************************************
+
+func TestNewGetPendingSubRequests(t *testing.T) {
+ expectedReq := " " +
+ "" +
+ " "
+
+ subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit")
+ subR.Id = "pending1"
+ if err != nil {
+ t.Fatalf("Could not create request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+}
+
+func TestNewGetPendingSubRequestsResp(t *testing.T) {
+ response := `
+
+
+
+
+ http://jabber.org/protocol/pubsub#subscribe_authorization
+
+
+
+
+
+
+
+
+`
+
+ 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 {
+ errors.New("this iq payload is not a command")
+ }
+
+ fMap, err := respIQ.GetFormFields()
+ if err != nil || len(fMap) != 2 {
+ errors.New("could not parse command form fields")
+ }
+
+}
+
+// ********************************************
+// * 8.7 Process Pending Subscription Requests
+// ********************************************
+
+func TestNewApprovePendingSubRequest(t *testing.T) {
+ expectedReq := " " +
+ " " +
+ " " +
+ "princely_musings "
+
+ subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit",
+ "pubsub-get-pending:20031021T150901Z-600",
+ "princely_musings")
+ subR.Id = "pending2"
+ if err != nil {
+ t.Fatalf("Could not create request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+}
+
+// ********************************************
+// * 8.8.1 Retrieve Subscriptions List
+// ********************************************
+
+func TestNewSubListRqPl(t *testing.T) {
+ expectedReq := " " +
+ " " +
+ " "
+
+ subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings")
+ subR.Id = "subman1"
+ if err != nil {
+ t.Fatalf("Could not create 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestNewSubListRqPlResp(t *testing.T) {
+ response := `
+
+
+
+
+
+
+
+
+
+
+`
+
+ 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 {
+ errors.New("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 := " " +
+ " " +
+ " "
+
+ subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings")
+ subR.Id = "ent1"
+ if err != nil {
+ t.Fatalf("Could not create 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestNewAffiliationListRequestResp(t *testing.T) {
+ response := `
+
+
+
+
+
+
+
+
+`
+
+ 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 {
+ errors.New("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 := " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ ""
+
+ 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)
+ subR.Id = "ent3"
+ if err != nil {
+ t.Fatalf("Could not create 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestGetFormFields(t *testing.T) {
+ response := `
+
+
+
+
+
+ http://jabber.org/protocol/pubsub#node_config
+
+
+ 0
+
+
+ 1028
+
+
+
+
+
+ never
+
+
+ 0
+
+
+
+
+ headline
+
+
+ http://www.w3.org/2005/Atom
+
+
+
+
+
+
+`
+ 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 := `
+
+
+
+
+ http://jabber.org/protocol/pubsub#subscribe_authorization
+
+
+
+
+
+
+
+
+`
+ 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 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 {
+ errors.New("this iq payload is not a pubsub of the owner namespace")
+ }
+
+ return pubsub, nil
+}
diff --git a/stanza/pubsub_test.go b/stanza/pubsub_test.go
new file mode 100644
index 0000000..95bf640
--- /dev/null
+++ b/stanza/pubsub_test.go
@@ -0,0 +1,921 @@
+package stanza_test
+
+import (
+ "encoding/xml"
+ "errors"
+ "gosrc.io/xmpp/stanza"
+ "strings"
+ "testing"
+)
+
+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 := " " +
+ " " +
+ " "
+
+ subInfo := stanza.SubInfo{
+ Node: "princely_musings", Jid: "francisco@denmark.lit",
+ }
+ subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo)
+ subR.Id = "sub1"
+ if err != nil {
+ t.Fatalf("Could not create a sub request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+
+}
+
+func TestNewSubResp(t *testing.T) {
+ response := `
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubGenericPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+
+ 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 := " " +
+ " " +
+ " "
+
+ subInfo := stanza.SubInfo{
+ Node: "princely_musings", Jid: "francisco@denmark.lit",
+ }
+ subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo)
+ subR.Id = "unsub1"
+ if err != nil {
+ t.Fatalf("Could not create a sub 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestNewUnsubResp(t *testing.T) {
+ response := `
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubGenericPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+
+ 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 := " " +
+ " " +
+ " "
+
+ subInfo := stanza.SubInfo{
+ Node: "princely_musings", Jid: "francisco@denmark.lit",
+ }
+ subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo)
+ subR.Id = "options1"
+ if err != nil {
+ t.Fatalf("Could not create a sub 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.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.Fatalf(err.Error())
+ }
+}
+
+func TestNewNewConfOptsRsp(t *testing.T) {
+ response := `
+
+
+
+
+
+ http://jabber.org/protocol/pubsub#subscribe_options
+
+
+ 1
+
+
+ 0
+
+
+ false
+
+
+
+
+
+
+
+ chat
+ online
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubGenericPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+
+ 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 := " " +
+ " " +
+ " " +
+ " http://jabber.org/protocol/pubsub#node_config " +
+ "Princely Musings (Atom) " +
+ "1 roster " +
+ " friends servants" +
+ " courtiers http://www.w3.org/2005/Atom " +
+ " " +
+ "headline " +
+ " "
+
+ subInfo := stanza.SubInfo{
+ Node: "princely_musings", Jid: "francisco@denmark.lit",
+ }
+
+ subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample)
+ subR.Id = "options2"
+ if err != nil {
+ t.Fatalf("Could not create a sub 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.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.Fatalf(err.Error())
+ }
+}
+
+// ***************************************
+// * 6.3.7 Subscribe and Configure
+// ***************************************
+
+func TestNewSubAndConfig(t *testing.T) {
+ expectedReq := "" +
+ " " +
+ "" +
+ " " +
+ " http://jabber.org/protocol/pubsub#node_config " +
+ "Princely Musings (Atom) " +
+ "1 roster " +
+ " friends servants" +
+ " courtiers http://www.w3.org/2005/Atom " +
+ " " +
+ "headline " +
+ " "
+
+ subInfo := stanza.SubInfo{
+ Node: "princely_musings", Jid: "francisco@denmark.lit",
+ }
+
+ subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample)
+ subR.Id = "sub1"
+ if err != nil {
+ t.Fatalf("Could not create a sub 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.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 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.Fatalf(err.Error())
+ }
+}
+
+func TestNewSubAndConfigResp(t *testing.T) {
+ response := `
+
+
+
+
+
+
+ http://jabber.org/protocol/pubsub#subscribe_options
+
+
+ 1
+
+
+ 0
+
+
+ false
+
+
+ chat
+ online
+ away
+
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubGenericPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ 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 tag to request all items from a service")
+ }
+}
+func TestNewItemsResp(t *testing.T) {
+ response := `
+
+
+
+ -
+
+ Alone
+ Now I am alone. O, what a rogue and peasant slave am I!
+
+ tag:denmark.lit,2003:entry-32396
+ 2003-12-13T11:09:53Z
+ 2003-12-13T11:09:53Z
+
+
+ -
+
+ Soliloquy
+ 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?
+
+ tag:denmark.lit,2003:entry-32397
+ 2003-12-13T18:30:02Z
+ 2003-12-13T18:30:02Z
+
+
+
+
+
+`
+
+ pubsub, err := getPubSubGenericPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ 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 := " " +
+ " " +
+ " "
+
+ subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901")
+ subR.Id = "items3"
+ 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")
+ }
+ data, err := xml.Marshal(subR)
+ if err := compareMarshal(expectedReq, string(data)); err != nil {
+ t.Fatalf(err.Error())
+ }
+}
+
+// ***************************************
+// * 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 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 := " " +
+ " - " +
+ " Soliloquy " +
+ " 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? " +
+ " " +
+ "tag:denmark.lit,2003:entry-32397 2003-12-13T18:30:02Z " +
+ "2003-12-13T18:30:02Z
" +
+ " " +
+ "http://jabber.org/protocol/pubsub#publish-options " +
+ "presence "
+
+ 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 := " " +
+ " " +
+ " "
+
+ subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil)
+ subR.Id = "retract1"
+ if err != nil {
+ t.Fatalf("Could not create a del item request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+}
+
+// ***************************************
+// * 8.1 Create a Node
+// ***************************************
+
+func TestNewCreateNode(t *testing.T) {
+ expectedReq := " " +
+ " "
+
+ subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings")
+ subR.Id = "create1"
+ if err != nil {
+ t.Fatalf("Could not create a create node request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+}
+
+func TestNewCreateNodeResp(t *testing.T) {
+ response := `
+
+
+
+
+
+`
+ pubsub, err := getPubSubGenericPayload(response)
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ 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 := " " +
+ " " +
+ " " +
+ "http://jabber.org/protocol/pubsub#node_config " +
+ "0 0 " +
+ " 1028 "
+
+ 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"}},
+ },
+ })
+ subR.Id = "create1"
+ if err != nil {
+ t.Fatalf("Could not create a create node request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+
+}
+
+// ********************************
+// * 5.7 Retrieve Subscriptions
+// ********************************
+
+func TestNewRetrieveAllSubsRequest(t *testing.T) {
+ expected := " " +
+ " "
+
+ subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit")
+ subR.Id = "subscriptions1"
+ if err != nil {
+ t.Fatalf("Could not create a create node request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+}
+
+func TestRetrieveAllSubsResp(t *testing.T) {
+ response := `
+
+
+
+
+
+
+
+
+
+
+
+`
+ 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 := " " +
+ " "
+
+ subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit")
+ subR.Id = "affil1"
+ if err != nil {
+ t.Fatalf("Could not create retreive all affiliations request : %s", err)
+ }
+
+ 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.Fatalf(err.Error())
+ }
+}
+
+func TestRetrieveAllAffilsResp(t *testing.T) {
+ response := `
+
+
+
+
+
+
+
+
+
+
+`
+ 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 {
+ errors.New("this iq payload is not a pubsub")
+ }
+
+ return pubsub, nil
+}
diff --git a/stanza/xmpp_test.go b/stanza/xmpp_test.go
index 420a053..473616a 100644
--- a/stanza/xmpp_test.go
+++ b/stanza/xmpp_test.go
@@ -2,12 +2,17 @@ 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
@@ -63,3 +68,14 @@ func xmlOpts() cmp.Options {
}
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
+}
diff --git a/tcp_server_mock.go b/tcp_server_mock.go
index c8f5d97..1a4f92e 100644
--- a/tcp_server_mock.go
+++ b/tcp_server_mock.go
@@ -280,7 +280,7 @@ func bind(t *testing.T, sc *ServerConn) {
%s
`
- fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real JID
+ fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real Jid
}
}