Back

Develop a Slack-bot using Golang

Learn how to build a Slack bot in Golang with this step-by-step tutorial

by Percy Bolmér, August 13, 2021

By [Andrea De Santis] on Unsplash
By [Andrea De Santis] on Unsplash

Slack is a communication tool used by developers and companies to share information and communicate. It has grown very popular in recent years.

In this article, we will cover how to set up and build a bot that can interact with the Slack workspace and channels. We will look into how to create slash commands and visualized requests such as buttons. The bot application will be sending requests to a Go backend via Websocket, something called Socket-Mode in the slack world.

Create the Slack workspace

If you don’t already have a workspace to use, make sure to create a new one by visiting slack and press Create a new Workspace.

Slack — Create a new Workspace button
Slack — Create a new Workspace button

Go ahead and fill all the forms, you will need to provide a name for the team or company, a new channel name, and eventually invite other teammates.

Creating the Slack Application

The first thing we need to do is to create the Slack application. Visit the slack website to create the application. Select the From scratch option.

Slack — Create a new Application for our Bot
Slack — Create a new Application for our Bot

You will be presented with the option to add a Name to the application and the Workspace to allow the application to be used. You should be able to see all workspaces that you are connected to. Select the appropriate workspace.

There are many different use cases for an Application. You will be asked to select what features to add, we will create a bot so select the Bot option.

Slack — Features / Functions to add to the application
Slack — Features / Functions to add to the application

After clicking Bots you will be redirected to a Help information page, select the option to add scopes. The first thing we need to add to the application is the actual permissions to perform anything.

Slack — Add scopes to allow the Bot to perform actions
Slack — Add scopes to allow the Bot to perform actions

After pressing Review Scopes to Add, scroll down to Bot scopes and start adding the 4 scopes I’ve added. The explanation of the scopes is present in the image.

Slack — Adding bot permissions to scan channel and post messages
Slack — Adding bot permissions to scan channel and post messages

After adding the scopes we are ready to install the application. If you’re the owner of the application you can simply install it, otherwise, like in my case, I have to request permission from an Admin.

Slack — Request or Install application to workspace
Slack — Request or Install application to workspace

If you can Install or are allowed to install you will see yet another screen with information, select the appropriate channels the bot can use to post on as an application.

SlackBot — Installing the bot to the workspace
SlackBot — Installing the bot to the workspace

Once you click Allow you will see long strings, one OAuth token, and one Webhook URL. Remember the location of these, or save them on another safe storage.

Open your slack client and log in to the workspace. We need to invite the Application into a channel that we want him to be available in. I’ve used a channel named percybot.

Go there and start typing a command message which is done by starting the message with /. We can invite the bot by typing /invite @NameOfYourbot.

Slack — Invite the bot to the Channel where it can be used.
Slack — Invite the bot to the Channel where it can be used.

Connecting to Slack from Golang

Now that we have the Slack application up and the authentication token we can start communicating with the Slack channel.

We will be using goslack which is a library that supports the regular REST API, WebSockets, RTM, and Events. We will also use godotenv to read environment variables.

Let’s create a new golang package and download it.

mkdir slack-bot
cd slack-bot
go mod init programmingpercy/slack-bot
go get -u github.com/slack-go/slack
go get -u github.com/joho/godotenv

First of all, we will create a  .env file that can be used to store your secret token. We will also store a channel ID here. You can find the Token in the web UI where you created the application, the channel can be found in the UI if you select the channel and go to Get channel details by pressing the carrot arrow.

Slack — Press the yellow circled item to find the Channel ID.
Slack — Press the yellow circled item to find the Channel ID.
SLACK_AUTH_TOKEN="YourTokenGoesHere"
SLACK_CHANNEL_ID="YourSlackChannelID"
.env — The secrets that we will use in the bot

Create main.go so we can start coding. We will begin by simply connecting to the workspace and posting a simple message to make sure everything is working.

We will use godotenv to read in the  .env file. Then create a Slack Attachment, which is a message that is sent to the channel. What’s important to understand is that the Slack package leverages a pattern where most functions take a Configuration slice. What this means is that there are Option functions that can be added in each request, and a variable amount.

