Example client with TUI
This commit is contained in:
parent
3c9b0db5b8
commit
1ba2add651
12
_examples/xmpp_chat_client/config.yml
Normal file
12
_examples/xmpp_chat_client/config.yml
Normal file
|
@ -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"
|
||||
|
||||
|
10
_examples/xmpp_chat_client/go.mod
Normal file
10
_examples/xmpp_chat_client/go.mod
Normal file
|
@ -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
|
||||
)
|
167
_examples/xmpp_chat_client/interface.go
Normal file
167
_examples/xmpp_chat_client/interface.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if err = client.Connect(); err != nil {
|
||||
fmt.Printf("XMPP connection failed: %s", err)
|
||||
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
|
||||
}
|
||||
startMessaging(client)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
}
|
||||
|
||||
func startMessaging(client xmpp.Sender) {
|
||||
reader := NewReader(os.Stdin)
|
||||
textChan := make(chan string)
|
||||
// ==========================
|
||||
// Client connection
|
||||
if err = client.Connect(); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// Start working
|
||||
//askForRoster(client, g)
|
||||
updateRosterFromConfig(g, config)
|
||||
startMessaging(client, config)
|
||||
}
|
||||
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||
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
|
||||
//})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
3
go.mod
3
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
|
||||
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue