Hi, I'm a slack bot written in Go. My name is inspired from one of the dumbest-smart robots out there, good ol' ED-209.
Instead of being triggered with a /slack
command, you interact with go209 through DMs. By definining a relatively simple JSON files, you can make go209 respond to various terms. Not only that, but you can also define linear or branching Q/A interactions with go209 too! Responses to these interactions can then be processed by arbitrary modules, for instance, emailing you the results.
redis
- this is required to track state between conversations, and also to keep the separate slack app and web hook server synchronized. (There's also a docker-compose setup too, which containerizes the whole thing if that's easier)
$ go get github.com/xntrik/go209
$ cd <into go209 folder - often ~/go/src/github.com/xntrik/go209>
$ make buildplugins
$ docker pull xntrik/go209
$ docker run --rm xntrik/go209 -h
$ go209 -h
NAME:
go209 - The dumbest-smart slack bot app (in go)
USAGE:
go209 [global options] command [command options] [arguments...]
COMMANDS:
start, s Start the slack bot.
modules Display the loaded modules
dump Dump the rules json file, makes sure it parses too
web, w Start the web app.
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--debug, -d enable debug output
--help, -h show help
--version, -v print the version
ENV VARIABLES:
SLACK_TOKEN Slack Bot User OAuth Access Token (required)
SLACK_SIGNING_SECRET Slack Bot Signing Secret (required)
REDIS_ADDR REDIS address (required)
REDIS_PWD REDIS password (default: "")
REDIS_DB REDIS DB (default: 0)
JSON_RULES The rule file (default: "rules.json")
WEB_ADDR The web listener address (default: "localhost:8000")
DYNAMIC_MODULES Optional .so plugins you want to load (separate with ":")
EmailModule Module ENV VARIABLES:
EMAILMODULE_FROM
EMAILMODULE_TO
EMAILMODULE_SMTPSERVER
EMAILMODULE_USERNAME
EMAILMODULE_PASSWORD
EMAILMODULE_SKIPTLS
SlackWebhookModule Module ENV VARIABLES:
SLACKWEBHOOKMODULE_URL
To run properly you need to be running:
- A redis instance
./go209 start
for the interactive slack app./go209 web
to handle web hooks from slack
To simplify this:
$ make docker-compose-up
This will start a redis container, and the two go209 containers. You can read more about the docker-compose setup below. You can read more about the docker-compose setup below
go209 requires a few different ENV VARs setup to run, but don't worry, you can just plonk them in your .env
file.
SLACK_TOKEN
This is the Slack Bot User OAuth Access Token (required) See below under Slack SetupSLACK_SIGNING_TOKEN
This is the Slack Bot Signing Secret (required) See below under Slack SetupREDIS_ADDR
Points to your redis instance. (required) If using docker-compose, set this toredis:6379
REDIS_PWD
If your redis requires authenticationREDIS_DB
If you want to use a redis DB other than 0JSON_RULES
go209 comes with a sample rules.json, if you want to point to the location of a different file, set it hereWEB_ADDR
This sets the go209 web server listening interfaceDYNAMIC_MODULES
If you want to load further modules, after you've compiled them, set their names here See below under Modules
Any modules that require env vars will also be displayed, for instance, if you want to send emails.
- You need to setup your go209 app bot in slack by visiting https://api.slack.com/ and then press the
Start Building
button. - Give your bot a name
- Select your slack workspace
- Click
Create App
- Under
Add features and functionality
clickBots
, then clickAdd a Bot User
, give it a name, and finalize by clickingAdd Bot User
- To generate the
SLACK_TOKEN
head toInstall App
on the left, thenInstall App to Workspace
- From here, you'll see the
Bot User OAuth Access Token
, set this to yourSLACK_TOKEN
- The
SLACK_SIGNING_SECRET
is available under theSigning Secret
portion underApp Credentials
on theBasic Information
page.
To handle interactive attachments, such as menu drop downs or button selection, you need to ensure that the web hook callback is configured to hit your instance of go209. You must have TLS configured, or else slack won't connect properly. go209 doesn't terminate TLS, so I would recommend spinning up a simple nginx in front of the web portion. (or ELB/ALB).
- Visit
Interactive Components
in the slack app's API page - Click
Interactivity
to on - Enter the URL into the
Request URL
that can hit your running instance ofgo209 web
, for instancehttps://yourdomain.com/slack/message_handler
The rules for go209 are defined in, by default, rules.json. In their simplest form, a rule could simply respond to a message:
{
"terms": ["hi"],
"response": "Hello"
}
If you want a single response to apply to multiple terms:
{
"terms": ["hi", "hello"],
"response": "Ohai"
}
You can also spice up your responses by using limited randomness and references.
{
"terms": ["hi", "hello"],
"response" "[[Hi||Hey]] {{.Username}}, How's [[things||stuff||life]]?"
}
If you want to add a single-layer of sub-search terms, you can do that too.
{
"terms": ["first term"],
"response": "Cool, did you mean the first or second term?",
"subterms": [
{
"terms": ["first"],
"response": "Cool, this is the first"
},
{
"terms": ["second"],
"response": "Cool, this is the second"
}
]
}
If you want go209 to ask questions, and store the results:
{
"terms": ["questionnaire"],
"response": "Hey {{.Username}}, I'm going to ask you some questions. If you want to finish early, just send me the word 'stop'.",
"interactions": [
{
"interaction_id": "q1",
"stop_word": "stop",
"type": "text",
"question": "How was your day yesterday?",
"next_interaction": "q2"
},
{
"interaction_id": "q2",
"stop_word": "stop",
"type": "text",
"question": "Do you think you'll have a good day tomorrow?",
"next_interaction": "end"
}
],
"interaction_start": "q1"
}
There's a bit to unravel. If in your rule, you add an interactions
array, you can define multiple interactions. All interactions in your rules file must have a unique interaction_id
. You also need to specify the interaction_start
in your rule, this specifies the first interaction to kick off when the user sends the message questionnaire
.
You have to ensure at least one interaction has the next_interaction
set to end
, otherwise it'll never finish, and that would be dreadful.
If you want to send a final response to an interaction, set the type
interaction attribute to finaltext
, and set a response
instead of a question
. For example:
{
"interaction_id": "q3",
"stop_word": "stop",
"type": "finaltext",
"response": "Great work!",
"next_interaction": "end"
}
Responses to these will just be echoed at the terminal, which isn't that useful. This is where modules can come into play. You can read more about modules below, but a default module includes the EmailModule
. If you start go209 with correct EMAILMODULE
ENV VARs, you can then adjust your rules to run a dynamic module at the end of the question/answers.
{
"terms": ["questionnaire"],
"response": "A quick questionnaire",
"interactions": [
{
"interaction_id": "qq1",
"stop_word": "stop",
"type": "text",
"question": "Do you like golang?"
"next_interaction": "end"
}
],
"interaction_start": "qq1",
"interaction_end_mods": ["EmailModule"]
}
Another module is the SlackWebhookModule
, which, if the ENV variable is configured, is able to post the response to a set of interactions to a Slack Webhook, such that it can be presented in a channel.
{
"terms": ["questionnaire"],
"response": "A quick questionnaire",
"interactions": [
{
"interaction_id": "qq1",
"stop_word": "stop",
"type": "text",
"question": "Do you like golang?"
"next_interaction": "end"
}
],
"interaction_start": "qq1",
"interaction_end_mods": ["EmailModule", "SlackWebhookModule"]
}
Sure, text-based q&a is fun, but what if you want to present and handle buttons or menus.
Attachments follow the exact same specification from nlopes/slack
{
"terms": ["questionnaire"],
"response": "A quick q",
"interactions": [
{
"interaction_id": "a1",
"stop_word": "stop",
"type": "attachment",
"question": "Do you like pineapple on pizza?",
"attachment": {
"fallback": "Do you like pineapple on pizza?",
"callback_id": "a1",
"actions": [
{
"name": "pineapple",
"text": "Yes"
"style": "danger",
"type": "button",
"value": "yes",
"confirm: {
"title": "Are you sure?",
"text": "You actually like pineapple on pizza?",
"ok_text": "Yes",
"dismiss_text": "No"
}
},
{
"name": "pineapple",
"text": "No",
"type": "button",
"value": "no"
}
]
},
"next_interaction": "end"
}
],
"interaction_start": "a1"
}
You can also branch to different interactions depending on the responses to buttons.
{
"terms": ["questionnaire"],
"response": "A quick q",
"interactions": [
{
"interaction_id": "a1",
"stop_word": "stop",
"type": "attachment",
"question": "Do you like pineapple on pizza?",
"attachment": {
"fallback": "Do you like pineapple on pizza?",
"callback_id": "a1",
"actions": [
{
"name": "pineapple",
"text": "Yes",
"style": "danger",
"type": "button",
"value": "yes",
"confirm": {
"title": "Are you sure?",
"text": "You actually like pineapple on pizza?",
"ok_text": "Yes",
"dismiss_text": "No"
}
},
{
"name": "pineapple",
"text": "No",
"type": "button",
"value": "no"
}
]
},
"next_interaction_dynamic": [
{
"response": "no",
"next_interaction": "end"
},
{
"response": "yes",
"next_interaction": "a2"
}
],
"next_interaction": "end"
},
{
"interaction_id": "a2",
"stop_word": "stop",
"type": "text",
"question": "Why do you like pineapple on pizza?",
"next_interaction": "end"
}
],
"interaction_start": "a1"
}
You can see we've defined the next_interaction_dynamic
array, which will branch off to a different interaction depending on the response. You'll still want a fallback next_interaction
, just in case.
In the root of the rules file you can also specify:
default
- This is what go209 will say if it doesn't understand something.interaction_cancelled_response
- If a user uses the stop word mid-interaction.interaction_complete_response
- Once a user completes a set of interactions.
Currently go209 supports output modules. These are executed, if configured in your rules.json, to perform arbitrary actions at the completion of a interaction/Q&A. go209 comes with an email module (loaded), slack webhook module (loaded) and a test module (not loaded).
To write your own, great a .go file in the pkg/go209/modules/
folder:
package main
import (
"fmt"
)
type testModule string
func (tm testModule) Name() string {
return "TestModule"
}
func (tm testModule) EnvVars() []string {
return []string{"One", "Two"}
}
func (tm testModule) Run(in interface{}, ev map[string]string) error {
fmt.Println("******* MODULE RUNNING!")
return nil
}
// Module is exported to be picked up by the plugin system
var Module testModule
Then build it:
$ make buildplugins
That should generate .so in the root go209 folder.
To ensure that these are loaded, with the DYNAMIC_MODULES
ENV VAR. For instance, say you compiled my-mod.go into my-mod.so:
$ DYNAMIC_MODULES=my-mod ./go209 modules
Will print out the dynamic modules. If you want to add more modules:
$ DYNAMIC_MODULES=my-mod:my-other-mod ./go209
$ make
all Clean, fmt, lint, vet and build!
build Builds the binary and plugins
static Build a static executable - don't forget to build the plugins statically as well
buildplugins Build .so files from contents of pkg/go209/modules/*.go
fmt Verifies all files have been `gofmt`ed.
lint Verifies `golint` passes.
vet Verifies `go vet` passes.
image Create docker image from the Dockerfile
docker-compose-build Build the docker compose
docker-compose-up Start the docker compose
docker-compose-upd Start the docker compose in background mode
clean Cleanup any build binaries or packages
$ make docker-compose-build
$ make docker-compose-up
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name _;
root /usr/share/nginx/html;
ssl_certificate "/etc/letsencrypt/live/YOURDOMAIN/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/YOURDOMAIN/privkey.pem";
# It is *strongly* recommended to generate unique DH parameters
# Generate them with: openssl dhparam -out /etc/pki/nginx/dhparams.pem 2048
ssl_dhparam "/etc/pki/nginx/dhparams.pem";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP;
ssl_prefer_server_ciphers on;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://localhost:8000;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}