We will also add some Fields to the message which can be used to send extra contextual data.

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/joho/godotenv"
	"github.com/slack-go/slack"
)

func main() {

	// Load Env variables from .dot file
	godotenv.Load(".env")

	token := os.Getenv("SLACK_AUTH_TOKEN")
	channelID := os.Getenv("SLACK_CHANNEL_ID")

	// Create a new client to slack by giving token
	// Set debug to true while developing
	client := slack.New(token, slack.OptionDebug(true))
	// Create the Slack attachment that we will send to the channel
	attachment := slack.Attachment{
		Pretext: "Super Bot Message",
		Text:    "some text",
		// Color Styles the Text, making it possible to have like Warnings etc.
		Color: "#36a64f",
		// Fields are Optional extra data!
		Fields: []slack.AttachmentField{
			{
				Title: "Date",
				Value: time.Now().String(),
			},
		},
	}
	// PostMessage will send the message away.
	// First parameter is just the channelID, makes no sense to accept it
	_, timestamp, err := client.PostMessage(
		channelID,
		// uncomment the item below to add a extra Header to the message, try it out :)
		//slack.MsgOptionText("New message from bot", false),
		slack.MsgOptionAttachments(attachment),
	)

	if err != nil {
		panic(err)
	}
	fmt.Printf("Message sent at %s", timestamp)
}
main.go — Sending a simple Message on a slack channel

Execute the program by running the main function, you should then see a new message in the slack channel.

go run main.go
Slack — Our first bot message sent
Slack — Our first bot message sent

Using the Slack Events API

The slack events API is a way to handle events that occur in the Slack channels. There are many events, but for our bot, we want to listen to the mentions event. This means that whenever somebody mentions the bot it will receive an Event to trigger on. The events are delivered via WebSocket.

You can find all the event types available in the documentation.

The first thing you need to do is attend your Application in the web UI. We will activate something called Socket Mode, this allows the bot to connect via WebSocket. The alternative is to have the bot host a public endpoint, but then you need a domain to host on.

Slack — Enable Socket Mode to allow Websocket instead of HTTP
Slack — Enable Socket Mode to allow Websocket instead of HTTP

Then we also need to add Event Subscriptions. You can find it in the Features tab, enter it, and activate it. Then add the app_mentions scope to the Event subscriptions. This will make mentions trigger a new event to the application

Slack — Enable Event subscriptions for the application
Slack — Enable Event subscriptions for the application
Slack — Make sure to subscribe to the correct event, app_mentions
Slack — Make sure to subscribe to the correct event, app_mentions

The final thing we need to do is generate an Application token. Right now we only have a Bot token, but for the Events, we need an Application token.

Go into Settings->Basic Information and scroll down to the chapter called App-Level Tokens and press Generate Tokens and Scope and fill in a name for your Token.

Slack — Creating an App level token
Slack — Creating an App level token

I’ve added the connections:write scope to that token, make sure you save the Token as well by adding it to the  .env file as SLACK_APP_TOKEN.

SLACK_AUTH_TOKEN="Your Bot Token"
SLACK_APP_TOKEN="App Level token"
SLACK_CHANNEL_ID="ChannelID"
.env — Add all necessary fields to the.env

To use Socket Mode we also need to get a sub package of slack-go which is called socket mode.

go get github.com/slack-go/slack/socketmode

The slack package will need to create a new Client for the socket mode, so we will have two clients. One that uses the regular API and one for the websocket events. Let’s begin by connecting to make sure all permissions are correct. Notice how the Websocket client is created by calling socketmode.New and given the regular client as input. I’ve also added a OptionAppLevelToken to the creation of the regular client since that is now needed to connect to the Socket.

package main

import (
	"fmt"
	"log"
	"os"
	"time"

	"github.com/joho/godotenv"
	"github.com/slack-go/slack"
	"github.com/slack-go/slack/socketmode"
)

