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 isimpersonal 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 work 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 real-time web applications much more enjoyable back in the day because you don'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 != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := sv.Ensure(); err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if eventsAPIEvent.Type == slackevents.URLVerification {
var r *slackevents.ChallengeResponse
err := json.Unmarshal([]byte(body), &r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text")
w.Write([]byte(r.Challenge))
}
if eventsAPIEvent.Type == slackevents.CallbackEvent {
innerEvent := eventsAPIEvent.InnerEvent
switch ev := innerEvent.Data.(type) {
case *slackevents.TeamJoinEvent:
text := strings.Replace(h.config.WelcomeMessage, "\\n", "\n", -1)
api.PostMessage(ev.User.ID, slack.MsgOptionText(text, false))
}
}
}
// Start starts the server
func (h *HTTPHandler) Start() error {
if h.config.Env == config.EnvDevelopment {
return http.ListenAndServe(h.config.BindAddr, nil)
}
algnhsa.ListenAndServe(http.DefaultServeMux, nil)
return nil
}
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 Token
when 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.
Also Published Here