Compare commits

...

22 Commits

Author SHA1 Message Date
IamTheFij afffddc07b Comply with updated linting
continuous-integration/drone/push Build is passing Details
2023-09-19 13:55:08 -07:00
IamTheFij b74edaff2f Update go version and slack dependency
continuous-integration/drone/push Build is failing Details
2023-09-19 13:18:44 -07:00
IamTheFij fb0fe18eb7 Don't delete certs when cleaning 2023-09-19 13:18:27 -07:00
IamTheFij 83279f4fbf Fix git links in pre-commit-config 2023-09-19 13:18:14 -07:00
IamTheFij a388a3f78b Add linux-arm64 builds
continuous-integration/drone/push Build is passing Details
2022-01-27 21:17:32 -08:00
IamTheFij 5d2f4b1bba Update readme to describe removal of openssl dependency
continuous-integration/drone/push Build is passing Details
2021-02-17 10:31:06 -08:00
IamTheFij 776a1129f9 Fix typo
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-02-17 10:24:34 -08:00
IamTheFij a47d73b669 Ensure certs exist before running linting
continuous-integration/drone/push Build is failing Details
2021-02-17 10:22:22 -08:00
IamTheFij 0b0b7fe395 Add embedded certificates
continuous-integration/drone/push Build is failing Details
Removes runtime requirement on openssl

Fixes #1
2021-02-17 09:43:53 -08:00
IamTheFij c303bc055d Add darwin-arm64 builds
Fixes #2
2021-02-16 13:08:32 -08:00
IamTheFij 461fa0f4e7 Update linting 2021-02-09 10:20:38 -08:00
IamTheFij 03399ac35d Move config to directory returned by os.UserConfigDir()
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
This version will migrate configuration from the old path, but migration
will be removed at some point in the future.
2021-02-05 11:43:23 -08:00
IamTheFij 1a1689cd88 Add format string for error wrap
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2021-02-05 10:34:49 -08:00
IamTheFij 62037b9dab Merge remote-tracking branch 'levy/master' into master
continuous-integration/drone/push Build is failing Details
2021-02-05 10:30:47 -08:00
Alex Levy aba5a5b6eb Use home dir if XDG_CONFIG_HOME unset 2021-02-05 10:22:39 -08:00
IamTheFij 9a03c1da00 Update install instructions 2021-02-05 10:08:39 -08:00
IamTheFij bae836fc91 Add install instructions 2021-02-05 10:08:39 -08:00
IamTheFij 58f246f1b3 Ignore .env file
continuous-integration/drone/push Build is passing Details
This will give me somewhere to put the oauth creds while I'm
developing
2021-02-03 13:15:27 -08:00
IamTheFij 315c486736 Update cert instructions 2021-02-03 13:01:53 -08:00
IamTheFij d19df7e3f9 Add drone builds
continuous-integration/drone/tag Build is passing Details
2021-02-03 13:01:53 -08:00
IamTheFij ee8ec2c8da Add pre-commit hooks 2021-02-03 13:01:53 -08:00
IamTheFij b5782a3d30 Add ability to support multiple workspaces
Adds a new perm to allow fetching the workspace domain
2021-02-03 13:01:53 -08:00
12 changed files with 537 additions and 165 deletions

85
.drone.yml Normal file
View File