func main() {

	// Load Env variables from .dot file
	godotenv.Load(".env")

	token := os.Getenv("SLACK_AUTH_TOKEN")
	appToken := os.Getenv("SLACK_APP_TOKEN")
	// Create a new client to slack by giving token
	// Set debug to true while developing
	// Also add a ApplicationToken option to the client
	client := slack.New(token, slack.OptionDebug(true), slack.OptionAppLevelToken(appToken))
	// go-slack comes with a SocketMode package that we need to use that accepts a Slack client and outputs a Socket mode client instead
	socketClient := socketmode.New(
		client,
		socketmode.OptionDebug(true),
		// Option to set a custom logger
		socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)),
	)

	socketClient.Run()
}
main.go — Creating a Bot that is connected to the EventsAPI via Socketmode

Make sure to run the program and verify the output that is connected, there will be a ping hello sent.

socketmode: 2021/08/10 08:37:50 socket_mode_managed_conn.go:91: WebSocket connection succeeded on try 0
socketmode: 2021/08/10 08:37:50 socket_mode_managed_conn.go:439: Starting to receive message
socketmode: 2021/08/10 08:37:50 socket_mode_managed_conn.go:481: Incoming WebSocket message: {
  "type": "hello",
  "num_connections": 1,
  "debug_info": {
    "host": "applink-canary-5467cdc868-2w5t7",
    "build_number": 18,
    "approximate_connection_time": 18060
  },
  "connection_info": {
    "app_id": "A02BBLA9PEU"
  }
}

socketmode: 2021/08/10 08:37:50 socket_mode_managed_conn.go:493: Finished to receive message
socketmode: 2021/08/10 08:37:50 socket_mode_managed_conn.go:439: Starting to receive message
socketmode: 2021/08/10 08:37:50 socket_mode_managed_conn.go:336: Received WebSocket message: {"type":"hello","num_connections":1,"debug_info":{"host":"applink-canary-5467cdc868-2w5t7","build_number":18,"approximate_connection_time":18060},"connection_info":{"app_id":"A02BBLA9PEU"}}
socketmode: 2021/08/10 08:37:58 socket_mode_managed_conn.go:561: WebSocket ping message received: Ping from applink-canary-5467cdc868-2w5t7
main.go — Output from running the program

It’s time to start selecting all events to listen for. At the end of the program, we call socketClient.Run() which will be blocking and ingesting new Websocket messages on a channel at socketClient.Events. So we can use a for loop to continuously wait for new events, also the slack-go library comes with predefined Event types, so we can use a type switch to handle different types of Events easily. All events can be found here.

Since socketClient.Run() is blocking, we will spawn a goroutine that handles incoming messages in the background.

We will begin by simply logging in whenever an Event on the EventAPI is triggered in Slack. Since we first need to type switch the message on the websocket if it’s an EventsAPI type, then switch again based on the actual Event that occurred we will break the Event handling out into a separate function to avoid deeply nested switches.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/joho/godotenv"
	"github.com/slack-go/slack"
	"github.com/slack-go/slack/slackevents"
	"github.com/slack-go/slack/socketmode"
)

func main() {

	// Load Env variables from .dot file
	godotenv.Load(".env")

	token := os.Getenv("SLACK_AUTH_TOKEN")
	appToken := os.Getenv("SLACK_APP_TOKEN")
	// Create a new client to slack by giving token
	// Set debug to true while developing
	// Also add a ApplicationToken option to the client
	client := slack.New(token, slack.OptionDebug(true), slack.OptionAppLevelToken(appToken))
	// go-slack comes with a SocketMode package that we need to use that accepts a Slack client and outputs a Socket mode client instead
	socketClient := socketmode.New(
		client,
		socketmode.OptionDebug(true),
		// Option to set a custom logger
		socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)),
	)

	// Create a context that can be used to cancel goroutine
	ctx, cancel := context.WithCancel(context.Background())
	// Make this cancel called properly in a real program , graceful shutdown etc
	defer cancel()

	go func(ctx context.Context, client *slack.Client, socketClient *socketmode.Client) {
		// Create a for loop that selects either the context cancellation or the events incomming
		for {
			select {
			// inscase context cancel is called exit the goroutine
			case <-ctx.Done():
				log.Println("Shutting down socketmode listener")
				return
			case event := <-socketClient.Events:
				// We have a new Events, let's type switch the event
				// Add more use cases here if you want to listen to other events.
				switch event.Type {
				// handle EventAPI events
				case socketmode.EventTypeEventsAPI:
					// The Event sent on the channel is not the same as the EventAPI events so we need to type cast it
					eventsAPIEvent, ok := event.Data.(slackevents.EventsAPIEvent)
					if !ok {
						log.Printf("Could not type cast the event to the EventsAPIEvent: %v\n", event)
						continue
					}
					// We need to send an Acknowledge to the slack server
					socketClient.Ack(*event.Request)
					// Now we have an Events API event, but this event type can in turn be many types, so we actually need another type switch
					log.Println(eventsAPIEvent)
				}

			}
		}
	}(ctx, client, socketClient)

	socketClient.Run()
}
main.go — We now listen for any events and print them out

