<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>The Ops Community ⚙️: David Abramov</title>
    <description>The latest articles on The Ops Community ⚙️ by David Abramov (@uduhamudu).</description>
    <link>https://community.ops.io/uduhamudu</link>
    <image>
      <url>https://community.ops.io/images/LMd_aEjwe7_EcYy-pSEgSjcC7lvGw77hXUXNda9Q-n4/rs:fill:90:90/g:sm/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL3Vz/ZXIvcHJvZmlsZV9p/bWFnZS8zMi82MzMw/NzE0ZS0wNDc5LTRh/ODktOTRmMC1mNWNm/NzgxNTMxYzQucG5n</url>
      <title>The Ops Community ⚙️: David Abramov</title>
      <link>https://community.ops.io/uduhamudu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://community.ops.io/feed/uduhamudu"/>
    <language>en</language>
    <item>
      <title>How to Build a Multi-Workspace Slack Application in Go</title>
      <dc:creator>David Abramov</dc:creator>
      <pubDate>Thu, 07 Jul 2022 07:00:55 +0000</pubDate>
      <link>https://community.ops.io/blinkops/how-to-build-a-multi-workspace-slack-application-in-go-54k6</link>
      <guid>https://community.ops.io/blinkops/how-to-build-a-multi-workspace-slack-application-in-go-54k6</guid>
      <description>&lt;p&gt;Recently, I started working on a Slack application that connects Slack and our own &lt;a href="https://www.blinkops.com/"&gt;Blink&lt;/a&gt; platform. If you’re not familiar, Blink is a no-code automation platform for cloud engineering teams, and it enables users to build automations within managed workspaces for different teams or projects. In order to support this use case, I needed a way to build a multi-workspace application in Slack.&lt;/p&gt;

&lt;p&gt;When I started reading about and playing with the Slack platform, it suddenly became clear to me that Slack is so much bigger and more complex than I’d ever realized.&lt;/p&gt;

&lt;p&gt;The Slack API &lt;a href="https://api.slack.com/docs"&gt;Docs&lt;/a&gt; seem very large and complex at first glance, but they are actually quite detailed and user-friendly. Unfortunately, the official &lt;a href="https://api.slack.com/tools/bolt"&gt;Bolt&lt;/a&gt; family of SDKs doesn’t include Go, my recent language of choice, and, although I did find some useful guides on the web, none of them really targeted my use case of building an app which can be distributed across multiple workspaces and eventually listed on the Slack app store, the &lt;a href="https://slack.com/apps"&gt;App Directory&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After my research, I ended up using the &lt;a href="https://pkg.go.dev/github.com/slack-go/slack"&gt;Slack API in Go&lt;/a&gt; to build the needed Slack application, and I’m writing this hands-on article to share my experience. While I built this application to support Blink’s particular use case, I’ve written the rest of this blog post as a general how-to guide for anyone building a similar Slack application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating a Slack Application&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before starting any coding, let’s create our application &lt;a href="https://api.slack.com/apps"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/4bXHenf6UgD_pCibqZ7Yx2AnRjnhQvZ4RgJPeiOlqnc/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3hrNGJs/bzdzcHIzcTl0ODU5/eXJwLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/4bXHenf6UgD_pCibqZ7Yx2AnRjnhQvZ4RgJPeiOlqnc/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3hrNGJs/bzdzcHIzcTl0ODU5/eXJwLnBuZw" alt="Image description" width="880" height="79"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the ‘Create New App’ button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/sxNEae3_oB1vvyrUvJqBRPyBBRVm7ampMFY6f4_jfXE/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3F1ZHlz/NXV4NTg2ZnpjdnB4/djJuLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/sxNEae3_oB1vvyrUvJqBRPyBBRVm7ampMFY6f4_jfXE/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3F1ZHlz/NXV4NTg2ZnpjdnB4/djJuLnBuZw" alt="Image description" width="880" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select the ‘From scratch’ option:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/3K883v-sTGtclz7tBgjhU_OZbWQbspWE5am7BqD5KSk/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3ZpdDdt/Z3ZiZjVscnkyNHZs/cmFzLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/3K883v-sTGtclz7tBgjhU_OZbWQbspWE5am7BqD5KSk/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3ZpdDdt/Z3ZiZjVscnkyNHZs/cmFzLnBuZw" alt="Image description" width="880" height="843"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give your app a name and select the workspace you will be developing your app in.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE&lt;/em&gt;: As our app will be publicly distributed, the workspace selection here is not very important, and we’ll build our app in a way which will allows us to install it on any workspace, regardless of the choice you make in this phase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Scenario&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s imagine that you are developing a really awesome REST application that accepts as input 2 integers and returns their sum.&lt;/p&gt;

&lt;p&gt;We’ll use the &lt;a href="https://github.com/gin-gonic/gin"&gt;Gin&lt;/a&gt; web framework to write our app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go get -u github.com/gin-gonic/gin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

import (
    "encoding/json"
    "net/http"

    "github.com/gin-gonic/gin"
)

type Input struct {
    Num1 int `json:"num1"`
    Num2 int `json:"num2"`
}

type Output struct {
    Sum int `json:"sum"`
}

func handle(c *gin.Context) {
    input := &amp;amp;Input{}
    if err := json.NewDecoder(c.Request.Body).Decode(input); err != nil {
        c.String(http.StatusBadRequest, "error reading request body: %s", err.Error())
        return
    }
    c.JSON(http.StatusOK, &amp;amp;Output{
        Sum: input.Num1 + input.Num2,
    })
}

func main() {
    app := gin.Default()
    app.POST("/", handle)
    _ = app.Run()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, if we don’t specify an argument for the ‘Run’ method, the app will listen on port 8080.&lt;/p&gt;

&lt;p&gt;Let’s try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go run main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -X POST http://localhost:8080 --data '{"num1":1,"num2":2}' -i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 21 May 2022 17:46:26 GMT
Content-Length: 9

{"sum":3}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s call this app our ‘Core’ service that we want to integrate with Slack, and create a standalone Slack dedicated service that will connect the users of our Slack app to the ‘Core’ app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/xlaj1i-p5Fd46LNeqitS4Nm5keeNFL5sUiTWarAcR04/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzY2Mm13/dDd2czU0dHdobHM2/NHlwLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/xlaj1i-p5Fd46LNeqitS4Nm5keeNFL5sUiTWarAcR04/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzY2Mm13/dDd2czU0dHdobHM2/NHlwLnBuZw" alt="Image description" width="880" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s visualize this:&lt;/p&gt;

&lt;p&gt;The above diagram shows the data flow. Each time a user will interact with our Slack app, the Slack platform will access our Slack service which will in turn access our ‘Core’ service to process the input and return a response which will be returned to the Slack platform (and then to the user).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supporting Multiple Slack Workspaces&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every access to the Slack API requires the caller to provide an access token.&lt;/p&gt;

&lt;p&gt;There are several &lt;a href="https://api.slack.com/authentication/token-types"&gt;types of tokens&lt;/a&gt;. Our app will work only with bot tokens.&lt;/p&gt;

&lt;p&gt;Every time our app will be installed on some workspace, we’ll get such a bot token from the Slack platform.&lt;/p&gt;

&lt;p&gt;The installation process on some workspace is actually an &lt;a href="https://api.slack.com/authentication/oauth-v2"&gt;OAuth authorization flow&lt;/a&gt;, in which we send the user to Slack to authorize our app on his workspace, and once he approves our app, we’ll need to exchange our temporary access code for a permanent access token which we’ll use in every operation which requires access to the Slack API.&lt;/p&gt;

&lt;p&gt;Let’s start with a small snippet of our Slack service code.&lt;/p&gt;

&lt;p&gt;To store these tokens, we’ll use the popular and quite simple &lt;a href="https://github.com/boltdb/bolt"&gt;Bolt&lt;/a&gt; DB framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go get github.com/boltdb/bolt/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s initialize the DB and create a bucket of tokens, in which tokens are mapped to their workspaces (by ID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

import (
    "log"

    "github.com/boltdb/bolt"
)

func configureDb() (*bolt.DB, error) {
    db, err := bolt.Open("slack.db", 0600, nil)
    if err != nil {
        return nil, err
    }
    if err = db.Update(func(tx *bolt.Tx) error {
        if _, err = tx.CreateBucketIfNotExists([]byte("tokens")); err != nil {
            return err
        }
        return nil
    }); err != nil {
        return nil, err
    }
    return db, nil
}

func main() {
    db, err := configureDb()
    if err != nil {
        log.Fatal(err)
    }
    defer func() { _ = db.Close() }()
...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;NOTE&lt;/em&gt;: When developing a real app, consider storing your access tokens in a more secure way!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developing Locally&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As we’re developing locally and the Slack platform requires us to provide publicly accessible URLs, we need to create some connection between our local device and the wider network. For this we can use &lt;a href="https://ngrok.com/"&gt;ngrok&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok http 5400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the above command should print a URL which exposes port 5400 on your local device to the outer world (e.g. ‘&lt;a href="https://03e2-109-66-212-180.ngrok.io%E2%80%99"&gt;https://03e2-109-66-212-180.ngrok.io’&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuring Our App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To keep things simple, we’ll register a &lt;a href="https://api.slack.com/interactivity/slash-commands"&gt;Slash Command&lt;/a&gt; for our app:&lt;/p&gt;

&lt;p&gt;Navigate to Features → Slash Commands and click the ‘Create New Command’ button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/IfNfRj5IG6e8UklKBofOCrUXv-1JIzrdQJVuxVlGbqI/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2NxbjZk/dWxkNW9hbDAwbWc4/cnI1LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/IfNfRj5IG6e8UklKBofOCrUXv-1JIzrdQJVuxVlGbqI/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2NxbjZk/dWxkNW9hbDAwbWc4/cnI1LnBuZw" alt="Image description" width="880" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill out the following form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/IN8XMUfWmWPC1PvWLx1WkE2QJ9Q88yQ0itt1Xb6gAFU/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL25qc3Zh/eWdsZHFmc3QwMHhy/czIyLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/IN8XMUfWmWPC1PvWLx1WkE2QJ9Q88yQ0itt1Xb6gAFU/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL25qc3Zh/eWdsZHFmc3QwMHhy/czIyLnBuZw" alt="Image description" width="880" height="1210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Provide the name of the command (e.g. ‘plus’), the ngrok URL from the previous section suffixed with ‘/cmd/’ and the command name (e.g. ‘&lt;a href="https://03e2-109-66-212-180.ngrok.io/cmd/plus%E2%80%99"&gt;https://03e2-109-66-212-180.ngrok.io/cmd/plus’&lt;/a&gt;) and a description (e.g. ‘Sum 2 Integers’). The rest of the fields are optional.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Setting Up App Installation *&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As we’re developing a multi workspace app, we need to enable the OAuth installation flow.&lt;/p&gt;

&lt;p&gt;Navigate to Features → OAuth &amp;amp; Permissions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/-QGjK0Q6nHOLqy1Na3_TpDQIDZCVphVX2s7Wf9dUdQY/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzV0MTZz/dXY3ODJic29xczQz/b2QzLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/-QGjK0Q6nHOLqy1Na3_TpDQIDZCVphVX2s7Wf9dUdQY/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzV0MTZz/dXY3ODJic29xczQz/b2QzLnBuZw" alt="Image description" width="880" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And click the ‘Add New Redirect URL’ button in the ‘Redirect URLs’ section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/SgtnFzis2b_F4HmNuGZ4dgIMiEeVSWgK0XnZsxm69K0/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2Yzemox/NHhwejRrMm4xcjhs/Y2U0LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/SgtnFzis2b_F4HmNuGZ4dgIMiEeVSWgK0XnZsxm69K0/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2Yzemox/NHhwejRrMm4xcjhs/Y2U0LnBuZw" alt="Image description" width="880" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter your ngrok URL with a ‘/install’ suffix attached to it (e.g. ‘&lt;a href="https://03e2-109-66-212-180.ngrok.io/install%E2%80%99"&gt;https://03e2-109-66-212-180.ngrok.io/install’&lt;/a&gt;), click ‘Add’ and then click ‘Save URLs’.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scopes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The scopes requested by our app are those that our app will request upon each installation to some workspace. You can see in the ‘Scopes’ section that we already have the ‘&lt;a href="https://api.slack.com/scopes/commands"&gt;commands&lt;/a&gt;’ bot scope, as we’ve added a Slash Command to our app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enabling Public Distribution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to Settings → Manage Distribution:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/lzpLdqjfC8tqnjGE0jA4HCDgmpnbQ2sM8u7MvMzkR0U/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3Q0MG40/NHp4YjJkenk1NG9o/NmV4LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/lzpLdqjfC8tqnjGE0jA4HCDgmpnbQ2sM8u7MvMzkR0U/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3Q0MG40/NHp4YjJkenk1NG9o/NmV4LnBuZw" alt="Image description" width="880" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And under the ‘Share Your App with Other Workspaces’ section, make sure that the hard coded information removal checkbox is checked:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/g_6jjvn8RLV-ReldS15w_eAkIaCXheajb0PPM4svzZA/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2VidWZu/cmhud3RiY2hmd294/amo4LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/g_6jjvn8RLV-ReldS15w_eAkIaCXheajb0PPM4svzZA/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2VidWZu/cmhud3RiY2hmd294/amo4LnBuZw" alt="Image description" width="880" height="832"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, click the ‘Activate Public Distribution’ button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connecting It All Together&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that we have our awesome ‘Core’ Service and we’ve configured our Slack app, we can go ahead and continue writing the code of our Slack service.&lt;/p&gt;

&lt;p&gt;We’ve already configured our storage mechanism, and you’ve probably noticed that we are already committed to implementing some endpoints we shared with the slack platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;/install - to install our app using the OAuth flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;/cmd/plus - to forward the request to sum 2 integers to our ‘Core’ service, using the input from the user on Slack.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implementing the OAuth Installation Flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As with every standard OAuth flow, we need a pair of client ID and secret. You can view both under the ‘App Credentials’ section after navigating to Settings → Basic Information:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/5-YkZI7OuqAR8r_5LPIQ_VFDaXuDzsb6-T-e97BnDd4/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2JiNG02/eHVub2I3MW43bW93/Y2FoLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/5-YkZI7OuqAR8r_5LPIQ_VFDaXuDzsb6-T-e97BnDd4/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2JiNG02/eHVub2I3MW43bW93/Y2FoLnBuZw" alt="Image description" width="880" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We need to access the Slack platform, so let’s import the Go SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go get -u github.com/slack-go/slack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s have a look at the installation handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func handleInstallation(db *bolt.DB) func(c *gin.Context) {
    return func(c *gin.Context) {
        _, errExists := c.GetQuery("error")
        if errExists {
            c.String(http.StatusOK, "error installing app")
            return
        }
        code, codeExists := c.GetQuery("code")
        if !codeExists {
            c.String(http.StatusBadRequest, "missing mandatory 'code' query parameter")
            return
        }
        resp, err := slack.GetOAuthV2Response(http.DefaultClient,
            os.Getenv("CLIENT_ID"),
            os.Getenv("CLIENT_SECRET"),
            code,
            "")
        if err != nil {
            c.String(http.StatusInternalServerError, "error exchanging temporary code for access token: %s", err.Error())
            return
        }
        if err = db.Update(func(tx *bolt.Tx) error {
            bucket := tx.Bucket([]byte("tokens"))
            if bucket == nil {
                return errors.New("error accessing tokens bucket")
            }
            return bucket.Put([]byte(resp.Team.ID), []byte(resp.AccessToken))
        }); err != nil {
            c.String(http.StatusInternalServerError, "error storing slack access token: %s", err.Error())
            return
        }
        c.Redirect(http.StatusFound, fmt.Sprintf("slack://app?team=%s&amp;amp;id=%s&amp;amp;tab=about", resp.Team.ID, resp.AppID))
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s break it down. First, we check for the presence of the ‘error’ query parameter. If this parameter is present, then it means the installation failed for some reason (e.g. the user declined to authorize our app). In that case we’ll abort the rest of the flow and just notify of an error.&lt;/p&gt;

&lt;p&gt;Next, as in every standard OAuth flow, we’re checking for the temporary code by looking for the ‘code’ query parameter, and if it is present we use the Slack Go SDK to exchange that code for an access token, and store that token in our storage, mapped to the workspace ID.Finally, we redirect the user to our app page in his Slack client using &lt;a href="https://api.slack.com/reference/deep-linking"&gt;Deep linking&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Notice how we’re reading the ‘CLIENT_ID’ and ‘CLIENT_SECRET’ env variables to perform the exchange operation.&lt;/p&gt;

&lt;p&gt;Let’s also register our installation handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func main() {
    db, err := configureDb()
    if err != nil {
        log.Fatal(err)
    }
    defer func() { _ = db.Close() }()
  app := gin.Default()
    app.Any("/install", handleInstallation(db))
...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;IMPORTANT&lt;/em&gt;: When developing a real app, consider adding a ‘state’ parameter to your OAuth flow for extra security!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementing the Slash Command&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we implement our slash command endpoint, let’s implement some middleware that will make sure that requests to this endpoint are really coming from Slack.&lt;/p&gt;

&lt;p&gt;There are several &lt;a href="https://api.slack.com/authentication/verifying-requests-from-slack"&gt;methods provided by the Slack platform&lt;/a&gt; to achieve such verification, but we’ll use the recommended and straightforward way of using the Signing Secret present under the ‘App Credentials’ section in Settings → Basic Information:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/YZrzPQTPwF4unRBTQ82cMy1vlTETLWiq1mfCaN7OQIs/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3FrbGpl/ZWY1Njd2NXN6Nnpu/YXNrLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/YZrzPQTPwF4unRBTQ82cMy1vlTETLWiq1mfCaN7OQIs/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3FrbGpl/ZWY1Njd2NXN6Nnpu/YXNrLnBuZw" alt="Image description" width="880" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s the signature verification middleware code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func signatureVerification(c *gin.Context) {
    verifier, err := slack.NewSecretsVerifier(c.Request.Header, os.Getenv("SIGNATURE_SECRET"))
    if err != nil {
        c.String(http.StatusBadRequest, "error initializing signature verifier: %s", err.Error())
        return
    }
    bodyBytes, err := ioutil.ReadAll(c.Request.Body)
    if err != nil {
        c.String(http.StatusInternalServerError, "error reading request body: %s", err.Error())
        return
    }
    bodyBytesCopy := make([]byte, len(bodyBytes))
    copy(bodyBytesCopy, bodyBytes)
    c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytesCopy))
    if _, err = verifier.Write(bodyBytes); err != nil {
        c.String(http.StatusInternalServerError, "error writing request body bytes for verification: %s", err.Error())
        return
    }
    if err = verifier.Ensure(); err != nil {
        c.String(http.StatusUnauthorized, "error verifying slack signature: %s", err.Error())
        return
    }
    c.Next()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we’re reading the signature verification secret from the ‘SIGNATURE_SECRET’ env variable.&lt;/p&gt;

&lt;p&gt;Then, we’re reading the request body, while copying it and preserving it for later reads which may be invoked by other handlers in the chain, and finally we verify the signature of the request body. If all is well, we’re calling the ‘Next’ method to invoke any other handlers in the chain.&lt;/p&gt;

&lt;p&gt;Let’s have a look at the code of the actual handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func handlePlus(db *bolt.DB) func(c *gin.Context) {
    return func(c *gin.Context) {
        cmd, err := slack.SlashCommandParse(c.Request)
        if err != nil {
            c.String(http.StatusBadRequest, "invalid slash command payload: %s", err.Error())
            return
        }
        inputArr := strings.Split(strings.TrimSpace(cmd.Text), " ")
        if len(inputArr) != 2 {
            c.String(http.StatusBadRequest, "invalid number of input parameters provided")
            return
        }
        num1, err := strconv.Atoi(inputArr[0])
        if err != nil {
            c.String(http.StatusBadRequest, "invalid 1st input parameter: %s", err.Error())
            return
        }
        num2, err := strconv.Atoi(inputArr[1])
        if err != nil {
            c.String(http.StatusBadRequest, "invalid 2nd input parameter: %s", err.Error())
            return
        }
        buf, err := json.Marshal(&amp;amp;Input{
            Num1: num1,
            Num2: num2,
        })
        if err != nil {
            c.String(http.StatusInternalServerError, "error preparing request body: %s", err.Error())
            return
        }
        resp, err := http.Post("http://localhost:8080", "application/json", bytes.NewBuffer(buf))
        if err != nil {
            c.String(http.StatusInternalServerError, "error preparing http request: %s", err.Error())
            return
        }
        if resp.StatusCode != http.StatusOK {
            c.String(http.StatusInternalServerError, "invalid status code returned: %d", resp.StatusCode)
            return
        }
        output := &amp;amp;Output{}
        if err = json.NewDecoder(resp.Body).Decode(output); err != nil {
            c.String(http.StatusInternalServerError, "invalid response: %s", err.Error())
            return
        }
        var token string
        if err = db.View(func(tx *bolt.Tx) error {
            bucket := tx.Bucket([]byte("tokens"))
            if bucket == nil {
                return errors.New("error accessing tokens bucket")
            }
            token = string(bucket.Get([]byte(cmd.TeamID)))
            return nil
        }); err != nil {
            c.String(http.StatusInternalServerError, "error reading slack access token: %s", err.Error())
            return
        }
        if _, _, _, err = slack.New(token).SendMessage(cmd.UserID, slack.MsgOptionText(fmt.Sprintf("Sum is %d", output.Sum), true)); err != nil {
            c.String(http.StatusInternalServerError, "error sending slack message: %s", err.Error())
            return
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s break it down. We’re parsing the request body to get the slash command input.&lt;/p&gt;

&lt;p&gt;Then, we’re parsing the parameters passed in with the command to verify we have 2 integers (e.g. ‘/plus 1 2’ is valid, but ‘/plus 1 a’ is not).&lt;/p&gt;

&lt;p&gt;Finally, we’re using the 2 provided parameters to invoke the endpoint on our ‘Core’ service to get the sum of the 2 given integers, sending the user a direct message with the calculated sum returned by the ‘Core’ service.&lt;/p&gt;

&lt;p&gt;In order to send a direct message to the user we are reading the token mapped to the relevant workspace from the DB.&lt;/p&gt;

&lt;p&gt;Also, we need to add the ‘&lt;a href="https://api.slack.com/scopes/chat:write"&gt;chat:write&lt;/a&gt;’ bot scope for our app, otherwise our app will not be able to send direct messages to users.&lt;/p&gt;

&lt;p&gt;Navigate to Settings → OAuth &amp;amp; Permissions, and under the ‘Scopes’ section add the required scope to the list of bot scopes required by your app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/WiMKAOTlaFvsMypz95DhpWY5vLXZIcIabgHwAk0PlSA/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2w4MHRz/ejN5ZndjYTJta2Ft/amRhLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/WiMKAOTlaFvsMypz95DhpWY5vLXZIcIabgHwAk0PlSA/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2w4MHRz/ejN5ZndjYTJta2Ft/amRhLnBuZw" alt="Image description" width="880" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let’s register our slash command handler under a new ‘cmd’ API group. Here’s the current code of our Slack service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"

    "github.com/boltdb/bolt"
    "github.com/gin-gonic/gin"
    "github.com/slack-go/slack"
    "github.com/slack-go/slack/slackevents"
)

type Input struct {
    Num1 int `json:"num1"`
    Num2 int `json:"num2"`
}

type Output struct {
    Sum int `json:"sum"`
}

func configureDb() (*bolt.DB, error) {
    db, err := bolt.Open("slack.db", 0600, nil)
    if err != nil {
        return nil, err
    }
    if err = db.Update(func(tx *bolt.Tx) error {
        if _, err = tx.CreateBucketIfNotExists([]byte("tokens")); err != nil {
            return err
        }
        return nil
    }); err != nil {
        return nil, err
    }
    return db, nil
}

func handleInstallation(db *bolt.DB) func(c *gin.Context) {
    return func(c *gin.Context) {
        _, errExists := c.GetQuery("error")
        if errExists {
            c.String(http.StatusOK, "error installing app")
            return
        }
        code, codeExists := c.GetQuery("code")
        if !codeExists {
            c.String(http.StatusBadRequest, "missing mandatory 'code' query parameter")
            return
        }
        resp, err := slack.GetOAuthV2Response(http.DefaultClient,
            os.Getenv("CLIENT_ID"),
            os.Getenv("CLIENT_SECRET"),
            code,
            "")
        if err != nil {
            c.String(http.StatusInternalServerError, "error exchanging temporary code for access token: %s", err.Error())
            return
        }
        if err = db.Update(func(tx *bolt.Tx) error {
            bucket := tx.Bucket([]byte("tokens"))
            if bucket == nil {
                return errors.New("error accessing tokens bucket")
            }
            return bucket.Put([]byte(resp.Team.ID), []byte(resp.AccessToken))
        }); err != nil {
            c.String(http.StatusInternalServerError, "error storing slack access token: %s", err.Error())
            return
        }
        c.Redirect(http.StatusFound, fmt.Sprintf("slack://app?team=%s&amp;amp;id=%s&amp;amp;tab=about", resp.Team.ID, resp.AppID))
    }
}

func signatureVerification(c *gin.Context) {
    verifier, err := slack.NewSecretsVerifier(c.Request.Header, os.Getenv("SIGNATURE_SECRET"))
    if err != nil {
        c.String(http.StatusBadRequest, "error initializing signature verifier: %s", err.Error())
        return
    }
    bodyBytes, err := ioutil.ReadAll(c.Request.Body)
    if err != nil {
        c.String(http.StatusInternalServerError, "error reading request body: %s", err.Error())
        return
    }
    bodyBytesCopy := make([]byte, len(bodyBytes))
    copy(bodyBytesCopy, bodyBytes)
    c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytesCopy))
    if _, err = verifier.Write(bodyBytes); err != nil {
        c.String(http.StatusInternalServerError, "error writing request body bytes for verification: %s", err.Error())
        return
    }
    if err = verifier.Ensure(); err != nil {
        c.String(http.StatusUnauthorized, "error verifying slack signature: %s", err.Error())
        return
    }
    c.Next()
}

func handlePlus(db *bolt.DB) func(c *gin.Context) {
    return func(c *gin.Context) {
        cmd, err := slack.SlashCommandParse(c.Request)
        if err != nil {
            c.String(http.StatusBadRequest, "invalid slash command payload: %s", err.Error())
            return
        }
        inputArr := strings.Split(strings.TrimSpace(cmd.Text), " ")
        if len(inputArr) != 2 {
            c.String(http.StatusBadRequest, "invalid number of input parameters provided")
            return
        }
        num1, err := strconv.Atoi(inputArr[0])
        if err != nil {
            c.String(http.StatusBadRequest, "invalid 1st input parameter: %s", err.Error())
            return
        }
        num2, err := strconv.Atoi(inputArr[1])
        if err != nil {
            c.String(http.StatusBadRequest, "invalid 2nd input parameter: %s", err.Error())
            return
        }
        buf, err := json.Marshal(&amp;amp;Input{
            Num1: num1,
            Num2: num2,
        })
        if err != nil {
            c.String(http.StatusInternalServerError, "error preparing request body: %s", err.Error())
            return
        }
        resp, err := http.Post("http://localhost:8080", "application/json", bytes.NewBuffer(buf))
        if err != nil {
            c.String(http.StatusInternalServerError, "error preparing http request: %s", err.Error())
            return
        }
        if resp.StatusCode != http.StatusOK {
            c.String(http.StatusInternalServerError, "invalid status code returned: %d", resp.StatusCode)
            return
        }
        output := &amp;amp;Output{}
        if err = json.NewDecoder(resp.Body).Decode(output); err != nil {
            c.String(http.StatusInternalServerError, "invalid response: %s", err.Error())
            return
        }
        var token string
        if err = db.View(func(tx *bolt.Tx) error {
            bucket := tx.Bucket([]byte("tokens"))
            if bucket == nil {
                return errors.New("error accessing tokens bucket")
            }
            token = string(bucket.Get([]byte(cmd.TeamID)))
            return nil
        }); err != nil {
            c.String(http.StatusInternalServerError, "error reading slack access token: %s", err.Error())
            return
        }
        if _, _, _, err = slack.New(token).SendMessage(cmd.UserID, slack.MsgOptionText(fmt.Sprintf("Sum is %d", output.Sum), true)); err != nil {
            c.String(http.StatusInternalServerError, "error sending slack message: %s", err.Error())
            return
        }
    }
}

func main() {
    db, err := configureDb()
    if err != nil {
        log.Fatal(err)
    }
    defer func() { _ = db.Close() }()
    app := gin.Default()
    app.Any("/install", handleInstallation(db))
    cmdGroup := app.Group("/cmd")
    cmdGroup.Use(signatureVerification)
    cmdGroup.POST("/plus", handlePlus(db))
    _ = app.Run()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For convenience, we copied the definitions of the ‘Input’ and ‘Output’ structs from our ‘Core’ service.&lt;/p&gt;

&lt;p&gt;Notice that when we’re sending the direct message to the user we’re using the access token of the workspace the user invoked the command from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playing With Our App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run your app using the following command, providing values for the required env variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT=5400 CLIENT_ID=... CLIENT_SECRET=... SIGNATURE_SECRET=... go run main.go

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value of the ‘PORT’ variable is used by the gin framework if no argument is passed to the ‘Run’ method.&lt;/p&gt;

&lt;p&gt;Let’s first install our app on some workspace. To do that, we need to initiate the OAuth installation flow. To do that, Navigate to Settings → Manage Distribution:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/MC_a28CjglFRBUPKQAlkdIRhKtKeG_zUpq69_FGzwiM/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL214enFt/eWlkZ3lxcXl5bDU4/M2JsLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/MC_a28CjglFRBUPKQAlkdIRhKtKeG_zUpq69_FGzwiM/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL214enFt/eWlkZ3lxcXl5bDU4/M2JsLnBuZw" alt="Image description" width="880" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press the ‘Copy’ button in the ‘Sharable URL’ section and paste the copied URL into a new tab in your browser.&lt;/p&gt;

&lt;p&gt;This should open a page which resembles the following page, allowing you to choose a workspace and install the app to it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/JIZ25amoMZCM9AZf3nPAtatDbiPtMcmi32hlrKdVIFc/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzQzcGhn/bWlsNXY1NzgybzFm/amRiLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/JIZ25amoMZCM9AZf3nPAtatDbiPtMcmi32hlrKdVIFc/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzQzcGhn/bWlsNXY1NzgybzFm/amRiLnBuZw" alt="Image description" width="880" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press ‘Allow’ and in the following window:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/I69YiUk0jfW74f1aOJmSqwohIjUZ1dptfWhMaBvbFcw/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2ZwNTRs/b3l0eGgxaTJ1Z2Vo/MWVtLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/I69YiUk0jfW74f1aOJmSqwohIjUZ1dptfWhMaBvbFcw/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2ZwNTRs/b3l0eGgxaTJ1Z2Vo/MWVtLnBuZw" alt="Image description" width="880" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press the ‘Open Slack’ button, which should take you to the ‘About’ page of your app in your Slack client:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/jz6k_O7A9lG4V8kU5LzZ553yUBEz-oA75fcfy6X6eWg/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2xjeDN4/dWQ0ajhheGw3bWx3/a3pqLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/jz6k_O7A9lG4V8kU5LzZ553yUBEz-oA75fcfy6X6eWg/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2xjeDN4/dWQ0ajhheGw3bWx3/a3pqLnBuZw" alt="Image description" width="880" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, navigate to some public channel in your workspace and type in ‘/plus’:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/krri1A82rUmLNykVA2RLcUgpl03waB67Q4oB6OWol-g/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2U1Nnp4/Zzc5eG9ldndvZGpz/YWZzLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/krri1A82rUmLNykVA2RLcUgpl03waB67Q4oB6OWol-g/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2U1Nnp4/Zzc5eG9ldndvZGpz/YWZzLnBuZw" alt="Image description" width="880" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press enter to choose the ‘/plus’ command of your app, suffix the ‘/plus’ command with 2 integers of your choice and send the message, e.g.:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/Yev5RIjgoknq0yNBvvBNFDgPhbsRQdn0lkKzz2Uu8UM/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2xodGxs/czJhaDNlMGNwNnpv/c3BqLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/Yev5RIjgoknq0yNBvvBNFDgPhbsRQdn0lkKzz2Uu8UM/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2xodGxs/czJhaDNlMGNwNnpv/c3BqLnBuZw" alt="Image description" width="880" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you send the message, you should receive a direct message from your app, notifying you of the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/xO9w0RTgvjbXT1IRQ0yqjhDMTQXivCq-8MJg4hiuqxo/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL291dno3/Z3d5c3FqOGV5cnFh/bzRmLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/xO9w0RTgvjbXT1IRQ0yqjhDMTQXivCq-8MJg4hiuqxo/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL291dno3/Z3d5c3FqOGV5cnFh/bzRmLnBuZw" alt="Image description" width="510" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/kRhNyknnJgpCWzzlRzxd7UhdYp6Na0OcJ9q9Q491_X8/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzR0bnJh/M3gxdmhvcGJ2OTFk/b3B6LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/kRhNyknnJgpCWzzlRzxd7UhdYp6Na0OcJ9q9Q491_X8/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzR0bnJh/M3gxdmhvcGJ2OTFk/b3B6LnBuZw" alt="Image description" width="880" height="45"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Listening to Events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://api.slack.com/apis/connections/events-api"&gt;Slack Events API&lt;/a&gt; is an integral part of any Slack application, especially those that are intended to be distributed across multiple workspaces.&lt;/p&gt;

&lt;p&gt;There are 3 kinds of events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;URL Verification - The Slack platform sends a challenge string, and our app should respond with that string and with a 200 OK status code. In this way the Slack platform checks that our app is alive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate Limiting - The Slack platform informs our app of some rate limit being reached on some API endpoint. We can just acknowledge this event with a 200 OK status code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Callback - The ‘interesting’ events. These events have an inner event which can have &lt;a href="https://api.slack.com/events"&gt;various types&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s register our app to a basic app deletion event.&lt;/p&gt;

&lt;p&gt;Here’s our Slack service code, including our event handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"

    "github.com/boltdb/bolt"
    "github.com/gin-gonic/gin"
    "github.com/slack-go/slack"
    "github.com/slack-go/slack/slackevents"
)

type Input struct {
    Num1 int `json:"num1"`
    Num2 int `json:"num2"`
}

type Output struct {
    Sum int `json:"sum"`
}

func configureDb() (*bolt.DB, error) {
    db, err := bolt.Open("slack.db", 0600, nil)
    if err != nil {
        return nil, err
    }
    if err = db.Update(func(tx *bolt.Tx) error {
        if _, err = tx.CreateBucketIfNotExists([]byte("tokens")); err != nil {
            return err
        }
        return nil
    }); err != nil {
        return nil, err
    }
    return db, nil
}

func handleInstallation(db *bolt.DB) func(c *gin.Context) {
    return func(c *gin.Context) {
        _, errExists := c.GetQuery("error")
        if errExists {
            c.String(http.StatusOK, "error installing app")
            return
        }
        code, codeExists := c.GetQuery("code")
        if !codeExists {
            c.String(http.StatusBadRequest, "missing mandatory 'code' query parameter")
            return
        }
        resp, err := slack.GetOAuthV2Response(http.DefaultClient,
            os.Getenv("CLIENT_ID"),
            os.Getenv("CLIENT_SECRET"),
            code,
            "")
        if err != nil {
            c.String(http.StatusInternalServerError, "error exchanging temporary code for access token: %s", err.Error())
            return
        }
        if err = db.Update(func(tx *bolt.Tx) error {
            bucket := tx.Bucket([]byte("tokens"))
            if bucket == nil {
                return errors.New("error accessing tokens bucket")
            }
            return bucket.Put([]byte(resp.Team.ID), []byte(resp.AccessToken))
        }); err != nil {
            c.String(http.StatusInternalServerError, "error storing slack access token: %s", err.Error())
            return
        }
        c.Redirect(http.StatusFound, fmt.Sprintf("slack://app?team=%s&amp;amp;id=%s&amp;amp;tab=about", resp.Team.ID, resp.AppID))
    }
}

func signatureVerification(c *gin.Context) {
    verifier, err := slack.NewSecretsVerifier(c.Request.Header, os.Getenv("SIGNATURE_SECRET"))
    if err != nil {
        c.String(http.StatusBadRequest, "error initializing signature verifier: %s", err.Error())
        return
    }
    bodyBytes, err := ioutil.ReadAll(c.Request.Body)
    if err != nil {
        c.String(http.StatusInternalServerError, "error reading request body: %s", err.Error())
        return
    }
    bodyBytesCopy := make([]byte, len(bodyBytes))
    copy(bodyBytesCopy, bodyBytes)
    c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytesCopy))
    if _, err = verifier.Write(bodyBytes); err != nil {
        c.String(http.StatusInternalServerError, "error writing request body bytes for verification: %s", err.Error())
        return
    }
    if err = verifier.Ensure(); err != nil {
        c.String(http.StatusUnauthorized, "error verifying slack signature: %s", err.Error())
        return
    }
    c.Next()
}

func handlePlus(db *bolt.DB) func(c *gin.Context) {
    return func(c *gin.Context) {
        cmd, err := slack.SlashCommandParse(c.Request)
        if err != nil {
            c.String(http.StatusBadRequest, "invalid slash command payload: %s", err.Error())
            return
        }
        inputArr := strings.Split(strings.TrimSpace(cmd.Text), " ")
        if len(inputArr) != 2 {
            c.String(http.StatusBadRequest, "invalid number of input parameters provided")
            return
        }
        num1, err := strconv.Atoi(inputArr[0])
        if err != nil {
            c.String(http.StatusBadRequest, "invalid 1st input parameter: %s", err.Error())
            return
        }
        num2, err := strconv.Atoi(inputArr[1])
        if err != nil {
            c.String(http.StatusBadRequest, "invalid 2nd input parameter: %s", err.Error())
            return
        }
        buf, err := json.Marshal(&amp;amp;Input{
            Num1: num1,
            Num2: num2,
        })
        if err != nil {
            c.String(http.StatusInternalServerError, "error preparing request body: %s", err.Error())
            return
        }
        resp, err := http.Post("http://localhost:8080", "application/json", bytes.NewBuffer(buf))
        if err != nil {
            c.String(http.StatusInternalServerError, "error preparing http request: %s", err.Error())
            return
        }
        if resp.StatusCode != http.StatusOK {
            c.String(http.StatusInternalServerError, "invalid status code returned: %d", resp.StatusCode)
            return
        }
        output := &amp;amp;Output{}
        if err = json.NewDecoder(resp.Body).Decode(output); err != nil {
            c.String(http.StatusInternalServerError, "invalid response: %s", err.Error())
            return
        }
        var token string
        if err = db.View(func(tx *bolt.Tx) error {
            bucket := tx.Bucket([]byte("tokens"))
            if bucket == nil {
                return errors.New("error accessing tokens bucket")
            }
            token = string(bucket.Get([]byte(cmd.TeamID)))
            return nil
        }); err != nil {
            c.String(http.StatusInternalServerError, "error reading slack access token: %s", err.Error())
            return
        }
        if _, _, _, err = slack.New(token).SendMessage(cmd.UserID, slack.MsgOptionText(fmt.Sprintf("Sum is %d", output.Sum), true)); err != nil {
            c.String(http.StatusInternalServerError, "error sending slack message: %s", err.Error())
            return
        }
    }
}

func handleEvent(db *bolt.DB) func(c *gin.Context) {
    return func(c *gin.Context) {
        bodyBytes, err := ioutil.ReadAll(c.Request.Body)
        if err != nil {
            c.String(http.StatusInternalServerError, "error reading slack event payload: %s", err.Error())
            return
        }
        event, err := slackevents.ParseEvent(bodyBytes, slackevents.OptionNoVerifyToken())
        switch event.Type {
        case slackevents.URLVerification:
            ve, ok := event.Data.(*slackevents.EventsAPIURLVerificationEvent)
            if !ok {
                c.String(http.StatusBadRequest, "invalid url verification event payload sent from slack")
                return
            }
            c.JSON(http.StatusOK, &amp;amp;slackevents.ChallengeResponse{
                Challenge: ve.Challenge,
            })
        case slackevents.AppRateLimited:
            c.String(http.StatusOK, "ack")
        case slackevents.CallbackEvent:
            ce, ok := event.Data.(*slackevents.EventsAPICallbackEvent)
            if !ok {
                c.String(http.StatusBadRequest, "invalid callback event payload sent from slack")
                return
            }
            ie := &amp;amp;slackevents.EventsAPIInnerEvent{}
            if err = json.Unmarshal(*ce.InnerEvent, ie); err != nil {
                c.String(http.StatusBadRequest, "invalid inner event payload sent from slack: %s", err.Error())
                return
            }
            switch ie.Type {
            case slackevents.AppUninstalled:
                if err = db.Update(func(tx *bolt.Tx) error {
                    bucket := tx.Bucket([]byte("tokens"))
                    if bucket == nil {
                        return errors.New("error accessing tokens bucket")
                    }
                    return bucket.Delete([]byte(event.TeamID))
                }); err != nil {
                    c.String(http.StatusInternalServerError, "error handling app uninstallation")
                }
            default:
                c.String(http.StatusBadRequest, "no handler for event of given type")
            }
        default:
            c.String(http.StatusBadRequest, "invalid event type sent from slack")
        }
    }
}

func main() {
    db, err := configureDb()
    if err != nil {
        log.Fatal(err)
    }
    defer func() { _ = db.Close() }()
    app := gin.Default()
    app.Any("/install", handleInstallation(db))
    cmdGroup := app.Group("/cmd")
    cmdGroup.Use(signatureVerification)
    cmdGroup.POST("/plus", handlePlus(db))
    eventGroup := app.Group("/event")
    eventGroup.Use(signatureVerification)
    eventGroup.POST("/handle", handleEvent(db))
    _ = app.Run()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an event is sent, we check the type, and respond accordingly. The only callback event we’re going to handle is app uninstallation. We handle it by deleting the access token we used to access the Slack platform when we needed to interact with the workspace the app was deleted from.&lt;/p&gt;

&lt;p&gt;Run the updated version of the app which includes the event handler and then navigate to Features → Event Subscriptions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/AUFKkm5jPE5r3rEY3526UbB1Jqd-UseCe66E-Wupljc/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3k1Nnpq/OTh1Y3MycnQ3d2Jw/MXA3LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/AUFKkm5jPE5r3rEY3526UbB1Jqd-UseCe66E-Wupljc/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3k1Nnpq/OTh1Y3MycnQ3d2Jw/MXA3LnBuZw" alt="Image description" width="880" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enable events and under the ‘Request URL’ section, enter your ngrok URL suffixed with ‘/event/handle’ (e.g. ‘&lt;a href="https://03e2-109-66-212-180.ngrok.io/event/handle%E2%80%99"&gt;https://03e2-109-66-212-180.ngrok.io/event/handle’&lt;/a&gt;) and wait for the Slack platform to verify your events URL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/VBWM80gyhKm4vcrbhTMAraqkKvkHsJiavOcNNTfqo4Q/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3psY2Fh/bDd5bzFwN3YzdW8z/azloLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/VBWM80gyhKm4vcrbhTMAraqkKvkHsJiavOcNNTfqo4Q/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3psY2Fh/bDd5bzFwN3YzdW8z/azloLnBuZw" alt="Image description" width="880" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, under the ‘Subscribe to bot events’ section, add the ‘&lt;a href="https://api.slack.com/events/app_uninstalled"&gt;app_uninstalled&lt;/a&gt;’ event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleting Our App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the direct message channel your app opened with your user to inform you of the result of summing 2 integers, click on the app name:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/AAuE9yoe6qF0K_jr2EU7DtRwxjcZWRBfaiiOjg05aOo/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3l1dWc4/ejVkcjhwZGswN2Iy/eHp6LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/AAuE9yoe6qF0K_jr2EU7DtRwxjcZWRBfaiiOjg05aOo/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3l1dWc4/ejVkcjhwZGswN2Iy/eHp6LnBuZw" alt="Image description" width="880" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press ‘Configuration’, which should open a browser window with the details of the app installation on your workspace:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/iwUHFPz0pLX7UwmOeii2xApJtSzqy5fz59mbvFyZjdE/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3Nrbmpj/OW1mcGx1ZWFrNXR0/YW1lLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/iwUHFPz0pLX7UwmOeii2xApJtSzqy5fz59mbvFyZjdE/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3Nrbmpj/OW1mcGx1ZWFrNXR0/YW1lLnBuZw" alt="Image description" width="880" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And press the ‘Remove App’ button under the section with the same name.&lt;/p&gt;

&lt;p&gt;Open your workspace from your Slack client. You should notice that the direct message channel with your app is not there anymore:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/TLmzxcz4Pcty92lhzhqgggoTgxwio5TuTAZsbmSvD0k/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzBoYXo3/dzRjeTZrNnFjbDN3/aTAwLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/TLmzxcz4Pcty92lhzhqgggoTgxwio5TuTAZsbmSvD0k/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzBoYXo3/dzRjeTZrNnFjbDN3/aTAwLnBuZw" alt="Image description" width="520" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, you can see in the output of your Slack service, that a request was made to the events endpoint and that it was responded with a 200 OK status code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[GIN] 2022/05/22 - 00:33:34 | 200 |   39.002459ms |             ::1 | POST     "/event/handle"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DAv10195/slack-example"&gt;Here&lt;/a&gt;’s a Github repository containing all of the code relevant for this article. The tools referenced in this article include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://pkg.go.dev/github.com/slack-go/slack"&gt;Slack API in GO&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/gin-gonic/gin"&gt;Gin&lt;/a&gt; web framework&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/boltdb/bolt"&gt;Bolt&lt;/a&gt; DB framework&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ngrok.com/"&gt;ngrok&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://api.slack.com/apis/connections/events-api"&gt;Slack Events API&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hope you enjoyed reading and writing a cool Slack application together!&lt;/p&gt;

</description>
      <category>slack</category>
      <category>devops</category>
      <category>tutorials</category>
      <category>cloudops</category>
    </item>
  </channel>
</rss>
