package main /* xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members */ import ( "context" "encoding/xml" "errors" "flag" "fmt" "github.com/awesome-gocui/gocui" "github.com/spf13/pflag" "github.com/spf13/viper" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" "log" "os" "path" "strconv" "strings" "time" ) const ( infoFormat = "====== " // Default configuration defaultConfigFilePath = "./" configFileName = "config" configType = "yaml" logStanzasOn = "logger_on" logFilePath = "logfile_path" // Keys in config serverAddressKey = "full_address" clientJid = "jid" clientPass = "pass" configContactSep = ";" ) var ( CorrespChan = make(chan string, 1) textChan = make(chan string, 5) rawTextChan = make(chan string, 5) killChan = make(chan error, 1) errChan = make(chan error) rosterChan = make(chan struct{}) logger *log.Logger disconnectErr = errors.New("disconnecting client") ) type config struct { Server map[string]string `mapstructure:"server"` Client map[string]string `mapstructure:"client"` Contacts string `string:"contact"` LogStanzas map[string]string `mapstructure:"logstanzas"` } func main() { // ============================================================ // 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 in YAML format..") pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() // ========================== // Read configuration c := readConfig() //================================ // Setup logger on, err := strconv.ParseBool(c.LogStanzas[logStanzasOn]) if err != nil { log.Panicln(err) } if on { f, err := os.OpenFile(path.Join(c.LogStanzas[logFilePath], "logs.txt"), os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) if err != nil { log.Panicln(err) } logger = log.New(f, "", log.Lshortfile|log.Ldate|log.Ltime) logger.SetOutput(f) defer f.Close() } // ========================== // 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 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: config.Server[serverAddressKey], }, Jid: config.Client[clientJid], Credential: xmpp.Password(config.Client[clientPass]), Insecure: true} var client *xmpp.Client var err error router := xmpp.NewRouter() handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if logger != nil { m, _ := xml.Marshal(msg) logger.Println(string(m)) } 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 } if len(strings.TrimSpace(msg.Body)) != 0 { _, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body) return err } return nil }) } router.HandleFunc("message", handlerWithGui) if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil { log.Panicln(fmt.Sprintf("Could not create a new client ! %s", err)) } // ========================== // 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 }) fmt.Println("Failed to connect to server. Exiting...") errChan <- servConnFail return } // ========================== // Start working updateRosterFromConfig(g, config) // Sending the default contact in a channel. Default value is the first contact in the list from the config. viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0] // Informing user of the default contact clw, _ := g.View(chatLogWindow) fmt.Fprintf(clw, infoFormat+"Now sending messages to "+viewState.currentContact+" in a private conversation\n") CorrespChan <- viewState.currentContact startMessaging(client, config, g) } func startMessaging(client xmpp.Sender, config *config, g *gocui.Gui) { var text string var correspondent string for { select { case err := <-killChan: if err == disconnectErr { sc := client.(xmpp.StreamClient) sc.Disconnect() } else { logger.Println(err) } return case text = <-textChan: reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, Type: stanza.MessageTypeChat}, Body: text} if logger != nil { raw, _ := xml.Marshal(reply) logger.Println(string(raw)) } err := client.Send(reply) if err != nil { fmt.Printf("There was a problem sending the message : %v", reply) return } case text = <-rawTextChan: if logger != nil { logger.Println(text) } err := client.SendRaw(text) if err != nil { fmt.Printf("There was a problem sending the message : %v", text) return } case crrsp := <-CorrespChan: correspondent = crrsp case <-rosterChan: askForRoster(client, g, config) } } } // Only reads and parses the configuration 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)) } // Check if we have contacts to message if len(strings.TrimSpace(config.Contacts)) == 0 { log.Panicln("You appear to have no contacts to message !") } // Check logging config.LogStanzas[logFilePath] = path.Clean(config.LogStanzas[logFilePath]) on, err := strconv.ParseBool(config.LogStanzas[logStanzasOn]) if err != nil { log.Panicln(err) } if d, e := isDirectory(config.LogStanzas[logFilePath]); (e != nil || !d) && on { log.Panicln("The log file path could not be found or is not a directory.") } return &config } // If an error occurs, this is used to kill the client func errorHandler(err error) { killChan <- err } // 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) { viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts) // Put a "go back" button at the end of the list viewState.contacts = append(viewState.contacts, backFromContacts) } // Updates the menu panel of the view with the current user's roster, by asking the server. func askForRoster(client xmpp.Sender, g *gocui.Gui, config *config) { // Craft a roster request req := stanza.NewIQ(stanza.Attrs{From: config.Client[clientJid], Type: stanza.IQTypeGet}) req.RosterItems() if logger != nil { m, _ := xml.Marshal(req) logger.Println(string(m)) } ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) // Send the roster request to the server c, err := client.SendIQ(ctx, req) if err != nil { logger.Panicln(err) } // Sending a IQ has a channel spawned to process the response once we receive it. // In order not to block the client, we spawn a goroutine to update the TUI once the server has responded. go func() { serverResp := <-c if logger != nil { m, _ := xml.Marshal(serverResp) logger.Println(string(m)) } // Update contacts with the response from the server chlw, _ := g.View(chatLogWindow) if rosterItems, ok := serverResp.Payload.(*stanza.RosterItems); ok { viewState.contacts = []string{} for _, item := range rosterItems.Items { viewState.contacts = append(viewState.contacts, item.Jid) } // Put a "go back" button at the end of the list viewState.contacts = append(viewState.contacts, backFromContacts) fmt.Fprintln(chlw, infoFormat+"Contacts list updated !") return } fmt.Fprintln(chlw, infoFormat+"Failed to update contact list !") }() } func isDirectory(path string) (bool, error) { fileInfo, err := os.Stat(path) if err != nil { return false, err } return fileInfo.IsDir(), err }