From b8328a0e3b1c3178ce1c1399c11195fa48f17d63 Mon Sep 17 00:00:00 2001 From: Martin Dosch Date: Fri, 10 Aug 2018 10:14:21 +0000 Subject: [PATCH] Sendxmpp compatibility --- README.md | 70 ++++++++----- go-sendxmpp.go | 273 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 275 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 004fdb4..e50e6cb 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A little tool to send messages to an XMPP contact or MUC inspired by (but not as If you have *[GOPATH](https://github.com/golang/go/wiki/SettingGOPATH)* set just run this commands: -```bash +```plain $ go get salsa.debian.org/mdosch-guest/go-sendxmpp $ go install salsa.debian.org/mdosch-guest/go-sendxmpp ``` @@ -23,40 +23,58 @@ You will find the binary in `$GOPATH/bin` or, if set, `$GOBIN`. ## usage -The account details for logging into your XMPP account and at least `contact` -or `muc` must be specified. +You can either pipe a programs output to `go-sendxmpp` or write in your terminal (put \^D in a new +line to finish). -If `-message` is not specified you can either pipe a programs output to -`go-sendxmpp` or write in your terminal (put ^D in a new line to finish). +The account data is expected at `~/.sendxmpprc` if no other configuration file location is specified with +`-f` or `--file`. The configuration file is expected to be in the following format: -If `-port` is not set, the standard port *5222* is used. +```plain +username: +jserver: +port: +password: +``` -```bash -Usage of ./go-sendxmpp: - -contact string - Recipient of the message. (default "alice@example.com") - -message string - The message you want to send. (default "Hello World!") - -muc string - MUC to send the message to. (default "offtopic@conference.example.com") - -muc-nick string - The nickname the bot uses in the MUC. (default "go-sendxmpp") - -pass string - Password for XMPP account. (default "ChangeThis!") - -port string - XMPP server port. (default "5222") - -server string - XMPP server address. (default "example.com") - -user string - Username for XMPP account. (default "bob@example.com") +If no configuration file is present or if the values should be overridden it is possible to define the +account details via command line options: + +```plain +./go-sendxmpp --help +Usage: go-sendxmpp [-cdtx] [-f value] [--help] [-j value] [-p value] [-r value] [-u value] [parameters ...] + -c, --chatroom Send message to a chatroom. + -d, --debug Show debugging info. + -f, --file=value Set configuration file. (Default: ~/.sendxmppr) + --help Show help. + -j, --jserver=value + XMPP server address. + -p, --password=value + Password for XMPP account. + -r, --resource=value + Set resource. When sending to a chatroom this is used as + 'alias'. (Default: go-sendxmpp) + -t, --tls Use TLS. + -u, --username=value + Username for XMPP account. + -x, --start-tls Use StartTLS. ``` ### examples +Send a message to two recipients using a configuration file. + ```bash -inxi -F | ./go-sendxmpp -pass 'ChangeThis!' -port '5222' -server 'example.com' -user 'bob@example.com' -muc 'test@conference.example.com' +cat message.txt | ./go-sendxmpp -f ./sendxmpp recipient1@example.com recipient2@example.com ``` +Send a message to two recipients directly defining account credentials. + ```bash -./go-sendxmpp -pass 'ChangeThis!' -port '5222' -server 'example.com' -user 'bob@example.com' -muc 'test@conference.example.com' -message 'Hello World!' +cat message.txt | ./go-sendxmpp -u bob@example.com -j example.com -p swordfish recipient1@example.com recipient2@example.com ``` + +Send a message to two groupchats (`-c`) using a configuration file. + +```bash +cat message.txt | ./go-sendxmpp -cf ./sendxmpp chat1@conference.example.com chat2@conference.example.com +``` \ No newline at end of file diff --git a/go-sendxmpp.go b/go-sendxmpp.go index 5235c16..d792b43 100644 --- a/go-sendxmpp.go +++ b/go-sendxmpp.go @@ -1,48 +1,222 @@ /* Copyright 2018 Martin Dosch + Licensed under the "MIT License" */ package main import ( "bufio" - "flag" + "errors" "io" "log" "os" + "os/user" + "strconv" + "strings" "time" "github.com/mattn/go-xmpp" + "github.com/pborman/getopt/v2" + "mellium.im/xmpp/jid" ) +type configuration struct { + username string + jserver string + port string + password string +} + +// Check that JIDs include localpart and serverpart +// and return it marshalled. +func marshalJID(input string) (string, error) { + parsedJid, err := jid.Parse(input) + if err != nil { + return input, err + } + + if parsedJid.Localpart() == "" || parsedJid.Domainpart() == "" { + return input, errors.New("Invalid JID: " + input) + } + + return parsedJid.String(), err +} + +// Opens the config file and returns the specified values +// for username, server and port. +func parseConfig(configPath string) (configuration, error) { + + var ( + output configuration + err error + ) + + // Use ~/.sendxmpprc if no config path is specified. + if configPath == "" { + // Get systems user config path. + osConfigDir := os.Getenv("$XDG_CONFIG_HOME") + if osConfigDir != "" { + configPath = osConfigDir + "/.sendxmpprc" + } else { + // Get the current user. + curUser, err := user.Current() + if err != nil { + return output, err + } + // Get home directory. + home := curUser.HomeDir + if home == "" { + return output, errors.New("No home directory found.") + } + configPath = home + "/.sendxmpprc" + } + } + + // Check that config file is existing. + info, err := os.Stat(configPath) + if os.IsNotExist(err) { + return output, err + } + + // Check for file permissions. Must be 600. + perm := info.Mode().Perm() + if strconv.FormatInt(int64(perm), 8) != "600" { + return output, errors.New("Wrong permissions for " + configPath + ": " + + strconv.FormatInt(int64(perm), 8) + " instead of 600.") + } + + // Open config file. + file, err := os.Open(configPath) + if err != nil { + return output, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + + // Read config file per line. + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "#") == true { + continue + } + + row := strings.Split(scanner.Text(), " ") + + switch row[0] { + case "username:": + output.username = row[1] + case "jserver:": + output.jserver = row[1] + case "password:": + output.password = row[1] + case "port:": + output.port = row[1] + default: + if len(row) >= 2 { + if strings.Contains(scanner.Text(), ";") == true { + output.username = strings.Split(row[0], ";")[0] + output.jserver = strings.Split(row[0], ";")[1] + output.password = row[1] + } else { + output.username = strings.Split(row[0], ":")[0] + output.jserver = strings.Split(row[0], "@")[1] + output.password = row[1] + } + } + } + } + + return output, err +} + func main() { var ( - err error - server = flag.String("server", "example.com", "XMPP server address.") - port = flag.String("port", "5222", "XMPP server port.") - user = flag.String("user", "bob@example.com", "Username for XMPP account.") - password = flag.String("pass", "ChangeThis!", "Password for XMPP account.") - contact = flag.String("contact", "alice@example.com", "Recipient of the message.") - muc = flag.String("muc", "offtopic@conference.example.com", "MUC to send the message to.") - mucNick = flag.String("muc-nick", "go-sendxmpp", "The nickname the bot uses in the MUC.") - messagePtr = flag.String("message", "Hello World!", "The message you want to send.") + err error + message string ) - flag.Parse() + // Define command line flags. + flagHelp := getopt.BoolLong("help", 0, "Show help.") + flagDebug := getopt.BoolLong("debug", 'd', "Show debugging info.") + flagServer := getopt.StringLong("jserver", 'j', "", "XMPP server address.") + flagUser := getopt.StringLong("username", 'u', "", "Username for XMPP account.") + flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.") + flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.") + // flagMessage := getopt.StringLong("message", 'm', "", "The message you want to send.") + flagTLS := getopt.BoolLong("tls", 't', "Use TLS.") + flagStartTLS := getopt.BoolLong("start-tls", 'x', "Use StartTLS.") + flagResource := getopt.StringLong("resource", 'r', "go-sendxmpp", "Set resource. "+ + "When sending to a chatroom this is used as 'alias'. (Default: go-sendxmpp)") + flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: ~/.sendxmppr)") + + // Parse command line flags. + getopt.Parse() + + // If requested, show help and quit. + if *flagHelp { + getopt.Usage() + os.Exit(0) + } + + // Read recipients from command line and quit if none are specified. + recipients := getopt.Args() + if len(recipients) == 0 { + log.Fatal("No recipient specified.") + } - message := *messagePtr + // Quit if unreasonable TLS setting is set. + if *flagStartTLS == true && *flagTLS == true { + log.Fatal("Use either TLS or StartTLS.") + } + + // Check that all recipient JIDs are valid. + for i, recipient := range recipients { + validatedJid, err := marshalJID(recipient) + if err != nil { + log.Fatal(err) + } + recipients[i] = validatedJid + } - if *contact == "alice@example.com" && *muc == "offtopic@conference.example.com" { - log.Fatal("No target specified.") + // Read configuration from file. + config, err := parseConfig(*flagFile) + if err != nil { + log.Println("Error parsing ", *flagFile, ": ", err) } + // Set connection options according to config. + user := config.username + server := config.jserver + password := config.password + if config.port != "" { + server = server + ":" + config.port + } + + // Overwrite user if specified via command line flag. + if *flagUser != "" { + user = *flagUser + } + + // Overwrite server if specified via command line flag. + if *flagServer != "" { + server = *flagServer + } + + // Overwrite password if specified via command line flag. + if *flagPassword != "" { + password = *flagPassword + } + + // Set XMPP connection options. options := xmpp.Options{ - Host: *server + ":" + *port, - User: *user, - Password: *password, - NoTLS: true, - StartTLS: true, - Debug: false, + Host: server, + User: user, + Resource: *flagResource, + Password: password, + NoTLS: !*flagTLS, + StartTLS: *flagStartTLS, + Debug: *flagDebug, } // Connect to server. @@ -51,15 +225,15 @@ func main() { log.Fatal(err) } - if message == "Hello World!" { + if message == "" { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { - if message == "Hello World!" { - message = string(scanner.Text()) + if message == "" { + message = scanner.Text() } else { - message = message + "\n" + string(scanner.Text()) + message = message + "\n" + scanner.Text() } } @@ -70,28 +244,43 @@ func main() { } } - if *muc != "offtopic@conference.example.com" { - // Join the MUC - mucStatus, err := client.JoinMUCNoHistory(*muc, *mucNick) - if err != nil { - log.Fatal(err) - } + // Send message to chatroom(s) if the flag is set. + if *flagChatroom { - // Exit if Status is > 300, see https://xmpp.org/registrar/mucstatus.html - if mucStatus > 300 { - log.Fatal("Couldn't join MUC. Status:", mucStatus) - } + for _, recipient := range recipients { - _, err = client.Send(xmpp.Chat{Remote: *muc, Type: "groupchat", Text: message}) - if err != nil { - log.Fatal(err) + // Join the MUC. + mucStatus, err := client.JoinMUCNoHistory(recipient, *flagResource) + if err != nil { + log.Fatal(err) + } + + // Exit if Status is > 300, see https://xmpp.org/registrar/mucstatus.html + if mucStatus > 300 { + log.Fatal("Couldn't join MUC. Status:", mucStatus) + } + + // Send the message. + _, err = client.Send(xmpp.Chat{Remote: recipient, Type: "groupchat", Text: message}) + if err != nil { + log.Fatal(err) + } + + // After sending the message, leave the Muc + _, err = client.LeaveMUC(recipient) + if err != nil { + log.Println(err) + } } - } + } else { - if *contact != "alice@example.com" { - _, err = client.Send(xmpp.Chat{Remote: *contact, Type: "chat", Text: message}) - if err != nil { - log.Fatal(err) + for _, recipient := range recipients { + + // If the chatroom flag is not set, send message to contact(s). + _, err = client.Send(xmpp.Chat{Remote: recipient, Type: "chat", Text: message}) + if err != nil { + log.Fatal(err) + } } } time.Sleep(1 * time.Second)