Skip to content

Commit

Permalink
feat(slack)!: add socket mode support (target#171)
Browse files Browse the repository at this point in the history
  • Loading branch information
wass3r committed Jul 19, 2021
1 parent 1909aa2 commit 0c4e051
Show file tree
Hide file tree
Showing 15 changed files with 325 additions and 474 deletions.
17 changes: 8 additions & 9 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ To ensure that all developers follow the same guidelines for development, we hav

### Prerequisites

- [Golang](https://golang.org/dl/) - the source code is written in Go.
- [dep](https://github.com/golang/dep) - our Go dependency management tool.
- Slack API token - obtain a Slack API token for development by creating a bot integration.
- [Go(lang)](https://golang.org/dl/) - the source code is written in Go.
- Slack API Token
- Slack App Token

### Development Process

- Clone this repository to your Go workspace:

```sh
# Make sure you are running go 1.11 or later
# if you plan to clone into your current GOPATH then set the environment variable GO111MODULE=on
# this will tell go to use the new modules support
# Make sure you are running go 1.16 or later

# Clone the project
git clone git@github.com:target/flottbot.git somepath/flottbot
Expand All @@ -37,16 +35,17 @@ make build

```sh
# Checkout a branch for your work
git checkout -b name_of_your_branch
git switch -c name_of_your_branch

# Code away!
```

- Build the project and run locally:

```sh
# Export your Slack API token (the token below is redacted)
# Export your Slack Token and Slack App Token (the tokens below is redacted)
export SLACK_TOKEN=xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx
export SLACK_APP_TOKEN=xapp-x-xxxxxxxxx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Build the binary and run flottbot
make run
Expand Down Expand Up @@ -74,7 +73,7 @@ DEBU[0001] Connection established!

- Submit a PR for your changes.

- After the Travis build passes and you have an approved review, we will merge your PR.
- After the Github Actions build passes and you have an approved review, we will merge your PR.

- We will tag a release for flottbot when the desired functionality is present and stable.
- Production images of your changes will be published to Docker Hub and new binaries will be built and made available via Github Releases
7 changes: 4 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
},
"args": [
"build",
"-i",
"-gcflags",
"'-N -l'",
"-o",
"debug",
"${workspaceFolder}/cmd/flottbot/main.go"
],
"problemMatcher": ["$go"]
"problemMatcher": [
"$go"
]
}
]
}
}
21 changes: 9 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ The philosophy behind flottbot is to create very simple, lightweight, "dumb" bot

### Using go

```bash
go get -u github.com/target/flottbot/cmd/flottbot
```sh
$ go get -u github.com/target/flottbot/cmd/flottbot
```

### Binaries

Binaries for Linux, macOS, and Windows are available as [Github Releases](https://github.com/target/flottbot/releases/latest).
Binaries for Linux, macOS, and Windows are available as [Github Releases](/target/flottbot/releases/latest).

## Docker Images

Expand All @@ -46,18 +46,18 @@ We currently provide a few Docker images:

[target/flottbot:golang](https://hub.docker.com/r/target/flottbot) - Alpine image, flottbot binary, and golang v1.16 installed

[target/flottbot:python](https://hub.docker.com/r/target/flottbot) - Alpine image, flottbot binary, and python v3.8 installed
[target/flottbot:python](https://hub.docker.com/r/target/flottbot) - Alpine image, flottbot binary, and python v3.9 installed

_Note: We highly recommend pinning your image to a version, ie. `target/flottbot:0.5.0` or `target/flottbot:ruby-0.5.0`_

_Note: The images run with the unprivileged `flottbot` user (uid/gid 900) by default_

## Helm Chart

To install using the [Helm](https://helm.sh/) chart located in this repo, clone this repo, create a [Kubernetes secret](https://kubernetes.io/docs/concepts/configuration/secret/) for your Slack Token in your namespace & install the chart:
To install using the [Helm](https://helm.sh/) chart located in this repo, clone this repo, create [Kubernetes secrets](https://kubernetes.io/docs/concepts/configuration/secret/) for your Slack Token and Slack App Token in your namespace & install the chart:

```bash
helm install helm/flottbot/
```sh
$ helm install helm/flottbot/
```

## Available remotes
Expand All @@ -84,9 +84,6 @@ Please do! Check [CONTRIBUTING.md](./.github/CONTRIBUTING.md) for info.

