Added Roster IQs

Added an overly primitive "disconnect" for the client to use in the chat client example
This commit is contained in:
CORNIERE Rémi 2019-12-23 09:04:18 +01:00
parent f0179ad90e
commit 390336b894
9 changed files with 278 additions and 11 deletions

View File

@ -15,6 +15,7 @@ const (
rawInputWindow = "rw" // Where raw stanzas are written rawInputWindow = "rw" // Where raw stanzas are written
contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable
menuWindow = "mw" // Where the menu is shown menuWindow = "mw" // Where the menu is shown
disconnectMsg = "msg"
// Menu options // Menu options
disconnect = "Disconnect" disconnect = "Disconnect"
@ -188,6 +189,12 @@ func setKeyBindings(g *gocui.Gui) {
log.Panicln(err) log.Panicln(err)
} }
// ==========================
// Disconnect message
if err := g.SetKeybinding(disconnectMsg, gocui.KeyEnter, gocui.ModNone, delMsg); err != nil {
log.Panicln(err)
}
} }
// General // General
@ -209,7 +216,20 @@ func getLine(g *gocui.Gui, v *gocui.View) error {
if len(cv.ViewBufferLines()) == 0 { if len(cv.ViewBufferLines()) == 0 {
printContactsToWindow(g, viewState.contacts) printContactsToWindow(g, viewState.contacts)
} }
} else if l == disconnect || l == askServerForRoster { } else if l == disconnect {
maxX, maxY := g.Size()
msg := "You disconnected from the server. Press enter to quit."
if v, err := g.SetView(disconnectMsg, maxX/2-30, maxY/2, maxX/2-29+len(msg), maxY/2+2, 0); err != nil {
if !gocui.IsUnknownView(err) {
return err
}
fmt.Fprintln(v, msg)
if _, err := g.SetCurrentView(disconnectMsg); err != nil {
return err
}
}
killChan <- disconnectErr
} else if l == askServerForRoster {
chlw, _ := g.View(chatLogWindow) chlw, _ := g.View(chatLogWindow)
fmt.Fprintln(chlw, infoFormat+" Not yet implemented !") fmt.Fprintln(chlw, infoFormat+" Not yet implemented !")
} else if l == rawMode { } else if l == rawMode {
@ -326,3 +346,11 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error {
} }
return nil return nil
} }
func delMsg(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteView(disconnectMsg); err != nil {
return err
}
errChan <- gocui.ErrQuit // Quit the program
return nil
}

View File

