Building a Slack bot using the Events API
In this guide you will learn how to personalize community onboarding using a custom built Slack bot and host it on AWS Lambda for free.
Ory Guest
In this guide you will learn how to personalize community onboarding using a custom built Slack bot and host it on AWS Lambda for free.
Ory Guest
I’m a huge fan of automating working processes. I try to automate everything and save my coworkers time.
This article I will show you how to improve the community onboarding journey with a custom Slack bot using the Slack Events API, hosted on AWS Lambda.
We used to have a bot greeting new members in the
Ory Community Slack. It messages new members with an
introduction to help get an overview of the Ory community and products.
Since this way of onboarding new members is impersonal and one-way so the
Ory DevRel team was looking for ways to improve it by making it personal and
two-way:
To keep the workload manageable, the initial message is pre-composed. A bot would either notify the team when new members join or message new members from a "human" account. So the options are:
We landed on the second option and let the bot use a User OAuth Token to send messages as a user. In the next part of the blog post, I’ll tell you how to build this bot.
Slack offers you a few examples of using APIs:
I built Slack bots before, and the following two chapters are about the difference between WebSockets integration and the Events API. Just for convenience, let’s say that the Events API and webhook integration works almost the same way.
Websocket is an excellent protocol. It's easy to implement, developer friendly, and works on top of HTTP1/1. The protocol was first described in 2008, and in 2010 there was broad browser support. Websockets made the developer experience building realtime web applications much more enjoyable back in the day because you did't need to deal with BOSH and chunked encoding of HTTP1/1. Furthermore, new architectural patterns and paradigms such as Event-Driven Architecture and Event-Driven SOA were described.
Despite the benefits described above, WebSockets have drawbacks. One of the most significant drawbacks is that it's built on-top-of HTTP/1.1 and keeps the connection open. It leads to scaling problems when you have more users for your application. We have two ways to scale our application:
Horizontal scaling requires load balancing between the servers. Balancing HTTP traffic is easier than WebSocket traffic, and almost all load balancers such as Nginx, Envoy, or Istio balance traffic on L4 of the OSI model. It would be best to implement load balancing mechanisms on L7 when you work with WebSockets to have evenly loaded servers.
Furthermore, it would be best to keep in mind that networks are unreliable or third parties may close connections and your application needs to handle it gracefully. In addition, you need to have a persistent listener of incoming events, which leads to the situation when you need to have EC2/VPS or any other server to run your code, but this is a cost-ineffective solution.
The Slack Events API works differently. You need to have a web server that is be publicly accessible and an event handler implemented in your app. According to the documentation of the Slack Events API, you need to implement the following steps:
The only thing we need to have is a web server with a simple REST API endpoint. It allows us to use various cloud features to run our bot and save money. For instance, we can use AWS Lambda, trigger it with API Gateway. The best part of this is that we can use the Free tier plan. That's why I'm a huge fan of the Events API from Slack.

