Merge pull request #139 from remicorniere/IQ_Roster
Added Roster IQs Added an overly primitive "disconnect" for the client to use in the chat client example
This commit is contained in:
commit
3037bf6db8
|
@ -9,12 +9,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Windows
|
// Windows
|
||||||
chatLogWindow = "clw" // Where (received and sent) messages are logged
|
chatLogWindow = "clw" // Where (received and sent) messages are logged
|
||||||
chatInputWindow = "iw" // Where messages are written
|
chatInputWindow = "iw" // Where messages are written
|
||||||
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,11 @@ 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 +215,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 +345,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
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
115
stanza/iq_roster.go
Normal 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
109
stanza/iq_roster_test.go
Normal 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
|
||||||
|
}
|
|
@ -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>"
|
||||||
|
|
Loading…
Reference in a new issue