diff --git a/README.md b/README.md index 08b911c..62c6518 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,13 @@ config := xmpp.Config{ - [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html) - [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html) +### Extensions + - [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html) + Note : "6.5.4 Returning Some Items" requires support for [XEP-0059: Result Set Management](https://xmpp.org/extensions/xep-0059.html), + and is therefore not supported yet. + - [XEP-0004: Data Forms](https://xmpp.org/extensions/xep-0004.html) + - [XEP-0050: Ad-Hoc Commands](https://xmpp.org/extensions/xep-0050.html) + ## Package overview ### Stanza subpackage @@ -108,15 +115,16 @@ func main() { Address: "localhost:5222", }, Jid: "test@localhost", - Credential: xmpp.Password("Test"), + Credential: xmpp.Password("test"), StreamLogger: os.Stdout, Insecure: true, + // TLSConfig: tls.Config{InsecureSkipVerify: true}, } router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -138,6 +146,11 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) { reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body} _ = s.Send(reply) } + +func errorHandler(err error) { + fmt.Println(err.Error()) +} + ``` ## Reference documentation diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go index 81642c6..d07587a 100644 --- a/_examples/delegation/delegation.go +++ b/_examples/delegation/delegation.go @@ -171,7 +171,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) { return } - pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub) + pubsub, ok := forwardedIQ.Payload.(*stanza.PubSubGeneric) if !ok { // We only support pubsub delegation return @@ -180,7 +180,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) { if pubsub.Publish.XMLName.Local == "publish" { // Prepare pubsub IQ reply iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id}) - payload := stanza.PubSub{ + payload := stanza.PubSubGeneric{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/pubsub", Local: "pubsub", diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go index 0c05edd..9f2c758 100644 --- a/_examples/xmpp_chat_client/interface.go +++ b/_examples/xmpp_chat_client/interface.go @@ -136,7 +136,11 @@ func writeInput(g *gocui.Gui, v *gocui.View) error { input := strings.Join(v.ViewBufferLines(), "\n") fmt.Fprintln(chatLogWindow, "Me : ", input) - textChan <- input + if viewState.input == rawInputWindow { + rawTextChan <- input + } else { + textChan <- input + } v.Clear() v.EditDeleteToStartOfLine() diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go index 5654a2b..b6c6766 100644 --- a/_examples/xmpp_echo/xmpp_echo.go +++ b/_examples/xmpp_echo/xmpp_echo.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -50,3 +50,7 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) { reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body} _ = s.Send(reply) } + +func errorHandler(err error) { + fmt.Println(err.Error()) +} diff --git a/_examples/xmpp_jukebox/README.md b/_examples/xmpp_jukebox/README.md new file mode 100644 index 0000000..9eeb3a4 --- /dev/null +++ b/_examples/xmpp_jukebox/README.md @@ -0,0 +1,37 @@ +# Jukebox example + +## Requirements +- You need mpg123 installed on your computer because the example runs it as a command : +[Official MPG123 website](https://mpg123.de/) +Most linux distributions have a package for it. +- You need a soundcloud ID to play a music from the website through mpg123. You currently cannot play music files with this example. +Your user ID is available in your account settings on the [soundcloud website](https://soundcloud.com/) +**One is provided for convenience.** +- You need a running jabber server. You can run your local instance of [ejabberd](https://www.ejabberd.im/) for example. +- You need a registered user on the running jabber server. + +## Run +You can edit the soundcloud ID in the example file with your own, or use the provided one : +```go +const scClientID = "dde6a0075614ac4f3bea423863076b22" +``` + +To run the example, build it with (while in the example directory) : +``` +go build xmpp_jukebox.go +``` + +then run it with (update the command arguments accordingly): +``` +./xmpp_jukebox -jid=MY_USERE@MY_DOMAIN/jukebox -password=MY_PASSWORD -address=MY_SERVER:MY_SERVER_PORT +``` +Make sure to have a resource, for instance "/jukebox", on your jid. + +Then you can send the following stanza to "MY_USERE@MY_DOMAIN/jukebox" (with the resource) to play a song (update the soundcloud URL accordingly) : +```xml + + + + + +``` diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index ce7ebc9..57137b8 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -3,6 +3,7 @@ package main import ( + "encoding/xml" "flag" "fmt" "log" @@ -19,7 +20,7 @@ import ( const scClientID = "dde6a0075614ac4f3bea423863076b22" func main() { - jid := flag.String("jid", "", "jukebok XMPP JID, resource is optional") + jid := flag.String("jid", "", "jukebok XMPP Jid, resource is optional") password := flag.String("password", "", "XMPP account password") address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)") flag.Parse() @@ -48,7 +49,7 @@ func main() { handleMessage(s, p, player) }) router.NewRoute(). - Packet("message"). + Packet("iq"). HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { handleIQ(s, p, player) }) @@ -108,11 +109,29 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { } func sendUserTune(s xmpp.Sender, artist string, title string) { - tune := stanza.Tune{Artist: artist, Title: title} - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "usertune-1", Lang: "en"}) - payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}} - iq.Payload = &payload - _ = s.Send(iq) + rq, err := stanza.NewPublishItemRq("localhost", + "http://jabber.org/protocol/tune", + "", + stanza.Item{ + XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"}, + Any: &stanza.Node{ + Nodes: []stanza.Node{ + { + XMLName: xml.Name{Local: "artist"}, + Content: artist, + }, + { + XMLName: xml.Name{Local: "title"}, + Content: title, + }, + }, + }, + }) + if err != nil { + fmt.Printf("failed to build the publish request : %s", err.Error()) + return + } + _ = s.Send(rq) } func playSCURL(p *mpg123.Player, rawURL string) { @@ -120,7 +139,7 @@ func playSCURL(p *mpg123.Player, rawURL string) { // TODO: Maybe we need to check the track itself to get the stream URL from reply ? url := soundcloud.FormatStreamURL(songID) - _ = p.Play(url) + _ = p.Play(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID)) } // TODO diff --git a/client.go b/client.go index be15540..a795934 100644 --- a/client.go +++ b/client.go @@ -107,14 +107,14 @@ Setting up the client / Checking the parameters */ // NewClient generates a new XMPP client, based on Config passed as parameters. -// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID. +// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the Jid. // Default the port to 5222. func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) { if config.KeepaliveInterval == 0 { config.KeepaliveInterval = time.Second * 30 } - // Parse JID - if config.parsedJid, err = NewJid(config.Jid); err != nil { + // Parse Jid + if config.parsedJid, err = stanza.NewJid(config.Jid); err != nil { err = errors.New("missing jid") return nil, NewConnError(err, true) } @@ -142,6 +142,11 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e } } } + if config.Domain == "" { + // Fallback to jid domain + config.Domain = config.parsedJid.Domain + } + c = new(Client) c.config = config c.router = r diff --git a/cmd/fluuxmpp/send.go b/cmd/fluuxmpp/send.go index 0942928..5a10c9e 100644 --- a/cmd/fluuxmpp/send.go +++ b/cmd/fluuxmpp/send.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "gosrc.io/xmpp/stanza" "os" "strings" "sync" @@ -48,7 +49,7 @@ func sendxmpp(cmd *cobra.Command, args []string) { wg.Add(1) // FIXME: Remove global variables - var mucsToLeave []*xmpp.Jid + var mucsToLeave []*stanza.Jid cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) { defer wg.Done() @@ -57,7 +58,7 @@ func sendxmpp(cmd *cobra.Command, args []string) { if isMUCRecipient { for _, muc := range receiver { - jid, err := xmpp.NewJid(muc) + jid, err := stanza.NewJid(muc) if err != nil { log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err) continue diff --git a/cmd/fluuxmpp/xmppmuc.go b/cmd/fluuxmpp/xmppmuc.go index 984b014..a00fdfc 100644 --- a/cmd/fluuxmpp/xmppmuc.go +++ b/cmd/fluuxmpp/xmppmuc.go @@ -7,7 +7,7 @@ import ( "gosrc.io/xmpp/stanza" ) -func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error { +func joinMUC(c xmpp.Sender, toJID *stanza.Jid) error { return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()}, Extensions: []stanza.PresExtension{ stanza.MucPresence{ @@ -16,7 +16,7 @@ func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error { }) } -func leaveMUCs(c xmpp.Sender, mucsToLeave []*xmpp.Jid) { +func leaveMUCs(c xmpp.Sender, mucsToLeave []*stanza.Jid) { for _, muc := range mucsToLeave { if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{ To: muc.Full(), diff --git a/config.go b/config.go index da4d4ab..178da2e 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package xmpp import ( + "gosrc.io/xmpp/stanza" "os" "time" ) @@ -11,7 +12,7 @@ type Config struct { TransportConfiguration Jid string - parsedJid *Jid // For easier manipulation + parsedJid *stanza.Jid // For easier manipulation Credential Credential StreamLogger *os.File // Used for debugging Lang string // TODO: should default to 'en' diff --git a/stanza/commands.go b/stanza/commands.go new file mode 100644 index 0000000..5a3191f --- /dev/null +++ b/stanza/commands.go @@ -0,0 +1,136 @@ +package stanza + +import "encoding/xml" + +// Implements the XEP-0050 extension + +const ( + CommandActionCancel = "cancel" + CommandActionComplete = "complete" + CommandActionExecute = "execute" + CommandActionNext = "next" + CommandActionPrevious = "prev" + + CommandStatusCancelled = "canceled" + CommandStatusCompleted = "completed" + CommandStatusExecuting = "executing" + + CommandNoteTypeErr = "error" + CommandNoteTypeInfo = "info" + CommandNoteTypeWarn = "warn" +) + +type Command struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"` + + CommandElement CommandElement `xml:",any"` + + BadAction *struct{} `xml:"bad-action,omitempty"` + BadLocale *struct{} `xml:"bad-locale,omitempty"` + BadPayload *struct{} `xml:"bad-payload,omitempty"` + BadSessionId *struct{} `xml:"bad-sessionid,omitempty"` + MalformedAction *struct{} `xml:"malformed-action,omitempty"` + SessionExpired *struct{} `xml:"session-expired,omitempty"` + + // Attributes + Action string `xml:"action,attr,omitempty"` + Node string `xml:"node,attr"` + SessionId string `xml:"sessionid,attr,omitempty"` + Status string `xml:"status,attr,omitempty"` + Lang string `xml:"lang,attr,omitempty"` +} + +func (c *Command) Namespace() string { + return c.XMLName.Space +} + +type CommandElement interface { + Ref() string +} + +type Actions struct { + Prev *struct{} `xml:"prev,omitempty"` + Next *struct{} `xml:"next,omitempty"` + Complete *struct{} `xml:"complete,omitempty"` + + Execute string `xml:"execute,attr,omitempty"` +} + +func (a *Actions) Ref() string { + return "actions" +} + +type Note struct { + Text string `xml:",cdata"` + Type string `xml:"type,attr,omitempty"` +} + +func (n *Note) Ref() string { + return "note" +} + +func (n *Node) Ref() string { + return "node" +} + +func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + c.XMLName = start.Name + + // Extract packet attributes + for _, attr := range start.Attr { + if attr.Name.Local == "action" { + c.Action = attr.Value + } + if attr.Name.Local == "node" { + c.Node = attr.Value + } + if attr.Name.Local == "sessionid" { + c.SessionId = attr.Value + } + if attr.Name.Local == "status" { + c.Status = attr.Value + } + if attr.Name.Local == "lang" { + c.Lang = attr.Value + } + } + + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + // Decode sub-elements + var err error + switch tt.Name.Local { + + case "affiliations": + a := Actions{} + d.DecodeElement(&a, &tt) + c.CommandElement = &a + case "configure": + nt := Note{} + d.DecodeElement(&nt, &tt) + c.CommandElement = &nt + default: + n := Node{} + e := d.DecodeElement(&n, &tt) + _ = e + c.CommandElement = &n + if err != nil { + return err + } + } + + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} diff --git a/stanza/commands_test.go b/stanza/commands_test.go new file mode 100644 index 0000000..4cdee0f --- /dev/null +++ b/stanza/commands_test.go @@ -0,0 +1,53 @@ +package stanza_test + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "testing" +) + +func TestMarshalCommands(t *testing.T) { + input := "Available Servi" + + "ces<" + + "field xmlns=\"jabber:x:data\" var=\"service\">httpdoffoffononpostgresqloffoffon" + + "onjabberdoffoffonon" + + var c stanza.Command + err := xml.Unmarshal([]byte(input), &c) + + if err != nil { + t.Fatalf("failed to unmarshal initial input") + } + + data, err := xml.Marshal(c) + if err != nil { + t.Fatalf("failed to marshal unmarshalled input") + } + + if err := compareMarshal(input, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} diff --git a/stanza/component_test.go b/stanza/component_test.go index d02531e..648131c 100644 --- a/stanza/component_test.go +++ b/stanza/component_test.go @@ -67,7 +67,7 @@ func TestParsingDelegationIQ(t *testing.T) { return } if forwardedIQ.Payload != nil { - if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok { + if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); ok { node = pubsub.Publish.Node } } diff --git a/stanza/error.go b/stanza/error.go index 0f416e4..5f42018 100644 --- a/stanza/error.go +++ b/stanza/error.go @@ -3,6 +3,7 @@ package stanza import ( "encoding/xml" "strconv" + "strings" ) // ============================================================================ @@ -53,10 +54,19 @@ func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"} - if elt.XMLName == textName { + // TODO : change the pubsub handling ? It kind of dilutes the information + // Handles : 6.1.3.11 Node Has Moved for XEP-0060 (PubSubGeneric) + goneName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "gone"} + if elt.XMLName == textName || // Regular error text + elt.XMLName == goneName { // Gone text for pubsub x.Text = elt.Content - } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" { - x.Reason = elt.XMLName.Local + } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" || + elt.XMLName.Space == "http://jabber.org/protocol/pubsub#errors" { + if strings.TrimSpace(x.Reason) != "" { + x.Reason = strings.Join([]string{elt.XMLName.Local}, ":") + } else { + x.Reason = elt.XMLName.Local + } } case xml.EndElement: diff --git a/stanza/form.go b/stanza/form.go new file mode 100644 index 0000000..b9a9932 --- /dev/null +++ b/stanza/form.go @@ -0,0 +1,67 @@ +package stanza + +import "encoding/xml" + +type FormType string + +const ( + FormTypeCancel = "cancel" + FormTypeForm = "form" + FormTypeResult = "result" + FormTypeSubmit = "submit" +) + +// See XEP-0004 and XEP-0068 +// Pointer semantics +type Form struct { + XMLName xml.Name `xml:"jabber:x:data x"` + Instructions []string `xml:"instructions"` + Title string `xml:"title,omitempty"` + Fields []Field `xml:"field,omitempty"` + Reported *FormItem `xml:"reported"` + Items []FormItem + Type string `xml:"type,attr"` +} + +type FormItem struct { + Fields []Field +} + +type Field struct { + XMLName xml.Name `xml:"field"` + Description string `xml:"desc,omitempty"` + Required *string `xml:"required"` + ValuesList []string `xml:"value"` + Options []Option `xml:"option,omitempty"` + Var string `xml:"var,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Label string `xml:"label,attr,omitempty"` +} + +func NewForm(fields []Field, formType string) *Form { + return &Form{ + Type: formType, + Fields: fields, + } +} + +type FieldType string + +const ( + FieldTypeBool = "boolean" + FieldTypeFixed = "fixed" + FieldTypeHidden = "hidden" + FieldTypeJidMulti = "jid-multi" + FieldTypeJidSingle = "jid-single" + FieldTypeListMulti = "list-multi" + FieldTypeListSingle = "list-single" + FieldTypeTextMulti = "text-multi" + FieldTypeTextPrivate = "text-private" + FieldTypeTextSingle = "text-Single" +) + +type Option struct { + XMLName xml.Name `xml:"option"` + Label string `xml:"label,attr,omitempty"` + ValuesList []string `xml:"value"` +} diff --git a/stanza/form_test.go b/stanza/form_test.go new file mode 100644 index 0000000..a68d88e --- /dev/null +++ b/stanza/form_test.go @@ -0,0 +1,107 @@ +package stanza + +import ( + "encoding/xml" + "strings" + "testing" +) + +const ( + formSubmit = "" + + "" + + "" + + "" + + "http://jabber.org/protocol/pubsub#node_config" + + "" + + "" + + "Princely Musings (Atom)" + + "" + + "" + + "1" + + "" + + "" + + "roster" + + "" + + "" + + "friends" + + "servants" + + "courtiers" + + "" + + "" + + "http://www.w3.org/2005/Atom" + + "" + + "" + + "headline" + + "" + + "" + + "" + + "" + + "" + + "" + + clientJid = "hamlet@denmark.lit/elsinore" + serviceJid = "pubsub.shakespeare.lit" + iqId = "config1" + serviceNode = "princely_musings" +) + +func TestMarshalFormSubmit(t *testing.T) { + formIQ := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet}) + formIQ.Payload = &PubSubOwner{ + OwnerUseCase: &ConfigureOwner{ + Node: serviceNode, + Form: &Form{ + Type: FormTypeSubmit, + Fields: []Field{ + {Var: "FORM_TYPE", Type: FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, + {Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}}, + {Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}}, + {Var: "pubsub#access_model", ValuesList: []string{"roster"}}, + {Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}}, + {Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}}, + { + Var: "pubsub#notification_type", + Type: "list-single", + Label: "Specify the delivery style for event notifications", + ValuesList: []string{"headline"}, + Options: []Option{ + {ValuesList: []string{"normal"}}, + {ValuesList: []string{"headline"}}, + }, + }, + }, + }, + }, + } + b, err := xml.Marshal(formIQ.Payload) + if err != nil { + t.Fatalf("Could not marshal formIQ : %v", err) + } + + if strings.ReplaceAll(string(b), " ", "") != strings.ReplaceAll(formSubmit, " ", "") { + t.Fatalf("Expected formIQ and marshalled one are different.\nExepected : %s\nMarshalled : %s", formSubmit, string(b)) + } + +} + +func TestUnmarshalFormSubmit(t *testing.T) { + var f PubSubOwner + mErr := xml.Unmarshal([]byte(formSubmit), &f) + if mErr != nil { + t.Fatalf("failed to unmarshal formSubmit ! %s", mErr) + } + + data, err := xml.Marshal(&f) + if err != nil { + t.Fatalf("failed to marshal formSubmit") + } + + if strings.ReplaceAll(string(data), " ", "") != strings.ReplaceAll(formSubmit, " ", "") { + t.Fatalf("failed unmarshal/marshal for formSubmit : %s\n%s", string(data), formSubmit) + } +} diff --git a/stanza/iq_disco_test.go b/stanza/iq_disco_test.go index 012952e..1f8ab8b 100644 --- a/stanza/iq_disco_test.go +++ b/stanza/iq_disco_test.go @@ -73,11 +73,11 @@ func TestDiscoItems_Builder(t *testing.T) { {xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"}, {xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}} if len(pp.Items) != len(items) { - t.Errorf("Items length mismatch: %#v", pp.Items) + t.Errorf("List length mismatch: %#v", pp.Items) } else { for i, item := range pp.Items { if item.JID != items[i].JID { - t.Errorf("JID Mismatch (expected: %s): %s", items[i].JID, item.JID) + t.Errorf("Jid Mismatch (expected: %s): %s", items[i].JID, item.JID) } if item.Node != items[i].Node { t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID) diff --git a/stanza/iq_roster_test.go b/stanza/iq_roster_test.go index 7228084..ca891df 100644 --- a/stanza/iq_roster_test.go +++ b/stanza/iq_roster_test.go @@ -69,11 +69,11 @@ func TestRosterBuilder(t *testing.T) { }, } if len(pp.Items) != len(items) { - t.Errorf("Items length mismatch: %#v", pp.Items) + t.Errorf("List length mismatch: %#v", pp.Items) } else { for i, item := range pp.Items { if item.Jid != items[i].Jid { - t.Errorf("JID Mismatch (expected: %s): %s", items[i].Jid, item.Jid) + t.Errorf("Jid Mismatch (expected: %s): %s", items[i].Jid, item.Jid) } if !reflect.DeepEqual(item.Groups, items[i].Groups) { t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid) diff --git a/jid.go b/stanza/jid.go similarity index 87% rename from jid.go rename to stanza/jid.go index cee292a..2fc9cc6 100644 --- a/jid.go +++ b/stanza/jid.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "fmt" @@ -20,9 +20,9 @@ func NewJid(sjid string) (*Jid, error) { } s1 := strings.SplitN(sjid, "@", 2) - if len(s1) == 1 { // This is a server or component JID + if len(s1) == 1 { // This is a server or component Jid jid.Domain = s1[0] - } else { // JID has a local username part + } else { // Jid has a local username part if s1[0] == "" { return jid, fmt.Errorf("invalid jid '%s", sjid) } @@ -41,10 +41,10 @@ func NewJid(sjid string) (*Jid, error) { } if !isUsernameValid(jid.Node) { - return jid, fmt.Errorf("invalid Node in JID '%s'", sjid) + return jid, fmt.Errorf("invalid Node in Jid '%s'", sjid) } if !isDomainValid(jid.Domain) { - return jid, fmt.Errorf("invalid domain in JID '%s'", sjid) + return jid, fmt.Errorf("invalid domain in Jid '%s'", sjid) } return jid, nil diff --git a/jid_test.go b/stanza/jid_test.go similarity index 99% rename from jid_test.go rename to stanza/jid_test.go index c6fee03..db86a9d 100644 --- a/jid_test.go +++ b/stanza/jid_test.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "testing" diff --git a/stanza/msg_pubsub_event.go b/stanza/msg_pubsub_event.go new file mode 100644 index 0000000..6ee3dfb --- /dev/null +++ b/stanza/msg_pubsub_event.go @@ -0,0 +1,214 @@ +package stanza + +import ( + "encoding/xml" +) + +// Implementation of the http://jabber.org/protocol/pubsub#event namespace +type PubSubEvent struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#event event"` + MsgExtension + EventElement EventElement + //List ItemsEvent +} + +func init() { + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/pubsub#event", Local: "event"}, PubSubEvent{}) +} + +type EventElement interface { + Name() string +} + +// ********************* +// Collection +// ********************* + +const PubSubCollectionEventName = "Collection" + +type CollectionEvent struct { + AssocDisassoc AssocDisassoc + Node string `xml:"node,attr,omitempty"` +} + +func (c CollectionEvent) Name() string { + return PubSubCollectionEventName +} + +// ********************* +// Associate/Disassociate +// ********************* +type AssocDisassoc interface { + GetAssocDisassoc() string +} + +// ********************* +// Associate +// ********************* +const Assoc = "Associate" + +type AssociateEvent struct { + XMLName xml.Name `xml:"associate"` + Node string `xml:"node,attr"` +} + +func (a *AssociateEvent) GetAssocDisassoc() string { + return Assoc +} + +// ********************* +// Disassociate +// ********************* +const Disassoc = "Disassociate" + +type DisassociateEvent struct { + XMLName xml.Name `xml:"disassociate"` + Node string `xml:"node,attr"` +} + +func (e *DisassociateEvent) GetAssocDisassoc() string { + return Disassoc +} + +// ********************* +// Configuration +// ********************* + +const PubSubConfigEventName = "Configuration" + +type ConfigurationEvent struct { + Node string `xml:"node,attr,omitempty"` + Form *Form +} + +func (c ConfigurationEvent) Name() string { + return PubSubConfigEventName +} + +// ********************* +// Delete +// ********************* +const PubSubDeleteEventName = "Delete" + +type DeleteEvent struct { + Node string `xml:"node,attr"` + Redirect *RedirectEvent `xml:"redirect"` +} + +func (c DeleteEvent) Name() string { + return PubSubConfigEventName +} + +// ********************* +// Redirect +// ********************* +type RedirectEvent struct { + URI string `xml:"uri,attr"` +} + +// ********************* +// List +// ********************* + +const PubSubItemsEventName = "List" + +type ItemsEvent struct { + XMLName xml.Name `xml:"items"` + Items []ItemEvent `xml:"item,omitempty"` + Node string `xml:"node,attr"` + Retract *RetractEvent `xml:"retract"` +} + +type ItemEvent struct { + XMLName xml.Name `xml:"item"` + Id string `xml:"id,attr,omitempty"` + Publisher string `xml:"publisher,attr,omitempty"` + Any *Node `xml:",any"` +} + +func (i ItemsEvent) Name() string { + return PubSubItemsEventName +} + +// ********************* +// List +// ********************* + +type RetractEvent struct { + XMLName xml.Name `xml:"retract"` + ID string `xml:"node,attr"` +} + +// ********************* +// Purge +// ********************* +const PubSubPurgeEventName = "Purge" + +type PurgeEvent struct { + XMLName xml.Name `xml:"purge"` + Node string `xml:"node,attr"` +} + +func (p PurgeEvent) Name() string { + return PubSubPurgeEventName +} + +// ********************* +// Subscription +// ********************* +const PubSubSubscriptionEventName = "Subscription" + +type SubscriptionEvent struct { + SubStatus string `xml:"subscription,attr,omitempty"` + Expiry string `xml:"expiry,attr,omitempty"` + SubInfo `xml:",omitempty"` +} + +func (s SubscriptionEvent) Name() string { + return PubSubSubscriptionEventName +} + +func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + pse.XMLName = start.Name + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + var ee EventElement + switch tt := t.(type) { + case xml.StartElement: + switch tt.Name.Local { + case "collection": + ee = &CollectionEvent{} + case "configuration": + ee = &ConfigurationEvent{} + case "delete": + ee = &DeleteEvent{} + case "items": + ee = &ItemsEvent{} + case "purge": + ee = &PurgeEvent{} + case "subscription": + ee = &SubscriptionEvent{} + default: + ee = nil + } + // known child element found, decode it + if ee != nil { + err = d.DecodeElement(ee, &tt) + if err != nil { + return err + } + pse.EventElement = ee + } + case xml.EndElement: + if tt == start.End() { + return nil + } + } + + } + return nil +} diff --git a/stanza/msg_pubsub_event_test.go b/stanza/msg_pubsub_event_test.go new file mode 100644 index 0000000..29a795d --- /dev/null +++ b/stanza/msg_pubsub_event_test.go @@ -0,0 +1,162 @@ +package stanza_test + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "strings" + "testing" +) + +func TestDecodeMsgEvent(t *testing.T) { + str := ` + + + + + Soliloquy + + To be, or not to be: that is the question: + Whether 'tis nobler in the mind to suffer + The slings and arrows of outrageous fortune, + Or to take arms against a sea of troubles, + And by opposing end them? + + + tag:denmark.lit,2003:entry-32397 + 2003-12-13T18:30:02Z + 2003-12-13T18:30:02Z + + + + + + ` + parsedMessage := stanza.Message{} + if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { + t.Errorf("message receipt unmarshall error: %v", err) + return + } + + if parsedMessage.Body != "" { + t.Errorf("Unexpected body: '%s'", parsedMessage.Body) + } + + if len(parsedMessage.Extensions) < 1 { + t.Errorf("no extension found on parsed message") + return + } + + switch ext := parsedMessage.Extensions[0].(type) { + case *stanza.PubSubEvent: + if ext.XMLName.Local != "event" { + t.Fatalf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local) + } + tmp, ok := parsedMessage.Extensions[0].(*stanza.PubSubEvent) + if !ok { + t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local) + } + ie, ok := tmp.EventElement.(*stanza.ItemsEvent) + if !ok { + t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local) + } + if ie.Items[0].Any.Nodes[0].Content != "Soliloquy" { + t.Fatalf("could not read title ! Read this : %s", ie.Items[0].Any.Nodes[0].Content) + } + + if len(ie.Items[0].Any.Nodes) != 6 { + t.Fatalf("some nodes were not correctly parsed") + } + default: + t.Fatalf("could not find pubsub event extension") + } + +} + +func TestEncodeEvent(t *testing.T) { + expected := "" + + "" + + "My pub item title" + + "My pub item content summary" + + "My pub item content ID2003-12-13T18:30:02Z" + + "2003-12-13T18:30:02Z" + message := stanza.Message{ + Extensions: []stanza.MsgExtension{ + stanza.PubSubEvent{ + EventElement: stanza.ItemsEvent{ + Items: []stanza.ItemEvent{ + { + Id: "ae890ac52d0df67ed7cfdf51b644e901", + Any: &stanza.Node{ + XMLName: xml.Name{ + Space: "http://www.w3.org/2005/Atom", + Local: "entry", + }, + Attrs: nil, + Content: "", + Nodes: []stanza.Node{ + { + XMLName: xml.Name{Space: "", Local: "title"}, + Attrs: nil, + Content: "My pub item title", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "summary"}, + Attrs: nil, + Content: "My pub item content summary", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "link"}, + Attrs: []xml.Attr{ + { + Name: xml.Name{Space: "", Local: "rel"}, + Value: "alternate", + }, + { + Name: xml.Name{Space: "", Local: "type"}, + Value: "text/html", + }, + { + Name: xml.Name{Space: "", Local: "href"}, + Value: "http://denmark.lit/2003/12/13/atom03", + }, + }, + }, + { + XMLName: xml.Name{Space: "", Local: "id"}, + Attrs: nil, + Content: "My pub item content ID", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "published"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "updated"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + }, + }, + }, + }, + Node: "princely_musings", + Retract: nil, + }, + }, + }, + } + + data, _ := xml.Marshal(message) + if strings.TrimSpace(string(data)) != strings.TrimSpace(expected) { + t.Errorf("event was not encoded properly : \nexpected:%s \ngot: %s", expected, string(data)) + } + +} diff --git a/stanza/pep.go b/stanza/pep.go index 7de57ea..cfd50c1 100644 --- a/stanza/pep.go +++ b/stanza/pep.go @@ -15,7 +15,7 @@ type Tune struct { Uri string `xml:"uri,omitempty"` } -// Mood defines deta model for XEP-0107 - User Mood +// Mood defines data model for XEP-0107 - User Mood // See: https://xmpp.org/extensions/xep-0107.html type Mood struct { MsgExtension // Mood can be added as a message extension diff --git a/stanza/pubsub.go b/stanza/pubsub.go index 010f8b0..1cbdf16 100644 --- a/stanza/pubsub.go +++ b/stanza/pubsub.go @@ -2,39 +2,383 @@ package stanza import ( "encoding/xml" + "errors" + "strings" ) -type PubSub struct { +type PubSubGeneric struct { XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"` - Publish *Publish - Retract *Retract - // TODO + + Create *Create `xml:"create,omitempty"` + Configure *Configure `xml:"configure,omitempty"` + + Subscribe *SubInfo `xml:"subscribe,omitempty"` + SubOptions *SubOptions `xml:"options,omitempty"` + + Publish *Publish `xml:"publish,omitempty"` + PublishOptions *PublishOptions `xml:"publish-options"` + + Affiliations *Affiliations `xml:"affiliations,omitempty"` + Default *Default `xml:"default,omitempty"` + + Items *Items `xml:"items,omitempty"` + Retract *Retract `xml:"retract,omitempty"` + Subscription *Subscription `xml:"subscription,omitempty"` + + Subscriptions *Subscriptions `xml:"subscriptions,omitempty"` + // To use in responses to sub/unsub for instance + // Subscription options + Unsubscribe *SubInfo `xml:"unsubscribe,omitempty"` } -func (p *PubSub) Namespace() string { +func (p *PubSubGeneric) Namespace() string { return p.XMLName.Space } +type Affiliations struct { + List []Affiliation `xml:"affiliation"` + Node string `xml:"node,attr,omitempty"` +} + +type Affiliation struct { + AffiliationStatus string `xml:"affiliation"` + Node string `xml:"node,attr"` +} + +type Create struct { + Node string `xml:"node,attr,omitempty"` +} + +type SubOptions struct { + SubInfo + Form *Form `xml:"x"` +} + +type Configure struct { + Form *Form `xml:"x"` +} +type Default struct { + Node string `xml:"node,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Form *Form `xml:"x"` +} + +type Subscribe struct { + XMLName xml.Name `xml:"subscribe"` + SubInfo +} +type Unsubscribe struct { + XMLName xml.Name `xml:"unsubscribe"` + SubInfo +} + +// SubInfo represents information about a subscription +// Node is the node related to the subscription +// Jid is the subscription JID of the subscribed entity +// SubID is the subscription ID +type SubInfo struct { + Node string `xml:"node,attr,omitempty"` + Jid string `xml:"jid,attr,omitempty"` + // Sub ID is optional + SubId *string `xml:"subid,attr,omitempty"` +} + +// validate checks if a node and a jid are present in the sub info, and if this jid is valid. +func (si *SubInfo) validate() error { + // Requests MUST contain a valid JID + if _, err := NewJid(si.Jid); err != nil { + return err + } + // SubInfo must contain both a valid JID and a node. See XEP-0060 + if strings.TrimSpace(si.Node) == "" { + return errors.New("SubInfo must contain the node AND the subscriber JID in subscription config options requests") + } + return nil +} + +// Handles the "5.6 Retrieve Subscriptions" of XEP-0060 +type Subscriptions struct { + XMLName xml.Name `xml:"subscriptions"` + List []Subscription `xml:"subscription,omitempty"` +} + +// Handles the "5.6 Retrieve Subscriptions" and the 6.1 Subscribe to a Node and so on of XEP-0060 +type Subscription struct { + SubStatus string `xml:"subscription,attr,omitempty"` + SubInfo `xml:",omitempty"` + // Seems like we can't marshal a self-closing tag for now : https://github.com/golang/go/issues/21399 + // subscribe-options should be like this as per XEP-0060: + // + // + // + // Used to indicate if configuration options is required. + Required *struct{} +} + +type PublishOptions struct { + XMLName xml.Name `xml:"publish-options"` + Form *Form +} + type Publish struct { XMLName xml.Name `xml:"publish"` Node string `xml:"node,attr"` - Item Item + Items []Item `xml:"item,omitempty"` // xsd says there can be many. See also 12.10 Batch Processing of XEP-0060 +} + +type Items struct { + List []Item `xml:"item,omitempty"` + MaxItems int `xml:"max_items,attr,omitempty"` + Node string `xml:"node,attr"` + SubId string `xml:"subid,attr,omitempty"` } type Item struct { - XMLName xml.Name `xml:"item"` - Id string `xml:"id,attr,omitempty"` - Tune *Tune - Mood *Mood + XMLName xml.Name `xml:"item"` + Id string `xml:"id,attr,omitempty"` + Publisher string `xml:"publisher,attr,omitempty"` + Any *Node `xml:",any"` } type Retract struct { XMLName xml.Name `xml:"retract"` Node string `xml:"node,attr"` - Notify string `xml:"notify,attr"` - Item Item + Notify *bool `xml:"notify,attr,omitempty"` + Items []Item `xml:"item"` +} + +type PubSubOption struct { + XMLName xml.Name `xml:"jabber:x:data options"` + Form `xml:"x"` +} + +// NewSubRq builds a subscription request to a node at the given service. +// It's a Set type IQ. +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.1 Subscribe to a Node +func NewSubRq(serviceId string, subInfo SubInfo) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Subscribe: &subInfo, + } + return iq, nil +} + +// NewUnsubRq builds an unsub request to a node at the given service. +// It's a Set type IQ +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.2 Unsubscribe from a Node +func NewUnsubRq(serviceId string, subInfo SubInfo) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Unsubscribe: &subInfo, + } + return iq, nil +} + +// NewSubOptsRq builds a request for the subscription options. +// It's a Get type IQ +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.3 Configure Subscription Options +func NewSubOptsRq(serviceId string, subInfo SubInfo) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + SubOptions: &SubOptions{ + SubInfo: subInfo, + }, + } + return iq, nil +} + +// NewFormSubmission builds a form submission pubsub IQ +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.3.5 Form Submission +func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + if form.Type != FormTypeSubmit { + return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type) + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + SubOptions: &SubOptions{ + SubInfo: subInfo, + Form: form, + }, + } + return iq, nil +} + +// NewSubAndConfig builds a subscribe request that contains configuration options for the service +// From XEP-0060 : The element MUST follow the element and +// MUST NOT possess a 'node' attribute or 'jid' attribute, +// since the value of the element's 'node' attribute specifies the desired NodeID and +// the value of the element's 'jid' attribute specifies the subscriber's JID +// 6.3.7 Subscribe and Configure +func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + if form.Type != FormTypeSubmit { + return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type) + } + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Subscribe: &subInfo, + SubOptions: &SubOptions{ + SubInfo: SubInfo{SubId: subInfo.SubId}, + Form: form, + }, + } + return iq, nil + +} + +// NewItemsRequest creates a request to query existing items from a node. +// Specify a "maxItems" value to request only the last maxItems items. If 0, requests all items. +// 6.5.2 Requesting All List AND 6.5.7 Requesting the Most Recent List +func NewItemsRequest(serviceId string, node string, maxItems int) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Items: &Items{Node: node}, + } + + if maxItems != 0 { + ps, _ := iq.Payload.(*PubSubGeneric) + ps.Items.MaxItems = maxItems + } + return iq, nil +} + +// NewItemsRequest creates a request to get a specific item from a node. +// 6.5.8 Requesting a Particular Item +func NewSpecificItemRequest(serviceId, node, itemId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Items: &Items{Node: node, + List: []Item{ + { + Id: itemId, + }, + }, + }, + } + return iq, nil +} + +// NewPublishItemRq creates a request to publish a single item to a node identified by its provided ID +func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (IQ, error) { + // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot publish without a target node ID") + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Publish: &Publish{Node: nodeID, Items: []Item{item}}, + } + + // "The element provided by the publisher MAY possess an 'id' attribute, + // specifying a unique ItemID for the item. + // If an ItemID is not provided in the publish request, + // the pubsub service MUST generate one and MUST ensure that it is unique for that node." + if strings.TrimSpace(pubItemID) != "" { + ps, _ := iq.Payload.(*PubSubGeneric) + ps.Publish.Items[0].Id = pubItemID + } + return iq, nil +} + +// NewPublishItemOptsRq creates a request to publish items to a node identified by its provided ID, along with configuration options +// A pubsub service MAY support the ability to specify options along with a publish request +//(if so, it MUST advertise support for the "http://jabber.org/protocol/pubsub#publish-options" feature). +func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (IQ, error) { + // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot publish without a target node ID") + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Publish: &Publish{Node: nodeID, Items: items}, + PublishOptions: options, + } + + return iq, nil +} + +// NewDelItemFromNode creates a request to delete and item from a node, given its id. +// To delete an item, the publisher sends a retract request. +// This helper function follows 7.2 Delete an Item from a Node +func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (IQ, error) { + // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot delete item without a target node ID") + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Retract: &Retract{Node: nodeID, Items: []Item{{Id: itemId}}, Notify: notify}, + } + return iq, nil +} + +// NewCreateAndConfigNode makes a request for node creation that has the desired node configuration. +// See 8.1.3 Create and Configure a Node +func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Create: &Create{Node: nodeID}, + Configure: &Configure{Form: confForm}, + } + return iq, nil +} + +// NewCreateNode builds a request to create a node on the service referenced by "serviceId" +// See 8.1 Create a Node +func NewCreateNode(serviceId, nodeName string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Create: &Create{Node: nodeName}, + } + return iq, nil +} + +// NewRetrieveAllSubsRequest builds a request to retrieve all subscriptions from all nodes +// In order to make the request, the requesting entity MUST send an IQ-get whose +// child contains an empty element with no attributes. +func NewRetrieveAllSubsRequest(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Subscriptions: &Subscriptions{}, + } + return iq, nil +} + +// NewRetrieveAllAffilsRequest builds a request to retrieve all affiliations from all nodes +// In order to make the request of the service, the requesting entity includes an empty element with no attributes. +func NewRetrieveAllAffilsRequest(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Affiliations: &Affiliations{}, + } + return iq, nil } func init() { - TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub", Local: "pubsub"}, PubSubGeneric{}) } diff --git a/stanza/pubsub_owner.go b/stanza/pubsub_owner.go new file mode 100644 index 0000000..054303f --- /dev/null +++ b/stanza/pubsub_owner.go @@ -0,0 +1,377 @@ +package stanza + +import ( + "encoding/xml" + "errors" + "strings" +) + +type PubSubOwner struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"` + OwnerUseCase OwnerUseCase +} + +func (pso *PubSubOwner) Namespace() string { + return pso.XMLName.Space +} + +type OwnerUseCase interface { + UseCase() string +} + +type AffiliationsOwner struct { + XMLName xml.Name `xml:"affiliations"` + Affiliations []AffiliationOwner `xml:"affiliation,omitempty"` + Node string `xml:"node,attr"` +} + +func (AffiliationsOwner) UseCase() string { + return "affiliations" +} + +type AffiliationOwner struct { + XMLName xml.Name `xml:"affiliation"` + AffiliationStatus string `xml:"affiliation,attr"` + Jid string `xml:"jid,attr"` +} + +const ( + AffiliationStatusMember = "member" + AffiliationStatusNone = "none" + AffiliationStatusOutcast = "outcast" + AffiliationStatusOwner = "owner" + AffiliationStatusPublisher = "publisher" + AffiliationStatusPublishOnly = "publish-only" +) + +type ConfigureOwner struct { + XMLName xml.Name `xml:"configure"` + Node string `xml:"node,attr,omitempty"` + Form *Form `xml:"x,omitempty"` +} + +func (*ConfigureOwner) UseCase() string { + return "configure" +} + +type DefaultOwner struct { + XMLName xml.Name `xml:"default"` + Form *Form `xml:"x,omitempty"` +} + +func (*DefaultOwner) UseCase() string { + return "default" +} + +type DeleteOwner struct { + XMLName xml.Name `xml:"delete"` + RedirectOwner *RedirectOwner `xml:"redirect,omitempty"` + Node string `xml:"node,attr,omitempty"` +} + +func (*DeleteOwner) UseCase() string { + return "delete" +} + +type RedirectOwner struct { + XMLName xml.Name `xml:"redirect"` + URI string `xml:"uri,attr"` +} + +type PurgeOwner struct { + XMLName xml.Name `xml:"purge"` + Node string `xml:"node,attr"` +} + +func (*PurgeOwner) UseCase() string { + return "purge" +} + +type SubscriptionsOwner struct { + XMLName xml.Name `xml:"subscriptions"` + Subscriptions []SubscriptionOwner `xml:"subscription"` + Node string `xml:"node,attr"` +} + +func (*SubscriptionsOwner) UseCase() string { + return "subscriptions" +} + +type SubscriptionOwner struct { + SubscriptionStatus string `xml:"subscription"` + Jid string `xml:"jid,attr"` +} + +const ( + SubscriptionStatusNone = "none" + SubscriptionStatusPending = "pending" + SubscriptionStatusSubscribed = "subscribed" + SubscriptionStatusUnconfigured = "unconfigured" +) + +// NewConfigureNode creates a request to configure a node on the given service. +// A form will be returned by the service, to which the user must respond using for instance the NewFormSubmission function. +// See 8.2 Configure a Node +func NewConfigureNode(serviceId, nodeName string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &ConfigureOwner{Node: nodeName}, + } + return iq, nil +} + +// NewDelNode creates a request to delete node "nodeID" from the "serviceId" service +// See 8.4 Delete a Node +func NewDelNode(serviceId, nodeID string) (IQ, error) { + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot delete a node without a target node ID") + } + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &DeleteOwner{Node: nodeID}, + } + return iq, nil +} + +// NewPurgeAllItems creates a new purge request for the "nodeId" node, at "serviceId" service +// See 8.5 Purge All Node Items +func NewPurgeAllItems(serviceId, nodeId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &PurgeOwner{Node: nodeId}, + } + return iq, nil +} + +// NewRequestDefaultConfig build a request to ask the service for the default config of its nodes +// See 8.3 Request Default Node Configuration Options +func NewRequestDefaultConfig(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &DefaultOwner{}, + } + return iq, nil +} + +// NewApproveSubRequest creates a new sub approval response to a request from the service to the owner of the node +// In order to approve the request, the owner shall submit the form and set the "pubsub#allow" field to a value of "1" or "true" +// For tracking purposes the message MUST reflect the 'id' attribute originally provided in the request. +// See 8.6 Manage Subscription Requests +func NewApproveSubRequest(serviceId, reqID string, apprForm *Form) (Message, error) { + if serviceId == "" { + return Message{}, errors.New("need a target service serviceId send approval serviceId") + } + if reqID == "" { + return Message{}, errors.New("the request ID is empty but must be used for the approval") + } + if apprForm == nil { + return Message{}, errors.New("approval form is nil") + } + apprMess := NewMessage(Attrs{To: serviceId}) + apprMess.Extensions = []MsgExtension{apprForm} + apprMess.Id = reqID + + return apprMess, nil +} + +// NewGetPendingSubRequests creates a new request for all pending subscriptions to all their nodes at a service +// This feature MUST be implemented using the Ad-Hoc Commands (XEP-0050) protocol +// 8.7 Process Pending Subscription Requests +func NewGetPendingSubRequests(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &Command{ + // the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending" + Node: "http://jabber.org/protocol/pubsub#get-pending", + Action: CommandActionExecute, + } + return iq, nil +} + +// NewGetPendingSubRequests creates a new request for all pending subscriptions to be approved on a given node +// Upon receiving the data form for managing subscription requests, the owner then MAY request pending subscription +// approval requests for a given node. +// See 8.7.4 Per-Node Request +func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error) { + if sessionId == "" { + return IQ{}, errors.New("the sessionId must be maintained for the command") + } + + form := &Form{ + Type: FormTypeSubmit, + Fields: []Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}}, + } + data, err := xml.Marshal(form) + if err != nil { + return IQ{}, err + } + var n Node + xml.Unmarshal(data, &n) + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &Command{ + // the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending" + Node: "http://jabber.org/protocol/pubsub#get-pending", + Action: CommandActionExecute, + SessionId: sessionId, + CommandElement: &n, + } + return iq, nil +} + +// NewSubListRequest creates a request to list subscriptions of the client, for all nodes at the service. +// It's a Get type IQ +// 8.8.1 Retrieve Subscriptions +func NewSubListRqPl(serviceId, nodeID string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &SubscriptionsOwner{Node: nodeID}, + } + return iq, nil +} + +func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &SubscriptionsOwner{Node: nodeID, Subscriptions: subs}, + } + return iq, nil +} + +// NewModifAffiliationRequest creates a request to either modify one or more affiliations, or delete one or more affiliations +// 8.9.2 Modify Affiliation & 8.9.2.4 Multiple Simultaneous Modifications & 8.9.3 Delete an Entity (just set the status to "none") +func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &AffiliationsOwner{ + Node: nodeID, + Affiliations: newAffils, + }, + } + return iq, nil +} + +// NewAffiliationListRequest creates a request to list all affiliated entities +// See 8.9.1 Retrieve List List +func NewAffiliationListRequest(serviceId, nodeID string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &AffiliationsOwner{ + Node: nodeID, + }, + } + return iq, nil +} + +// GetFormFields gets the fields from a form in a IQ stanza of type result, as a map. +// Key is the "var" attribute of the field, and field is the value. +// The user can then select and modify the fields they want to alter, and submit a new form to the service using the +// NewFormSubmission function to build the IQ. +// TODO : remove restriction on IQ type ? +func (iq *IQ) GetFormFields() (map[string]Field, error) { + if iq.Type != IQTypeResult { + return nil, errors.New("this IQ is not a result type IQ. Cannot extract the form from it") + } + switch payload := iq.Payload.(type) { + // We support IOT Control IQ + case *PubSubGeneric: + fieldMap := make(map[string]Field) + for _, elt := range payload.Configure.Form.Fields { + fieldMap[elt.Var] = elt + } + return fieldMap, nil + case *PubSubOwner: + fieldMap := make(map[string]Field) + co, ok := payload.OwnerUseCase.(*ConfigureOwner) + if !ok { + return nil, errors.New("this IQ does not contain a PubSub payload with a configure tag for the owner namespace") + } + for _, elt := range co.Form.Fields { + fieldMap[elt.Var] = elt + } + return fieldMap, nil + default: + if iq.Any != nil { + fieldMap := make(map[string]Field) + if iq.Any.XMLName.Local != "command" { + return nil, errors.New("this IQ does not contain a form") + } + + for _, nde := range iq.Any.Nodes { + if nde.XMLName.Local == "x" { + for _, n := range nde.Nodes { + if n.XMLName.Local == "field" { + f := Field{} + data, err := xml.Marshal(n) + if err != nil { + continue + } + err = xml.Unmarshal(data, &f) + if err == nil { + fieldMap[f.Var] = f + } + } + } + } + } + return fieldMap, nil + } + return nil, errors.New("this IQ does not contain a form") + } +} + +func (pso *PubSubOwner) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + pso.XMLName = start.Name + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + // Decode sub-elements + var err error + switch tt.Name.Local { + + case "affiliations": + aff := AffiliationsOwner{} + d.DecodeElement(&aff, &tt) + pso.OwnerUseCase = &aff + case "configure": + co := ConfigureOwner{} + d.DecodeElement(&co, &tt) + pso.OwnerUseCase = &co + case "default": + def := DefaultOwner{} + d.DecodeElement(&def, &tt) + pso.OwnerUseCase = &def + case "delete": + del := DeleteOwner{} + d.DecodeElement(&del, &tt) + pso.OwnerUseCase = &del + case "purge": + pu := PurgeOwner{} + d.DecodeElement(&pu, &tt) + pso.OwnerUseCase = &pu + case "subscriptions": + subs := SubscriptionsOwner{} + d.DecodeElement(&subs, &tt) + pso.OwnerUseCase = &subs + if err != nil { + return err + } + } + + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub#owner", Local: "pubsub"}, PubSubOwner{}) +} diff --git a/stanza/pubsub_owner_test.go b/stanza/pubsub_owner_test.go new file mode 100644 index 0000000..8af6194 --- /dev/null +++ b/stanza/pubsub_owner_test.go @@ -0,0 +1,833 @@ +package stanza_test + +import ( + "encoding/xml" + "errors" + "gosrc.io/xmpp/stanza" + "testing" +) + +// ****************************** +// * 8.2 Configure a Node +// ****************************** +func TestNewConfigureNode(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "config1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.Node == "" { + t.Fatalf("could not parse node from config tag") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewConfigureNodeResp(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#node_config + + + 0 + + + 1028 + + + + + + never + + + 0 + + + + + headline + + + http://www.w3.org/2005/Atom + + + + + + +` + + pubsub, err := getPubSubOwnerPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.Form == nil { + t.Fatalf("form is nil in the parsed config tag") + } + + if len(ownrUsecase.Form.Fields) != 8 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// ************************************************* +// * 8.3 Request Default Node Configuration Options +// ************************************************* + +func TestNewRequestDefaultConfig(t *testing.T) { + expectedReq := " " + + " " + + subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit") + subR.Id = "def1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + _, ok = pubsub.OwnerUseCase.(*stanza.DefaultOwner) + if !ok { + t.Fatalf("owner use case is not a default tag") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewRequestDefaultConfigResp(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#node_config + + + 0 + + + 1028 + + + + + + never + + + 0 + + + + + headline + + + http://www.w3.org/2005/Atom + + + + + + +` + + pubsub, err := getPubSubOwnerPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.Form == nil { + t.Fatalf("form is nil in the parsed config tag") + } + + if len(ownrUsecase.Form.Fields) != 8 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// *********************** +// * 8.4 Delete a Node +// *********************** + +func TestNewDelNode(t *testing.T) { + expectedReq := "" + + " " + + " " + + subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "delete1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + _, ok = pubsub.OwnerUseCase.(*stanza.DeleteOwner) + if !ok { + t.Fatalf("owner use case is not a delete tag") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewDelNodeResp(t *testing.T) { + response := ` + + + + + + + +` + + pubsub, err := getPubSubOwnerPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.DeleteOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.RedirectOwner == nil { + t.Fatalf("redirect is nil in the delete tag") + } + + if ownrUsecase.RedirectOwner.URI == "" { + t.Fatalf("could not parse redirect uri") + } +} + +// **************************** +// * 8.5 Purge All Node Items +// **************************** + +func TestNewPurgeAllItems(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "purge1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + purge, ok := pubsub.OwnerUseCase.(*stanza.PurgeOwner) + if !ok { + t.Fatalf("owner use case is not a delete tag") + } + + if purge.Node == "" { + t.Fatalf("could not parse purge targer node") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// ************************************ +// * 8.6 Manage Subscription Requests +// ************************************ +func TestNewApproveSubRequest(t *testing.T) { + expectedReq := " " + + " " + + "http://jabber.org/protocol/pubsub#subscribe_authorization " + + " 123-abc princely_musings " + + " horatio@denmark.lit " + + "true " + + apprForm := &stanza.Form{ + Type: stanza.FormTypeSubmit, + Fields: []stanza.Field{ + {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#subscribe_authorization"}}, + {Var: "pubsub#subid", ValuesList: []string{"123-abc"}}, + {Var: "pubsub#node", ValuesList: []string{"princely_musings"}}, + {Var: "pubsub#subscriber_jid", ValuesList: []string{"horatio@denmark.lit"}}, + {Var: "pubsub#allow", ValuesList: []string{"true"}}, + }, + } + + subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm) + subR.Id = "approve1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + frm, ok := subR.Extensions[0].(*stanza.Form) + if !ok { + t.Fatalf("extension is not a from !") + } + + var allowField *stanza.Field + + for _, f := range frm.Fields { + if f.Var == "pubsub#allow" { + allowField = &f + } + } + if allowField == nil || allowField.ValuesList[0] != "true" { + t.Fatalf("could not correctly parse the allow field in the response from") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// ******************************************** +// * 8.7 Process Pending Subscription Requests +// ******************************************** + +func TestNewGetPendingSubRequests(t *testing.T) { + expectedReq := " " + + "" + + " " + + subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit") + subR.Id = "pending1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + command, ok := subR.Payload.(*stanza.Command) + if !ok { + t.Fatalf("payload is not a command !") + } + + if command.Action != stanza.CommandActionExecute { + t.Fatalf("command should be execute !") + } + + if command.Node != "http://jabber.org/protocol/pubsub#get-pending" { + t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewGetPendingSubRequestsResp(t *testing.T) { + response := ` + + + + + http://jabber.org/protocol/pubsub#subscribe_authorization + + + + + + + + +` + + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + if err != nil { + t.Fatalf("could not parse iq") + } + + _, ok := respIQ.Payload.(*stanza.Command) + if !ok { + errors.New("this iq payload is not a command") + } + + fMap, err := respIQ.GetFormFields() + if err != nil || len(fMap) != 2 { + errors.New("could not parse command form fields") + } + +} + +// ******************************************** +// * 8.7 Process Pending Subscription Requests +// ******************************************** + +func TestNewApprovePendingSubRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + "princely_musings " + + subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit", + "pubsub-get-pending:20031021T150901Z-600", + "princely_musings") + subR.Id = "pending2" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + command, ok := subR.Payload.(*stanza.Command) + if !ok { + t.Fatalf("payload is not a command !") + } + + if command.Action != stanza.CommandActionExecute { + t.Fatalf("command should be execute !") + } + + //if command.Node != "http://jabber.org/protocol/pubsub#get-pending"{ + // t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !") + //} + // + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// ******************************************** +// * 8.8.1 Retrieve Subscriptions List +// ******************************************** + +func TestNewSubListRqPl(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "subman1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub in namespace owner !") + } + + subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain a subscriptions node !") + } + + if subs.Node != "princely_musings" { + t.Fatalf("subs node attribute should be princely_musings. Found %s", subs.Node) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewSubListRqPlResp(t *testing.T) { + response := ` + + + + + + + + + + +` + + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + if err != nil { + t.Fatalf("could not parse iq") + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) + if !ok { + errors.New("this iq payload is not a command") + } + + subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain a subscriptions node !") + } + + if len(subs.Subscriptions) != 4 { + t.Fatalf("expected to find 4 subscriptions but got %d", len(subs.Subscriptions)) + } + +} + +// ******************************************** +// * 8.9.1 Retrieve Affiliations List +// ******************************************** + +func TestNewAffiliationListRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "ent1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub in namespace owner !") + } + + affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain an affiliations node !") + } + + if affils.Node != "princely_musings" { + t.Fatalf("affils node attribute should be princely_musings. Found %s", affils.Node) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewAffiliationListRequestResp(t *testing.T) { + response := ` + + + + + + + + +` + + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + if err != nil { + t.Fatalf("could not parse iq") + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) + if !ok { + errors.New("this iq payload is not a command") + } + + affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain an affiliations node !") + } + + if len(affils.Affiliations) != 2 { + t.Fatalf("expected to find 2 subscriptions but got %d", len(affils.Affiliations)) + } + +} + +// ******************************************** +// * 8.9.2 Modify Affiliation +// ******************************************** + +func TestNewModifAffiliationRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + " " + + " " + + "" + + affils := []stanza.AffiliationOwner{ + { + AffiliationStatus: stanza.AffiliationStatusNone, + Jid: "hamlet@denmark.lit", + }, + { + AffiliationStatus: stanza.AffiliationStatusNone, + Jid: "polonius@denmark.lit", + }, + { + AffiliationStatus: stanza.AffiliationStatusPublisher, + Jid: "bard@shakespeare.lit", + }, + } + + subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils) + subR.Id = "ent3" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub in namespace owner !") + } + + as, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain an affiliations node !") + } + + if as.Node != "princely_musings" { + t.Fatalf("affils node attribute should be princely_musings. Found %s", as.Node) + } + if len(as.Affiliations) != 3 { + t.Fatalf("expected 3 affiliations, found %d", len(as.Affiliations)) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestGetFormFields(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#node_config + + + 0 + + + 1028 + + + + + + never + + + 0 + + + + + headline + + + http://www.w3.org/2005/Atom + + + + + + +` + var iq stanza.IQ + err := xml.Unmarshal([]byte(response), &iq) + if err != nil { + t.Fatalf("could not parse IQ") + } + + fields, err := iq.GetFormFields() + if len(fields) != 8 { + t.Fatalf("could not correctly parse fields. Expected 8, found : %v", len(fields)) + } + +} + +func TestGetFormFieldsCmd(t *testing.T) { + response := ` + + + + + http://jabber.org/protocol/pubsub#subscribe_authorization + + + + + + + + +` + var iq stanza.IQ + err := xml.Unmarshal([]byte(response), &iq) + if err != nil { + t.Fatalf("could not parse IQ") + } + + fields, err := iq.GetFormFields() + if len(fields) != 2 { + t.Fatalf("could not correctly parse fields. Expected 2, found : %v", len(fields)) + } + +} + +func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) { + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + return &stanza.PubSubOwner{}, err + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) + if !ok { + errors.New("this iq payload is not a pubsub of the owner namespace") + } + + return pubsub, nil +} diff --git a/stanza/pubsub_test.go b/stanza/pubsub_test.go new file mode 100644 index 0000000..95bf640 --- /dev/null +++ b/stanza/pubsub_test.go @@ -0,0 +1,921 @@ +package stanza_test + +import ( + "encoding/xml" + "errors" + "gosrc.io/xmpp/stanza" + "strings" + "testing" +) + +var submitFormExample = stanza.NewForm([]stanza.Field{ + {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, + {Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}}, + {Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}}, + {Var: "pubsub#access_model", ValuesList: []string{"roster"}}, + {Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}}, + {Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}}, + { + Var: "pubsub#notification_type", + Type: "list-single", + Label: "Specify the delivery style for event notifications", + ValuesList: []string{"headline"}, + Options: []stanza.Option{ + {ValuesList: []string{"normal"}}, + {ValuesList: []string{"headline"}}, + }, + }, +}, stanza.FormTypeSubmit) + +// *********************************** +// * 6.1 Subscribe to a Node +// *********************************** + +func TestNewSubRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo) + subR.Id = "sub1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } + +} + +func TestNewSubResp(t *testing.T) { + response := ` + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + + if pubsub.Subscription == nil { + t.Fatalf("subscription node is nil") + } + if pubsub.Subscription.Node == "" || + pubsub.Subscription.Jid == "" || + pubsub.Subscription.SubId == nil || + pubsub.Subscription.SubStatus == "" { + t.Fatalf("one or more of the subscription attributes was not successfully decoded") + } + +} + +// *********************************** +// * 6.2 Unsubscribe from a Node +// *********************************** + +func TestNewUnSubRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo) + subR.Id = "unsub1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Unsubscribe == nil { + t.Fatalf("Unsubscribe tag should be present in sub config options request") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewUnsubResp(t *testing.T) { + response := ` + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + + if pubsub.Subscription == nil { + t.Fatalf("subscription node is nil") + } + if pubsub.Subscription.Node == "" || + pubsub.Subscription.Jid == "" || + pubsub.Subscription.SubId == nil || + pubsub.Subscription.SubStatus == "" { + t.Fatalf("one or more of the subscription attributes was not successfully decoded") + } + +} + +// *************************************** +// * 6.3 Configure Subscription Options +// *************************************** +func TestNewSubOptsRq(t *testing.T) { + expectedReq := " " + + " " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo) + subR.Id = "options1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.SubOptions == nil { + t.Fatalf("Options tag should be present in sub config options request") + } + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewNewConfOptsRsp(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + + 1 + + + 0 + + + false + + + + + + + + chat + online + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + + if pubsub.SubOptions == nil { + t.Fatalf("sub options node is nil") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("the response form is nil") + } + + if len(pubsub.SubOptions.Form.Fields) != 5 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// *************************************** +// * 6.3.5 Form Submission +// *************************************** +func TestNewFormSubmission(t *testing.T) { + expectedReq := " " + + " " + + " " + + " http://jabber.org/protocol/pubsub#node_config " + + "Princely Musings (Atom) " + + "1 roster " + + " friends servants" + + " courtiers http://www.w3.org/2005/Atom " + + " " + + "headline " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + + subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample) + subR.Id = "options2" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.SubOptions == nil { + t.Fatalf("Options tag should be present in sub config options request") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("No form in form submit request !") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// *************************************** +// * 6.3.7 Subscribe and Configure +// *************************************** + +func TestNewSubAndConfig(t *testing.T) { + expectedReq := "" + + " " + + "" + + " " + + " http://jabber.org/protocol/pubsub#node_config " + + "Princely Musings (Atom) " + + "1 roster " + + " friends servants" + + " courtiers http://www.w3.org/2005/Atom " + + " " + + "headline " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + + subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample) + subR.Id = "sub1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.SubOptions == nil { + t.Fatalf("Options tag should be present in sub config options request") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("No form in form submit request !") + } + + // The element MUST NOT possess a 'node' attribute or 'jid' attribute + // See XEP-0060 + if pubsub.SubOptions.SubInfo.Node != "" || pubsub.SubOptions.SubInfo.Jid != "" { + t.Fatalf("SubInfo node and jid should be empty for the options tag !") + } + if pubsub.Subscribe.Node == "" || pubsub.Subscribe.Jid == "" { + t.Fatalf("SubInfo node and jid should NOT be empty for the subscribe tag !") + } + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewSubAndConfigResp(t *testing.T) { + response := ` + + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + + 1 + + + 0 + + + false + + + chat + online + away + + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.Subscription == nil { + t.Fatalf("sub node is nil") + } + + if pubsub.SubOptions == nil { + t.Fatalf("sub options node is nil") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("the response form is nil") + } + + if len(pubsub.SubOptions.Form.Fields) != 5 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// *************************************** +// * 6.5.2 Requesting All List +// *************************************** +func TestNewItemsRequest(t *testing.T) { + subR, err := stanza.NewItemsRequest("pubsub.shakespeare.lit", "princely_musings", 0) + if err != nil { + t.Fatalf("Could not create an items request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Items == nil { + t.Fatalf("List tag should be present to request items from a service") + } + if len(pubsub.Items.List) != 0 { + t.Fatalf("There should be no items in the tag to request all items from a service") + } +} +func TestNewItemsResp(t *testing.T) { + response := ` + + + + + + Alone + Now I am alone. O, what a rogue and peasant slave am I! + + tag:denmark.lit,2003:entry-32396 + 2003-12-13T11:09:53Z + 2003-12-13T11:09:53Z + + + + + Soliloquy + To be, or not to be: that is the question: Whether 'tis nobler in the + mind to suffer The slings and arrows of outrageous fortune, Or to take arms + against a sea of troubles, And by opposing end them? + + tag:denmark.lit,2003:entry-32397 + 2003-12-13T18:30:02Z + 2003-12-13T18:30:02Z + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.Items == nil { + t.Fatalf("sub options node is nil") + } + if pubsub.Items.List == nil { + t.Fatalf("the response form is nil") + } + + if len(pubsub.Items.List) != 2 { + t.Fatalf("one or more items in the response could not be parsed correctly") + } +} + +// *************************************** +// * 6.5.8 Requesting a Particular Item +// *************************************** +func TestNewSpecificItemRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901") + subR.Id = "items3" + if err != nil { + t.Fatalf("Could not create an items request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Items == nil { + t.Fatalf("List tag should be present to request items from a service") + } + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// *************************************** +// * 7.1 Publish an Item to a Node +// *************************************** +func TestNewPublishItemRq(t *testing.T) { + item := stanza.Item{ + XMLName: xml.Name{}, + Id: "", + Publisher: "", + Any: &stanza.Node{ + XMLName: xml.Name{ + Space: "http://www.w3.org/2005/Atom", + Local: "entry", + }, + Attrs: nil, + Content: "", + Nodes: []stanza.Node{ + { + XMLName: xml.Name{Space: "", Local: "title"}, + Attrs: nil, + Content: "My pub item title", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "summary"}, + Attrs: nil, + Content: "My pub item content summary", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "link"}, + Attrs: []xml.Attr{ + { + Name: xml.Name{Space: "", Local: "rel"}, + Value: "alternate", + }, + { + Name: xml.Name{Space: "", Local: "type"}, + Value: "text/html", + }, + { + Name: xml.Name{Space: "", Local: "href"}, + Value: "http://denmark.lit/2003/12/13/atom03", + }, + }, + }, + { + XMLName: xml.Name{Space: "", Local: "id"}, + Attrs: nil, + Content: "My pub item content ID", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "published"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "updated"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + }, + }, + } + + subR, err := stanza.NewPublishItemRq("pubsub.shakespeare.lit", "princely_musings", "bnd81g37d61f49fgn581", item) + if err != nil { + t.Fatalf("Could not create an item pub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if strings.TrimSpace(pubsub.Publish.Node) == "" { + t.Fatalf("the element MUST possess a 'node' attribute, specifying the NodeID of the node.") + } + if pubsub.Publish.Items[0].Id == "" { + t.Fatalf("an id was provided for the item and it should be used") + } +} + +// *************************************** +// * 7.1.5 Publishing Options +// *************************************** + +func TestNewPublishItemOptsRq(t *testing.T) { + expectedReq := " " + + " " + + " Soliloquy " + + " To be, or not to be: that is the question: Whether \"tis nobler in the mind to suffer The " + + "slings and arrows of outrageous fortune, Or to take arms against a sea of troubles, And by opposing end them? " + + " " + + "tag:denmark.lit,2003:entry-32397 2003-12-13T18:30:02Z " + + "2003-12-13T18:30:02Z " + + " " + + "http://jabber.org/protocol/pubsub#publish-options " + + "presence " + + var iq stanza.IQ + err := xml.Unmarshal([]byte(expectedReq), &iq) + if err != nil { + t.Fatalf("could not unmarshal example request : %s", err) + } + + pubsub, ok := iq.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Publish == nil { + t.Fatalf("Publish tag is empty") + } + if len(pubsub.Publish.Items) != 1 { + t.Fatalf("could not parse item properly") + } +} + +// *************************************** +// * 7.2 Delete an Item from a Node +// *************************************** + +func TestNewDelItemFromNode(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil) + subR.Id = "retract1" + if err != nil { + t.Fatalf("Could not create a del item request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Retract == nil { + t.Fatalf("Retract tag should be present to del an item from a service") + } + + if strings.TrimSpace(pubsub.Retract.Items[0].Id) == "" { + t.Fatalf("Item id, for the item to delete, should be non empty") + } + if pubsub.Retract.Items[0].Any != nil { + t.Fatalf("Item node must be empty") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// *************************************** +// * 8.1 Create a Node +// *************************************** + +func TestNewCreateNode(t *testing.T) { + expectedReq := " " + + " " + + subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "create1" + if err != nil { + t.Fatalf("Could not create a create node request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Create == nil { + t.Fatalf("Create tag should be present to create a node on a service") + } + + if strings.TrimSpace(pubsub.Create.Node) == "" { + t.Fatalf("Expected node name to be present") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewCreateNodeResp(t *testing.T) { + response := ` + + + + + +` + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.Create == nil { + t.Fatalf("create segment is nil") + } + if pubsub.Create.Node == "" { + t.Fatalf("could not parse generated nodeId") + } + +} + +// *************************************** +// * 8.1.3 Create and Configure a Node +// *************************************** + +func TestNewCreateAndConfigNode(t *testing.T) { + expectedReq := " " + + " " + + " " + + "http://jabber.org/protocol/pubsub#node_config " + + "0 0 " + + " 1028 " + + subR, err := stanza.NewCreateAndConfigNode("pubsub.shakespeare.lit", + "princely_musings", + &stanza.Form{ + Type: stanza.FormTypeSubmit, + Fields: []stanza.Field{ + {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, + {Var: "pubsub#notify_retract", ValuesList: []string{"0"}}, + {Var: "pubsub#notify_sub", ValuesList: []string{"0"}}, + {Var: "pubsub#max_payload_size", ValuesList: []string{"1028"}}, + }, + }) + subR.Id = "create1" + if err != nil { + t.Fatalf("Could not create a create node request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Create == nil { + t.Fatalf("Create tag should be present to create a node on a service") + } + + if strings.TrimSpace(pubsub.Create.Node) == "" { + t.Fatalf("Expected node name to be present") + } + + if pubsub.Configure == nil { + t.Fatalf("Configure tag should be present to configure a node during its creation on a service") + } + + if pubsub.Configure.Form == nil { + t.Fatalf("Expected a form to be present, to configure the node") + } + if len(pubsub.Configure.Form.Fields) != 4 { + t.Fatalf("Expected 4 elements to be present in the config form but got : %v", len(pubsub.Configure.Form.Fields)) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } + +} + +// ******************************** +// * 5.7 Retrieve Subscriptions +// ******************************** + +func TestNewRetrieveAllSubsRequest(t *testing.T) { + expected := " " + + " " + + subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit") + subR.Id = "subscriptions1" + if err != nil { + t.Fatalf("Could not create a create node request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expected, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestRetrieveAllSubsResp(t *testing.T) { + response := ` + + + + + + + + + + + +` + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + t.Fatalf("could not unmarshal response: %s", err) + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("umarshalled payload is not a pubsub") + } + + if pubsub.Subscriptions == nil { + t.Fatalf("subscriptions node is nil") + } + if len(pubsub.Subscriptions.List) != 5 { + t.Fatalf("incorrect number of decoded subscriptions") + } +} + +// ******************************** +// * 5.7 Retrieve Affiliations +// ******************************** + +func TestNewRetrieveAllAffilsRequest(t *testing.T) { + expected := " " + + " " + + subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit") + subR.Id = "affil1" + if err != nil { + t.Fatalf("Could not create retreive all affiliations request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated retreive all affiliations request : %s", e) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expected, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestRetrieveAllAffilsResp(t *testing.T) { + response := ` + + + + + + + + + + +` + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + t.Fatalf("could not unmarshal response: %s", err) + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("umarshalled payload is not a pubsub") + } + + if pubsub.Affiliations == nil { + t.Fatalf("subscriptions node is nil") + } + if len(pubsub.Affiliations.List) != 4 { + t.Fatalf("incorrect number of decoded subscriptions") + } +} + +func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) { + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + return &stanza.PubSubGeneric{}, err + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric) + if !ok { + errors.New("this iq payload is not a pubsub") + } + + return pubsub, nil +} diff --git a/stanza/xmpp_test.go b/stanza/xmpp_test.go index 420a053..473616a 100644 --- a/stanza/xmpp_test.go +++ b/stanza/xmpp_test.go @@ -2,12 +2,17 @@ package stanza_test import ( "encoding/xml" + "errors" + "regexp" "testing" "github.com/google/go-cmp/cmp" "gosrc.io/xmpp/stanza" ) +var reLeadcloseWhtsp = regexp.MustCompile(`^[\s\p{Zs}]+|[\s\p{Zs}]+$`) +var reInsideWhtsp = regexp.MustCompile(`[\s\p{Zs}]`) + // ============================================================================ // Marshaller / unmarshaller test @@ -63,3 +68,14 @@ func xmlOpts() cmp.Options { } return opts } + +func delSpaces(s string) string { + return reInsideWhtsp.ReplaceAllString(reLeadcloseWhtsp.ReplaceAllString(s, ""), "") +} + +func compareMarshal(expected, data string) error { + if delSpaces(expected) != delSpaces(data) { + return errors.New("failed to verify unmarshal->marshal. Expected :" + expected + "\ngot: " + data) + } + return nil +} diff --git a/tcp_server_mock.go b/tcp_server_mock.go index c8f5d97..1a4f92e 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -280,7 +280,7 @@ func bind(t *testing.T, sc *ServerConn) { %s ` - fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real JID + fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real Jid } }