If you want to test it, run the program and then enter Slack and mention by bot by using @yourbotname.

go run main.go
Slack — Mentioning the bot
Slack — Mentioning the bot

You should be able to see the Event being logged in the command line running the bot.

{
  "envelope_id":"61b1699c-c2af-47fa-ba05-e1bfc5a0bb3b",
  "payload":{
    "token":"2Ns65jvtOBULYISLdEdinjyg",
    "team_id":"T02B0KKSX4Z",
    "api_app_id":"A02BBLA9PEU",
    "event":{
      "client_msg_id":"e64f8121-a4e0-45a9-8f10-670f229bf242",
      "type":"app_mention",
      "text":"<@U02AN8BG2E7>",
      "user":"U02AN1FAY4S",
      "ts":"1628599090.000200",
      "team":"T02B0KKSX4Z",
      "blocks":[
        {"type":"rich_text","block_id":"N3sMC","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U02AN8BG2E7"}]}]}
      ],
      "channel":"C02A79GSNG7",
      "event_ts":"1628599090.000200"
    },
    "type":"event_callback",
    "event_id":"Ev02BEH5MBQ8",
    "event_time":1628599090,
    "authorizations":[{"enterprise_id":null,"team_id":"T02B0KKSX4Z","user_id":"U02AN8BG2E7","is_bot":true,"is_enterprise_install":false}],
    "is_ext_shared_channel":false,
    "event_context":"4-eyJldCI6ImFwcF9tZW50aW9uIiwidGlkIjoiVDAyQjBLS1NYNFoiLCJhaWQiOiJBMDJCQkxBOVBFVSIsImNpZCI6IkMwMkE3OUdTTkc3In0"},
  "type":"events_api",
  "accepts_response_payload":false,
  "retry_attempt":0,
  "retry_reason":""
}
main.go — The output of the event log

Look at the Event printed and you will understand why we need to use multiple type switches. The event we get is of the type event_callback, and that event contains a payload with the actual event that was performed.

So first we need to test if it’s a Callback event and then if it’s an app_mention payload event.

Let’s implement the handleEventMessage that will continue the type switching. We can use the type field to know how to handle the Event. Then we can reach the payload event by using the InnerEvent field.

// handleEventMessage will take an event and handle it properly based on the type of event
func handleEventMessage(event slackevents.EventsAPIEvent) error {
	switch event.Type {
	// First we check if this is an CallbackEvent
	case slackevents.CallbackEvent:

		innerEvent := event.InnerEvent
		// Yet Another Type switch on the actual Data to see if its an AppMentionEvent
		switch ev := innerEvent.Data.(type) {
		case *slackevents.AppMentionEvent:
			// The application has been mentioned since this Event is a Mention event
			log.Println(ev)
		}
	default:
		return errors.New("unsupported event type")
	}
	return nil
}
handleEventMessagev1 — Function to handle the callback event and see if it’s an AppMentionEvent

Replace the previous log in the main function that printed the event with the new handleEventMessage function instead.

// Now we have an Events API event, but this event type can in turn be many types, so we actually need another type switch
err := handleEventMessage(eventsAPIEvent)
if err != nil {
  // Replace with actual err handeling
	log.Fatal(err)
}
Main.go — Use handleEventMessage instead of nesting type switches

Now logging the event does not make a fun bot. We should make the bot respond to the user who mentioned him and if they said hello it should also greet them.

Begin by logging into the application and adding the users:read scope to the bot token. I trust you to make that without guidance now, or go back and read how we did before.