@ -0,0 +1,85 @@
---
kind: pipeline
name: test
steps:
- name: check
image: iamthefij/drone-pre-commit:personal
commands:
- make certs
- pre-commit run --all-files
---
kind: pipeline
name: build and publish
depends_on:
- test
trigger:
event:
- push
- tag
refs:
- refs/heads/master
- refs/tags/v*
steps:
- name: build all binaries
image: golang:1.16
environment:
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
CLIENT_ID:
from_secret: slack_client_id
CLIENT_SECRET:
from_secret: slack_client_secret
commands:
- make all
- name: compress binaries for release
image: ubuntu
commands:
- find ./dist -type f -executable -execdir tar -czvf {}.tar.gz {} \;
when:
event: tag
- name: upload gitea release
image: plugins/gitea-release
settings:
title: ${DRONE_TAG}
files: dist/*.tar.gz
checksum:
- md5
- sha1
- sha256
- sha512
base_url:
from_secret: gitea_base_url
api_key:
from_secret: gitea_token
when:
event: tag
---
kind: pipeline
name: notify
depends_on:
- test
trigger:
status:
- failure
steps:
- name: notify
image: drillster/drone-email
settings:
host:
from_secret: SMTP_HOST # pragma: whitelist secret
username:
from_secret: SMTP_USER # pragma: whitelist secret
password:
from_secret: SMTP_PASS # pragma: whitelist secret
from: drone@iamthefij.com

2
.gitignore vendored
View File

@ -18,3 +18,5 @@
slack-status
slack-status-cli
dist/
.env
certs/

29
.golangci.yml Normal file
View File

@ -0,0 +1,29 @@
---
linters:
enable:
- asciicheck
- bodyclose
- dogsled
- dupl
- exhaustive
- gochecknoinits
- gocognit
- gocritic
- gocyclo
- goerr113
- gofumpt
- goimports
- gomnd
- goprintffuncname
- gosec
- interfacer
- maligned
- misspell
- nakedret
- nestif
- nlreturn
- noctx
- unparam
- wsl
disable:
- gochecknoglobals

16
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,16 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
hooks:
- id: check-added-large-files
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.3.5
hooks:
- id: go-fmt
- id: go-imports
# - id: gometalinter
- id: golangci-lint

View File

@ -1,7 +1,8 @@
OUTPUT = slack-status
GOFILES = *.go go.mod go.sum
DIST_ARCH = darwin-amd64 linux-amd64
DIST_ARCH = darwin-amd64 darwin-arm64 linux-amd64 linux-arm64
DIST_TARGETS = $(addprefix dist/$(OUTPUT)-,$(DIST_ARCH))
VERSION ?= $(shell git describe --tags --dirty)
.PHONY: default
default: slack-status
@ -15,22 +16,41 @@ all: dist
.PHONY: test
test:
pre-commit run --all-files
go test
slack-status: $(GOFILES)
slack-status: $(GOFILES) certs
go build -o $(OUTPUT)
.PHONY: dist
dist: $(DIST_TARGETS)
$(DIST_TARGETS): $(GOFILES)
$(DIST_TARGETS): $(GOFILES) certs
@mkdir -p ./dist
GOOS=$(word 3, $(subst -, ,$(@))) GOARCH=$(word 4, $(subst -, ,$(@))) \
go build \
-ldflags '-X "main.defaultClientID=$(CLIENT_ID)" -X "main.defaultClientSecret=$(CLIENT_SECRET)"' \
-ldflags '-X "main.version=${VERSION}" -X "main.defaultClientID=$(CLIENT_ID)" -X "main.defaultClientSecret=$(CLIENT_SECRET)"' \
-o $@
.PHONY: certs
certs: certs/key.pem
certs/cert.pem: certs/key.pem
certs/key.pem:
mkdir -p certs/
openssl req -x509 -subj "/C=US/O=Slack Status CLI/CN=localhost:8888" \
-nodes -days 365 -newkey "rsa:2048" \
-addext "subjectAltName=DNS:localhost:8888" \
-keyout certs/key.pem -out certs/cert.pem
.PHONY: clean
clean:
rm ./slack-status
rm -f ./slack-status
rm -fr ./dist
.PHONY: clean-certs
clean-certs:
rm -fr ./certs
.PHONY: install-hooks
install-hooks:
pre-commit install --overwrite --install-hooks

View File

@ -4,11 +4,11 @@ Set your Slack status via the command line
## Requirements
Rather than host a web server that you would need to trust, this command runs one on your local machine to retrieve the OAuth code. This page is hosted with an auto generated, self-signed certificate.
Rather than host a web server that you would need to trust, this command runs one on your local machine to retrieve the OAuth code. This page is hosted with an embedded, self-signed certificate.
This requires you to have `openssl` installed on your machine and, when the page loads for the first time, it will require you to trust the certificate or ignore the warning.
When the page loads for the first time, it will require you to trust the certificate or ignore the warning.
Here's how to do that on [Firefox](https://support.mozilla.org/en-US/kb/error-codes-secure-websites?as=u&utm_source=inproduct#w_self-signed-certificate). On Chrome, you may have to enable `chrome://flags/#allow-insecure-localhost`.
Here's how to do that on [Firefox](https://support.mozilla.org/en-US/kb/error-codes-secure-websites?as=u&utm_source=inproduct#w_self-signed-certificate). On Chrome, you may have to enable `chrome://flags/#allow-insecure-localhost`. If you can't get the link to open in Chrome, you can copy the URL into a different browser and try it there. It should work in Firefox or Safari.
## Setup
@ -18,9 +18,20 @@ Here's how to do that on [Firefox](https://support.mozilla.org/en-US/kb/error-co
## Example usage
Set auth token (it will store it in `~/.config/slack-status-cli` or your `$XDG_CONFIG_HOME` dir
Login (it will store it in `~/.config/slack-status-cli` or your `$XDG_CONFIG_HOME` dir
slack-status -auth-token <your auth token>
slack-status -login
slack-status -login -domain custom-workspace-name
slack-status -make-default -domain new-default-workspace
Specify domain to use
# Use the deafult domain
slack-status :face_with_cowboy_hat: Howdy partner
# Specify domain for this status
slack-status -domain my-workspace :face_with_cowboy_hat: Howdy partner
# Specify domain for this status and make it the new default
slack-status -domain my-workspace -make-default :face_with_cowboy_hat: Howdy partner
Set status without emoji
@ -29,25 +40,25 @@ Set status without emoji
Set status with emoji
slack-status :walking-the-dog: Walking the dog
slack-status --emoji :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
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
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
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"
@ -57,8 +68,8 @@ Clear existing status and snooze durations
Snooze notifications without updating your status
slack-status --duration 15m --snooze
slack-status -duration 15m -snooze
## Future
## Development
I plan to do a bit of work to bundle this for easier distribution and maybe support multiple workspaces.
Building requires you to have `openssl` installed on your machine and to export `CLIENT_ID` and `CLIENT_SECRET` environment variables. The certificate and client info can be overridden at runtime for development purposes by running with exported variables post compilation as well as placing the your openssl pem files in the application's config directory. See `auth.go` for more information.

102
auth.go
View File

@ -2,12 +2,12 @@ package main
import (
"context"
"crypto/tls"
_ "embed"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strings"
"time"
@ -15,19 +15,21 @@ import (
)
var (
// These are set via build flags but can be overriden via environment variables.
// These are set via build flags but can be overridden via environment variables.
defaultClientID = ""
defaultClientSecret = ""
//go:embed "certs/cert.pem"
certPem []byte
//go:embed "certs/key.pem"
keyPem []byte
)
func getEnvOrDefault(name, defaultValue string) string {
val, ok := os.LookupEnv(name)
if ok {
return val
}
return defaultValue
}
const (
httpReadTimeout = 5 * time.Second
httpWriteTimeout = 10 * time.Second
httpIdleTimeout = 120 * time.Second
)
type slackApp struct {
clientID, clientSecret, redirectURI string
@ -52,12 +54,42 @@ func (app slackApp) listenForCode() (string, error) {
// start an http listener and listen for the redirect and return the code from params
var code string
certPath, err := getConfigFilePath("cert.pem")
if err != nil {
return "", fmt.Errorf("failed checking config path for cert: %w", err)
}
keyPath, err := getConfigFilePath("key.pem")
if err != nil {
return "", fmt.Errorf("failed checking config path for key: %w", err)
}
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
}
// If config files don't exist, use embedded
if !fileExists(certPath) && !fileExists(keyPath) {
cert, err := tls.X509KeyPair(certPem, keyPem)
if err != nil {
return "", fmt.Errorf("failed loading embedded key pair: %w", err)
}
tlsCfg.Certificates = make([]tls.Certificate, 1)
tlsCfg.Certificates[0] = cert
// Empty out paths since they don't exist so embedded certs will be used
certPath = ""
keyPath = ""
}
// Also, should generate TLS certificate to use since https is a required scheme
server := http.Server{
Addr: app.listenHost,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ReadTimeout: httpReadTimeout,
WriteTimeout: httpWriteTimeout,
IdleTimeout: httpIdleTimeout,
TLSConfig: tlsCfg,
}
http.HandleFunc(app.listenPath, func(w http.ResponseWriter, r *http.Request) {
@ -78,15 +110,6 @@ func (app slackApp) listenForCode() (string, error) {
}()
})
certPath := getConfigFilePath("cert.pem")
keyPath := getConfigFilePath("key.pem")
if !fileExists(certPath) || !fileExists(keyPath) {
if err := generateSelfSignedCertificates(certPath, keyPath); err != nil {
return "", err
}
}
if err := server.ListenAndServeTLS(certPath, keyPath); err != nil && !errors.Is(err, http.ErrServerClosed) {
return "", err
}
@ -94,39 +117,10 @@ func (app slackApp) listenForCode() (string, error) {
return code, nil
}
func fileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
func generateSelfSignedCertificates(certPath, keyPath string) error {
command := exec.Command(
"openssl",
"req",
"-x509",
"-subj",
"/C=US/O=Slack Status CLI/CN=localhost:8888",
"-nodes",
"-days",
"365",
"-newkey",
"rsa:2048",
"-keyout",
keyPath,
"-out",
certPath,
)
return command.Run()
}
func authenticate() (string, error) {
app := slackApp{
userScopes: []string{"dnd:write", "users.profile:write"},
scopes: []string{"dnd:write", "users.profile:write"},
userScopes: []string{"dnd:write", "users.profile:write", "team:read"},
scopes: []string{"dnd:write", "users.profile:write", "team:read"},
clientID: getEnvOrDefault("CLIENT_ID", defaultClientID),
clientSecret: getEnvOrDefault("CLIENT_SECRET", defaultClientSecret),
redirectURI: "https://localhost:8888/auth",

173
config.go Normal file
View File

@ -0,0 +1,173 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
)
const (
PERM_OWNER_RW = 0o600
PERM_RW_ALL = 0o755
)
var errUnknownDomain = errors.New("unknown domain")
type configData struct {
DefaultDomain string
DomainTokens map[string]string
}
// getConfigFilePath returns the path of a given file within the UserConfigDir.
func getConfigFilePath(filename string) (string, error) {
configApplicationName := "slack-status-cli"
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("error getting current config: %w", err)
}
configDir = filepath.Join(configDir, configApplicationName)
_ = os.MkdirAll(configDir, PERM_RW_ALL)
configFile := filepath.Join(configDir, filename)
// Handle migration of old config file path
// NOTE: Will be removed in future versions
if !fileExists(configFile) {
// Get old config path to see if we should migrate
userHomeDir, _ := os.UserHomeDir()
legacyConfigFile := filepath.Join(
userHomeDir,
".config",
configApplicationName,
filename,
)
if fileExists(legacyConfigFile) {
log.Printf("Migrating config from %s to %s\n", legacyConfigFile, configFile)
err = os.Rename(legacyConfigFile, configFile)
if err != nil {
err = fmt.Errorf(
"error migrating old config from %s: %w",
legacyConfigFile,
err,
)
}
}
}
return configFile, err
}
// readConfig returns the current configuration
func readConfig() (*configData, error) {
configPath, err := getConfigFilePath("config.json")
if err != nil {
return nil, err
}
if !fileExists(configPath) {
return &configData{DomainTokens: map[string]string{}}, nil
}
content, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("error reading config from file: %w", err)
}
var config configData
err = json.Unmarshal(content, &config)
if err != nil {
return nil, fmt.Errorf("failed parsing json from config file: %w", err)
}
return &config, nil
}
// writeConfig writes the provided config data
func writeConfig(config configData) error {
configPath, err := getConfigFilePath("config.json")
if err != nil {
return err
}
contents, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed converting config to json: %w", err)
}
if err = os.WriteFile(configPath, contents, PERM_OWNER_RW); err != nil {
return fmt.Errorf("error writing config to file: %w", err)
}
return nil
}
// getDefaultLogin returns the default login from the configuration
func getDefaultLogin() (string, error) {
config, err := readConfig()
if err != nil {
return "", err
}
accessToken, exists := config.DomainTokens[config.DefaultDomain]
if !exists {
return "", errUnknownDomain
}
return accessToken, nil
}
// getLogin returns the token for a specified login domain
func getLogin(domain string) (string, error) {
config, err := readConfig()
if err != nil {
return "", err
}
accessToken, exists := config.DomainTokens[domain]
if !exists {
return "", errUnknownDomain
}
return accessToken, nil
}
// saveLogin writes the provided token to the provided domain
func saveLogin(domain, accessToken string) error {
config, err := readConfig()
if err != nil {
return err
}
config.DomainTokens[domain] = accessToken
// If this is the only domain, make it default
if len(config.DomainTokens) == 1 {
config.DefaultDomain = domain
}
return writeConfig(*config)
}
// saveDefaultLogin saves the specified domain as the default
func saveDefaultLogin(domain string) error {
config, err := readConfig()
if err != nil {
return err
}
_, exists := config.DomainTokens[domain]
if !exists {
return fmt.Errorf("cannot set domain to default: %w", errUnknownDomain)
}
config.DefaultDomain = domain
return writeConfig(*config)
}

6
go.mod
View File

@ -1,5 +1,7 @@
module github.com/iamthefij/slack-status-cli
go 1.15
go 1.21
require github.com/slack-go/slack v0.7.4
require github.com/slack-go/slack v0.12.3
require github.com/gorilla/websocket v1.5.0 // indirect

15
go.sum
View File

@ -1,14 +1,11 @@
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/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88=
github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

187
main.go
View File

@ -3,22 +3,26 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/slack-go/slack"
)
var version = "dev"
// statusInfo contains all args passed from the command line
type statusInfo struct {
emoji, statusText string
duration time.Duration
snooze bool
accessToken string
}
// commandOptions contains non-status options passed to the command
type commandOptions struct {
login, makeDefault, showVersion bool
domain string
}
// getExipirationTime returns epoch time that status should expire from the duration.
@ -30,43 +34,6 @@ func (si statusInfo) getExpirationTime() int64 {
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)
}
// writeAccessToken writes the access token to a file for future use.
func writeAccessToken(accessToken string) error {
tokenFile := getConfigFilePath("token")
if err := ioutil.WriteFile(tokenFile, []byte(accessToken), 0600); err != nil {
return fmt.Errorf("error writing access token %w", err)
}
return nil
}
// readAccessToken retreive access token from a file
func readAccessToken() (string, error) {
tokenFile := getConfigFilePath("token")
content, err := ioutil.ReadFile(tokenFile)
if err != nil {
return "", fmt.Errorf("error reading access token from file %w", err)
}
return string(content), 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".
@ -103,11 +70,17 @@ func readDurationArgs(args []string) ([]string, *time.Duration) {
}
// readFlags will read all flags off the command line.
func readFlags() statusInfo {
func readFlags() (statusInfo, commandOptions) {
// Non-status flags
login := flag.Bool("login", false, "login to a Slack workspace")
domain := flag.String("domain", "", "domain to set status on")
makeDefault := flag.Bool("make-default", false, "set the current domain to default")
showVersion := flag.Bool("version", false, "show version and exit")
// Status flags
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()
@ -134,56 +107,106 @@ func readFlags() statusInfo {
statusText := strings.Join(args, " ")
return statusInfo{
duration: *duration,
snooze: *snooze,
emoji: *emoji,
accessToken: *accessToken,
statusText: statusText,
}
duration: *duration,
snooze: *snooze,
emoji: *emoji,
statusText: statusText,
}, commandOptions{
login: *login,
domain: *domain,
makeDefault: *makeDefault,
showVersion: *showVersion,
}
}
func getAccessToken(accessToken string) (string, error) {
// If provided, save and return
if accessToken != "" {
return accessToken, writeAccessToken(accessToken)
}
// Try to get from stored file
accessToken, err := readAccessToken()
if accessToken != "" && err == nil {
// Successfully read from file
return accessToken, nil
}
// Begin auth process to fetch a new token
accessToken, err = authenticate()
if err == nil {
// Successful authentication, save the token
err = writeAccessToken(accessToken)
}
return accessToken, err
}
func main() {
args := readFlags()
accessToken, err := getAccessToken(args.accessToken)
// loginAndSave will return a client after a new login flow and save the results
func loginAndSave(domain string) (*slack.Client, error) {
accessToken, err := authenticate()
if err != nil {
fmt.Println("error getting access token")
log.Fatal(err)
return nil, fmt.Errorf("failed to authenticate new login: %w", err)
}
client := slack.New(accessToken)
err = client.SetUserCustomStatus(args.statusText, args.emoji, args.getExpirationTime())
if domain == "" {
info, err := client.GetTeamInfo()
if err != nil {
return client, fmt.Errorf("failed to get team info: %w", err)
}
domain = info.Domain
}
err = saveLogin(domain, accessToken)
if err != nil {
return client, fmt.Errorf("failed saving new login info: %w", err)
}
return client, err
}
// getClient returns a client either via the provided login or default login
func getClient(domain string) (*slack.Client, error) {
var accessToken string
var err error
if domain == "" {
accessToken, err = getDefaultLogin()
if err != nil {
return nil, fmt.Errorf("failed to get default login: %w", err)
}
} else {
accessToken, err = getLogin(domain)
if err != nil {
return nil, fmt.Errorf("failed to get login for domain %s: %w", domain, err)
}
}
return slack.New(accessToken), nil
}
func main() {
status, options := readFlags()
if options.showVersion {
fmt.Println("version:", version)
return
}
var client *slack.Client
var err error
// If the new-auth flag is present, force an auth flow
if options.login {
client, err = loginAndSave(options.domain)
} else {
client, err = getClient(options.domain)
}
// We encountered some error in logging in
if err != nil {
fmt.Println("Unable to create Slack client. Have you logged in yet? Try using `-login`")
log.Fatal(fmt.Errorf("failed to get or save client: %w", err))
}
// If a domain is provided and asked to make default, save it to config
if options.makeDefault && options.domain != "" {
if err = saveDefaultLogin(options.domain); err != nil {
log.Fatal(fmt.Errorf("failed saving default domain %s: %w", options.domain, err))
}
}
err = client.SetUserCustomStatus(status.statusText, status.emoji, status.getExpirationTime())
if err != nil {
fmt.Println("error setting status")
panic(err)
}
if args.snooze {
_, err = client.SetSnooze(int(args.duration.Minutes()))
if status.snooze {
_, err = client.SetSnooze(int(status.duration.Minutes()))
if err != nil {
fmt.Println("error setting snooze")
panic(err)

20
util.go Normal file
View File

@ -0,0 +1,20 @@
package main
import "os"
func getEnvOrDefault(name, defaultValue string) string {
val, ok := os.LookupEnv(name)
if ok {
return val
}
return defaultValue
}
func fileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}