@ -6,6 +6,7 @@ xmpp_chat_client is a demo client that connect on an XMPP server to chat with ot
import ( import (
"encoding/xml" "encoding/xml"
"errors"
"flag" "flag"
"fmt" "fmt"
"github.com/awesome-gocui/gocui" "github.com/awesome-gocui/gocui"
@ -40,10 +41,11 @@ var (
CorrespChan = make(chan string, 1) CorrespChan = make(chan string, 1)
textChan = make(chan string, 5) textChan = make(chan string, 5)
rawTextChan = make(chan string, 5) rawTextChan = make(chan string, 5)
killChan = make(chan struct{}, 1) killChan = make(chan error, 1)
errChan = make(chan error) errChan = make(chan error)
logger *log.Logger logger *log.Logger
disconnectErr = errors.New("disconnecting client")
) )
type config struct { type config struct {
@ -160,7 +162,7 @@ func startClient(g *gocui.Gui, config *config) {
router.HandleFunc("message", handlerWithGui) router.HandleFunc("message", handlerWithGui)
if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil { if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil {
panic(fmt.Sprintf("Could not create a new client ! %s", err)) log.Panicln(fmt.Sprintf("Could not create a new client ! %s", err))
} }
@ -196,7 +198,13 @@ func startMessaging(client xmpp.Sender, config *config) {
var correspondent string var correspondent string
for { for {
select { select {
case <-killChan: case err := <-killChan:
if err == disconnectErr {
sc := client.(xmpp.StreamClient)
sc.Disconnect()
} else {
logger.Println(err)
}
return return
case text = <-textChan: case text = <-textChan:
reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, From: config.Client[clientJid], Type: stanza.MessageTypeChat}, Body: text} reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, From: config.Client[clientJid], Type: stanza.MessageTypeChat}, Body: text}
@ -265,8 +273,7 @@ func readConfig() *config {
// If an error occurs, this is used to kill the client // If an error occurs, this is used to kill the client
func errorHandler(err error) { func errorHandler(err error) {
fmt.Printf("%v", err) killChan <- err
killChan <- struct{}{}
} }
// Read the client roster from the config. This does not check with the server that the roster is correct. // Read the client roster from the config. This does not check with the server that the roster is correct.

View File

@ -206,7 +206,12 @@ func (c *Client) Resume(state SMState) error {
} }
func (c *Client) Disconnect() { func (c *Client) Disconnect() {
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect // TODO : Wait for server response for clean disconnect
presence := stanza.NewPresence(stanza.Attrs{From: c.config.Jid})
presence.Type = stanza.PresenceTypeUnavailable
c.Send(presence)
c.SendRaw(stanza.StreamClose)
if c.transport != nil { if c.transport != nil {
_ = c.transport.Close() _ = c.transport.Close()
} }

View File

@ -111,7 +111,6 @@ func (c *Component) Resume(sm SMState) error {
c.updateState(StatePermanentError) c.updateState(StatePermanentError)
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true) return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
} }
return err
} }
func (c *Component) Disconnect() { func (c *Component) Disconnect() {

View File

@ -8,6 +8,7 @@ import (
// Disco Info // Disco Info
const ( const (
// NSDiscoInfo defines the namespace for disco IQ stanzas
NSDiscoInfo = "http://jabber.org/protocol/disco#info" NSDiscoInfo = "http://jabber.org/protocol/disco#info"
) )
@ -21,6 +22,7 @@ type DiscoInfo struct {
Features []Feature `xml:"feature"` Features []Feature `xml:"feature"`
} }
// Namespace lets DiscoInfo implement the IQPayload interface
func (d *DiscoInfo) Namespace() string { func (d *DiscoInfo) Namespace() string {
return d.XMLName.Space return d.XMLName.Space
} }
@ -112,7 +114,7 @@ func (d *DiscoItems) Namespace() string {
// DiscoItems builds a default DiscoItems payload // DiscoItems builds a default DiscoItems payload
func (iq *IQ) DiscoItems() *DiscoItems { func (iq *IQ) DiscoItems() *DiscoItems {
d := DiscoItems{ d := DiscoItems{
XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", Local: "query"}, XMLName: xml.Name{Space: NSDiscoItems, Local: "query"},
} }
iq.Payload = &d iq.Payload = &d
return &d return &d

View File

@ -50,7 +50,7 @@ func TestDiscoInfo_Builder(t *testing.T) {
// Implements XEP-0030 example 17 // Implements XEP-0030 example 17
// https://xmpp.org/extensions/xep-0030.html#example-17 // https://xmpp.org/extensions/xep-0030.html#example-17
func TestDiscoItems_Builder(t *testing.T) { func TestDiscoItems_Builder(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "catalog.shakespeare.lit", iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit",
To: "romeo@montague.net/orchard", Id: "items-2"}) To: "romeo@montague.net/orchard", Id: "items-2"})
iq.DiscoItems(). iq.DiscoItems().
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare"). AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").

115
stanza/iq_roster.go Normal file
View File

@ -0,0 +1,115 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Roster
const (
// NSRoster is the Roster IQ namespace
NSRoster = "jabber:iq:roster"
// SubscriptionNone indicates the user does not have a subscription to
// the contact's presence, and the contact does not have a subscription
// to the user's presence; this is the default value, so if the subscription
// attribute is not included then the state is to be understood as "none"
SubscriptionNone = "none"
// SubscriptionTo indicates the user has a subscription to the contact's
// presence, but the contact does not have a subscription to the user's presence.
SubscriptionTo = "to"
// SubscriptionFrom indicates the contact has a subscription to the user's
// presence, but the user does not have a subscription to the contact's presence
SubscriptionFrom = "from"
// SubscriptionBoth indicates the user and the contact have subscriptions to each
// other's presence (also called a "mutual subscription")
SubscriptionBoth = "both"
)
// ----------
// Namespaces
// Roster struct represents Roster IQs
type Roster struct {
XMLName xml.Name `xml:"jabber:iq:roster query"`
}
// Namespace defines the namespace for the RosterIQ
func (r *Roster) Namespace() string {
return r.XMLName.Space
}
// ---------------
// Builder helpers
// RosterIQ builds a default Roster payload
func (iq *IQ) RosterIQ() *Roster {
r := Roster{
XMLName: xml.Name{
Space: NSRoster,
Local: "query",
},
}
iq.Payload = &r
return &r
}
// -----------
// SubElements
// RosterItems represents the list of items in a roster IQ
type RosterItems struct {
XMLName xml.Name `xml:"jabber:iq:roster query"`
Items []RosterItem `xml:"item"`
}
// Namespace lets RosterItems implement the IQPayload interface
func (r *RosterItems) Namespace() string {
return r.XMLName.Space
}
// RosterItem represents an item in the roster iq
type RosterItem struct {
XMLName xml.Name `xml:"jabber:iq:roster item"`
Jid string `xml:"jid,attr"`
Ask string `xml:"ask,attr,omitempty"`
Name string `xml:"name,attr,omitempty"`
Subscription string `xml:"subscription,attr,omitempty"`
Groups []string `xml:"group"`
}
// ---------------
// Builder helpers
// RosterItems builds a default RosterItems payload
func (iq *IQ) RosterItems() *RosterItems {
ri := RosterItems{
XMLName: xml.Name{Space: "jabber:iq:roster", Local: "query"},
}
iq.Payload = &ri
return &ri
}
// AddItem builds an item and ads it to the roster IQ
func (r *RosterItems) AddItem(jid, subscription, ask, name string, groups []string) *RosterItems {
item := RosterItem{
Jid: jid,
Name: name,
Groups: groups,
Subscription: subscription,
Ask: ask,
}
r.Items = append(r.Items, item)
return r
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, Roster{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, RosterItems{})
}

109
stanza/iq_roster_test.go Normal file
View File

@ -0,0 +1,109 @@
package stanza
import (
"encoding/xml"
"reflect"
"testing"
)
func TestRosterBuilder(t *testing.T) {
iq := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"})
var noGroup []string
iq.RosterItems().AddItem("xl8ceawrfu8zdneomw1h6h28d@crypho.com",
SubscriptionBoth,
"",
"xl8ceaw",
[]string{"0flucpm8i2jtrjhxw01uf1nd2",
"bm2bajg9ex4e1swiuju9i9nu5",
"rvjpanomi4ejpx42fpmffoac0"}).
AddItem("9aynsym60zbu78jbdvpho7s68@crypho.com",
SubscriptionBoth,
"",
"9aynsym60",
[]string{"mzaoy73i6ra5k502182zi1t97"}).
AddItem("admin@crypho.com",
SubscriptionBoth,
"",
"admin",
noGroup)
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*RosterItems)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check items
items := []RosterItem{
{
XMLName: xml.Name{},
Name: "xl8ceaw",
Ask: "",
Jid: "xl8ceawrfu8zdneomw1h6h28d@crypho.com",
Subscription: SubscriptionBoth,
Groups: []string{"0flucpm8i2jtrjhxw01uf1nd2",
"bm2bajg9ex4e1swiuju9i9nu5",
"rvjpanomi4ejpx42fpmffoac0"},
},
{
XMLName: xml.Name{},
Name: "9aynsym60",
Ask: "",
Jid: "9aynsym60zbu78jbdvpho7s68@crypho.com",
Subscription: SubscriptionBoth,
Groups: []string{"mzaoy73i6ra5k502182zi1t97"},
},
{
XMLName: xml.Name{},
Name: "admin",
Ask: "",
Jid: "admin@crypho.com",
Subscription: SubscriptionBoth,
Groups: noGroup,
},
}
if len(pp.Items) != len(items) {
t.Errorf("Items 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)
}
if !reflect.DeepEqual(item.Groups, items[i].Groups) {
t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
if item.Name != items[i].Name {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
if item.Ask != items[i].Ask {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
if item.Subscription != items[i].Subscription {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
}
}
}
func checkMarshalling(t *testing.T, iq IQ) (*IQ, error) {
// Marshall
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
return nil, err
}
// Unmarshall
var parsedIQ IQ
err = xml.Unmarshal(data, &parsedIQ)
if err != nil {
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
}
return &parsedIQ, err
}

View File

@ -12,3 +12,5 @@ type Stream struct {
Id string `xml:"id,attr"` Id string `xml:"id,attr"`
Version string `xml:"version,attr"` Version string `xml:"version,attr"`
} }
const StreamClose = "</stream:stream>"