From 1ba2add651723955733a20e67197408f3b8e7026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Mon, 16 Dec 2019 01:42:27 +0100 Subject: [PATCH] Example client with TUI --- _examples/xmpp_chat_client/config.yml | 12 + _examples/xmpp_chat_client/go.mod | 10 + _examples/xmpp_chat_client/interface.go | 167 +++++++++++++ .../xmpp_chat_client/xmpp_chat_client.go | 230 +++++++++++++++--- cmd/go.mod | 2 +- go.mod | 3 + 6 files changed, 384 insertions(+), 40 deletions(-) create mode 100644 _examples/xmpp_chat_client/config.yml create mode 100644 _examples/xmpp_chat_client/go.mod create mode 100644 _examples/xmpp_chat_client/interface.go diff --git a/_examples/xmpp_chat_client/config.yml b/_examples/xmpp_chat_client/config.yml new file mode 100644 index 0000000..2ebfe1b --- /dev/null +++ b/_examples/xmpp_chat_client/config.yml @@ -0,0 +1,12 @@ +# Default config for the client +Server : + - full_address: "localhost:5222" + - port: 5222 +Client : + - name: "testuser2" + - jid: "testuser2@localhost" + - pass: "pass123" #Password in a config file yay + +Contacts : "testuser1@localhost;testuser3@localhost" + + diff --git a/_examples/xmpp_chat_client/go.mod b/_examples/xmpp_chat_client/go.mod new file mode 100644 index 0000000..8d510f6 --- /dev/null +++ b/_examples/xmpp_chat_client/go.mod @@ -0,0 +1,10 @@ +module go-xmpp/_examples/xmpp_chat_client + +go 1.13 + +require ( + github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.6.1 + gosrc.io/xmpp v0.3.1-0.20191212145100-27130d72926b +) diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go new file mode 100644 index 0000000..a64f182 --- /dev/null +++ b/_examples/xmpp_chat_client/interface.go @@ -0,0 +1,167 @@ +package main + +import ( + "fmt" + "github.com/awesome-gocui/gocui" + "log" +) + +const ( + chatLogWindow = "clw" + inputWindow = "iw" + menuWindow = "menw" +) + +func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { + if _, err := g.SetCurrentView(name); err != nil { + return nil, err + } + return g.SetViewOnTop(name) +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + + if v, err := g.SetView(chatLogWindow, maxX/5, 0, maxX-1, 5*maxY/6-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Chat log" + v.Wrap = true + v.Autoscroll = true + } + + if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Contacts" + v.Wrap = true + v.Autoscroll = true + } + + if v, err := g.SetView(inputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Write a message :" + v.Editable = true + v.Wrap = true + + if _, err = setCurrentViewOnTop(g, inputWindow); err != nil { + return err + } + } + + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +// Sends an input line from the user to the backend while also printing it in the chatlog window. +func writeInput(g *gocui.Gui, v *gocui.View) error { + log, _ := g.View(chatLogWindow) + for _, line := range v.ViewBufferLines() { + textChan <- line + fmt.Fprintln(log, "Me : ", line) + } + v.Clear() + v.EditDeleteToStartOfLine() + return nil +} + +func setKeyBindings(g *gocui.Gui) { + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(inputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(inputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + + } + if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + log.Panicln(err) + + } + if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + log.Panicln(err) + + } + if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil { + log.Panicln(err) + } + +} + +// When we select a new correspondent, we change it in the client, and we display a message window confirming the change. +func getLine(g *gocui.Gui, v *gocui.View) error { + var l string + var err error + + _, cy := v.Cursor() + if l, err = v.Line(cy); err != nil { + l = "" + } + // Updating the current correspondent, back-end side. + CorrespChan <- l + + // Showing a message to the user, and switching back to input after the new contact is selected. + message := "Now sending messages to : " + l + " in a private conversation" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + g.SetCurrentView(inputWindow) + return nil +} + +// Changing view between input and "menu" (= basically contacts only right now) when pressing the specific key. +func nextView(g *gocui.Gui, v *gocui.View) error { + if v == nil || v.Name() == inputWindow { + _, err := g.SetCurrentView(menuWindow) + return err + } + _, err := g.SetCurrentView(inputWindow) + return err +} + +func cursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + // Avoid going below the list of contacts + cv := g.CurrentView() + h := cv.LinesHeight() + if cy+1 >= h-1 { + return nil + } + // Lower cursor + if err := v.SetCursor(cx, cy+1); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + } + return nil +} + +func cursorUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + } + return nil +} diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index 2b2d2e7..9e3c2c6 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -6,90 +6,242 @@ Note that this example sends to a very specific user. User logic is not implemen */ import ( - . "bufio" + "flag" "fmt" - "os" - + "github.com/awesome-gocui/gocui" + "github.com/spf13/pflag" + "github.com/spf13/viper" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" + "log" + "strings" ) const ( - currentUserAddress = "localhost:5222" - currentUserJid = "testuser@localhost" - currentUserPass = "testpass" - correspondantJid = "testuser2@localhost" + infoFormat = "====== " + // Default configuration + defaultConfigFilePath = "./" + + configFileName = "config" + configType = "yaml" + // Keys in config + serverAddressKey = "full_address" + clientJid = "jid" + clientPass = "pass" + configContactSep = ";" ) +var ( + CorrespChan = make(chan string, 1) + textChan = make(chan string, 5) + killChan = make(chan struct{}, 1) +) + +type config struct { + Server map[string]string `mapstructure:"server"` + Client map[string]string `mapstructure:"client"` + Contacts string `string:"contact"` +} + func main() { - config := xmpp.Config{ + // ============================================================ + // Parse the flag with the config directory path as argument + flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+ + " file you want to use. Config file should be named \"config\" and be of YAML format..") + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.Parse() + + // ========================== + // Read configuration + c := readConfig() + + // ========================== + // Create TUI + g, err := gocui.NewGui(gocui.OutputNormal, true) + if err != nil { + log.Panicln(err) + } + defer g.Close() + g.Highlight = true + g.Cursor = true + g.SelFgColor = gocui.ColorGreen + g.SetManagerFunc(layout) + setKeyBindings(g) + + // ========================== + // Run TUI + errChan := make(chan error) + go func() { + errChan <- g.MainLoop() + }() + + // ========================== + // Start XMPP client + go startClient(g, c) + + select { + case err := <-errChan: + if err == gocui.ErrQuit { + log.Println("Closing client.") + } else { + log.Panicln(err) + } + } +} + +func startClient(g *gocui.Gui, config *config) { + + // ========================== + // Client setup + clientCfg := xmpp.Config{ TransportConfiguration: xmpp.TransportConfiguration{ - Address: currentUserAddress, + Address: config.Server[serverAddressKey], }, - Jid: currentUserJid, - Credential: xmpp.Password(currentUserPass), + Jid: config.Client[clientJid], + Credential: xmpp.Password(config.Client[clientPass]), Insecure: true} var client *xmpp.Client var err error router := xmpp.NewRouter() - router.HandleFunc("message", handleMessage) - if client, err = xmpp.NewClient(config, router, errorHandler); err != nil { - fmt.Println("Error new client") + + handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) { + msg, ok := p.(stanza.Message) + v, err := g.View(chatLogWindow) + if !ok { + fmt.Fprintf(v, "%sIgnoring packet: %T\n", infoFormat, p) + return + } + if err != nil { + return + } + g.Update(func(g *gocui.Gui) error { + if msg.Error.Code != 0 { + _, err := fmt.Fprintf(v, "Error from server : %s : %s \n", msg.Error.Reason, msg.XMLName.Space) + return err + } + _, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body) + return err + }) } - // Connecting client and handling messages - // To use a stream manager, just write something like this instead : - //cm := xmpp.NewStreamManager(client, startMessaging) - //log.Fatal(cm.Run()) //=> this will lock the calling goroutine + router.HandleFunc("message", handlerWithGui) + if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil { + panic(fmt.Sprintf("Could not create a new client ! %s", err)) + } + + // ========================== + // Client connection if err = client.Connect(); err != nil { - fmt.Printf("XMPP connection failed: %s", err) + msg := fmt.Sprintf("%sXMPP connection failed: %s", infoFormat, err) + g.Update(func(g *gocui.Gui) error { + v, err := g.View(chatLogWindow) + fmt.Fprintf(v, msg) + return err + }) return } - startMessaging(client) + // ========================== + // Start working + //askForRoster(client, g) + updateRosterFromConfig(g, config) + startMessaging(client, config) } -func startMessaging(client xmpp.Sender) { - reader := NewReader(os.Stdin) - textChan := make(chan string) +func startMessaging(client xmpp.Sender, config *config) { var text string + // Update this with a channel. Default value is the first contact in the list from the config. + correspondent := strings.Split(config.Contacts, configContactSep)[0] for { - fmt.Print("Enter text: ") - go readInput(reader, textChan) select { case <-killChan: return case text = <-textChan: - reply := stanza.Message{Attrs: stanza.Attrs{To: correspondantJid}, Body: text} + reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent}, Body: text} err := client.Send(reply) if err != nil { fmt.Printf("There was a problem sending the message : %v", reply) return } + case crrsp := <-CorrespChan: + correspondent = crrsp } + } } -func readInput(reader *Reader, textChan chan string) { - text, _ := reader.ReadString('\n') - textChan <- text -} +func readConfig() *config { + viper.SetConfigName(configFileName) // name of config file (without extension) + viper.BindPFlags(pflag.CommandLine) + viper.AddConfigPath(viper.GetString("c")) // path to look for the config file in + err := viper.ReadInConfig() // Find and read the config file + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + log.Fatalf("%s %s", err, "Please make sure you give a path to the directory of the config and not to the config itself.") + } else { + log.Panicln(err) + } + } + viper.SetConfigType(configType) + var config config + err = viper.Unmarshal(&config) + if err != nil { + panic(fmt.Errorf("Unable to decode Config: %s \n", err)) + } -var killChan = make(chan struct{}) + return &config +} -// If an error occurs, this is used +// If an error occurs, this is used to kill the client func errorHandler(err error) { fmt.Printf("%v", err) killChan <- struct{}{} } -func handleMessage(s xmpp.Sender, p stanza.Packet) { - msg, ok := p.(stanza.Message) - if !ok { - _, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p) - return - } - _, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From) +// Read the client roster from the config. This does not check with the server that the roster is correct. +// If user tries to send a message to someone not registered with the server, the server will return an error. +func updateRosterFromConfig(g *gocui.Gui, config *config) { + g.Update(func(g *gocui.Gui) error { + menu, _ := g.View(menuWindow) + for _, contact := range strings.Split(config.Contacts, configContactSep) { + fmt.Fprintln(menu, contact) + } + return nil + }) +} + +// Updates the menu panel of the view with the current user's roster. +// Need to add support for Roster IQ stanzas to make this work. +func askForRoster(client *xmpp.Client, g *gocui.Gui) { + //ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + //iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: currentUserJid, To: "localhost", Lang: "en"}) + //disco := iqReq.DiscoInfo() + //iqReq.Payload = disco + // + //// Handle a possible error + //errChan := make(chan error) + //errorHandler := func(err error) { + // errChan <- err + //} + //client.ErrorHandler = errorHandler + //res, err := client.SendIQ(ctx, iqReq) + //if err != nil { + // t.Errorf(err.Error()) + //} + // + //select { + //case <-res: + //} + + //roster := []string{"testuser1", "testuser2", "testuser3@localhost"} + // + //g.Update(func(g *gocui.Gui) error { + // menu, _ := g.View(menuWindow) + // for _, contact := range roster { + // fmt.Fprintln(menu, contact) + // } + // return nil + //}) } diff --git a/cmd/go.mod b/cmd/go.mod index 85df002..1c4684f 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -6,7 +6,7 @@ require ( github.com/bdlm/log v0.1.19 github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 github.com/spf13/cobra v0.0.5 - github.com/spf13/viper v1.4.0 + github.com/spf13/viper v1.6.1 gosrc.io/xmpp v0.1.1 ) diff --git a/go.mod b/go.mod index f31fe40..d3b3273 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module gosrc.io/xmpp go 1.13 require ( + github.com/awesome-gocui/gocui v0.6.0 // indirect github.com/google/go-cmp v0.3.1 github.com/google/uuid v1.1.1 + github.com/spf13/viper v1.6.1 // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 nhooyr.io/websocket v1.6.5 + )