Building a Twitch.tv Chat Bot with Go - Part 2
Tags: showdev, webdev, go, api
Published: 2017-12-27 03:35:08

---
title: Building a Twitch.tv Chat Bot with Go - Part 2
published: true
description: A walkthrough of how to build a basic Twitch.tv IRC chat bot.
tags: showdev, webdev, go, api
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ileql7czrfggl7slt9n.jpg
series: Building a Twitch.tv Chat Bot with Go
---

EDIT (5/13/2019): Since the time of writing the package has been renamed from "twitchbot" to "bot". However, the tagged releases used for this tutorial still use the old name.

All of the disclaimers and warnings at the beginning of Part 1 apply here.

## Step 1

Go ahead and jump to Step 1 via the following:

```bash
$ git reset --hard step-1
```

In the previous step we laid out the data types that the `BasicBot` struct would need to function properly. Now, we'll lay out the behavior for the bot. To do that we'll start by filling out the `TwitchBot` interface.

From the notes just below the import block, we need to make functions that do the following:

- Connect to the Twitch IRC server
- Disconnect from the Twitch IRC server
- Parse and react to messages from the chat
- Join a specific channel once connected
- Read from the super-top-secret credentials file to get the bot's password
- Send messages to the current chat channel

So, in Go terms:

- Connect()
- Disconnect()
- HandleChat() error
- JoinChannel()
- ReadCredentials() (*OAuthCred, error)
- Say(msg string) error

The `ReadCredentials()` function looks funny, because the `OAuthCred` pointer being returned is a mistake. Don't worry, it gets fixed in the following steps. I thought about omitting it for the sake of the walkthrough, but it's bound to make anyone following along think, "Huh?", while turning their head sideways.

We'll also need a function to kick everything off, so the `Start()` function will be added. Pop open `twitchbot.go` and make the following changes to the `TwitchBot` interface:

```go
type TwitchBot interface {
	Connect()
	Disconnect()
	HandleChat() error
	JoinChannel()
	ReadCredentials() (*OAuthCred, error)
	Say(msg string) error
	Start()
}
```

Since `OAuthCred` is undefined, we'll add that too. Let's add it above `TwitchBot`.

```go
type OAuthCred struct {
	Password string `json:"password,omitempty"`
}

type TwitchBot interface {
...
```

Now that `OAuthCred` has been defined, let's add a pointer to an `OAuthCred` instance to the BasicBot struct, for later. The `BasicBot` definition should now look like:

```go
type BasicBot struct {
	Channel string
	Credentials *OAuthCred // add the pointer field
	MsgRate time.Duration
	Name string
	Port string
	PrivatePath string
	Server string
}
```

Go ahead and build now, if there are no explosions we can move on.

```bash
$ go fmt ./... && go build
```

## Step 2

Let's run the following to jump to Step 2:

```bash
$ git reset --hard step-2
```

Now, we have to make the `BasicBot` implement the functions that we added to the `TwitchBot` interface. Let's start go in order.

#### Connect()