Inspired by [Hexbot.io](https://github.com/mmcquillan/hex)

## Authors
## Contributors

- [David May](https://github.com/wass3r)
- [Sean Quinn](https://github.com/sjqnn)
- [Raphael Santo Domingo](https://github.com/pa3ng)
- [Jordan Sussman](https://github.com/JordanSussman)
* [List of contributors](/target/flottbot/graphs/contributors)
126 changes: 70 additions & 56 deletions core/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ func initLogger(b *models.Bot) {
// configureChatApplication configures a user's specified chat application
// TODO: Refactor to keep remote specifics in remote/
func configureChatApplication(bot *models.Bot) {
// emptyMap for substitute function
// (it will only replace from env vars)
emptyMap := map[string]string{}

// update the bot name
token, err := utils.Substitute(bot.Name, map[string]string{})
token, err := utils.Substitute(bot.Name, emptyMap)
if err != nil {
bot.Log.Warn().Msgf("Could not configure bot Name: %s", err)
bot.Log.Warn().Msgf("could not configure bot 'name' field: %s", err.Error())
}

bot.Name = token
Expand All @@ -52,124 +55,123 @@ func configureChatApplication(bot *models.Bot) {
switch strings.ToLower(bot.ChatApplication) {
case "discord":
// Discord bot token
token, err := utils.Substitute(bot.DiscordToken, map[string]string{})
token, err := utils.Substitute(bot.DiscordToken, emptyMap)
if err != nil {
bot.Log.Warn().Msgf("Could not set Discord Token: %s", err)
bot.RunChat = false
}

if token == "" {
bot.Log.Warn().Msgf("Discord Token is empty: '%s'", token)
bot.Log.Error().Msgf("could not set discord_token: %s", err.Error())
bot.RunChat = false
}

bot.DiscordToken = token

// Discord Server ID
// See https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-
serverID, err := utils.Substitute(bot.DiscordServerID, map[string]string{})
serverID, err := utils.Substitute(bot.DiscordServerID, emptyMap)
if err != nil {
bot.Log.Warn().Msgf("Could not set Discord Server ID: %s", err)
bot.Log.Error().Msgf("could not set discord_server_id: %s", err.Error())
bot.RunChat = false
}

if serverID == "" {
bot.Log.Warn().Msgf("Discord Server ID is empty: '%s'", serverID)
bot.DiscordServerID = serverID

if !isSet(token, serverID) {
bot.Log.Error().Msg("bot is not configured correctly for discord - check that discord_token and discord_server_id are set")
bot.RunChat = false
}

bot.DiscordServerID = serverID

case "slack":
configureSlackBot(bot)

case "telegram":
token, err := utils.Substitute(bot.TelegramToken, map[string]string{})
token, err := utils.Substitute(bot.TelegramToken, emptyMap)
if err != nil {
bot.Log.Warn().Msgf("Could not set telegram Token: %s", err)
bot.Log.Error().Msgf("could not set telegram_token: %s", err.Error())
bot.RunChat = false
}

if token == "" {
bot.Log.Warn().Msgf("telegram Token is empty: '%s'", token)
if !isSet(token) {
bot.Log.Error().Msg("bot is not configured correctly for telegram - check that telegram_token is set")
bot.RunChat = false
}

bot.TelegramToken = token

default:
bot.Log.Error().Msgf("Chat application '%s' is not supported", bot.ChatApplication)
bot.Log.Error().Msgf("chat application '%s' is not supported", bot.ChatApplication)
bot.RunChat = false
}
}
}

func configureSlackBot(bot *models.Bot) {
// Slack bot token
token, err := utils.Substitute(bot.SlackToken, map[string]string{})
if err != nil {
bot.Log.Warn().Msgf("Could not set Slack Token: %s", err)
bot.RunChat = false
}
// emptyMap for substitute function
// (it will only replace from env vars)
emptyMap := map[string]string{}

if token == "" {
bot.Log.Warn().Msgf("Slack Token is empty: %s", token)
bot.RunChat = false
// slack_token
token, err := utils.Substitute(bot.SlackToken, emptyMap)
if err != nil {
bot.Log.Error().Msgf("could not set slack_token: %s", err.Error())
}

bot.SlackToken = token

// Slack signing secret
signingSecret, err := utils.Substitute(bot.SlackSigningSecret, map[string]string{})
// slack_app_token
appToken, err := utils.Substitute(bot.SlackAppToken, emptyMap)
if err != nil {
bot.Log.Warn().Msgf("Could not set Slack Signing Secret: %s", err)
bot.Log.Warn().Msg("Defaulting to use Slack RTM")
bot.Log.Warn().Msgf("could not set slack_app_token: %s", err.Error())
}

signingSecret = ""
bot.SlackAppToken = appToken

// slack_signing_secret
signingSecret, err := utils.Substitute(bot.SlackSigningSecret, emptyMap)
if err != nil {
bot.Log.Warn().Msgf("could not set slack_signing_secret: %s", err.Error())
}

bot.SlackSigningSecret = signingSecret

// Get Slack Events path
eCallbackPath, err := utils.Substitute(bot.SlackEventsCallbackPath, map[string]string{})
// slack_events_callback_path
eCallbackPath, err := utils.Substitute(bot.SlackEventsCallbackPath, emptyMap)
if err != nil {
bot.Log.Error().Msgf("Could not set Slack Events API callback path: %s", err)
bot.Log.Warn().Msg("Defaulting to use Slack RTM")
bot.SlackSigningSecret = ""
bot.Log.Warn().Msgf("could not set slack_events_callback_path: %s", err.Error())
}

bot.SlackEventsCallbackPath = eCallbackPath

// Get Slack Interactive Components path
iCallbackPath, err := utils.Substitute(bot.SlackInteractionsCallbackPath, map[string]string{})
// slack_interactions_callback_path
iCallbackPath, err := utils.Substitute(bot.SlackInteractionsCallbackPath, emptyMap)
if err != nil {
bot.Log.Error().Msgf("Could not set Slack Interactive Components callback path: %s", err)
bot.InteractiveComponents = false
}

if iCallbackPath == "" {
bot.Log.Warn().Msgf("Slack Interactive Components callback path is empty: %s", iCallbackPath)
bot.InteractiveComponents = false
bot.Log.Warn().Msgf("could not set slack_interactions_callback_path: %s", err.Error())
}

bot.SlackInteractionsCallbackPath = iCallbackPath

// Get Slack HTTP listener port
lPort, err := utils.Substitute(bot.SlackListenerPort, map[string]string{})
// slack_listener_port
lPort, err := utils.Substitute(bot.SlackListenerPort, emptyMap)
if err != nil {
bot.Log.Error().Msgf("Could not set Slack listener port: %s", err)
bot.SlackListenerPort = ""
bot.Log.Warn().Msgf("could not set slack_listener_port: %s", err.Error())
}

// set slack http listener port from config file or default
lPortEnvWasUnset := strings.Contains(lPort, "${") // e.g. slack_listener_port: ${PORT}
if lPort == "" || lPortEnvWasUnset {
bot.Log.Warn().Msgf("Slack listener port is empty: %s", lPort)
bot.Log.Info().Str("defaultSlackListenerPort", defaultSlackListenerPort).Msg("Using default slack listener port.")
if !isSet(lPort) {
bot.Log.Warn().Msgf("slack_listener_port not set: %s", lPort)
bot.Log.Info().Str("defaultSlackListenerPort", defaultSlackListenerPort).Msg("using default slack listener port.")
lPort = defaultSlackListenerPort
}

bot.SlackListenerPort = lPort

// check for valid setup
// needs one of the following to be valid
// 1. SLACK_TOKEN + SLACK_APP_TOKEN (socket mode)
// 2. SLACK_TOKEN + SLACK_SIGNING_SECRET + SLACK_EVENTS_CALLBACK_PATH (events api)
isSocketMode := isSet(token, appToken)
isEventsAPI := isSet(token, signingSecret, eCallbackPath)
if !isSocketMode && !isEventsAPI {
bot.Log.Error().Msg("bot is not configured correctly for slack - check that either slack_token and slack_app_token OR slack_token, slack_signing_secret, and slack_events_callback_path are set")
bot.RunChat = false
}
}

func validateRemoteSetup(bot *models.Bot) {
Expand Down Expand Up @@ -198,3 +200,15 @@ func validateRemoteSetup(bot *models.Bot) {
}
}
}

// isSet is a helper function to check whether any of the supplied
// strings are empty or unsubstituted (ie. still in ${<string>} format)
func isSet(s ...string) bool {
for _, v := range s {
if v == "" || strings.HasPrefix(v, "${") {
return false
}
}

return true
}
Loading

0 comments on commit 0c4e051

Please sign in to comment.