Once that’s done we will create the handleAppMentionEvent function. This function will take a *slackevents.AppMentionEvent and a slack.Client as input so it can respond.

The event does contain the user ID in the event.User so we can use that ID to grab user information. The channel to respond to is also available in the event.Channel. The final piece of information we need is the actual message the user sent when mentioning, which is found in the event.Text.

// handleAppMentionEvent is used to take care of the AppMentionEvent when the bot is mentioned
func handleAppMentionEvent(event *slackevents.AppMentionEvent, client *slack.Client) error {

	// Grab the user name based on the ID of the one who mentioned the bot
	user, err := client.GetUserInfo(event.User)
	if err != nil {
		return err
	}
	// Check if the user said Hello to the bot
	text := strings.ToLower(event.Text)

	// Create the attachment and assigned based on the message
	attachment := slack.Attachment{}
	// Add Some default context like user who mentioned the bot
	attachment.Fields = []slack.AttachmentField{
		{
			Title: "Date",
			Value: time.Now().String(),
		}, {
			Title: "Initializer",
			Value: user.Name,
		},
	}
	if strings.Contains(text, "hello") {
		// Greet the user
		attachment.Text = fmt.Sprintf("Hello %s", user.Name)
		attachment.Pretext = "Greetings"
		attachment.Color = "#4af030"
	} else {
		// Send a message to the user
		attachment.Text = fmt.Sprintf("How can I help you %s?", user.Name)
		attachment.Pretext = "How can I be of service"
		attachment.Color = "#3d3d3d"
	}
	// Send the message to the channel
	// The Channel is available in the event message
	_, _, err = client.PostMessage(event.Channel, slack.MsgOptionAttachments(attachment))
	if err != nil {
		return fmt.Errorf("failed to post message: %w", err)
	}
	return nil
}
handleAppMentionEvent — The handler for mentions in the bot

To begin using this function we need to add the Client as an input parameter as well. So we have to update handleEventMessage to accept it.

// handleEventMessage will take an event and handle it properly based on the type of event
func handleEventMessage(event slackevents.EventsAPIEvent, client *slack.Client) error {
	switch event.Type {
	// First we check if this is an CallbackEvent
	case slackevents.CallbackEvent:

		innerEvent := event.InnerEvent
		// Yet Another Type switch on the actual Data to see if its an AppMentionEvent
		switch ev := innerEvent.Data.(type) {
		case *slackevents.AppMentionEvent:
			// The application has been mentioned since this Event is a Mention event
			err := handleAppMentionEvent(ev, client)
			if err != nil {
				return err
			}
		}
	default:
		return errors.New("unsupported event type")
	}
	return nil
}
handleEventMessage — Now accepting the client as input parameter

Restart the program and try saying Hello and also saying something else to see that it works as expected. If you get a “missing_scope” error you have missed some scope.

All scopes currently needed to make to bot run
All scopes currently needed to make to bot run

Here is the output of my currently running bot

Slack — The bot responds as expected
Slack — The bot responds as expected

It’s time to move forward and looking at how to add Slash commands.

Adding slash commands to the Slack bot

Most commonly I’ve seen slash commands being used by Slack bots. What this means is you can type /send a special command. There are many built-in commands such as /call which allows you to start a call etc.

We will be adding a custom command which will be /hello. When this command is triggered we will make the bot send a greetings message.

Again, you need to add the command in the web UI. Visit the website and select the Slash Command in the features tab.

Slack — Add a new Slash command in the UI.
Slack — Add a new Slash command in the UI.

We will make a command that accepts a single parameter which is the username to greet. Fill in the fields asked for, note that we are using socket-mode so we don’t need to provide a request URL.

Slack — Adding a new slash command to hello usernames
Slack — Adding a new slash command to hello usernames

Don’t forget to reinstall the application after we have added the command. This is needed since we have changed the application. If you have forgotten how then revisit the earlier part of the article where we installed the application. You can verify that everything has been installed by opening up the slack and the channel where the application is invited and type the /hello command.

Percy-Bot showing up when typing /hello
Percy-Bot showing up when typing /hello