`BasicBot.Connect()` is going to require the [`net`](https://golang.org/pkg/net/) and [`fmt`](https://golang.org/pkg/fmt/) packages, so we'll add those to the import block:

```go
import (
	"fmt"
	"net"
	...
)
```

Since we want to see what the bot is doing when it connects, we'll need to write to standard output. Unfortunately that can get a bit unwieldy, so lets add in some helper functions to timestamp the log messages. We'll also add a constant time-format string for `timeStamp()` to use, just after the import block.

```go
import (
...
)

const PSTFormat = "Jan 2 15:04:05 PST"

...

// for BasicBot
func timeStamp() string {
	return TimeStamp(PSTFormat)
}

// the generic variation, for bots using the TwitchBot interface
func TimeStamp(format string) string {
	return time.Now().Format(format)
}
```

And, then the function itself:

```go
// Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it
// succeeds or is manually shutdown.
func (bb *BasicBot) Connect() {
	var err error
	fmt.Printf("[%s] Connecting to %s...\n", timeStamp(), bb.Server)

	// makes connection to Twitch IRC server
	bb.conn, err = net.Dial("tcp", bb.Server+":"+bb.Port)
	if nil != err {
		fmt.Printf("[%s] Cannot connect to %s, retrying.\n", timeStamp(), bb.Server)
		bb.Connect()
		return
	}
	fmt.Printf("[%s] Connected to %s!\n", timeStamp(), bb.Server)
	bb.startTime = time.Now()
}
```

Great, now we've got the `BasicBot.Connect()` function, and it's set to create a connection to the Twitch IRC server with all the necessary information. However, we don't have a way of storing the connection, like `bb.conn` assumes within the `BasicBot.Connect()` function. We're also missing a `bb.startTime` field to hold the time at which the bot successfully connected to the server. Let's add them:

```go
type BasicBot struct {
	Channel string
	conn net.Conn // add this field
	Credentials *OAuthCred
	MsgRate time.Duration
	Name string
	Port string
	PrivatePath string
	Server string
	startTime time.Time // add this field
}
```

#### Disconnect()

```go
// Officially disconnects the bot from the Twitch IRC server.
func (bb *BasicBot) Disconnect() {
	bb.conn.Close()
	upTime := time.Now().Sub(bb.startTime).Seconds()
	fmt.Printf("[%s] Closed connection from %s! | Live for: %fs\n", timeStamp(), bb.Server, upTime)
}
```

This function does what it says on the tin, and outputs some info on the duration of the bot's connection. Neat!

#### HandleChat()

`BasicBot.HandleChat()` is going to be doing the heavy lifting, so there are four packages that are going to be added to the import block:

```go
import (
	"bufio"         // 1
	"errors"        // 2
	"fmt"
	"net"
	"net/textproto" // 3
	"regexp"        // 4
	"time"
)
```

In order for the bot to understand and react to chat messages, we're going to need to parse the raw messages and use context. That's why we've imported the `regexp` package. Let's compile some regular expressions for use within the function later. You can add the following under the import block:

```go
// Regex for parsing PRIVMSG strings.
//
// First matched group is the user's name and the second matched group is the content of the
// user's message.
var msgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #\w+(?: :(.*))?$`)

