Writing an Interactive Message Bot for Slack in Golang

Hey everyone, @deeeet here, from the Site Reliability Engineering (SRE) team.

At Mercari, we use Slack Bots to automate a lot of our tasks. For example, releases of our main API are automated with a bot, and over 10 releases per day occur on Slack across Mercari JP, US and UK.

At first, Slack Bots were mainly text-based, with the exception of being able to return an image, e.g., graph, as a response. However, bots have gotten much more useful since Slack started offering interactive messages, allowing us to add dropdown menus, buttons, and more!

Buttons have actually been available for a while, but using them meant you had to open up your bot to other teams, and also support OAuth–basically, there were quite a few hurdles to overcome. But once Slack started offering internal integrations, it became super simple to develop bots using interactive messages for use within your own team.

Mercari already uses multiple interactive Slack Bots, and we continue to develop more as new needs arise. In this article, I will explain how to write a bot with interactive messages. The sample code is all public (tcnksm/go-slack-interactive), so feel free to fork it and create a unique bot for yourself! (If you want to use Node, you might want to take a look at Slack’s sample Node app.)

Benefits of Interactive Messages

What’s so good about making a bot with interactive messages, you ask?? Because who doesn’t like clicking buttons!buttons are awesome!

With a single button, you can start a process and confirm that its requirements have been met. For really important processes, you can limit the number of people that can press the button and create an easy approval flow. If you use a menu, you can show the user a list of selections from the bot and get a preset response, which makes data validation easier compared to free-text input. Additionally, buttons are easier to use in the Slack mobile app.

It’s probably already obvious, but the main motivation behind making a bot is to collaborate with other team members in your channel. “If we’re all engineers, why not just use a command line tool?” you may ask, but our teammates are not all engineers. That’s what makes Slack bots great–they are accessible to everyone.

An Example at Mercari

At Mercari, we use interactive messages with a bot like the one below.

Kubernetes Deploy Bot

For Mercari US, we run some services on Kubernetes (GKE). (We generally use Kubernetes when making new microservices, too.) The Kubernetes Deploy Bot deploys a newly made docker image to our production cluster. When the user asks the bot to deploy, the bot shows the user a list of deployable images (tags) in a menu, and the selected one is deployed to the cluster.

In addition to this, this bot also makes a pull request by rewriting the Docker image version in Kubernetes manifest file, so we have ensured that the repository setting file and the actual image version on cluster is same.

Account Bot

SRE are in charge of creating creating VPNs and accounts to access designated servers. In the past, the SRE were manually logging into the server, but this is now automated with a bot. When a user makes an account creation request, the bot requests approval from an SRE, who clicks the “accept” button. Thus, while it is automated after the approval, we ensure that the necessary approvals are in place before anyone creates an account.

By the way, this bot works on Google App Engine, and when it receives an account creation request, it submits a job to Google Cloud Pub Sub. Workers that actually create accounts are running in each of our three regions (JP, US and UK), and each subscribes to the appropriate topics for each region.

Bot Example

Below are instructions on how to make a bot with interactive messages, including some sample code.

As an example, we’ll create @beerbot in order to order beer. (Unfortunately, it doesn’t really order beer. :( ) When you chat with the bot, it shows a menu with a list of the beers that you can order. You can then use a button to confirm your order, or cancel. See how it works here:

Slack App Preparation

Now we’ll get into actually making the bot. Interactive Message Bots need to be created as a Slack App. Firstly, we’ll make a new app.

Next, add a Bot user from the features panel and make Interactive Message active. At this point, it is necessary to set a specialized request URL. The results of a user’s action to an interactive message will be posted to this URL.

Lastly, we install the Slack app we made into the team Slack. From doing this, we can get a verification token to verify the bot’s User OAuth Access Token and Interactive Message request on the server side required for the bot to access the Slack API. This value is required to operate the Bot. With this, the preparation is finished.

Bot Development with Golang

Now, we will actually start writing the bot. It will need to

  • Watch for Slack events and respond appropriately (Slack Event Listener)
  • Receive the results of a user’s interaction with our menu and buttons (Interactive Handler)

Slack Event Listener

Firstly, we write the Slack Event Listener. In this example, it would respond to the message event “@beerbot hey” and show a menu to the user. For the Slack API Client, we use the nlopes/slack package. We write the below listener and watch for the necessary MessageEvent.

func (s *SlackListener) ListenAndResponse() { 
// Start listening slack events
rtm := s.client.NewRTM()
go rtm.ManageConnection()

// Handle slack events
for msg := range rtm.IncomingEvents {
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
if err := s.handleMessageEvent(ev); err != nil {
log.Printf(“[ERROR] Failed to handle message: %s”, err)
}
}
}
}

Next, we’ll write the function below to process the MessageEvent.

func (s *SlackListener) handleMessageEvent(ev *slack.MessageEvent) error

In the function, firstly we validate the MessageEvent. At the very least, we need to confirm the following:

  • Whether a message comes from expected channel (You must limit channel where bot work. For example, bots that do releases should only work in the release channel).
  • Whether the bot has been mentioned

Next, the actual thing said to the bot (MessageEvent.Msg.Text) needs to be parsed. We can think of this in the same way as when we are writing a command line tool. The parsed message can inform the menu or other elements shown. (In this example, it just responds to “hey”.)