That was easy enough, let’s also redo what we did with the EventsAPI, but this time we will add a type switch for EventTypeSlashCommand.

We will find the command called in the SlashCommand.Command and the input text in SlashCommand.Text. So we will first route the command based on the input of the command and then return greetings to the text field. Begin by updating the main.go file to include the listener for the new type of message events on the websocket.

func main() {

	// Load Env variables from .dot file
	godotenv.Load(".env")

	token := os.Getenv("SLACK_AUTH_TOKEN")
	appToken := os.Getenv("SLACK_APP_TOKEN")
	// Create a new client to slack by giving token
	// Set debug to true while developing
	// Also add a ApplicationToken option to the client
	client := slack.New(token, slack.OptionDebug(true), slack.OptionAppLevelToken(appToken))
	// go-slack comes with a SocketMode package that we need to use that accepts a Slack client and outputs a Socket mode client instead
	socketClient := socketmode.New(
		client,
		socketmode.OptionDebug(true),
		// Option to set a custom logger
		socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)),
	)

	// Create a context that can be used to cancel goroutine
	ctx, cancel := context.WithCancel(context.Background())
	// Make this cancel called properly in a real program , graceful shutdown etc
	defer cancel()

	go func(ctx context.Context, client *slack.Client, socketClient *socketmode.Client) {
		// Create a for loop that selects either the context cancellation or the events incomming
		for {
			select {
			// inscase context cancel is called exit the goroutine
			case <-ctx.Done():
				log.Println("Shutting down socketmode listener")
				return
			case event := <-socketClient.Events:
				// We have a new Events, let's type switch the event
				// Add more use cases here if you want to listen to other events.
				switch event.Type {
				// handle EventAPI events
				case socketmode.EventTypeEventsAPI:
					// The Event sent on the channel is not the same as the EventAPI events so we need to type cast it
					eventsAPIEvent, ok := event.Data.(slackevents.EventsAPIEvent)
					if !ok {
						log.Printf("Could not type cast the event to the EventsAPIEvent: %v\n", event)
						continue
					}
					// We need to send an Acknowledge to the slack server
					socketClient.Ack(*event.Request)
					// Now we have an Events API event, but this event type can in turn be many types, so we actually need another type switch
					err := handleEventMessage(eventsAPIEvent, client)
					if err != nil {
						// Replace with actual err handeling
						log.Fatal(err)
					}
				// Handle Slash Commands
				case socketmode.EventTypeSlashCommand:
					// Just like before, type cast to the correct event type, this time a SlashEvent
					command, ok := event.Data.(slack.SlashCommand)
					if !ok {
						log.Printf("Could not type cast the message to a SlashCommand: %v\n", command)
						continue
					}
					// Dont forget to acknowledge the request
					socketClient.Ack(*event.Request)
					// handleSlashCommand will take care of the command
					err := handleSlashCommand(command, client)
					if err != nil {
						log.Fatal(err)
					}

				}
			}

		}
	}(ctx, client, socketClient)

	socketClient.Run()
}
Main.go — Added a case for EventTypeSlashCommand messages.

Do not forget to send the Acknowledgement, or else you will see an error message in slack that the message was not dispatched properly.

Slack — If you forget to acknowledge the retrieval of the websocket message
Slack — If you forget to acknowledge the retrieval of the websocket message

We will have a router function called handleSlashCommand which will simply redirect to another function. This might seem overkill for now, but if you plan to add more functions it’s easier to make multiple small functions. Especially if you are using unit tests.

The actual response will come from handleHelloCommand which will simply take the username set after the /hello command and send a greeting in the channel.

// handleSlashCommand will take a slash command and route to the appropriate function
func handleSlashCommand(command slack.SlashCommand, client *slack.Client) error {
	// We need to switch depending on the command
	switch command.Command {
	case "/hello":
		// This was a hello command, so pass it along to the proper function
		return handleHelloCommand(command, client)
	}

	return nil
}