Let's build our bot, huh?
We will use the Go programming language to build our bot. What we need:
net/http package for local development and testing.Let's create a config package
package config
import "github.com/kelseyhightower/envconfig"
const (
// EnvProduction is a production environment
EnvProduction = "production"
// EnvDevelopment is a development environment
EnvDevelopment = "development"
)
// BotConfig is a struct that stores configuration parsed by `envconfig`
// environment variables
type BotConfig struct {
Env string `envconfig:"ENV" default:"development"`
BindAddr string `envconfig:"BIND_ADDR" default:":12022"`
SigningSecret string `envconfig:"SLACK_SIGNING_SECRET"`
SlackBotToken string `envconfig:"SLACK_BOT_TOKEN"`
WelcomeMessage string `envconfig:"WELCOME_MESSAGE"`
}
// Parse parses and returns BotConfig structure
func Parse() (*BotConfig, error) {
var c BotConfig
err := envconfig.Process("", &c)
return &c, err
}
It's always a good idea to have some tests.
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParse(t *testing.T) {
os.Setenv("SLACK_SIGNING_SECRET", "something")
os.Setenv("SLACK_BOT_TOKEN", "amazing")
os.Setenv("WELCOME_MESSAGE", "i guess")
c, err := Parse()
assert.NoError(t, err)
assert.Equal(t, "something", c.SigningSecret)
assert.Equal(t, "amazing", c.SlackBotToken)
assert.Equal(t, "i guess", c.WelcomeMessage)
assert.Equal(t, EnvDevelopment, c.Env)
assert.Equal(t, ":12022", c.BindAddr)
}
We can check it by simply running go test ./…
We can start with the example taken from the Slack Go SDK because of its simplicity. We need to extend this example with the following parts:
package app
import (
"encoding/json"
"greeter_bot/config"
"io/ioutil"
"net/http"
"strings"
"github.com/akrylysov/algnhsa"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
)
type (
// Handler is an interface for the webserver that handles
// incoming requests from Slack events API
//
// You can add support of any cloud provider by implementing this interface
Handler interface {
Init(c *config.BotConfig)
Start() error
}
// HTTPHandler is an implementation of webserver for local development/testing
HTTPHandler struct {
Handler
config *config.BotConfig
}
)
// NewHandler creates slack events api handler
// It creates HTTPHandler for development environment
// and LambdaHandler for production env
func NewHandler(c *config.BotConfig) Handler {
var h Handler
h = &HTTPHandler{}
h.Init(c)
return h
}
// Init initializes handler
func (h *HTTPHandler) Init(c *config.BotConfig) {
h.config = c
http.HandleFunc("/", h.handle)
}
// handle handles incoming data from
func (h *HTTPHandler) handle(w http.ResponseWriter, r *http.Request) {
var api = slack.New(h.config.SlackBotToken)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
sv, err := slack.NewSecretsVerifier(r.Header, h.config.SigningSecret)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if _, err := sv.Write(body); err != {
w.WriteHeader(http.StatusInternalServerError)
}
err := sv.Ensure(); err != {
w.WriteHeader(http.StatusUnauthorized)
}
eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken())
err != {
w.WriteHeader(http.StatusInternalServerError)
}
eventsAPIEvent.Type == slackevents.URLVerification {
r *slackevents.ChallengeResponse
err := json.Unmarshal([](body), &r)
err != {
w.WriteHeader(http.StatusInternalServerError)
}
w.Header().Set(, )
w.Write([](r.Challenge))
}
eventsAPIEvent.Type == slackevents.CallbackEvent {
innerEvent := eventsAPIEvent.InnerEvent
ev := innerEvent.Data.() {
*slackevents.TeamJoinEvent:
text := strings.Replace(h.config.WelcomeMessage, , , )
api.PostMessage(ev.User.ID, slack.MsgOptionText(text, ))
}
}
}
Start() {
h.config.Env == config.EnvDevelopment {
http.ListenAndServe(h.config.BindAddr, )
}
algnhsa.ListenAndServe(http.DefaultServeMux, )
}
To start everything up we need cmd/mrrobot/main.go:
package main
import (
"greeter_bot/app"
"greeter_bot/config"
"log"
)
func main() {
c, err := config.Parse()
if err != nil {
log.Fatal(err)
}
handler := app.NewHandler(c)
log.Fatal(handler.Start())
}
Signing Secret and store it somewhere. You'll need it to configure
AWS Lambda function.OAuth & Permissions and generate access tokens under
Oauth Tokens for YOur Workspace. You need the User Oauth Tokenwhen you
want to send message from a user and the Bot User Oauth Token for the bot
user. You'll need it to configure AWS Lambda.chat:write for both the Bot Token and User token
scopes.Create function button.Author from scratch input the name of the function. Choose Go 1.x
for the runtime and architecture x86_64 and then create a function.Configuration -> Environment variables.Signing Token to SLACK_SIGNING_SECRET variable.WELCOME_MESSAGE variable.SLACK_BOT_TOKEN variable.production to ENV variable.Code section and scroll down to Runtime settings and change
Handler from hello to mrrobot.ANY method to handle requests to
/ (check "Use Lambda Proxy Integration").

{proxy+} resource to handle requests to every other path
(check "Configure as proxy resource").

You need to upload the zip archive with compiled binary to the AWS lambda. In
that case, make can help us -
make allows us to
automate operations since 1976. Also it always a great idea to have them
self-documented:
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
lint: ## Runs linter against the code
golangci-lint run ./...
test: ## Run tests locally
go test ./...
build_docker: ## Build docker image
docker build -t mrrobot .
build_linux: ## Build executable for linux system
GOOS=linux GOARCH=amd64 go build -o mrrobot cmd/mrrobot/main.go
zip: build_linux ## Build and create a zip archive for deploying to AWS lambda
zip main.zip mrrobot
make zip.AWS Lambda.Upload from under the source code section.Add features and functionality and choose Event subscriptions.Request URL field, pass the
verification step and click Save.Subscribe to bot events and choose team_join event and then press
Save.That's it. You can now invite a new member to your Slack and test the bot.
As you can see the Slack Events API helps you to build bots and automate your routine. Also, if you just build a simple bot you don’t need to spend money on hosting.
Join the Ory Community Slack to see the bot in action and let us know what you think!