diff --git a/.gitignore b/.gitignore index f4d432a..baa666d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ +slack-status +slack-status-cli diff --git a/README.md b/README.md index 8dbe5d0..b5f36ba 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,50 @@ # slack-status-cli -Set your Slack status via the command line \ No newline at end of file +Set your Slack status via the command line + +## Example usage + +Set auth token (it will store it in `~/.config/slack-status-cli` or your `$XDG_CONFIG_HOME` dir + + slack-status -auth-token + +Set status without emoji + + slack-status Walking the dog + +Set status with emoji + + slack-status :walking-the-dog: Walking the dog + slack-status --emoji :walking-the-dog: Walking the dog + +Set status with duration (eg. `10m`, `2h`, `7d12h`) + + slack-status 10m :walking-the-dog: Walking the dog + slack-status :walking-the-dog: Walking the dog for 10m + slack-status --duration 10m --emoji :walking-the-dog: Walking the dog + +Set status with duration and snooze notifications + + slack-status --snooze --duration 12h --emoji :bed: Good night + slack-status --snooze :bed: Good night for 12h + +Set a status that contains a duration + + # Set status to "On a break" for 5 minutes + slack-status :sleeping: On a break for 5m + # Set status to "On a break for 5m" for 5 minutes + slack-status --duration 5m :sleeping: On a break for 5m + # Set status to "On a break for 5m" with no duration + slack-status :sleeping: "On a break for 5m" + +Clear existing status and snooze durations + + slack-status + +Snooze notifications without updating your status + + slack-status --duration 15m --snooze + +## Future + +I plan to do a bit of work to bundle this for easier distribution and maybe support multiple workspaces. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..af5cb4a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/iamthefij/slack-status-cli + +go 1.15 + +require github.com/slack-go/slack v0.7.4 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c9b3a1a --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/slack-go/slack v0.7.4 h1:Z+7CmUDV+ym4lYLA4NNLFIpr3+nDgViHrx8xsuXgrYs= +github.com/slack-go/slack v0.7.4/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a9ccd7b --- /dev/null +++ b/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/slack-go/slack" +) + +// statusInfo contains all args passed from the command line +type statusInfo struct { + emoji, statusText string + duration time.Duration + snooze bool + accessToken string +} + +// getExipirationTime returns epoch time that status should expire from the duration. +func (si statusInfo) getExpirationTime() int64 { + if si.duration == 0 { + return 0 + } + + return time.Now().Add(si.duration).Unix() +} + +// getConfigFilePath returns the path of a given file within the config folder. +// The config folder will be created in ~/.local/config/slack-status-cli if it does not exist. +func getConfigFilePath(filename string) string { + configHome := os.Getenv("XDG_CONFIG_HOME") + if configHome == "" { + configHome = "~/.local/config" + } + + configDir := filepath.Join(configHome, "slack-status-cli") + _ = os.MkdirAll(configDir, 0755) + + return filepath.Join(configDir, filename) +} + +// readWriteAccessToken will store and retrieve access tokens for future use. +func readWriteAccessToken(accessToken string) (string, error) { + tokenFile := getConfigFilePath("token") + + if accessToken != "" { + err := ioutil.WriteFile(tokenFile, []byte(accessToken), 0600) + if err != nil { + fmt.Println("Error writing access token") + } + + return accessToken, err + } + + content, err := ioutil.ReadFile(tokenFile) + if err != nil { + fmt.Println("No token provided on command line or in file") + + return "", err + } + + return string(content), nil +} + +// createClient will return a Slack client with the provided access token. +// If that token is empty, it will try to get one from the config folder. +func createClient(accessToken string) (*slack.Client, error) { + var err error + + accessToken, err = readWriteAccessToken(accessToken) + if err != nil { + fmt.Println("error reading access token") + + return nil, err + } + + client := slack.New(accessToken) + + return client, nil +} + +// readDurationArgs will attempt to find a duration within command line args rather than flags. +// It will look for a prefixed duration. eg. "5m :cowboy: Howdy y'all" and a postfix duration +// following the word "for". eg. ":dancing: Dancing for 1h". +func readDurationArgs(args []string) ([]string, *time.Duration) { + // If there are no args, we have no duration + if len(args) == 0 { + return args, nil + } + + // Try to parse the first value + durationVal, err := time.ParseDuration(args[0]) + if err == nil { + // Found a duration, return the trimmed args and duration + return args[1:], &durationVal + } + + // If the args are less than two, then we don't have a "for " expression + minArgsForSuffix := 2 + if len(args) < minArgsForSuffix { + return args, nil + } + + // Check for a "for " expression at end of args + if strings.ToLower(args[len(args)-2]) == "for" { + durationVal, err = time.ParseDuration(args[len(args)-1]) + if err == nil { + // Found a duration, return the trimmed args and duration + return args[:len(args)-2], &durationVal + } + } + + // Default return input + return args, nil +} + +// readFlags will read all flags off the command line. +func readFlags() statusInfo { + snooze := flag.Bool("snooze", false, "snooze notifications") + duration := flag.Duration("duration", 0, "duration to set status for") + emoji := flag.String("emoji", "", "emoji to use as status") + accessToken := flag.String("access-token", "", "slack access token") + + flag.Parse() + + // Freeform input checks the first argument to see if it's a duration + args := flag.Args() + + // Duration was not set via a flag, check the args + if *duration == 0 { + var parsedDuration *time.Duration + args, parsedDuration = readDurationArgs(args) + + if parsedDuration != nil { + duration = parsedDuration + } + } + + if *emoji == "" && len(args) > 0 { + if args[0][0] == ':' && args[0][len(args[0])-1] == ':' { + emoji = &args[0] + args = args[1:] + } + } + + statusText := strings.Join(args, " ") + + return statusInfo{ + duration: *duration, + snooze: *snooze, + emoji: *emoji, + accessToken: *accessToken, + statusText: statusText, + } +} + +func main() { + args := readFlags() + + client, err := createClient(args.accessToken) + if err != nil { + fmt.Println("error getting client") + panic(err) + } + + err = client.SetUserCustomStatus(args.statusText, args.emoji, args.getExpirationTime()) + if err != nil { + fmt.Println("error setting status") + panic(err) + } + + if args.snooze { + _, err = client.SetSnooze(int(args.duration.Minutes())) + if err != nil { + fmt.Println("error setting snooze") + panic(err) + } + } else { + _, err = client.EndSnooze() + if err != nil && err.Error() != "snooze_not_active" { + fmt.Println("error ending snooze") + panic(err) + } + } +}