// handleHelloCommand will take care of /hello submissions
func handleHelloCommand(command slack.SlashCommand, client *slack.Client) error {
	// The Input is found in the text field so
	// Create the attachment and assigned based on the message
	attachment := slack.Attachment{}
	// Add Some default context like user who mentioned the bot
	attachment.Fields = []slack.AttachmentField{
		{
			Title: "Date",
			Value: time.Now().String(),
		}, {
			Title: "Initializer",
			Value: command.UserName,
		},
	}

	// Greet the user
	attachment.Text = fmt.Sprintf("Hello %s", command.Text)
	attachment.Color = "#4af030"

	// Send the message to the channel
	// The Channel is available in the command.ChannelID
	_, _, err := client.PostMessage(command.ChannelID, slack.MsgOptionAttachments(attachment))
	if err != nil {
		return fmt.Errorf("failed to post message: %w", err)
	}
	return nil
}
handleSlashCommand — Router function that reroutes /hello to the correct function

Restart the program and try sending a command from the slack client. When I input /hello reader I see the following output.

Slack — Output from the /hello command
Slack — Output from the /hello command

Advance slash commands and interactions

We will look at how we can implement a slash command which triggers the bot to ask a question that we can answer with a Yes and No button.

Begin by adding the new command in the Slack web UI so we can trigger it.

Slack — Adding the new command
Slack — Adding the new command

We will make a small change to the main function first, In the type switch that accepts the Slash commands events, we currently acknowledge before we process the message. We will change this and return the response in the Acknowledge since this is possible.

// Handle Slash Commands
case socketmode.EventTypeSlashCommand:
	// Just like before, type cast to the correct event type, this time a SlashEvent
	command, ok := event.Data.(slack.SlashCommand)
	if !ok {
		log.Printf("Could not type cast the message to a SlashCommand: %v\n", command)
		continue
	}
	// handleSlashCommand will take care of the command
	payload, err := handleSlashCommand(command, client)
	if err != nil {
		log.Fatal(err)
	}
	// Dont forget to acknowledge the request and send the payload
        // The payload is the response
	socketClient.Ack(*event.Request, payload)
main.go — Update the type switch where we accept Slash commands

Now you will see how easily we can add new commands, all we need to do is add a new case option in handleSlashCommand to check for. Of course, we need to handle the actual command as well, but the structure is easily scaled. We will update handleSlashCommand so that it returns an interface{} also. This is the payload response that will be included in the acknowledgment.


// handleSlashCommand will take a slash command and route to the appropriate function
func handleSlashCommand(command slack.SlashCommand, client *slack.Client) (interface{}, error) {
	// We need to switch depending on the command
	switch command.Command {
	case "/hello":
		// This was a hello command, so pass it along to the proper function
		return nil, handleHelloCommand(command, client)
	case "/was-this-article-useful":
		return handleIsArticleGood(command, client)
	}

	return nil, nil
}
handleSlashCommand — We now route both slash commands to their appropriate handler

We will route to a function called handleIsArticleGood that will trigger a two-button questionnaire to the user using something called Block-Kit. It’s a Slack implementation that allows us to send HTML components. There are a ton of options and components to send, but let’s stick to buttons for now.

The blocks are added to the slack.Attachment that we used previously to send simple messages. It has a field called Blocks which accepts an array of blocks to send. Each block is a visual component to send.

We will be using a Section block, and the Slack library helps us create one using the NewSectionBlock() which will accept a few parameters.

The first parameter is a slack.TextBlockObject which is a standard way of sending text contains the type to use, in which we will use markdown. It also contains the value to display in the text block.

The second parameter is fields to add, such as we used before to add contextual data, let’s leave it as nil. The third parameter is a slack.Accessory which a container for a block element, you can find the JSON layout in the slack documentation. We will add a Checkbox element to the Accessory, which contains two options, [Yes, No]. Remember that we simply return the response, in this case, we don’t send it as in the hello handler. Notice the answer in the CheckBoxGroupsBlockElement, this is the action that is used to identify what kind of interaction was performed.