// Regex for parsing user commands, from already parsed PRIVMSG strings.
//
// First matched group is the command name and the second matched group is the argument for the
// command.
var cmdRegex *regexp.Regexp = regexp.MustCompile(`^!(\w+)\s?(\w+)?`)
```

You can (and should!) read the [Twitch Docs](https://dev.twitch.tv/docs/irc) on the various types of IRC messages that get sent, but the gist of it is: 

- there are PING messages that must trigger a PONG response from the bot, otherwise it will be disconnected
- there are PRIVMSG messages that are sent as a result of someone (including the bot itself) talking in the current chat channel (despite being called "PRIVMSG" messages, they are public to the current chat channel)

The `msgRegex` variable is going to be used to parse the initial PRIVMSGs and the `cmdRegex` variable will be used for further parsing to catch bot commands in chat.

Then we'll have the function itself:

```go
// Listens for and logs messages from chat. Responds to commands from the channel owner. The bot
// continues until it gets disconnected, told to shutdown, or forcefully shutdown.
func (bb *BasicBot) HandleChat() error {
	fmt.Printf("[%s] Watching #%s...\n", timeStamp(), bb.Channel)

	// reads from connection
	tp := textproto.NewReader(bufio.NewReader(bb.conn))

	// listens for chat messages
	for {
		line, err := tp.ReadLine()
		if nil != err {

			// officially disconnects the bot from the server
			bb.Disconnect()

			return errors.New("bb.Bot.HandleChat: Failed to read line from channel. Disconnected.")
		}

		// logs the response from the IRC server
		fmt.Printf("[%s] %s\n", timeStamp(), line)

		if "PING :tmi.twitch.tv" == line {

			// respond to PING message with a PONG message, to maintain the connection
			bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n"))
			continue
		} else {

			// handle a PRIVMSG message
			matches := msgRegex.FindStringSubmatch(line)
			if nil != matches {
				userName := matches[1]
				msgType := matches[2]

				switch msgType {
				case "PRIVMSG":
					msg := matches[3]
					fmt.Printf("[%s] %s: %s\n", timeStamp(), userName, msg)

					// parse commands from user message
					cmdMatches := cmdRegex.FindStringSubmatch(msg)
					if nil != cmdMatches {
						cmd := cmdMatches[1]
						//arg := cmdMatches[2]

						// channel-owner specific commands
						if userName == bb.Channel {
							switch cmd {
							case "tbdown":
								fmt.Printf(
									"[%s] Shutdown command received. Shutting down now...\n",
									timeStamp(),
								)

								bb.Disconnect()
								return nil
							default:
								// do nothing
							}
						}
					}
				default:
					// do nothing
				}
			}
		}
		time.Sleep(bb.MsgRate)
	}
}
```

There's a lot, but the most important bits are where we read from the bot's connection...

```go
...
// reads from connection
tp := textproto.NewReader(bufio.NewReader(bb.conn))
...
```

...and attempt to keep it alive, by responding to PINGs.

```go
...
if "PING :tmi.twitch.tv" == line {

	// respond to PING message with a PONG message, to maintain the connection
	bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n"))
	continue
}
...
```

The handling of the chat occurs in an infinite loop and has a slight delay to prevent the bot from breaking the message limits, if it were to send chat messages itself.

#### JoinChannel()

```go
// Makes the bot join its pre-specified channel.
func (bb *BasicBot) JoinChannel() {
	fmt.Printf("[%s] Joining #%s...\n", timeStamp(), bb.Channel)
	bb.conn.Write([]byte("PASS " + bb.Credentials.Password + "\r\n"))
	bb.conn.Write([]byte("NICK " + bb.Name + "\r\n"))
	bb.conn.Write([]byte("JOIN #" + bb.Channel + "\r\n"))

	fmt.Printf("[%s] Joined #%s as @%s!\n", timeStamp(), bb.Channel, bb.Name)
}
```

`BasicBot.JoinChannel()` is quite important as it passes along all the necessary information to Twitch, including the bot's password, to create the desired connection.

`bb.Credentials.Password` will be initialized by the `BasicBot.ReadCredentials()` function, which is coming up.

#### ReadCredentials()

In order for `BasicBot.ReadCredentials()` to do its job, it will need to be able to parse a JSON file at a pre-specified path (stored in the `PrivatePath` field). So, we'll need to add four more packages to the import block.

```go
import (
	"bufio"
	"encoding/json" // 1
	"errors"
	"fmt"
	"io"            // 2
	"io/ioutil"     // 3
	"net"
	"net/textproto"
	"regexp"
	"strings"       // 4
	"time"
)
```

Then, the function itself:

```go
// Reads from the private credentials file and stores the data in the bot's Credentials field.
func (bb *BasicBot) ReadCredentials() error {

	// reads from the file
	credFile, err := ioutil.ReadFile(bb.PrivatePath)
	if nil != err {
		return err
	}

	bb.Credentials = &OAuthCred{}

	// parses the file contents
	dec := json.NewDecoder(strings.NewReader(string(credFile)))
	if err = dec.Decode(bb.Credentials); nil != err && io.EOF != err {
		return err
	}

	return nil
}
```

Note: An `*OAuthCred` is no longer being returned. So update the definition for `TwitchBot.ReadCredentials()`:

```go
type TwitchBot interface {
	...
    ReadCredentials() error
	...
}
```

#### Say()

```go
// Makes the bot send a message to the chat channel.
func (bb *BasicBot) Say(msg string) error {
	if "" == msg {
		return errors.New("BasicBot.Say: msg was empty.")
	}
	_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG #%s %s\r\n", bb.Channel, msg)))
	if nil != err {
		return err
	}
	return nil
}
```

#### Start()

```go
// Starts a loop where the bot will attempt to connect to the Twitch IRC server, then connect to the
// pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to
// shut down, or is forcefully shutdown.
func (bb *BasicBot) Start() {
	err := bb.ReadCredentials()
	if nil != err {
		fmt.Println(err)
		fmt.Println("Aborting...")
		return
	}

	for {
		bb.Connect()
		bb.JoinChannel()
		err = bb.HandleChat()
		if nil != err {

			// attempts to reconnect upon unexpected chat error
			time.Sleep(1000 * time.Millisecond)
			fmt.Println(err)
			fmt.Println("Starting bot again...")
		} else {
			return
		}
	}
}
```

`BasicBot.Start()` attempts to initialize the bot's credentials before kicking everything else off.

Go ahead and build now, if there are no explosions you're done!

```bash
$ go fmt ./... && go build
```

BasicBot should now be fully functional.

## Step 3/4

```bash
$ git reset --hard step-3
```

Cool, now that we've got a fully functioning bot, let's run it. Unfortunately that can't be done within the `twitchbot` package since it's just a library. You can however grab my test repo!

```bash
$ go get github.com/foresthoffman/twitchbotex
```

Navigate your terminal into the twitchbotex directory...

Linux/MacOS

```bash
$ cd $GOPATH/src/github.com/foresthoffman/twitchbotex
```

Windows

```powershell
> cd %GOPATH%\src\github.com\foresthoffman\twitchbotex
```

...and open the `main.go` file. You're going to want to replace the `Channel` and `Name` values with your own channel usernames. Remember that the `Channel` value **must** be lowercase.

Then, from your terminal, you're going to want to add a `private/` directory in the `twitchbotex` package directory.

`private/` is where your `oauth.json` file and the bot's password will be contained. To retrieve this password, you need to connect to Twitch using their [OAuth Password Generator](http://twitchapps.com/tmi/). The token will have the following pattern: "oauth:secretsecretsecretsecretsecret".

Once you have the OAuth token for the bot account, you can place it in the `oauth.json` file containing the following JSON (using your token):

```json
{
	"password": "oauth:secretsecretsecretsecretsecret"
}
```

This will allow the bot to read in the credentials before attempting to connect to Twitch's servers.

With all that done, you should now be able to build and run the bot:

```bash
$ go fmt ./... && go build && ./twitchbotex
```

Entering `!tbdown` into your chat, with the bot running, will gently tell the bot to shutdown, per this block from `BasicBot.HandleChat()`:

```go
...
// channel-owner specific commands
if userName == bb.Channel {
    switch cmd {
    case "tbdown":
        fmt.Printf(
            "[%s] Shutdown command received. Shutting down now...\n",
            timeStamp(),
        )

        bb.Disconnect()
        return nil
    default:
        // do nothing
    }
}
...
```

## Pretty Logs

If you'd like to add a dash of color to the bot's log messages, you can switch back to the `twitchbot` package really quick and run:

```bash
$ git reset --hard step-4
```

Then rebuild and run `twitchbotex`, and the logs will be a little easier on the eyes.

![An image comparison of the console without and with colored text.](https://foresthoffman.com/wp-content/uploads/2017/12/slice1.jpg "Standard Output Color Comparison")

Thank you for reading! If you have any questions you can hit me up on Twitter ([@forestjhoffman](https://twitter.com/forestjhoffman)) or email (forestjhoffman@gmail.com).

## Image Attribution

- Photo by <a href="https://unsplash.com/@casparrubin?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Caspar Camille Rubin</a> on <a href="https://unsplash.com/s/photos/twitch?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
- Glitch, Copyright of Twitch.tv