Using the Events API to Build a Slack Bot

Written by gen1us2k | Published 2022/07/18
Tech Story Tags: slack-app | slack | go | aws-lambda | slack-bot | slack-integration | hack-slack | building-a-slack-app

TLDRThe Slack Events API is hosted on AWS Lambda. The bot is a custom Slack tool for onboarding new members in the community Slack. It's easy to implement, developer-friendly, and works on top of HTTP1/1.1. Websocket is an excellent protocol, but it's built on-top-of HTTP/1 and keeps the connection open. In the next part of the blog post, I’ll tell you how to build this bot using Slack API.via the TL;DR App

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.

Motivation

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:

  1. Message each new member personally.
  2. Ask them what their biggest challenges or struggles are or what they want to achieve by joining the Ory community.
  3. Point them to the right place or people who can help with that challenge or help them achieve their goals.

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:

  • Ping the Ory DevRel team and notify them to greet a user.
  • Send a greeting message from a Slack user.

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.

Using Slack API

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.

Websockets

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:

  • Vertical scaling. You add more resources such as RAM/disk/CPU or buy a new server.
  • Horizontal scaling. You add more servers. You can move a database, webserver, or something else to a different server. Sometimes you need to scale your application horizontally.

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.

Events API

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:

  • Use their signing tokens for incoming requests.
  • Handle the URL verification challenge by your app. The app needs to sign the incoming request and return the incoming challenge parameter.

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.

![Illustration of Gopher at work](https://cdn.hackernoon.com/images/illustration-of-gopher-at-work.jpg"Illustration of Gopher at work")

Let's build our bot, huh?

Implementing the bot

We will use the Go programming language to build our bot. What we need:

  • Configuration for the bot using 12factors.net. In that case we will use the envconfig package because of its simplicity.
  • net/http package for local development and testing.
  • Ngrok to make our web app accessible from the internet.
  • AWS Go SDK.

Implementing configuration

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 ./…

Slack events API handler

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:

  • Make it configurable what message we want to send as welcome.
  • Support of AWS Lambda since it has its SDK. To get support for Go we can use the algnhsa library. Check out the code below.

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())
}

Creating Slack bot

  1. Press Create New App button on Slack apps page.
  2. Click on From scratch and then input your bot name and choose a workspace to install the bot.
  3. Copy the Signing Secret and store it somewhere. You'll need it to configure AWS Lambda function.
  4. Go to the 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.
  5. Under the scopes add chat:write for both the Bot Token and User token scopes.

Deploying to AWS Lambda

  1. Sign in to AWS Console.
  2. Go to the Lambda section and press Create function button.
  3. Choose 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.
  4. Open Configuration -> Environment variables.
  5. Set your Signing Token to SLACK_SIGNING_SECRET variable.
  6. Set your welcome message to WELCOME_MESSAGE variable.
  7. Set your bot token to SLACK_BOT_TOKEN variable.
  8. Set production to ENV variable.
  9. Go to the Code section and scroll down to Runtime settings and change Handler from hello to mrrobot.

Configuring AWS Gateway

  1. Create a new REST API.
  2. In the "Resources" section create a new ANY method to handle requests to / (check "Use Lambda Proxy Integration").
  3. Add a catch-all {proxy+} resource to handle requests to every other path (check "Configure as proxy resource").

Final steps

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
  1. Run make zip.
  2. Open your function on AWS Lambda.
  3. Choose Upload from under the source code section.
  4. Upload it and run.
  5. Choose your application on Apps page on Slack, click on Add features and functionality and choose Event subscriptions.
  6. Insert URL of your API Gateway to the Request URL field, pass the verification step, and click Save.
  7. Open 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.

Conclusion and takeaways

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


Written by gen1us2k | Software Engineer
Published by HackerNoon on 2022/07/18