Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
afffddc07b | |||
b74edaff2f | |||
fb0fe18eb7 | |||
83279f4fbf | |||
a388a3f78b | |||
5d2f4b1bba | |||
776a1129f9 | |||
a47d73b669 | |||
0b0b7fe395 | |||
c303bc055d | |||
461fa0f4e7 | |||
03399ac35d | |||
1a1689cd88 | |||
62037b9dab | |||
|
aba5a5b6eb | ||
9a03c1da00 | |||
bae836fc91 | |||
58f246f1b3 | |||
315c486736 | |||
d19df7e3f9 | |||
ee8ec2c8da | |||
b5782a3d30 |
85
.drone.yml
Normal file
85
.drone.yml
Normal 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
2
.gitignore
vendored
@ -18,3 +18,5 @@
|
||||
slack-status
|
||||
slack-status-cli
|
||||
dist/
|
||||
.env
|
||||
certs/
|
||||
|
29
.golangci.yml
Normal file
29
.golangci.yml
Normal 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
16
.pre-commit-config.yaml
Normal 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
|
30
Makefile
30
Makefile
@ -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
|
||||
|
37
README.md
37
README.md
@ -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
102
auth.go
@ -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
173
config.go
Normal 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
6
go.mod
@ -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
15
go.sum
@ -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
187
main.go
@ -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
20
util.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user