diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 0000000..3af30e5 --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:3.7 + +ARG APP + +COPY "${APP}" /usr/local/bin/slack-duty-bot +COPY .docker/docker-entrypoint /usr/local/bin/ + +RUN apk --no-cache add ca-certificates + +ENTRYPOINT ["docker-entrypoint"] + +CMD ["slack-duty-bot"] diff --git a/.docker/docker-entrypoint b/.docker/docker-entrypoint new file mode 100755 index 0000000..c77389c --- /dev/null +++ b/.docker/docker-entrypoint @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e + +if [ "${1#-}" != "$1" ]; then + set -- slack-duty-bot "$@" +fi + +exec "$@" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5ff03c1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +*.md +Gopkg* +Makefile +LICENSE +/vendor +/idea +config.yaml diff --git a/.gitignore b/.gitignore index 10c2cb0..61898ec 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,9 @@ .glide/ /.idea !/bin/.gitkeep -/bin/* \ No newline at end of file +/bin/* + +/vendor +slack-duty-bot +config.yaml +.kubernetes/deploy.yaml diff --git a/.kubernetes/deploy.yaml.tpl b/.kubernetes/deploy.yaml.tpl new file mode 100644 index 0000000..f35459f --- /dev/null +++ b/.kubernetes/deploy.yaml.tpl @@ -0,0 +1,69 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: slack-duty-bot-${SDB_NAME}-secret +type: Opaque +data: + token: ${SDB_SLACK_TOKEN_BASE64} +--- + +kind: ConfigMap +apiVersion: v1 +metadata: + name: slack-duty-bot-${SDB_NAME}-config-map +data: + config.yaml: |- + slack: + keyword: + - ${SDB_KEYWORD} + group: + id: ${SDB_SLACK_GROUP_ID} + name: ${SDB_SLACK_GROUP_NAME} + duties: + - [${SDB_SLACK_DEFAULT_USER}] + - [${SDB_SLACK_DEFAULT_USER}] + - [${SDB_SLACK_DEFAULT_USER}] + - [${SDB_SLACK_DEFAULT_USER}] + - [${SDB_SLACK_DEFAULT_USER}] + - [${SDB_SLACK_DEFAULT_USER}] + - [${SDB_SLACK_DEFAULT_USER}] +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: slack-duty-bot-${SDB_NAME} + labels: + app: slack-duty-bot-${SDB_NAME} +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 50% + maxSurge: 1 + template: + metadata: + labels: + app: slack-duty-bot-${SDB_NAME} + spec: + containers: + - name: slack-duty-bot-${SDB_NAME} + image: insidieux/slack-duty-bot:${SDB_TAG} + imagePullPolicy: Always + args: ["--config.path=/etc/slack-duty-bot"] + env: + - name: SDB_SLACK_TOKEN + valueFrom: + secretKeyRef: + name: slack-duty-bot-${SDB_NAME}-secret + key: token + volumeMounts: + - name: slack-duty-bot-${SDB_NAME}-config-volume + mountPath: /etc/slack-duty-bot + volumes: + - name: slack-duty-bot-${SDB_NAME}-config-volume + configMap: + name: slack-duty-bot-${SDB_NAME}-config-map + restartPolicy: Always + terminationGracePeriodSeconds: 30 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..265c2e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +CHANGELOG +========= + +1.0.0 +----- + * Using Slack RTM + * Configuration with config file, environment variables and flags + * Live reload config file + * Docker image + * Simple Kubernetes deploy + +Init +---- + * Simple slack-duty-bot working with http outgoing slack webhook diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..e587eb8 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,135 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" + +[[projects]] + name = "github.com/gorilla/websocket" + packages = ["."] + revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" + version = "v1.2.0" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token" + ] + revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" + +[[projects]] + name = "github.com/magiconair/properties" + packages = ["."] + revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" + version = "v1.7.6" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" + +[[projects]] + name = "github.com/nlopes/slack" + packages = ["."] + revision = "8ab4d0b364ef1e9af5d102531da20d5ec902b6c4" + version = "v0.2.0" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" + version = "v1.1.0" + +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" + version = "v1.0.5" + +[[projects]] + name = "github.com/spf13/afero" + packages = [ + ".", + "mem" + ] + revision = "63644898a8da0bc22138abf860edaf5277b6102e" + version = "v1.1.0" + +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" + +[[projects]] + name = "github.com/spf13/pflag" + packages = ["."] + revision = "583c0c0531f06d5278b7d917446061adc344b5cd" + version = "v1.0.1" + +[[projects]] + name = "github.com/spf13/viper" + packages = ["."] + revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" + version = "v1.0.2" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + revision = "b49d69b5da943f7ef3c9cf91c8777c1f78a0cc3c" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "unix", + "windows" + ] + revision = "cbbc999da32df943dac6cd71eb3ee39e1d7838b9" + +[[projects]] + name = "golang.org/x/text" + packages = [ + "internal/gen", + "internal/triegen", + "internal/ucd", + "transform", + "unicode/cldr", + "unicode/norm" + ] + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "d3a415f3af1a28399f44a9084b7e1d220ccf60870db7dc4f8fbadb9f01cc10a1" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..0cd69d0 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,38 @@ +# Gopkg.toml example +# +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/spf13/pflag" + version = "1.0.1" + +[[constraint]] + name = "github.com/spf13/viper" + version = "1.0.2" + +[prune] + go-tests = true + unused-packages = true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..47f3be5 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +all: build + +APP?=slack-duty-bot +ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +BUILD_OS?=linux +BUILD_ARCH?=amd64 +DOCKER_IMAGE?=insidieux/${APP} +DOCKER_TAG?=1.0.0 +DOCKER_USER?=user +DOCKER_PASSWORD?=password +SDB_SLACK_TOKEN?=some-token +SDB_SLACK_KEYWORD?=keyword + +clean: + rm -f ${APP} + +dep-install: + go get -v -u github.com/golang/dep/cmd/dep + +dep-ensure: dep-install + rm -rf vendor + dep ensure + +test: dep-ensure + go test -v -race ./... + +build: clean dep-ensure + env GOOS=${BUILD_OS} GOARCH=${BUILD_ARCH} CGO_ENABLED=0 go build -v -o ${APP} + +container: build + docker rmi ${DOCKER_IMAGE}:${DOCKER_TAG} || true + docker build \ + --build-arg APP=${APP} \ + -f .docker/Dockerfile \ + -t ${DOCKER_IMAGE}:${DOCKER_TAG} \ + . + +run: container + docker stop ${APP} || true && docker rm ${APP} || true + docker run \ + --name ${APP} \ + --rm \ + -e SDB_SLACK_TOKEN=${SDB_SLACK_TOKEN} \ + ${DOCKER_IMAGE}:${DOCKER_TAG} \ + --slack.keyword ${SDB_SLACK_KEYWORD} + +push: container + docker login docker.io -u ${DOCKER_USER} -p ${DOCKER_PASSWORD} + docker push ${DOCKER_IMAGE}:${DOCKER_TAG} diff --git a/README.md b/README.md index 0332ea2..1081ecb 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,77 @@ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Go Report Card](https://goreportcard.com/badge/github.com/iqoption/slack-duty-bot)](https://goreportcard.com/report/github.com/iqoption/slack-duty-bot) -### How usage -1. Create new custom integration `Outgoing WebHooks` (e.g https://{team}.slack.com/apps/manage/custom-integrations) +### Usage +1. Create new custom integration `Bots` (e.g https://{team}.slack.com/apps/manage/custom-integrations) +2. Add bot to channels you want to listen 2. Build for your environment -3. Add a schedule for the attendants +3. Prepare config.yaml with duties list 3. Run with the required parameters +```bash +SDB_SLACK_TOKEN=your-token-here ./slack-duty-bot \ + --slack.keyword keyword-1 \ + --slack.keyword keyword-2 \ + --slack.group.id your-group-id \ + --slack.group.name your-group-name +``` + +You also can run in application in docker +```bash +docker run \ + --name slack-duty-bot \ + --restart on-failure \ + -v $(pwd)/config:/etc/slack-duty-bot \ + -e SDB_SLACK_TOKEN=your-token-here \ + -d \ + insidieux/slack-duty-bot:1.0.0 \ + --config.path=/etc/slack-duty-bot \ + --slack.keyword keyword-1 \ + --slack.keyword keyword-2 + --slack.group.id your-group-id \ + --slack.group.name your-group-name \ +``` + ### Build package +Build +```bash +go get -u github.com/golang/dep/cmd/dep +dep ensure +env GOOS=linux GOARCH=amd64 go build -v +``` +Build via makefile +```bash +make BUILD_OS=linux BUILD_ARCH=amd64 +``` +Build in docker ```bash -$ env GOOS=linux GOARCH=amd64 go get -v -d ./src/slack-duty-bot -$ env GOOS=linux GOARCH=amd64 go build -o ./bin/slack-duty-bot ./src/slack-duty-bot/main.go +docker run --rm -v $(pwd):/go/src/slack-duty-bot -w /go/src/slack-duty-bot golang:1.10 make BUILD_OS=linux BUILD_ARCH=amd64 ``` -### Config +### Configuration + +#### Configuration flags, environment variables +Environment variables are prefixed with `SDB_` and **MUST** be uppercase with `_` delimiter +Available variables: +* `SDB_SLACK_TOKEN` +* `SDB_SLACK_GROUP_ID` +* `SDB_SLACK_GROUP_NAME` +* `SDB_SLACK_THREADS` + +Every environment variable can be overwritten by startup flags +Available flags: +* `--config.path` - path to config.yaml file (default: . and $HOME/.slack-duty-bot) +* `--slack.token` - Slack API client token +* `--slack.keyword` - Case insensitive keywords slice to search in message text, can be set multiple times (default: []) +* `--slack.group.name` - Slack user group name, to mention in channel if duty list is empty +* `--slack.group.id` - Slack user group ID, to mention in channel if duty list is empty +* `--slack.threads` - Use threads as reply target or push message direct to channel (default: true) + +You can get IDS from api or just use [testing page](https://api.slack.com/methods/usergroups.list/test) + +#### Configuration file +Configuration file **MUST** contain `duties` key with **7** slices of Slack user names ```yaml -token: %some-token% -log: /var/log/slack-duty-bot.log -ids: - username.one: U11GZZZZZ - username.two: U11VZZZZZ duties: - [username.one, username.two] # Sunday - [username.one] # Monday @@ -31,8 +83,92 @@ duties: - [username.one, username.two] # Saturday ``` -### Available arguments -* `--port` - bot http listen port (default: 8003) -* `--config` - path to yml config (default: /var/slack-bot/config.yml) -* `--period` - period in seconds after which the config will be updated (default: 5) -* `--restore` - enable restore previous stable config version process +#### Configuration priority +* Flags +* Environment variables +* Config file + +### Deploy to Kubernetes + +#### Create namespace +```bash +kubectl create namespace slack-duty-bot +``` + +#### Create namespace quota +```yaml +#namespace-quota.yaml +apiVersion: v1 +kind: ResourceQuota +metadata: + name: slack-duty-bot-quota +spec: + hard: + requests.cpu: "2" + requests.memory: 1Gi + limits.cpu: "4" + limits.memory: 2Gi +``` +```bash +kubectl create -f namespace-quota.yaml --namespace=slack-duty-bot +``` + +#### Create limit range +```yaml +#namespace-limit-range.yaml +apiVersion: v1 +kind: LimitRange +metadata: + name: slack-duty-bot-limit-range +spec: +limits: + - default: + cpu: "200m" + memory: 128Mi + defaultRequest: + cpu: "100m" + memory: 64Mi + type: Container +``` +```bash +kubectl create -f namespace-limit-range.yaml --namespace=slack-duty-bot +``` + +#### Prepare your deployment file +```bash +(docker run \ + --rm \ + -it \ + -v $(pwd):/tmp \ + -e SDB_SLACK_TOKEN_BASE64=your-token-hash \ + -e SDB_NAME=your-deployment-name \ + -e SDB_TAG=1.0.0 \ + -e SDB_KEYWORD=your-keyword \ + -e SDB_SLACK_DEFAULT_USER=default-username \ + -e SDB_SLACK_GROUP_ID=group-id \ + -e SDB_SLACK_GROUP_NAME=group-name \ + supinf/envsubst /tmp/.kubernetes/deploy.yaml.tpl) > $(pwd)/.kubernetes/deploy.yaml + +``` +or use native `envsubst` +```bash +(SDB_SLACK_TOKEN_BASE64=your-token-hash \ + SDB_NAME=your-deployment-name \ + SDB_TAG=1.0.0 \ + SDB_KEYWORD=your-keyword \ + SDB_SLACK_DEFAULT_USER=default-username \ + SDB_SLACK_GROUP_ID=group-id \ + SDB_SLACK_GROUP_NAME=group-name \ + envsubst < $(pwd)/.kubernetes/deploy.yaml.tpl) $(pwd)/.kubernetes/deploy.yaml +``` + +After that you can change configuration with `kubect` or edit config map directly from Kubernetes dashboard + +#### Deploy! +```bash +kubectl apply -f $(pwd)/.kubernetes/deploy.yaml --namespace slack-duty-bot +``` + +## Authors +* [Konstantin Perminov](https://github.com/SpiLLeR) +* [Ageev Pavel](https://github.com/insidieux) diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..3908d45 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,9 @@ +ROADMAP +======== + +* Tests +* Signal handlers +* HTTP rest API for edit duties configuration +* Persistent storage for duties configuration +* Multiple instance mode with ex-locks +* HELM support diff --git a/bin/.gitkeep b/bin/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/main.go b/main.go new file mode 100644 index 0000000..fb7d4f1 --- /dev/null +++ b/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "fmt" + "github.com/fsnotify/fsnotify" + "github.com/nlopes/slack" + log "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "strings" + "time" +) + +const ( + incomingErrorRetry = 500 +) + +func init() { + viper.SetConfigType("yaml") + viper.AddConfigPath("$HOME/.slack-duty-bot") + viper.AddConfigPath(".") + + viper.SetEnvPrefix("SDB") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + viper.BindEnv("slack.token") + viper.BindEnv("slack.group.id") + viper.BindEnv("slack.group.name") + viper.BindEnv("slack.threads") + viper.AutomaticEnv() + + pflag.String("config.path", "", "Config path") + pflag.String("slack.token", "", "Slack API client token config") + // We need ID and name only because bot users can't read user groups info via api + pflag.String("slack.group.name", "", "Slack group ID for calling in fallback mode") + pflag.String("slack.group.id", "", "Slack group name for calling in fallback mode") + pflag.StringSlice("slack.keyword", []string{}, "Slack keywords to lister") + pflag.Bool("slack.threads", true, "Usage of Slack threads to reply on messages") + viper.BindPFlags(pflag.CommandLine) + + log.SetFormatter(&log.TextFormatter{DisableColors: true}) + log.SetLevel(log.DebugLevel) +} + +func main() { + pflag.Parse() + + viper.AddConfigPath(viper.GetString("config.path")) + viper.ReadInConfig() + viper.WatchConfig() + viper.OnConfigChange(func(e fsnotify.Event) { + log.Infoln("Config file was changed") + }) + + if viper.GetString("slack.token") == "" { + log.Fatalln("Parameter slack.token is required") + } + if len(viper.GetStringSlice("slack.keyword")) == 0 { + log.Fatalln("Parameter slack.keyword is required") + } + if viper.GetString("config.path") != "" { + viper.AddConfigPath(viper.GetString("config.path")) + } + + var ( + client = slack.New(viper.GetString("slack.token")) + rtm = client.NewRTM() + ) + + log.Infoln("Send request for RTM connection") + go rtm.ManageConnection() + + var incomingErrorCount = 0 + for packet := range rtm.IncomingEvents { + switch event := packet.Data.(type) { + case *slack.ConnectedEvent: + log.Infoln("RTM connection established") + + case *slack.InvalidAuthEvent: + rtm.Disconnect() + log.Fatalf("Could not authenticate, invalid Slack token passed, terminate") + + case *slack.IncomingEventError: + incomingErrorCount++ + log.Warningf("RTM incoming error: %+v", event.Error()) + if incomingErrorCount >= incomingErrorRetry { + rtm.Disconnect() + log.Fatalf("Reached error reconnect limit %d on %s type error, terminate", incomingErrorRetry, packet.Type) + } + + case *slack.MessageEvent: + log.Printf("Incoming message event") + if err := handleMessageEvent(rtm, event); err != nil { + log.Warningf("Handle message event error: %v", err) + } + } + } +} + +func handleMessageEvent(rtm *slack.RTM, event *slack.MessageEvent) error { + // check text + if event.Text == "" { + return fmt.Errorf("incoming message with empty text") + } + // check keywords + var keywords = viper.GetStringSlice("slack.keyword") + contains := any(keywords, func(keyword string) bool { + return strings.Contains(strings.ToLower(event.Text), strings.ToLower(keyword)) + }) + if contains == false { + return fmt.Errorf("incoming message text does not contain any suitable keywords (%s)", strings.Join(keywords, ", ")) + } + log.Infof("Incoming message text: %s", event.Text) + // collection user ids for make duties list + var userIds = make(map[string]string, 0) + users, err := rtm.Client.GetUsers() + if err != nil { + log.Warningf("Failed to get users list from Slack API: %v", err) + } + if users != nil { + for _, user := range users { + userIds[user.Name] = user.ID + } + } + var ( + config = struct { + Duties [][]string // we need this hack cause viper cannot resolve slice of slice + }{} + duties []string + ) + viper.Unmarshal(&config) + for _, username := range config.Duties[int(time.Now().Weekday())] { + userId, ok := userIds[username] + if !ok { + log.Warningf("Failed to get user id by username %s", username) + } + duties = append(duties, fmt.Sprintf("<@%s|%s>", userId, username)) + } + if len(duties) == 0 && viper.GetString("slack.group.id") != "" && viper.GetString("slack.group.name") != "" { + duties = append(duties, fmt.Sprintf("", viper.GetString("slack.group.id"), viper.GetString("slack.group.name"))) + } + // send message + var outgoing = rtm.NewOutgoingMessage(strings.Join(duties, ", "), event.Channel) + if viper.GetBool("slack.threads") == true { + outgoing.ThreadTimestamp = event.Timestamp + } + log.Infof("Outgoing message: %+v", outgoing) + rtm.SendMessage(outgoing) + return nil +} + +func any(vs []string, f func(string) bool) bool { + for _, v := range vs { + if f(v) { + return true + } + } + return false +} diff --git a/pkg/.gitkeep b/pkg/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/slack-duty-bot/main.go b/src/slack-duty-bot/main.go deleted file mode 100644 index 58ea0f3..0000000 --- a/src/slack-duty-bot/main.go +++ /dev/null @@ -1,155 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "os" - "strings" - "time" - - "gopkg.in/yaml.v2" -) - -type Config struct { - Token string - Log string - Ids map[string]string - Duties [][]string -} - -type Response struct { - Text string `json:"text"` -} - -var ( - booted = false - config = &Config{} - file = flag.String("config", "/var/slack-duty-bot/config.yml", "Full path to yml config") - port = flag.String("port", "8003", "HTTP port to listen by bot") - period = flag.Int("period", 5, "Period in seconds after which the config will be updated") - restore = flag.Bool("restore", false, "Enable restore previous stable config version process") -) - -func main() { - flag.Parse() - go updateConfig() - http.HandleFunc("/", rootHandler) - http.ListenAndServe(fmt.Sprintf(":%s", *port), nil) -} - -func readConfig() (*Config, error) { - content, err := ioutil.ReadFile(*file) - if err != nil { - return nil, err - } - current := &Config{} - if err := yaml.Unmarshal(content, current); err != nil { - return nil, err - } - if current.Token == "" { - return nil, errors.New("Config token is empty") - } - if len(current.Duties) < 7 { - return nil, errors.New(fmt.Sprintf("Invalid number (%d) of messages in config", len(config.Duties))) - } - if current.Log == "" { - return nil, errors.New("Config log is empty") - } - return current, nil -} - -func updateConfig() { - for { - current, err := readConfig() - if err != nil { - if !*restore || booted == false { - log.Panicln(fmt.Sprintf("Failed to init config. Error: %s", err.Error())) - } - log.Printf("An error occured during update config (\"%s\"). Restore previous version.", err.Error()) - current = config - } - config = current - if booted == false { - booted = true - } - time.Sleep(time.Second * time.Duration(*period)) - } -} - -func rootHandler(w http.ResponseWriter, r *http.Request) { - if result, err := validate(r.Body); !result { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - response, err := getResponse() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - js, err := json.Marshal(response) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(js) -} - -func validate(context io.ReadCloser) (result bool, err error) { - data, err := ioutil.ReadAll(context) - if err != nil { - return false, err - } - - body := string(data) - if !strings.Contains(body, config.Token) { - return false, errors.New("Bad token") - } - - descriptor, err := os.OpenFile(config.Log, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0755) - if err != nil { - return false, err - } - - defer descriptor.Close() - descriptor.WriteString(fmt.Sprintf("[%s] Incoming request body: %s", time.Now(), body)) - - return true, err -} - -func getResponse() (Response, error) { - duties, err := arrayMap(config.Duties[time.Now().Weekday()], func(user string) (string, error) { - id, ok := config.Ids[user] - if !ok { - return "", errors.New(fmt.Sprintf("Unknown user \"%s\" called from config", user)) - } - return fmt.Sprintf("<@%s|%s>", id, user), nil - }) - response := Response{} - if err != nil { - return response, err - } - response.Text = strings.Join(duties, " ") - return response, nil -} - -func arrayMap(array []string, callback func(string) (string, error)) ([]string, error) { - arrayCopy := make([]string, len(array)) - for index, value := range array { - value, err := callback(value) - if err != nil { - return nil, err - } - arrayCopy[index] = value - } - return arrayCopy, nil -}