DEV Community

loading...
Cover image for Properly Welcome Users in Slack with Golang using Socket Mode

Properly Welcome Users in Slack with Golang using Socket Mode

Alexandre Couedelo
I embrace the DevOps culture since I started my career by contributing to the digital transformation of a leading financial institution in Canada.
Originally published at levelup.gitconnected.com ・8 min read

When creating a Slack Application to increase engagement, it is essential to start small meaningful one-on-one interaction. For instance, you can send a short tutorial on how to your App in the Slack App Home. You can also introduce the purpose and rules of a channel whenever a user joins, with a message only he can see as not to pollute other members.

I want this article to help you understand some core features of Slack Applications. Before moving on into coding, let me showcase two use cases and explain Slack's terminology. With this base setup, you will be able to create much more exciting interactions with your users. I consider those as the basis of any Slack application.

This tutorial guides you into implementing those two use cases I mentioned at the very beginning in Golang with the slack-go library and using Slack Socket Mode.

Why Socket Mode?

You don't need a public server. In other words, your laptop, your raspberry pi, or a private server can host your bot. Socket mode is perfect for a small application that you do not intend to distribute via App Directory

Use Case 1: Sending Ephemeral greeting message when a user joins a channel

Ephemeral messages are visible only by a specific user and are not persisted in the conversation. Those are ideal for reaction to user interaction such as joining a channel, answering with sensitive information, giving instruction, etc.

Mentioning the SlackBot triggers an Ephemeral message

Use Case 2: React to messages that mentioned your App or Bot with a message in your App Home

App Home is a dedicated space for your Slack Application. You can create a customized landing page, add an about page to document your App, and have a private message thread between your App and a user. Unlike Ephemeral messages, those messages will be persisted, meaning that all the tips and tricks you send will be easily accessible. Also, I personally prefer sending informative messages in the Slack App Home over sending them as a private message via a Bot. You want to convince your users that your App Home is the one place to find information about your App or Bot.

Joining a channel triggers a message in App Home

Tutorial Step 1: Configure your Application

Create a new Application. Please give it a cool name.

Create Application in Slack Backend UI

Activate Socket Mode in the appropriate section. Do not forget to save the Application token provided to you.

Activate Event Api and subscribe to the following event:

  • app_mentionned
  • member_join_channel

Activate Slack Event Api and subscribe to events

Go to OAuth & Permissions save the Bot User OAuth Token we will need it and add the following permissions:

  • chat:write - let you post messages in channels
  • users:read - In this example, I collect the user name to personalize the greeting message

Register OAuth scope in Slack Backend UI

Tutorial Step 2: Create the project repository

First, create a new go project and import slack-go library.

go mod init
go get -u github.com/slack-go/slack
Enter fullscreen mode Exit fullscreen mode

I use my fork of slack-go in this tutorial because one feature I am demonstrating has not yet been merged #PR904.

To use a fork, we need to add a replace statement in go.mod:

replace github.com/slack-go/slack => github.com/xnok/slack
Enter fullscreen mode Exit fullscreen mode

Then we force that change to be taken into consideration:

go mod tidy
Enter fullscreen mode Exit fullscreen mode

Then you can create the following project structure or refer to it as we progress in the tutorial

