mirror of
				https://github.com/FluuxIO/go-xmpp.git
				synced 2025-10-31 00:13:44 -07:00 
			
		
		
		
	Example client with TUI
This commit is contained in:
		
							
								
								
									
										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") | ||||
|  | ||||
| 	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)) | ||||
| 	} | ||||
|  | ||||
| 	return &config | ||||
| } | ||||
|  | ||||
| var killChan = make(chan struct{}) | ||||
|  | ||||
| // 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 | ||||
| 	//}) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| ) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 CORNIERE Rémi
					CORNIERE Rémi