DEV Community

Cover image for A Simple Slack Bot in Go - The Bot
Steve Layton
Steve Layton

Posted on

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!

It's alive!

Next time we'll go over how I have deployed the bot and prep for a "better" deployment method. Until then...



Top comments (10)

Collapse
 
sergioteixeiraptc profile image
Sergio Teixeira

Hello, how would you run shell commands with the same approach?

Collapse
 
shindakun profile image
Steve Layton

If you are looking to run shell commands after someone says something in slack you could probably use the exec package from the standard library. Just update whatever your match text is and have that trigger a function or run exec directly. I haven't really tried it though so your mileage may vary.

Collapse
 
shomali11 profile image
Raed Shomali

This is great!

I put together github.com/shomali11/slacker to encapsulate most of this and try to make it simpler to create Bots in Slack.

Check it out. Let me know what you think.

Collapse
 
shindakun profile image
Steve Layton

Very cool! Definitely worth a look for a larger bot project.

Collapse
 
ajinkyax profile image
Ajinkya Borade

part 2 would be a great addition to this helpful tutorial #golang
thanks again.

Collapse
 
shindakun profile image
Steve Layton

Thanks for the comment! I hope to have part two up in a day or so.

Collapse
 
eosorio profile image
Ed Osorio

Sadly nlopes/slack uses deprecated Slack user tokens instead Slack applications

Collapse
 
shindakun profile image
Steve Layton

True, there are workarounds to get a newer oauth token which would probably work fine for a bot like this. Maybe I'll check it out and post an update.

Collapse
 
vicentdev profile image
Vicent

Why did you use label "loop" instead of boolean condition for breaking the loop? I'm pretty noob with golang and I don't know a lot of about performance with this language -.-'

Collapse
 
shindakun profile image
Steve Layton

In this case, it's so we can break all the way out of the for/select/switch blocks all in one go and it keeps the code a tad more readable I think. Bill Kennedy has a good post about this over on Ardan Labs.