+ controllers
|`- greetingController.go
+ drivers
|`- Slack.go
+ views
|`+ greetingViewsAssets
| | `- greeting.json
|`- greetingViews.go
+ main.go
Enter fullscreen mode Exit fullscreen mode

I use an MVC structure and organize the files accordingly. Next, I will be explaining each of the elements and their implementation. You can find the complete code for my tutorial here. It might help to fork the code to follow along.

Tutorial Step 3: Foundation > main.go

The file main.go essentially manage initialization and dependencies. First, it read from a file the two Slack token we need:

SLACK_APP_TOKEN=xapp-xxxxxxxxx
SLACK_BOT_TOKEN=xoxb-xxxxxxxxx
Enter fullscreen mode Exit fullscreen mode

In created a small helper ConnectToSlackViaSocketmode to validate those two environment variables as well as instanciating the Slack client. See drivers/slack.go full code

Then we initialize our event listener and our Controller. Finally, we start the event loop so that the application begins receiving events we subscribed to from Slack.

func main() {

  // read bot token from .env file
    err := godotenv.Load("./test_slack.env")
    if err != nil {
        log.Fatal().Msg("Error loading .env file")
    }

    // Instantiate slack socket mode client
    client, err := drivers.ConnectToSlackViaSocketmode()
    if err != nil {
        log.Error().
            Str("error", err.Error()).
            Msg("Unable to connect to slack")

        os.Exit(1)
    }

    // Inject deps in event handler
    socketmodeHandler := socketmode.NewsSocketmodeHandler(client)

    // Inject deps to Controller
    controllers.NewGreetingController(socketmodeHandler)

    socketmodeHandler.RunEventLoop()

}
Enter fullscreen mode Exit fullscreen mode

See main.go full code

For that piece of code to do anything, we need to create our Controller so let get started.

Tutorial Step 4: Create the controller > greetingController.go

The flow we are trying to achieve with that Controller is the following:

Sequence diagram of the tutorial

The top part of our Controller manages dependencies and event handling. NewGreetingController is the initialization constructor for GreetingController. It used the EventHandler to register which event we want to listen to and which function to callback to process it. This Slack Application uses the Handler and Middleware design pattern to tackle every event, and if you need more explanation, you can read the dedicated article here

type GreetingController struct {
    EventHandler *socketmode.SocketmodeHandler
}

func NewGreetingController(eventhandler *socketmode.SocketmodeHandler) GreetingController {
    c := GreetingController{
        EventHandler: eventhandler,
    }

    // App Home (2)
    c.EventHandler.HandleEventsAPI(
        slackevents.AppMention,
        c.reactToMention,
    )

    // App Home (2)
    c.EventHandler.HandleEventsAPI(
        slackevents.MemberJoinedChannel,
        c.postGreetingMessage,
    )

    return c

}
Enter fullscreen mode Exit fullscreen mode

We will define the core logic of our Bot in reactToMention and postGreetingMessage. Let's dig into it.

reactToMention

There are five important steps in the code snippet:

  1. *socketmode.Event is a generic type of event, you register that function to receive AppMentionEvent, therefore you need to convert the incoming event, so it is more convenient to handle.
  2. You need to acknowledge that you received the message. It is specific to Socket Mode and may cause errors if you forget.
  3. We need the actual name of our User, so we use GetUserInfo. Slack provides only the UserID because this data may be considered sensitive.
  4. We generate your message using our View. We did not implement it yet!
  5. We send the message to our App Home. This is the tricky part. To send a message in your App Home you need to set the channelID as the UserID. In other words, your App Home channelID is the user UserID. I personally find that API implementation/convention a bit of a stretch, but now you Know!!
func (c *GreetingController) reactToMention(evt *socketmode.Event, clt *socketmode.Client) {
    // we need to cast our socketmode.Event into slackevents.AppMentionEvent
    evt_api, _ := evt.Data.(slackevents.EventsAPIEvent)
    evt_app_mention, ok := evt_api.InnerEvent.Data.(*slackevents.AppMentionEvent)

    clt.Ack(*evt.Request)

    if ok != true {
        log.Printf("ERROR converting event to slackevents.MemberJoinedChannelEvent: %v", ok)
    }

    userInfo, err := clt.GetApiClient().GetUserInfo(evt_app_mention.User)

    if err != nil {
        log.Printf("ERROR unable to retrive user info: %v", err)
    }

    // create the View using block-kit
    blocks := views.GreetingMessage(userInfo.Name)

    // Post greeting message (3) in User's App Home
    // Pass a user's ID as the value of channel to post to that User's App Home
    // We get the Api client from `clt`
    _, _, err = clt.GetApiClient().PostMessage(
        evt_app_mention.User,
        slack.MsgOptionBlocks(blocks...),
    )

    //Handle errors
    if err != nil {
        log.Printf("ERROR reactToMention: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

postGreetingMessage

We have the same five key points as reactToMention except for item 4; we send the Ephemeral Message via a dedicated function instead.

func (c *GreetingController) postGreetingMessage(evt *socketmode.Event, clt *socketmode.Client) {
    // we need to cast our socketmode.Event into slackevents.AppHomeOpenedEvent
    evt_api, _ := evt.Data.(slackevents.EventsAPIEvent)
    evt_member_join, ok := evt_api.InnerEvent.Data.(*slackevents.MemberJoinedChannelEvent)

    clt.Ack(*evt.Request)

    if ok != true {
        log.Printf("ERROR converting event to slackevents.AppMentionEvent: %v", ok)
    }

    userInfo, err := clt.GetApiClient().GetUserInfo(evt_member_join.User)

    if err != nil {
        log.Printf("ERROR unable to retrive user info: %v", err)
    }

    // create the View using block-kit
    blocks := views.GreetingMessage(userInfo.Name)

    // Post greeting message (3)
    // We get the Api client from `clt`
    _, err = clt.GetApiClient().PostEphemeral(
        evt_member_join.Channel,
        evt_member_join.User,
        slack.MsgOptionBlocks(blocks...),
    )

    //Handle errors
    if err != nil {
        log.Printf("ERROR postGreetingMessage: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

See controllers/greetingController.go) full code

Tutorial Step 5: Create the View> greetingViews.go

To make my life easy when creating Views for Slack I use a few tricks:

  1. I design my UI with Block-kit exclusively and save the JSON payload in a JSON file. See greetingViewsAssets/greeting.json for instance.
  2. I use Go template to render my View and inject variable elements such as user name.
  3. Since Golang 1.16, I use the embed package to access files embedded in my project.

If you need more explanation about those tricks, you can read the dedicated article here

Creating this View requires a few simple steps. First, rendering the template injects the user name into the message. Then you end up with a generated JSON that you can unmarshal into as Slack Message.

//go:embed greetingViewsAssets/*
var greetingAssets embed.FS

func GreetingMessage(user string) []slack.Block {

    // we need a stuct to hold template arguments
    type args struct {
        User string
    }

    tpl := renderTemplate(greetingAssets, "greetingViewsAssets/greeting.json", args{User: user})

    // we convert the View into a message struct
    view := slack.Msg{}

  // dump the renderd template into the view
    str, _ := ioutil.ReadAll(&tpl)
    json.Unmarshal(str, &view)

    // We only return the block because of the way the PostEphemeral function works
    // we are going to use slack.MsgOptionBlocks in the controller
    return view.Blocks.BlockSet
}
Enter fullscreen mode Exit fullscreen mode

See views/greetingViews.go full code

Try the app

Once you have successfully completed the tutorial, you can run your app:

go run ./main.go
Enter fullscreen mode Exit fullscreen mode

You can also directly clone my repository to try it beforehand.

Final thoughts

Completing this tutorial should provide you with a base to interact with your users. Technically speaking, you haven't learned from me how to send a regular message. But I believe it is essential to know how to use all communication methods offered by Slack to reduce notification fatigue. A horde of SlackBots can create a lot of noise if not well crafted.

From that point forward, you can look into more advanced features from Slack that bring your workspace's automation to the next level: App Home Tabs, Slash command, Shortcuts, Interaction, Workflows.

Links to understand Slack Terminology

Discussion (0)