Next we actually show the menu. We will use the Attachment field of the Slack Post Message API to show the menu. When written with Golang, it will be like the below.

var attachment = slack.Attachment{
Text: “Which beer do you want? :beer:”,
Color: “#f9a41b”,
CallbackID: “beer”,
Actions: []slack.AttachmentAction{
{
Name: actionSelect,
Type: “select”,
Options: []slack.AttachmentActionOption{
{
Text: “Asahi Super Dry”,
Value: “Asahi Super Dry”,
},
{
Text: “Kirin Lager Beer”,
Value: “Kirin Lager Beer”,
},
{
Text: “Sapporo Black Label”,
Value: “Sapporo Black Label”,
},
{
Text: “Suntory Malt’s”,
Value: “Suntory Malts”,
},
{
Text: “Yona Yona Ale”,
Value: “Yona Yona Ale”,
},
},
},
{
Name: actionCancel,
Text: “Cancel”,
Type: “button”,
Style: “danger”,
},
},
}

Firstly, we select either a menu (“select” type) or button for AttachmentAction.Type . AttachmentAction.Options slice will appear in the menu options (beer brands in this case). All that’s left to do is post this in the channel that received the event with the Slack Post API. After doing so, the menu will appear as below.

Interactive Handler

Next, we’ll write the handler that receives the results of the interactive message. See the example handler below:

func (h interactionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
log.Printf(“[ERROR] Invalid method: %s”, r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

buf, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf(“[ERROR] Failed to read request body: %s”, err)
w.WriteHeader(http.StatusInternalServerError)
return
}

jsonStr, err := url.QueryUnescape(string(buf)[8:])
if err != nil {
log.Printf(“[ERROR] Failed to unescape request body: %s”, err)
w.WriteHeader(http.StatusInternalServerError)
return
}

var message slack.AttachmentActionCallback
if err := json.Unmarshal([]byte(jsonStr), &message); err != nil {
log.Printf(“[ERROR] Failed to decode json message from slack: %s”, jsonStr)
w.WriteHeader(http.StatusInternalServerError)
return
}

// Only accept message from slack with valid token
if message.Token != h.verificationToken {
log.Printf(“[ERROR] Invalid token: %s”, message.Token)
w.WriteHeader(http.StatusUnauthorized)
return
}
...
}

The above examples are for the first half handler processing. (This will be the same for any interactive message) It is processing like the one shown below.

  • Determining whether it is POST
  • Unescape the payload
  • Unmarshal the unescaped payload to slack.AttachmentActionCallback
  • Check that the token of the message received matches the verification token issued when you registered the Slack App.

All we need to do now is to process the result of the user’s action. We’ll write this part like this;

func (h interactionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
action := message.Actions[0]
switch action.Name {
case actionSelect:
value := action.SelectedOptions[0].Value
// Overwrite original drop down message.
originalMessage := message.OriginalMessage
originalMessage.Attachments[0].Text = fmt.Sprintf(“OK to order %s ?”, strings.Title(value))
originalMessage.Attachments[0].Actions = []slack.AttachmentAction{
{
Name: actionStart,
Text: “Yes”,
Type: “button”,
Value: “start”,
Style: “primary”,
},
{
Name: actionCancel,
Text: “No”,
Type: “button”,
Style: “danger”,
},
}
w.Header().Add(“Content-type”, “application/json”)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(&originalMessage)
return
case actionStart:
title := “:ok: your order was submitted! yay!”
responseMessage(w, message.OriginalMessage, title, “”)
return
case actionCancel:
title := fmt.Sprintf(“:x: @%s canceled the request”, message.User.Name)
responseMessage(w, message.OriginalMessage, title, “”)
return
default:
log.Printf(“[ERROR] ]Invalid action was submitted: %s”, action.Name)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

At this point, we will do processing for each action received. For dividing up the actions, we will use the definitions in AttachmentAction.Name. This time, we defined the below three actions.

  • actionSelect — Selecting a menu option
  • actionStart — Confirming an order
  • actionCancel — Canceling an order

For example, in the case of actionSelect, we determine their selection (beer brand) and confirm whether they really want to place an order with a button. In the event of actionCancel, the fact that the order was cancelled is returned as a response.

With Interactive Messages, the response to the user’s action overwrites their original message. This is to stop people from being able to press the button after they’ve sent the request. In order to do this, it is convenient to have the below function prepared. We delete the AttachmentAction of buttons etc. and return a response that overwrites the old message with a new one.

func responseMessage(w http.ResponseWriter, original slack.Message, title, value string) {
original.Attachments[0].Actions = []slack.AttachmentAction{} // empty buttons
original.Attachments[0].Fields = []slack.AttachmentField{
{
Title: title,
Value: value,
Short: false,
},
}
w.Header().Add(“Content-Type”, “application/json”)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(&original)
}

It is important to show who executed the action in the action response. If you don’t do this, you’ll lose track of who initiated the action in the first place. With this bot, it has been made to allow people to know who pressed the button. (For help designing interactive buttons, see guidelines for building messages.)

Conclusion

In this article, I explained how to develop and write a Slack bot with interactive messages in Golang. I hope it helps you to create easy-to-use bots with interactive messages to automate time-consuming tasks! At Mercari, we are always looking for SREs who love automation and Golang. See our hiring page for more details!