PubSub protocol support (#142)
* PubSub protocol support Added support for : - XEP-0050 (Command)) - XEP-0060 (PubSub) - XEP-0004 (Forms) Fixed the NewClient function by adding parsing of the domain from the JID if no domain is provided in transport config. Updated xmpp_jukebox example * Delete useless pubsub errors * README.md update Fixed import in echo example * Typo * Fixed raw send on client example * Fixed jukebox example and added a README.md
This commit is contained in:
parent
6e2ba9ca57
commit
947fcf0432
17
README.md
17
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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -136,7 +136,11 @@ func writeInput(g *gocui.Gui, v *gocui.View) error {
|
|||
input := strings.Join(v.ViewBufferLines(), "\n")
|
||||
|
||||
fmt.Fprintln(chatLogWindow, "Me : ", input)
|
||||
if viewState.input == rawInputWindow {
|
||||
rawTextChan <- input
|
||||
} else {
|
||||
textChan <- input
|
||||
}
|
||||
|
||||
v.Clear()
|
||||
v.EditDeleteToStartOfLine()
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
37
_examples/xmpp_jukebox/README.md
Normal file
37
_examples/xmpp_jukebox/README.md
Normal file
|
@ -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
|
||||
<iq id="1" to="MY_USERE@MY_DOMAIN/jukebox" type="set">
|
||||
<set xml:lang="en" xmlns="urn:xmpp:iot:control">
|
||||
<string name="url" value="https://soundcloud.com/UPDATE/ME"/>
|
||||
</set>
|
||||
</iq>
|
||||
```
|
|
@ -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
|
||||
|
|
11
client.go
11
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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'
|
||||
|
|
136
stanza/commands.go
Normal file
136
stanza/commands.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
stanza/commands_test.go
Normal file
53
stanza/commands_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalCommands(t *testing.T) {
|
||||
input := "<command xmlns=\"http://jabber.org/protocol/commands\" node=\"list\" sessionid=\"list:20020923T213616Z-700\" status=\"completed\"><x " +
|
||||
"xmlns=\"jabber:x:data\" type=\"result\"><title xmlns=\"jabber:x:data\">Available Servi" +
|
||||
"ces</title><reported xmlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" label=\"S" +
|
||||
"ervice\" var=\"service\"></field><field xmlns=\"jabber:x:data\" label=\"Single-User mo" +
|
||||
"de\" var=\"runlevel-1\"></field><field xmlns=\"jabber:x:data\" label=\"Non-Networked M" +
|
||||
"ulti-User mode\" var=\"runlevel-2\"></field><field xmlns=\"jabber:x:data\" label=\"Ful" +
|
||||
"l Multi-User mode\" var=\"runlevel-3\"></field><field xmlns=\"jabber:x:data\" label=\"" +
|
||||
"X-Window mode\" var=\"runlevel-5\"></field></reported><item xmlns=\"jabber:x:data\"><" +
|
||||
"field xmlns=\"jabber:x:data\" var=\"service\"><value xmlns=\"jabber:x:data\">httpd</va" +
|
||||
"lue></field><field xmlns=\"jabber:x:data\" var=\"runlevel-1\"><value xmlns=\"jabber:x" +
|
||||
":data\">off</value></field><field xmlns=\"jabber:x:data\" var=\"runlevel-2\"><value x" +
|
||||
"mlns=\"jabber:x:data\">off</value></field><field xmlns=\"jabber:x:data\" var=\"runlev" +
|
||||
"el-3\"><value xmlns=\"jabber:x:data\">on</value></field><field xmlns=\"jabber:x:data" +
|
||||
"\" var=\"runlevel-5\"><value xmlns=\"jabber:x:data\">on</value></field></item><item x" +
|
||||
"mlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" var=\"service\"><value xmlns=\"ja" +
|
||||
"bber:x:data\">postgresql</value></field><field xmlns=\"jabber:x:data\" var=\"runleve" +
|
||||
"l-1\"><value xmlns=\"jabber:x:data\">off</value></field><field xmlns=\"jabber:x:data" +
|
||||
"\" var=\"runlevel-2\"><value xmlns=\"jabber:x:data\">off</value></field><field xmlns=" +
|
||||
"\"jabber:x:data\" var=\"runlevel-3\"><value xmlns=\"jabber:x:data\">on</value></field>" +
|
||||
"<field xmlns=\"jabber:x:data\" var=\"runlevel-5\"><value xmlns=\"jabber:x:data\">on</v" +
|
||||
"alue></field></item><item xmlns=\"jabber:x:data\"><field xmlns=\"jabber:x:data\" var" +
|
||||
"=\"service\"><value xmlns=\"jabber:x:data\">jabberd</value></field><field xmlns=\"jab" +
|
||||
"ber:x:data\" var=\"runlevel-1\"><value xmlns=\"jabber:x:data\">off</value></field><fi" +
|
||||
"eld xmlns=\"jabber:x:data\" var=\"runlevel-2\"><value xmlns=\"jabber:x:data\">off</val" +
|
||||
"ue></field><field xmlns=\"jabber:x:data\" var=\"runlevel-3\"><value xmlns=\"jabber:x:" +
|
||||
"data\">on</value></field><field xmlns=\"jabber:x:data\" var=\"runlevel-5\"><value xml" +
|
||||
"ns=\"jabber:x:data\">on</value></field></item></x></command>"
|
||||
|
||||
var c stanza.Command
|
||||
err := xml.Unmarshal([]byte(input), &c)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal initial input")
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(c)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal unmarshalled input")
|
||||
}
|
||||
|
||||
if err := compareMarshal(input, string(data)); err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package stanza
|
|||
import (
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
|
@ -53,11 +54,20 @@ 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" {
|
||||
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" ||
|
||||
elt.XMLName.Space == "http://jabber.org/protocol/pubsub#errors" {
|
||||
if strings.TrimSpace(x.Reason) != "" {
|
||||
x.Reason = strings.Join([]string{elt.XMLName.Local}, ":")
|
||||
} else {
|
||||
x.Reason = elt.XMLName.Local
|
||||
}
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
|
|
67
stanza/form.go
Normal file
67
stanza/form.go
Normal file
|
@ -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"`
|
||||
}
|
107
stanza/form_test.go
Normal file
107
stanza/form_test.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
formSubmit = "<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\">" +
|
||||
"<configure node=\"princely_musings\">" +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\">" +
|
||||
"<field var=\"FORM_TYPE\" type=\"hidden\">" +
|
||||
"<value>http://jabber.org/protocol/pubsub#node_config</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#title\">" +
|
||||
"<value>Princely Musings (Atom)</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#deliver_notifications\">" +
|
||||
"<value>1</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#access_model\">" +
|
||||
"<value>roster</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#roster_groups_allowed\">" +
|
||||
"<value>friends</value>" +
|
||||
"<value>servants</value>" +
|
||||
"<value>courtiers</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#type\">" +
|
||||
"<value>http://www.w3.org/2005/Atom</value>" +
|
||||
"</field>" +
|
||||
"<field var=\"pubsub#notification_type\" type=\"list-single\"" +
|
||||
"label=\"Specify the delivery style for event notifications\">" +
|
||||
"<value>headline</value>" +
|
||||
"<option>" +
|
||||
"<value>normal</value>" +
|
||||
"</option>" +
|
||||
"<option>" +
|
||||
"<value>headline</value>" +
|
||||
"</option>" +
|
||||
"</field>" +
|
||||
"</x>" +
|
||||
"</configure>" +
|
||||
"</pubsub>"
|
||||
|
||||
clientJid = "hamlet@denmark.lit/elsinore"
|
||||
serviceJid = "pubsub.shakespeare.lit"
|
||||
iqId = "config1"
|
||||
serviceNode = "princely_musings"
|
||||
)
|
||||
|
||||
func TestMarshalFormSubmit(t *testing.T) {
|
||||
formIQ := 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"testing"
|
214
stanza/msg_pubsub_event.go
Normal file
214
stanza/msg_pubsub_event.go
Normal file
|
@ -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
|
||||
}
|
162
stanza/msg_pubsub_event_test.go
Normal file
162
stanza/msg_pubsub_event_test.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeMsgEvent(t *testing.T) {
|
||||
str := `<message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo'>
|
||||
<event xmlns='http://jabber.org/protocol/pubsub#event'>
|
||||
<items node='princely_musings'>
|
||||
<item id='ae890ac52d0df67ed7cfdf51b644e901'>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom'>
|
||||
<title>Soliloquy</title>
|
||||
<summary>
|
||||
To be, or not to be: that is the question:
|
||||
Whether 'tis nobler in the mind to suffer
|
||||
The slings and arrows of outrageous fortune,
|
||||
Or to take arms against a sea of troubles,
|
||||
And by opposing end them?
|
||||
</summary>
|
||||
<link rel='alternate' type='text/html'
|
||||
href='http://denmark.lit/2003/12/13/atom03'/>
|
||||
<id>tag:denmark.lit,2003:entry-32397</id>
|
||||
<published>2003-12-13T18:30:02Z</published>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
</entry>
|
||||
</item>
|
||||
</items>
|
||||
</event>
|
||||
</message>
|
||||
`
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message receipt unmarshall error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedMessage.Body != "" {
|
||||
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
|
||||
}
|
||||
|
||||
if len(parsedMessage.Extensions) < 1 {
|
||||
t.Errorf("no extension found on parsed message")
|
||||
return
|
||||
}
|
||||
|
||||
switch ext := parsedMessage.Extensions[0].(type) {
|
||||
case *stanza.PubSubEvent:
|
||||
if ext.XMLName.Local != "event" {
|
||||
t.Fatalf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
tmp, ok := parsedMessage.Extensions[0].(*stanza.PubSubEvent)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
ie, ok := tmp.EventElement.(*stanza.ItemsEvent)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
||||
if ie.Items[0].Any.Nodes[0].Content != "Soliloquy" {
|
||||
t.Fatalf("could not read title ! Read this : %s", ie.Items[0].Any.Nodes[0].Content)
|
||||
}
|
||||
|
||||
if len(ie.Items[0].Any.Nodes) != 6 {
|
||||
t.Fatalf("some nodes were not correctly parsed")
|
||||
}
|
||||
default:
|
||||
t.Fatalf("could not find pubsub event extension")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEncodeEvent(t *testing.T) {
|
||||
expected := "<message><event xmlns=\"http://jabber.org/protocol/pubsub#event\">" +
|
||||
"<items node=\"princely_musings\"><item id=\"ae890ac52d0df67ed7cfdf51b644e901\">" +
|
||||
"<entry xmlns=\"http://www.w3.org/2005/Atom\"><title>My pub item title</title>" +
|
||||
"<summary>My pub item content summary</summary><link rel=\"alternate\" " +
|
||||
"type=\"text/html\" href=\"http://denmark.lit/2003/12/13/atom03\">" +
|
||||
"</link><id>My pub item content ID</id><published>2003-12-13T18:30:02Z</published>" +
|
||||
"<updated>2003-12-13T18:30:02Z</updated></entry></item></items></event></message>"
|
||||
message := stanza.Message{
|
||||
Extensions: []stanza.MsgExtension{
|
||||
stanza.PubSubEvent{
|
||||
EventElement: stanza.ItemsEvent{
|
||||
Items: []stanza.ItemEvent{
|
||||
{
|
||||
Id: "ae890ac52d0df67ed7cfdf51b644e901",
|
||||
Any: &stanza.Node{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://www.w3.org/2005/Atom",
|
||||
Local: "entry",
|
||||
},
|
||||
Attrs: nil,
|
||||
Content: "",
|
||||
Nodes: []stanza.Node{
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "title"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item title",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "summary"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content summary",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "link"},
|
||||
Attrs: []xml.Attr{
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "rel"},
|
||||
Value: "alternate",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "type"},
|
||||
Value: "text/html",
|
||||
},
|
||||
{
|
||||
Name: xml.Name{Space: "", Local: "href"},
|
||||
Value: "http://denmark.lit/2003/12/13/atom03",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "id"},
|
||||
Attrs: nil,
|
||||
Content: "My pub item content ID",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "published"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
{
|
||||
XMLName: xml.Name{Space: "", Local: "updated"},
|
||||
Attrs: nil,
|
||||
Content: "2003-12-13T18:30:02Z",
|
||||
Nodes: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Node: "princely_musings",
|
||||
Retract: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, _ := xml.Marshal(message)
|
||||
if strings.TrimSpace(string(data)) != strings.TrimSpace(expected) {
|
||||
t.Errorf("event was not encoded properly : \nexpected:%s \ngot: %s", expected, string(data))
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
366
stanza/pubsub.go
366
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 <configure/>
|
||||
|
||||
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:
|
||||
// <subscribe-options>
|
||||
// <required/>
|
||||
// </subscribe-options>
|
||||
// Used to indicate if configuration options is required.
|
||||
Required *struct{}
|
||||
}
|
||||
|
||||
type PublishOptions struct {
|
||||
XMLName xml.Name `xml:"publish-options"`
|
||||
Form *Form
|
||||
}
|
||||
|
||||
type Publish struct {
|
||||
XMLName xml.Name `xml:"publish"`
|
||||
Node string `xml:"node,attr"`
|
||||
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
|
||||
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 <options/> element MUST follow the <subscribe/> element and
|
||||
// MUST NOT possess a 'node' attribute or 'jid' attribute,
|
||||
// since the value of the <subscribe/> element's 'node' attribute specifies the desired NodeID and
|
||||
// the value of the <subscribe/> element's 'jid' attribute specifies the subscriber's JID
|
||||
// 6.3.7 Subscribe and Configure
|
||||
func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (IQ, error) {
|
||||
if e := subInfo.validate(); e != nil {
|
||||
return 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 <publish/> 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 <item/> element provided by the publisher MAY possess an 'id' attribute,
|
||||
// specifying a unique ItemID for the item.
|
||||
// If an ItemID is not provided in the publish request,
|
||||
// the pubsub service MUST generate one and MUST ensure that it is unique for that node."
|
||||
if strings.TrimSpace(pubItemID) != "" {
|
||||
ps, _ := iq.Payload.(*PubSubGeneric)
|
||||
ps.Publish.Items[0].Id = pubItemID
|
||||
}
|
||||
return iq, nil
|
||||
}
|
||||
|
||||
// NewPublishItemOptsRq creates a request to publish items to a node identified by its provided ID, along with configuration options
|
||||
// A pubsub service MAY support the ability to specify options along with a publish request
|
||||
//(if so, it MUST advertise support for the "http://jabber.org/protocol/pubsub#publish-options" feature).
|
||||
func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (IQ, error) {
|
||||
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return 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 <retract/> 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 <pubsub/>
|
||||
// child contains an empty <subscriptions/> 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 <affiliations/> 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{})
|
||||
}
|
||||
|
|
377
stanza/pubsub_owner.go
Normal file
377
stanza/pubsub_owner.go
Normal file
|
@ -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{})
|
||||
}
|
833
stanza/pubsub_owner_test.go
Normal file
833
stanza/pubsub_owner_test.go
Normal file
|
@ -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 := "<iq type=\"get\" id=\"config1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <configure node=\"princely_musings\"></configure> " +
|
||||
"</pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
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 := `
|
||||
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<configure node="princely_musings">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||
</field>
|
||||
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||
<value>1028</value>
|
||||
</field>
|
||||
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||
<option label="Never">
|
||||
<value>never</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed">
|
||||
<value>on_sub</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||
<value>on_sub_and_presence</value>
|
||||
</option>
|
||||
<value>never</value>
|
||||
</field>
|
||||
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||
<option>
|
||||
<value>normal</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>headline</value>
|
||||
</option>
|
||||
<value>headline</value>
|
||||
</field>
|
||||
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||
<value>http://www.w3.org/2005/Atom</value>
|
||||
</field>
|
||||
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubOwnerPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"get\" id=\"def1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <default></default> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit")
|
||||
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 := `
|
||||
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<configure node="princely_musings">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||
</field>
|
||||
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||
<value>1028</value>
|
||||
</field>
|
||||
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||
<option label="Never">
|
||||
<value>never</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed">
|
||||
<value>on_sub</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||
<value>on_sub_and_presence</value>
|
||||
</option>
|
||||
<value>never</value>
|
||||
</field>
|
||||
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||
<option>
|
||||
<value>normal</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>headline</value>
|
||||
</option>
|
||||
<value>headline</value>
|
||||
</field>
|
||||
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||
<value>http://www.w3.org/2005/Atom</value>
|
||||
</field>
|
||||
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubOwnerPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"set\" id=\"delete1\" to=\"pubsub.shakespeare.lit\" >" +
|
||||
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<delete node=\"princely_musings\"></delete> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
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 := `
|
||||
<iq id="delete1" to="pubsub.shakespeare.lit" type="set">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<delete node="princely_musings">
|
||||
<redirect uri="xmpp:hamlet@denmark.lit"/>
|
||||
</delete>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubOwnerPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"set\" id=\"purge1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<purge node=\"princely_musings\"></purge> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings")
|
||||
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 := "<message id=\"approve1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||
"<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value> </field> <field var=\"pubsub#subid\">" +
|
||||
" <value>123-abc</value> </field> <field var=\"pubsub#node\"> <value>princely_musings</value> </field> " +
|
||||
"<field var=\"pubsub#subscriber_jid\"> <value>horatio@denmark.lit</value> </field> <field var=\"pubsub#allow\"> " +
|
||||
"<value>true</value> </field> </x> </message>"
|
||||
|
||||
apprForm := &stanza.Form{
|
||||
Type: stanza.FormTypeSubmit,
|
||||
Fields: []stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#subscribe_authorization"}},
|
||||
{Var: "pubsub#subid", ValuesList: []string{"123-abc"}},
|
||||
{Var: "pubsub#node", ValuesList: []string{"princely_musings"}},
|
||||
{Var: "pubsub#subscriber_jid", ValuesList: []string{"horatio@denmark.lit"}},
|
||||
{Var: "pubsub#allow", ValuesList: []string{"true"}},
|
||||
},
|
||||
}
|
||||
|
||||
subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm)
|
||||
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 := "<iq type=\"set\" id=\"pending1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\" node=\"http://jabber.org/protocol/pubsub#get-pending\" >" +
|
||||
"</command> </iq>"
|
||||
|
||||
subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit")
|
||||
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 := `
|
||||
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
|
||||
</field>
|
||||
<field type="list-single" var="pubsub#node">
|
||||
<option>
|
||||
<value>princely_musings</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>news_from_elsinore</value>
|
||||
</option>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`
|
||||
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse iq")
|
||||
}
|
||||
|
||||
_, ok := respIQ.Payload.(*stanza.Command)
|
||||
if !ok {
|
||||
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 := "<iq type=\"set\" id=\"pending2\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\"" +
|
||||
"node=\"http://jabber.org/protocol/pubsub#get-pending\"sessionid=\"pubsub-get-pending:20031021T150901Z-600\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field xmlns=\"jabber:x:data\" var=\"pubsub#node\"> " +
|
||||
"<value xmlns=\"jabber:x:data\">princely_musings</value> </field> </x> </command> </iq>"
|
||||
|
||||
subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit",
|
||||
"pubsub-get-pending:20031021T150901Z-600",
|
||||
"princely_musings")
|
||||
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 := "<iq type=\"get\" id=\"subman1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<subscriptions node=\"princely_musings\"></subscriptions> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings")
|
||||
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 := `
|
||||
<iq from="pubsub.shakespeare.lit" id="subman1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<subscriptions node="princely_musings">
|
||||
<subscription jid="hamlet@denmark.lit" subscription="subscribed"></subscription>
|
||||
<subscription jid="polonius@denmark.lit" subscription="unconfigured"></subscription>
|
||||
<subscription jid="bernardo@denmark.lit" subid="123-abc" subscription="subscribed"></subscription>
|
||||
<subscription jid="bernardo@denmark.lit" subid="004-yyy" subscription="subscribed"></subscription>
|
||||
</subscriptions>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse iq")
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
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 := "<iq type=\"get\" id=\"ent1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
|
||||
"<affiliations node=\"princely_musings\"></affiliations> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings")
|
||||
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 := `
|
||||
<iq from="pubsub.shakespeare.lit" id="ent1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<affiliations node="princely_musings">
|
||||
<affiliation affiliation="owner" jid="hamlet@denmark.lit"/>
|
||||
<affiliation affiliation="outcast" jid="polonius@denmark.lit"/>
|
||||
</affiliations>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse iq")
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
|
||||
if !ok {
|
||||
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 := "<iq type=\"set\" id=\"ent3\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <affiliations node=\"princely_musings\"> " +
|
||||
"<affiliation affiliation=\"none\" jid=\"hamlet@denmark.lit\"></affiliation> " +
|
||||
"<affiliation affiliation=\"none\" jid=\"polonius@denmark.lit\"></affiliation> " +
|
||||
"<affiliation affiliation=\"publisher\" jid=\"bard@shakespeare.lit\"></affiliation> </affiliations> </pubsub> " +
|
||||
"</iq>"
|
||||
|
||||
affils := []stanza.AffiliationOwner{
|
||||
{
|
||||
AffiliationStatus: stanza.AffiliationStatusNone,
|
||||
Jid: "hamlet@denmark.lit",
|
||||
},
|
||||
{
|
||||
AffiliationStatus: stanza.AffiliationStatusNone,
|
||||
Jid: "polonius@denmark.lit",
|
||||
},
|
||||
{
|
||||
AffiliationStatus: stanza.AffiliationStatusPublisher,
|
||||
Jid: "bard@shakespeare.lit",
|
||||
},
|
||||
}
|
||||
|
||||
subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils)
|
||||
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 := `
|
||||
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
|
||||
<configure node="princely_musings">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#node_config</value>
|
||||
</field>
|
||||
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
|
||||
<value>1028</value>
|
||||
</field>
|
||||
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
|
||||
<option label="Never">
|
||||
<value>never</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed">
|
||||
<value>on_sub</value>
|
||||
</option>
|
||||
<option label="When a new subscription is processed and whenever a subscriber comes online">
|
||||
<value>on_sub_and_presence</value>
|
||||
</option>
|
||||
<value>never</value>
|
||||
</field>
|
||||
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
|
||||
<option>
|
||||
<value>normal</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>headline</value>
|
||||
</option>
|
||||
<value>headline</value>
|
||||
</field>
|
||||
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
|
||||
<value>http://www.w3.org/2005/Atom</value>
|
||||
</field>
|
||||
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
|
||||
</x>
|
||||
</configure>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
var iq stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &iq)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse IQ")
|
||||
}
|
||||
|
||||
fields, err := iq.GetFormFields()
|
||||
if len(fields) != 8 {
|
||||
t.Fatalf("could not correctly parse fields. Expected 8, found : %v", len(fields))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetFormFieldsCmd(t *testing.T) {
|
||||
response := `
|
||||
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
|
||||
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
|
||||
<x type="form" xmlns="jabber:x:data">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
|
||||
</field>
|
||||
<field type="list-single" var="pubsub#node">
|
||||
<option>
|
||||
<value>princely_musings</value>
|
||||
</option>
|
||||
<option>
|
||||
<value>news_from_elsinore</value>
|
||||
</option>
|
||||
</field>
|
||||
</x>
|
||||
</command>
|
||||
</iq>
|
||||
`
|
||||
var iq stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &iq)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse IQ")
|
||||
}
|
||||
|
||||
fields, err := iq.GetFormFields()
|
||||
if len(fields) != 2 {
|
||||
t.Fatalf("could not correctly parse fields. Expected 2, found : %v", len(fields))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func 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
|
||||
}
|
921
stanza/pubsub_test.go
Normal file
921
stanza/pubsub_test.go
Normal file
|
@ -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 := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></subscribe>" +
|
||||
" </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo)
|
||||
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 := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscription node="princely_musings" jid="francisco@denmark.lit"
|
||||
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"set\"id=\"unsub1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||
"<unsubscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></unsubscribe> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo)
|
||||
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 := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="unsub1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscription node="princely_musings" jid="francisco@denmark.lit" subscription="none"
|
||||
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3"/>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"get\"id=\"options1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||
"<options node=\"princely_musings\" jid=\"francisco@denmark.lit\"></options> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo)
|
||||
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 := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="options1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<options node="princely_musings" jid="francisco@denmark.lit">
|
||||
<x xmlns="jabber:x:data" type="form">
|
||||
<field var="FORM_TYPE" type="hidden">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
|
||||
</field>
|
||||
<field var="pubsub#deliver" type="boolean" label="Enable delivery?">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#digest" type="boolean"
|
||||
label="Receive digest notifications (approx. one per day)?">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field var="pubsub#include_body" type="boolean"
|
||||
label="Receive message body in addition to payload?">
|
||||
<value>false</value>
|
||||
</field>
|
||||
<field var="pubsub#show-values" type="list-multi"
|
||||
label="Select the presence types which are
|
||||
allowed to receive event notifications">
|
||||
<option label="Want to Chat">
|
||||
<value>chat</value>
|
||||
</option>
|
||||
<option label="Available">
|
||||
<value>online</value>
|
||||
</option>
|
||||
<option label="Away">
|
||||
<value>away</value>
|
||||
</option>
|
||||
<option label="Extended Away">
|
||||
<value>xa</value>
|
||||
</option>
|
||||
<option label="Do Not Disturb">
|
||||
<value>dnd</value>
|
||||
</option>
|
||||
<value>chat</value>
|
||||
<value>online</value>
|
||||
</field>
|
||||
</x>
|
||||
</options>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"set\" id=\"options2\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <options node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\">" +
|
||||
" <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#title\"> " +
|
||||
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
|
||||
"<value>1</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
|
||||
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value>" +
|
||||
" <value>courtiers</value> </field> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
|
||||
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
|
||||
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
|
||||
"</field> </x> </options> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
|
||||
subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||
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 := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\">" +
|
||||
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
|
||||
"</subscribe>" +
|
||||
"<options> <x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\">" +
|
||||
" <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#title\"> " +
|
||||
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
|
||||
"<value>1</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
|
||||
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value>" +
|
||||
" <value>courtiers</value> </field> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
|
||||
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
|
||||
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
|
||||
"</field> </x> </options> </pubsub> </iq>"
|
||||
|
||||
subInfo := stanza.SubInfo{
|
||||
Node: "princely_musings", Jid: "francisco@denmark.lit",
|
||||
}
|
||||
|
||||
subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample)
|
||||
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 <options/> element MUST NOT possess a 'node' attribute or 'jid' attribute
|
||||
// See XEP-0060
|
||||
if pubsub.SubOptions.SubInfo.Node != "" || pubsub.SubOptions.SubInfo.Jid != "" {
|
||||
t.Fatalf("SubInfo node and jid should be empty for the options tag !")
|
||||
}
|
||||
if pubsub.Subscribe.Node == "" || pubsub.Subscribe.Jid == "" {
|
||||
t.Fatalf("SubInfo node and jid should NOT be empty for the subscribe tag !")
|
||||
}
|
||||
data, err := xml.Marshal(subR)
|
||||
if err := compareMarshal(expectedReq, string(data)); err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSubAndConfigResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscription node="princely_musings" jid="francisco@denmark.lit"
|
||||
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
|
||||
<options>
|
||||
<x xmlns="jabber:x:data" type="result">
|
||||
<field var="FORM_TYPE" type="hidden">
|
||||
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
|
||||
</field>
|
||||
<field var="pubsub#deliver">
|
||||
<value>1</value>
|
||||
</field>
|
||||
<field var="pubsub#digest">
|
||||
<value>0</value>
|
||||
</field>
|
||||
<field var="pubsub#include_body">
|
||||
<value>false</value>
|
||||
</field>
|
||||
<field var="pubsub#show-values">
|
||||
<value>chat</value>
|
||||
<value>online</value>
|
||||
<value>away</value>
|
||||
</field>
|
||||
</x>
|
||||
</options>
|
||||
</pubsub>
|
||||
</iq>
|
||||
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.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 <items> tag to request all items from a service")
|
||||
}
|
||||
}
|
||||
func TestNewItemsResp(t *testing.T) {
|
||||
response := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="items2">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<items node="princely_musings">
|
||||
<item id="4e30f35051b7b8b42abe083742187228">
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Alone</title>
|
||||
<summary> Now I am alone. O, what a rogue and peasant slave am I! </summary>
|
||||
<link rel="alternate" type="text/html"
|
||||
href="http://denmark.lit/2003/12/13/atom03"/>
|
||||
<id>tag:denmark.lit,2003:entry-32396</id>
|
||||
<published>2003-12-13T11:09:53Z</published>
|
||||
<updated>2003-12-13T11:09:53Z</updated>
|
||||
</entry>
|
||||
</item>
|
||||
<item id="ae890ac52d0df67ed7cfdf51b644e901">
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Soliloquy</title>
|
||||
<summary> To be, or not to be: that is the question: Whether 'tis nobler in the
|
||||
mind to suffer The slings and arrows of outrageous fortune, Or to take arms
|
||||
against a sea of troubles, And by opposing end them? </summary>
|
||||
<link rel="alternate" type="text/html"
|
||||
href="http://denmark.lit/2003/12/13/atom03"/>
|
||||
<id>tag:denmark.lit,2003:entry-32397</id>
|
||||
<published>2003-12-13T18:30:02Z</published>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
</entry>
|
||||
</item>
|
||||
</items>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"get\" id=\"items3\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <items node=\"princely_musings\"> " +
|
||||
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </items> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901")
|
||||
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 <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node.")
|
||||
}
|
||||
if pubsub.Publish.Items[0].Id == "" {
|
||||
t.Fatalf("an id was provided for the item and it should be used")
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 7.1.5 Publishing Options
|
||||
// ***************************************
|
||||
|
||||
func TestNewPublishItemOptsRq(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"pub1\"to=\"pubsub.shakespeare.lit\"> <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
|
||||
"<publish node=\"princely_musings\"> <item id=\"ae890ac52d0df67ed7cfdf51b644e901\"> " +
|
||||
"<entry xmlns=\"http://www.w3.org/2005/Atom\"> <title>Soliloquy</title> " +
|
||||
"<summary> To be, or not to be: that is the question: Whether \"tis nobler in the mind to suffer The " +
|
||||
"slings and arrows of outrageous fortune, Or to take arms against a sea of troubles, And by opposing end them? " +
|
||||
"</summary> <link rel=\"alternate\" type=\"text/html\"href=\"http://denmark.lit/2003/12/13/atom03\"></link> " +
|
||||
"<id>tag:denmark.lit,2003:entry-32397</id> <published>2003-12-13T18:30:02Z</published> " +
|
||||
"<updated>2003-12-13T18:30:02Z</updated> </entry> </item> </publish> <publish-options> " +
|
||||
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
|
||||
"<value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var=\"pubsub#access_model\"> " +
|
||||
"<value>presence</value> </field> </x> </publish-options> </pubsub> </iq>"
|
||||
|
||||
var iq stanza.IQ
|
||||
err := xml.Unmarshal([]byte(expectedReq), &iq)
|
||||
if err != nil {
|
||||
t.Fatalf("could not unmarshal example request : %s", err)
|
||||
}
|
||||
|
||||
pubsub, ok := iq.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("payload is not a pubsub !")
|
||||
}
|
||||
if pubsub.Publish == nil {
|
||||
t.Fatalf("Publish tag is empty")
|
||||
}
|
||||
if len(pubsub.Publish.Items) != 1 {
|
||||
t.Fatalf("could not parse item properly")
|
||||
}
|
||||
}
|
||||
|
||||
// ***************************************
|
||||
// * 7.2 Delete an Item from a Node
|
||||
// ***************************************
|
||||
|
||||
func TestNewDelItemFromNode(t *testing.T) {
|
||||
expectedReq := "<iq type=\"set\"id=\"retract1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <retract node=\"princely_musings\"> " +
|
||||
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </retract> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil)
|
||||
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 := "<iq type=\"set\"id=\"create1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings")
|
||||
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 := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="hamlet@denmark.lit/elsinore" id="create2">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<create node="25e3d37dabbab9541f7523321421edc5bfeb2dae"/>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
pubsub, err := getPubSubGenericPayload(response)
|
||||
if err != nil {
|
||||
t.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 := "<iq type=\"set\" id=\"create1\" to=\"pubsub.shakespeare.lit\" > " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> " +
|
||||
"<configure> <x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\" > " +
|
||||
"<value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#notify_retract\"> " +
|
||||
"<value>0</value> </field> <field var=\"pubsub#notify_sub\"> <value>0</value> </field> " +
|
||||
"<field var=\"pubsub#max_payload_size\"> <value>1028</value> </field> </x> </configure> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewCreateAndConfigNode("pubsub.shakespeare.lit",
|
||||
"princely_musings",
|
||||
&stanza.Form{
|
||||
Type: stanza.FormTypeSubmit,
|
||||
Fields: []stanza.Field{
|
||||
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
|
||||
{Var: "pubsub#notify_retract", ValuesList: []string{"0"}},
|
||||
{Var: "pubsub#notify_sub", ValuesList: []string{"0"}},
|
||||
{Var: "pubsub#max_payload_size", ValuesList: []string{"1028"}},
|
||||
},
|
||||
})
|
||||
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 := "<iq type=\"get\" id=\"subscriptions1\" to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscriptions></subscriptions> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit")
|
||||
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 := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="subscriptions1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<subscriptions>
|
||||
<subscription node="node1" jid="francisco@denmark.lit" subscription="subscribed"/>
|
||||
<subscription node="node2" jid="francisco@denmark.lit" subscription="subscribed"/>
|
||||
<subscription node="node5" jid="francisco@denmark.lit" subscription="unconfigured"/>
|
||||
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
|
||||
subid="123-abc"/>
|
||||
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
|
||||
subid="004-yyy"/>
|
||||
</subscriptions>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("could not unmarshal response: %s", err)
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("umarshalled payload is not a pubsub")
|
||||
}
|
||||
|
||||
if pubsub.Subscriptions == nil {
|
||||
t.Fatalf("subscriptions node is nil")
|
||||
}
|
||||
if len(pubsub.Subscriptions.List) != 5 {
|
||||
t.Fatalf("incorrect number of decoded subscriptions")
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************
|
||||
// * 5.7 Retrieve Affiliations
|
||||
// ********************************
|
||||
|
||||
func TestNewRetrieveAllAffilsRequest(t *testing.T) {
|
||||
expected := "<iq type=\"get\"id=\"affil1\"to=\"pubsub.shakespeare.lit\"> " +
|
||||
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <affiliations></affiliations> </pubsub> </iq>"
|
||||
|
||||
subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit")
|
||||
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 := `
|
||||
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="affil1">
|
||||
<pubsub xmlns="http://jabber.org/protocol/pubsub">
|
||||
<affiliations>
|
||||
<affiliation node="node1" affiliation="owner"/>
|
||||
<affiliation node="node2" affiliation="publisher"/>
|
||||
<affiliation node="node5" affiliation="outcast"/>
|
||||
<affiliation node="node6" affiliation="owner"/>
|
||||
</affiliations>
|
||||
</pubsub>
|
||||
</iq>
|
||||
`
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("could not unmarshal response: %s", err)
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
t.Fatalf("umarshalled payload is not a pubsub")
|
||||
}
|
||||
|
||||
if pubsub.Affiliations == nil {
|
||||
t.Fatalf("subscriptions node is nil")
|
||||
}
|
||||
if len(pubsub.Affiliations.List) != 4 {
|
||||
t.Fatalf("incorrect number of decoded subscriptions")
|
||||
}
|
||||
}
|
||||
|
||||
func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) {
|
||||
var respIQ stanza.IQ
|
||||
err := xml.Unmarshal([]byte(response), &respIQ)
|
||||
|
||||
if err != nil {
|
||||
return &stanza.PubSubGeneric{}, err
|
||||
}
|
||||
|
||||
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
|
||||
if !ok {
|
||||
errors.New("this iq payload is not a pubsub")
|
||||
}
|
||||
|
||||
return pubsub, nil
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -280,7 +280,7 @@ func bind(t *testing.T, sc *ServerConn) {
|
|||
<jid>%s</jid>
|
||||
</bind>
|
||||
</iq>`
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue