A Simple Slack Bot in Go - The Bot

Praise the Sun

The Real Dark Souls Slack Bot Begins Here

Some friends of mine and I have our own Slack instance that we use for general communication and other assorted nonsense. Occasionally, that turns to the discussion of video games. One game that seems to come up often is Dark Souls - so when I was doing some looking into building a Slack bot in Go I decided I would create a bot based on “Solaire of Astora” and the common Dark Souls refrain of “Praise the Sun!”

This is a very simple project which uses the Slack Go package from nlopes. It is not a conversation bot and doesn’t make use of any machine learning or “AI” components - nothing but a simple regular expression. Read on and see the magic.

The core of any Go code - we declare our package as main. From there we import from the standard Go library and our Slack package.

package main

import (
  "fmt"
  "os"
  "regexp"
  "strings"

  "github.com/nlopes/slack"
)

Next the getenv() function takes in the string of the environment variable we want to make sure is set and returns it. If the environment variable is not set we’re just going to panic. I have a module version available over on Github which is what recent versions of the bot use but I wanted to keep this code as simple as possible to make it easier to cover. We’re pulling from the environment since the resulting bot is hosted on Google Compute Engine and we don’t want to hard code our Slack token - I’ll cover how that’s currently configured in a future post.

func getenv(name string) string {
  v := os.Getenv(name)
  if v == "" {
    panic("missing required environment variable " + name)
  }
  return v
}

Since the bot is so simple and we’re not doing any testing we’re not setting up any other functions - we’re jumping right into main(). First, we’ll get our Slack bot’s token from the environment and use it to instantiate and start up our bot.

func main() {
  token := getenv("SLACKTOKEN")
  api := slack.New(token)
  rtm := api.NewRTM()
  go rtm.ManageConnection()

The heart of our bot is the following loop. It’s a bit bigger then I want to typically show in one big code block. However, breaking it up might make it a tad difficult to follow - I’ll pull the important section out it a bit so we can go over it a little closer. Basically, we’re starting an infinite loop which is only broken by invalid credentials at startup, a Control-C, or SIGHUP. Our for loop allows us to loop through and keep an eye out for incoming events. When we see one we check it’s type and if it matches a slack.MessageEvent we begin the “heavy lifting”. Other possible events we’re watching out for are the aforementioned authorization errors or real-time messaging (RTM) error.

Loop:
  for {
    select {
    case msg := <-rtm.IncomingEvents:
      fmt.Print("Event Received: ")
      switch ev := msg.Data.(type) {

      case *slack.MessageEvent:
        info := rtm.GetInfo()

        text := ev.Text
        text = strings.TrimSpace(text)
        text = strings.ToLower(text)

        matched, _ := regexp.MatchString("dark souls", text)

        if ev.User != info.User.ID && matched {
          rtm.SendMessage(rtm.NewOutgoingMessage("\\[T]/ Praise the Sun \\[T]/", ev.Channel))
        }

      case *slack.RTMError:
        fmt.Printf("Error: %s\n", ev.Error())

      case *slack.InvalidAuthEvent:
        fmt.Printf("Invalid credentials")
        break Loop

      default:
        // Take no action
      }
    }
  }
}

Let’s take a closer look at our message event case. First, we call rtm.GetInfo() to pull in the information on the bot connection. It’s not really needed though in this case it is simply being used to check the bots ID is not the one triggering our message - which it wouldn’t likely ever trigger anyway since it doesn’t say the triggering text.

Once we have the bots information we then pull in the text body of the event using ev.Test. The text is then trimmed to remove leading and trailing spaces (again not really needed but done just to keep things tidy I suppose). Then the text is changed to lowercase - finally, the resulting string is matched against our trigger phrase. So saying the words “dark souls” or “Dark Souls” (or any other combination of upper and lower) should always result in a match.

If we have a match a new outgoing message is created and set to the channel the triggering text was found in.

      case *slack.MessageEvent:
        info := rtm.GetInfo()

        text := ev.Text
        text = strings.TrimSpace(text)
        text = strings.ToLower(text)

        matched, _ := regexp.MatchString("dark souls", text)

        if ev.User != info.User.ID && matched {
          rtm.SendMessage(rtm.NewOutgoingMessage("\\[T]/ Praise the Sun \\[T]/", ev.Channel))
        }

\[T]/ Praise the Sun \[T]/

We could easily extend the bot to respond to more phrases and mix up responses but I like the idea of keeping it as simple as possible.

To run our bot we can simply call go run or build it being sure to include the Slack token in the command.

SLACKTOKEN=slacktoken go run main.go

Full code listing

package main

import (
  "fmt"
  "os"
  "regexp"
  "strings"

  "github.com/nlopes/slack"
)

func getenv(name string) string {
  v := os.Getenv(name)
  if v == "" {
    panic("missing required environment variable " + name)
  }
  return v
}

func main() {
  token := getenv("SLACKTOKEN")
  api := slack.New(token)
  rtm := api.NewRTM()
  go rtm.ManageConnection()

Loop:
  for {
    select {
    case msg := <-rtm.IncomingEvents:
      fmt.Print("Event Received: ")
      switch ev := msg.Data.(type) {

      case *slack.MessageEvent:
        info := rtm.GetInfo()

        text := ev.Text
        text = strings.TrimSpace(text)
        text = strings.ToLower(text)

        matched, _ := regexp.MatchString("dark souls", text)

        if ev.User != info.User.ID && matched {
          rtm.SendMessage(rtm.NewOutgoingMessage("\\[T]/ Praise the Sun \\[T]/", ev.Channel))
        }

      case *slack.RTMError:
        fmt.Printf("Error: %s\n", ev.Error())

      case *slack.InvalidAuthEvent:
        fmt.Printf("Invalid credentials")
        break Loop

      default:
        // Take no action
      }
    }
  }
}

And there we have it!

Next time we’ll go over how I have deployed the bot and prep for a “better” deployment method. Until then…


Enjoy this post?
How about buying me a coffee?