DEV Community

loading...
Cover image for Build Your Slack App Home in Golang Using Socket Mode

Build Your Slack App Home in 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 betterprogramming.pub ・10 min read

This tutorial feature implementing an App Home in Golang with the slack-go library and using Slack Socket Mode. It is inspired by this article in the slack documentation.

What is an App home?

App home is that space with your App's name that appears under the App section in the conversation list. It is a fully customizable space to provide documentation and interaction with your App.

App Homes in Slack UI

Why Socket Mode, you may ask?

With socket mode, you don't need a server with a publicly available IP address. In other words, your laptop, your raspberry pi, or a private server can host your bot. Socket mode is perfect for small Application that you do not intend to distribute via App Directory

Step 1: Configure your Application

To start this tutorial, you will need a Slack Application with the proper permissions and Socket Mode activated.

Please refer yourself to the documentation Setting up your App to create your App and add the permissions.

Also, activate Socket Mode in the appropriate section.

Alt Text

Step 2: Create the project repository

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

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 the 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 v0.8.1-0.20210415015007-5ceeab881540
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
`- appHomeController.go
+ drivers
`- slack.go
+ views
`+ appHomeViewsAssets
  `- AppHomeView.json
   - CreateStickieNoteModal.json
   - NoteBlock.json
`- apphomeViews.go
+ main.go
Enter fullscreen mode Exit fullscreen mode

Drivers > slack.go

In drivers/slack.go, we create a utility function to initialize our Slack client using environment variables SLACK_APP_TOKEN and SLACK_BOT_TOKEN. In addition, it would be a good idea to add some validation. Slack provides two types of tokens:

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

Therefore I validate if the token exists and the beginning of the token to prevent inverting by mistake.

Slack driver code

Step 3: Create Controllers > appHomeController.go

I create a sequence diagrame using PlantUML, inspired by Tomomi Imura's, to visually represent what we are about to code. I believe that it is convenient to keep such diagrams alongside my code. It should make it much easier to follow this tutorial. Besides, I added references to the diagram in my code as comments.

Alt Text

Handling events

First, In controllers/appHomeController.go create a struct representing our Controller to handle dependencies injection. So far, we only require socketmode.SocketmodeHandler to register Slack Events we want to handle. But in a more extensive application, you might have other dependencies such as repositories to handle database requests.

type AppHomeController struct {
    EventHandler *socketmode.SocketmodeHandler
}
Enter fullscreen mode Exit fullscreen mode

Second, create an initialization function for our Controller. This function is in charge of registering which Event we want to receive and which function should handle that Event. If you refer to the sequence diagrame this Controller needs listening to 3 events:

  • An Event API called app_home_opened (2)
  • An interaction with the create button (12)
  • An interaction with the modal submit button (22).

To register an event with EventHandler, you need first to identify what type of event we need to handle. At this point, you should get familiar with slack terminology, including Event API, Interaction, Block Action.

Each of those handler functions works the same way. You provide the type of Event you expect (use autocompletion to find the one you need) and a callback function. The callback function requires:

  1. a pointer to the Event (*socketmode.Event)
  2. a pointer to the Socket mode Client (*socketmode.Client).

For instance, this is a valid function:

func callback(evt *socketmode.Event, clt *socketmode.Client) {}
Enter fullscreen mode Exit fullscreen mode

A better alternative is to use a method that belongs to AppHomeController. That way, we benefit from the dependencies injected in AppHomeController:

func (c *AppHomeController) callback(evt *socketmode.Event, clt *socketmode.Client) {}
Enter fullscreen mode Exit fullscreen mode

To conclude this section, here is my initialization constructor:

func NewAppHomeController(eventhandler *socketmode.SocketmodeHandler) AppHomeController {
    c := AppHomeController{
        EventHandler: eventhandler,
    }

    // App Home (2)
    c.EventHandler.HandleEventsAPI(
        slackevents.AppHomeOpened,
        c.publishHomeTabView,
    )

    // Create Stickie note Triggered (12)
    c.EventHandler.HandleInteractionBlockAction(
        views.AddStockieNoteActionID,
        c.openCreateStickieNoteModal,
    )

    // Create Stickie note Submitted (22)
    c.EventHandler.HandleInteraction(
        slack.InteractionTypeViewSubmission,
        c.createStickieNote,
    )

    return c

}
Enter fullscreen mode Exit fullscreen mode

At this point, our Controller is ready. We only need to implement each of our three Event handling methods:

  • publishHomeTabView
  • openCreateStickieNoteModal
  • createStickieNote

Implementing publishHomeTabView

the goal of this function is simply to display the App Home Tab:

GIF App Home with Add Stickies

All of our Handlers have roughly the same structure:

  1. Cast socketmode.Event into the desired type. socketmode.Event is the generic struct for all Events, but when registering this handler, we specified which event type we are expecting. Therefore converting struct, in that case, is granted.
  2. Create the View. We have not implemented any View yet, so create placeholder functions for now. I prefer introducing how to make views in the same section.
  3. Send the View to Slack
func (c *AppHomeController) publishHomeTabView(evt *socketmode.Event, clt *socketmode.Client) {
    // we need to cast our socketmode.Event into slackevents.AppHomeOpenedEvent
    evt_api, _ := evt.Data.(slackevents.EventsAPIEvent)
    evt_app_home_opened, _ := evt_api.InnerEvent.Data.(slackevents.AppHomeOpenedEvent)

    // create the view using block-kit
    view := views.AppHomeTabView()

    // Publish the view (3)
    // We get the Api client from `clt` and post our view
    _, err := clt.GetApiClient().PublishView(evt_app_home_opened.User, view, "")

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

Implementing openCreateStickieNoteModal

This method opens a Modal whenever a user clicks Add a Stickie.

GIF App Home with an open modal

Same structure as publishHomeTabView, except that we must acknowledge that we received the Event; otherwise, our users will see an error symbol on their side.

func (c *AppHomeController) openCreateStickieNoteModal(evt *socketmode.Event, clt *socketmode.Client) {
    // we need to cast our socketmode.Event
    interaction := evt.Data.(slack.InteractionCallback)

    // Make sure to respond to the server to avoid an error
    clt.Ack(*evt.Request)

    // create the view using block-kit
    view := views.CreateStickieNoteModal()

    // Open Modal (13)
    _, err := clt.GetApiClient().OpenView(interaction.TriggerID, view)

    //Handle errors
    if err != nil {
        log.Printf("ERROR openCreateStickieNoteModal: %v", err)
    }

}
Enter fullscreen mode Exit fullscreen mode

Implementing createStickieNote

The last part is updating the App Home when the user submits its information via the modal.

Once again, same structure. We acknowledge the Event because it is an interaction. Extracting the values to create the sticky note is a bit tricky, I found.

Slack requires unique ID in the UI to identify Blocks and Action. To minimize error I created constants :

  • views.ModalDescriptionBlockID
  • views.ModalDescriptionActionID
  • views.ModalColorBlockID
  • views.ModalColorActionID

Those constants are defined in views\appHomeViews.go.

func (c *AppHomeController) createStickieNote(evt *socketmode.Event, clt *socketmode.Client) {
    // we need to cast our socketmode.Event into slack.InteractionCallback
    view_submission := evt.Data.(slack.InteractionCallback)

    // Make sure to respond to the server to avoid an error
    clt.Ack(*evt.Request)

    // Create the model
    note := views.StickieNote{
        Description: view_submission.View.State.Values[views.ModalDescriptionBlockID][views.ModalDescriptionActionID].Value,
        Color:       view_submission.View.State.Values[views.ModalColorBlockID][views.ModalColorActionID].SelectedOption.Value,
        Timestamp:   time.Unix(time.Now().Unix(), 0).String(),
    }

    // create the view using block-kit
    view := views.AppHomeCreateStickieNote(note)

    // Publish the view (23)
    // We get the Api client from `clt` and post our view
    _, err := clt.GetApiClient().PublishView(view_submission.User.ID, view, "")

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

Step 4: Views > appHomeViews.go

For all Views, we will utilize the power of slack block-kit to its total capacity. Therefore we create a View by storing the JSON payload provided by Block-kit into files. Then we can load them when needed. To achieve that, I decided to use the newest Golang 1.16 feature created to help manage static assets, namely embed.

This way, we manage our slack Application like a simple web MVC application, with the views stored as static assets. It makes updating our App much easier using block-kit and copy-pasting the result.

Note: I covered this process in detail in another article: Manage Static Assets with embed (Golang 1.16)

The basic structure of views/appHomeViews.go:

package views

import (
    "bytes"
    "embed"
    "html/template"
    "io/ioutil"
    "log"

    "encoding/json"

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

const (
    // Define Action_id as constant so we can refet to them in the controller
    AddStockieNoteActionID   = "add_note"
    ModalDescriptionBlockID  = "note_description"
    ModalDescriptionActionID = "content"
    ModalColorBlockID        = "note_color"
    ModalColorActionID       = "color"
)

type StickieNote struct {
    Description string
    Color       string
    Timestamp   string
}

//go:embed appHomeViewsAssets/*
var appHomeAssets embed.FS

Enter fullscreen mode Exit fullscreen mode

Publish the App Home View

Save the Block-kit payload in views/appHomeViewsAssets/AppHomeView.json.

Then we create a function that reads AppHomeView.json and unmarshals into slack.HomeTabViewRequest, so we can send it as is via slack API.

func AppHomeTabView() slack.HomeTabViewRequest {

    str, err := appHomeAssets.ReadFile("appHomeViewsAssets/AppHomeView.json")
    if err != nil {
        log.Printf("Unable to read view `AppHomeView`: %v", err)
    }
    view := slack.HomeTabViewRequest{}
    json.Unmarshal([]byte(str), &view)

    return view
}
Enter fullscreen mode Exit fullscreen mode

Opening a modal dialog

Save the modal Block-kit payload into views/appHomeViewsAssets/CreateStickieNoteModal.json.

Then, we create a function that reads CreateStickieNoteModal.json and unmarshals into slack.ModalViewRequest, so we can send it via slack API.

func CreateStickieNoteModal() slack.ModalViewRequest {

    str, err := appHomeAssets.ReadFile("appHomeViewsAssets/CreateStickieNoteModal.json")
    if err != nil {
        log.Printf("Unable to read view `CreateStickieNoteModal`: %v", err)
    }
    view := slack.ModalViewRequest{}
    json.Unmarshal([]byte(str), &view)

    return view
}
Enter fullscreen mode Exit fullscreen mode

Updating the App Home view

Save the sticky note Block-kit payload as views/appHomeViewsAssets/NoteBlock.json.

This view is slightly more complicated because it is dynamically generated.

First, this view is the combination of views/appHomeViewsAssets/CreateStickieNoteModal.json and views/appHomeViewsAssets/NoteBlock.json.

Second, we collected the modal information and stored them in a struct views.StickieNote. Well! We want that information to appear on our sticky. Therefore, we use Go template for that. The placeholder {{ .Timestamp }}, {{ .Description }} and, {{ .Color }} are added NoteBlock.json wherever the content should be dynamic.

The following function generates our final view. If you need more explanation, read the dedicated article here

func AppHomeCreateStickieNote(note StickieNote) slack.HomeTabViewRequest {

    // Base elements
    str, err := appHomeAssets.ReadFile("appHomeViewsAssets/AppHomeView.json")
    if err != nil {
        log.Printf("Unable to read view `AppHomeView`: %v", err)
    }
    view := slack.HomeTabViewRequest{}
    json.Unmarshal(str, &view)

    // New Notes
    t, err := template.ParseFS(appHomeAssets, "appHomeViewsAssets/NoteBlock.json")
    if err != nil {
        panic(err)
    }
    var tpl bytes.Buffer
    err = t.Execute(&tpl, note)
    if err != nil {
        panic(err)
    }
    str, _ = ioutil.ReadAll(&tpl)
    note_view := slack.HomeTabViewRequest{}
    json.Unmarshal(str, &note_view)

    view.Blocks.BlockSet = append(view.Blocks.BlockSet, note_view.Blocks.BlockSet...)

    return view
}
Enter fullscreen mode Exit fullscreen mode

Next Steps

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.

Can you improve it?

I only covered the fundamentals here, so there is more that can be done. For instance, you may have noticed that you keep overriding the same sticky note over and over again. Not ideal, right?

Discussion (0)