// handleIsArticleGood will trigger a Yes or No question to the initializer
func handleIsArticleGood(command slack.SlashCommand, client *slack.Client) (interface{}, error) {
	// Create the attachment and assigned based on the message
	attachment := slack.Attachment{}

	// Create the checkbox element
	checkbox := slack.NewCheckboxGroupsBlockElement("answer",
		slack.NewOptionBlockObject("yes", &slack.TextBlockObject{Text: "Yes", Type: slack.MarkdownType}, &slack.TextBlockObject{Text: "Did you Enjoy it?", Type: slack.MarkdownType}),
		slack.NewOptionBlockObject("no", &slack.TextBlockObject{Text: "No", Type: slack.MarkdownType}, &slack.TextBlockObject{Text: "Did you Dislike it?", Type: slack.MarkdownType}),
	)
	// Create the Accessory that will be included in the Block and add the checkbox to it
	accessory := slack.NewAccessory(checkbox)
	// Add Blocks to the attachment
	attachment.Blocks = slack.Blocks{
		BlockSet: []slack.Block{
			// Create a new section block element and add some text and the accessory to it
			slack.NewSectionBlock(
				&slack.TextBlockObject{
					Type: slack.MarkdownType,
					Text: "Did you think this article was helpful?",
				},
				nil,
				accessory,
			),
		},
	}

	attachment.Text = "Rate the tutorial"
	attachment.Color = "#4af030"
	return attachment, nil
}
handleIsArticleGood — The handler that builds our visual response that is sent in the acknowledgement

Restart your bot and try executing the command in slack.

Slack — Response from the /was-this-article-useful command
Slack — Response from the /was-this-article-useful command

When you select something nothing will happen since we don’t accept the response in the backend yet. The response is will trigger an Interaction Event, so if we want to accept the response we need to listen for this event in the main function.

The process is the same as before, typecast into the correct message type. In this case, it’s an InteractionCallback.

case socketmode.EventTypeInteractive:
  interaction, ok := event.Data.(slack.InteractionCallback)
	if !ok {
	  log.Printf("Could not type cast the message to a Interaction callback: %v\n", interaction)
		continue
  }

	err := handleInteractionEvent(interaction, client)
	if err != nil {
		log.Fatal(err)
	}
	socketClient.Ack(*event.Request)
//end of switch
}
main.go — Adding support for interactions

We will add a handleInteractionEvent that will simply print information about the interaction and the selected option.

func handleInteractionEvent(interaction slack.InteractionCallback, client *slack.Client) error {
	// This is where we would handle the interaction
	// Switch depending on the Type
	log.Printf("The action called is: %s\n", interaction.ActionID)
	log.Printf("The response was of type: %s\n", interaction.Type)
	switch interaction.Type {
	case slack.InteractionTypeBlockActions:
		// This is a block action, so we need to handle it

		for _, action := range interaction.ActionCallback.BlockActions {
			log.Printf("%+v", action)
			log.Println("Selected option: ", action.SelectedOptions)

		}

	default:

	}

	return nil
}
handleInteractionEvent — Printing the information of the interaction

Try executing the command, and select an option.

2021/08/11 08:52:49 The response was of type: block_actions
2021/08/11 08:52:49 &{ActionID:answer BlockID:n3t Type:checkboxes Text:{Type: Text: Emoji:false Verbatim:false} Value: ActionTs:1628664769.502596 SelectedOption:{Text:<nil> Value: Description:<nil> URL:} SelectedOptions:[{Text:0xc0003b2f00 Value:yes Description:0xc0003b2f30 URL:}] SelectedUser: SelectedUsers:[] SelectedChannel: SelectedChannels:[] SelectedConversation: SelectedConversations:[] SelectedDate: SelectedTime: InitialOption:{Text:<nil> Value: Description:<nil> URL:} InitialUser: InitialChannel: InitialConversation: InitialDate: InitialTime:}
2021/08/11 08:52:49 Selected option:  [{0xc0003b2f00 yes 0xc0003b2f30 }]
Output from the interaction when pressing Yes/No

Conclusion

We have covered most of the items needed to get started with building your bot.

We have covered these topics

  • How to set up a Slack-Bot application
  • Connecting to the application from a Golang service
  • Listening for bot mentions in a channel
  • Adding slash commands to the bot
  • Using the Slack Events API
  • Sending visualized blocks to Slack
  • Listening for User interactions

This is it for this time, hopefully you have enjoyed the article. As always feel free to reach out and give me feedback or questions.

Now go out there and build some bots!

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter