DEV Community

Cover image for Creating a GIF sending Discord bot with Golang
Hasan Behbahani
Hasan Behbahani

Posted on

Creating a GIF sending Discord bot with Golang

Discord has been used by almost every gamer, and nowadays, it's made its way into the lives of programmers.
I myself got into Discord in late 2017, and ever since then, I found myself more drawn to using this social media service than any other app out there.
In any case, anyone who has ever used Discord knows that bots play a huge role. These bots are AI-powered tools that can perform many actions, from community engagement to content moderating, to even streaming music from YouTube & Spotify.
In this article, we are going to be making a simple Discord Bot with Golang that you can add to your server. What will this Bot be doing, you might ask? Well, you can search GIFs by keywords, and you can share them with your buddies on your favorite server!

anyway, let's get started!


Github Repo For this Project

First things first, you need to let Discord know that you're not just some average Discord user, but that you're a developer! So you need to enable developer mode in your Discord account. Go to your Discord application, go to Settings button. Then, click on Advanced and then enable Developer Mode:

Image description

Head off to the Discord Developer Portal & create a new application. We'll name this one "Giffie" because it's about GIFs, and we want to be creative.
From there, head to the Bot tab and create a new bot and also give it the username "Giffie".
Finally, if we want to add this bot to our server, go to the oAuth2 tab, select URL generator, under scopes check bot and visit the generated URL on the bottom of the page:

Image description

You'll see the usual screen for adding custom bots to Discord servers. Select your server and you should see it on your server under offline users! See how easy that was. pretty cool right:

Image description

Now comes the exciting part. Coding our bot! so grab a coffee and make yourself comfortable.




First, through the command-line, let's create our Go project:
$ mkdir giffie-bot 
$ cd giffie-bot
$ go mod init giffie
Enter fullscreen mode Exit fullscreen mode

The commands above will create a giffie-bot directory and a go.mod file, which'll look something like this:

module giffie

go 1.17
Enter fullscreen mode Exit fullscreen mode

There are several libraries out there to hit Discord’s API, each with its own traits and capabilities, but ultimately they all achieve the same thing. Since we are using Go, we'll use bwmarrin's DiscordGo library.

According to this repo's readme:

DiscordGo is a Go package that provides low level bindings to the Discord chat client API. DiscordGo has nearly complete support for all of the Discord API endpoints, websocket interface, and voice interface.

This library has more than what we need.
before we go any further, what does our bot do?

  • Users will type a prefix followed by a keyword. (i.e !search <keyword> or =search <keyword>)
  • Our bot will go ahead and search for a Gif related to that keyword and show that to the user and everyone to see in the server.

The end results will look something like this:

Image description

So let's first install our needed libraries. Run the commands below:

$ go get -u github.com/bwmarrin/discordgo
$ go get -u github.com/joho/godotenv
Enter fullscreen mode Exit fullscreen mode

The second line installs the library needed for using .env files, so we can safely & securely use tokens or keys without hard-coding it in our code. we'll talk about its use-case later.

Let's write our first lines of code! Create a main.go file in the giffie-bot directory right next to the go.mod file. We then initialize the package, called main, and all dependencies/libraries we need to import and use in our main file:

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "os/signal"
    "strings"
    "syscall"

    "github.com/bwmarrin/discordgo"
    "github.com/joho/godotenv"
)
Enter fullscreen mode Exit fullscreen mode

next, we'll initialize our main function, which'll create a Discord session, registers our handlers and runs our Bot.

Before we write our main function, we'll need our Discord bot token. To get that, head over back to Discord Developer Portal, click on your application, then on the left panel, click on Bot and the Reset Token button.( remember that if you forget or lose access to your token, you'll need to regenerate a new one. so be careful as much as you can)
After getting your token, create a .env file right next to main.go, so we can handle our environment variables.
That's where we'll be using the Godotenv library, which'll help us load environment variables.
your .env file should look something similar to this:

DISCORD_TOKEN=< your discord token >
Enter fullscreen mode Exit fullscreen mode

So let's write our main function:

func main() {
    // 1
    err := godotenv.Load(".env")
    Token := os.Getenv("DISCORD_TOKEN")
    if err != nil {
        log.Fatal(err)
    }

    // 2
    dg, err := discordgo.New("Bot " + Token)
    if err != nil {
        fmt.Println("Error creating a discord Session, ", err)
    }

    // 3
    dg.AddHandler(ready)
    dg.AddHandler(messageCreate)

    // 4
    err = dg.Open()
    if err != nil {
        fmt.Println("Error opening Discord Session, ", err)
    }
    fmt.Println("The bot is now running. Press CTRL-C to exit.")

    // 5
    sc := make(chan os.Signal, 1)
    signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
    <-sc
}
Enter fullscreen mode Exit fullscreen mode

That's a lot of code. But I'll try and explain the important lines.

  1. We first load our .env file with the godotenv.Load() function, and pass in our file's name as its argument. After loading the file, we'll use the built-in os Go package to use our environment variable called DISCORD_TOKEN and assign it to the variable Token.
  2. We then create a new Discord session using the provided bot token from the last step.
  3. We register the ready & messageCreate as callbacks for Event & MessageCreate events. Furthermore, We'll declare those functions later.
  4. Open a websocket connection to Discord and begin listening.
  5. Wait here until CTRL-C or other term signal is received.

Pretty basic stuff, right?
Now let's write our handlers.

Our first handler, the ready function, is a simple one. It'll update the status of our bot.
The declaration of the function is as simple as it gets:

func ready(s *discordgo.Session, event *discordgo.Event) {
    s.UpdateGameStatus(0, "!search < keyword >")
}
Enter fullscreen mode Exit fullscreen mode

the UpdateGameStatus() function(as the name suggests), will update the status of the bot. To see this in action, comment out the dg.addHandler(messageCreate) line in your main function, and let's start our bot!
Run the below commands in the giffie-bot directory:

$ go build 
$ ./giffie
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward stuff. This'll build our project and run the generated binary file.
Let's go back to our Discord server and check the right panel:

Image description

Well, what do you know? The bot is now online! With the status that we provided in our UpdateGameStatus() function!

But the bot is not going to do anything. We want it to listen to messages sent by users, and trigger a command every time a user types !search followed by a keyword.

Uncomment the dg.addHandler(messageCreate) line in our main function and let's create the messageCreate handler.

But before we do that, let me explain how are we going to search for a specific GIF based on a keyword.
We're going to be using an API. More specifically, an API that'll help us find a GIF based on a word.
There are many GIF websites out there that offer a free API services, most notably Giphy & Tenor.
We're going to be using Giphy, but if you prefer the Tenor API or any other service, feel free to use them, but we're going to stick to the Giphy API for this tutorial.
To read more on the Giphy & Tenor APIs, check out the below links:

So in order to use the Giphy API, we'll need a token.
Head over to Giphy and create an account if you don't already have one, and login.
On the developer dashboard, which'll look something like this:
Image description
Go ahead and click on the Create an App button, click on Next Step, and you'll be presented with a form. Fill it in and you'll be presented with your API Key:

Image description

Go back to your .env file and add this line:

GIPHY_TOKEN=< your Giphy key >
Enter fullscreen mode Exit fullscreen mode

You'll now have access to your Giphey API using the os.Getenv() function.
Now how are we gonna be able to search for a GIF?
After searching in the Giphy docs, I came across this endpoint:

api.giphy.com/v1/gifs/random
Enter fullscreen mode Exit fullscreen mode

We'll use this endpoint to get our desired Gifs link!
But in order to use this endpoint, we need to provide it with a Required request parameter, the api_key and a tag, which is our keyword.
Before writing the code, let's use cURL to see what we get when we use this endpoint.
Run the below command:

curl -X GET  "https://api.giphy.com/v1/gifs/random?api_key=<Giphy API KEY>&tag=<Keyword>"
Enter fullscreen mode Exit fullscreen mode

If the request was a success, you'll receive a huge and complex JSON response.
We have to turn that JSON into a Golang Struct, so our code can understand our response.
We'll use JSON-to-Go.
Copy and paste the JSON response in there, and you'll be presented with a neat but huge Golang struct.
But don't worry about it, just copy the Struct and paste it in your code, right before the main function, and rename it to GifSearch.
That's a lot of work for just sending a GET request and deconstructing the JSON response. But believe me, you'll realize how it'll make your life easier in the future.

Now, back to our code.
In Go, we'll use the http built-in library for adding headers to our URL endpoint.(you can see that in Go, we don't need to install 3rd party packages because most of the useful stuff are built-in).
So let's write the messageCreate function:

func messageCreate(s *discordgo.Session, message *discordgo.MessageCreate) {
    // 1
    err := godotenv.Load(".env")
    giphyToken := os.Getenv("GIPHY_TOKEN")
    if err != nil {
        log.Fatal(err)
    }
    // 2
    if message.Author.ID == s.State.User.ID {
        return
    }
    // 3
    command := strings.Split(message.Content, " ")
    // 4
    if command[0] == "!search" && len(command) > 1 {
        url := "https://api.giphy.com/v1/gifs/random"
        var result GifSearch
        // 5
        gifKeyword := strings.Join(command[1:], " ")

        // 6
        req, err := http.NewRequest("GET", url, nil)
        if err != nil {
            fmt.Println("Error in making a new Request", err)
        }
        query := req.URL.Query()
        query.Add("api_key", giphyToken)
        query.Add("tag", gifKeyword)
        req.URL.RawQuery = query.Encode()
        client := http.Client{}
        res, err := client.Do(req)
        if err != nil {
            fmt.Println("Error in getting a response, ", err)
        }
        body, _ := ioutil.ReadAll(res.Body)
        if err := json.Unmarshal(body, &result); err != nil {
            fmt.Println("Can not unmarshall JSON", err)
        }
        // 7
        s.ChannelMessageSend(message.ChannelID, result.Data.EmbedUrl)
        res.Body.Close()
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, most of the heavy work is done in this handler.
So let's go through the code step-by-step:

  1. We'll first load the GIPHY_TOKEN environment variable from the .env file, just like we did last time, and assign it to the variable giphyToken.
  2. this if statement is just to check if the messages sent, are by a user or by the Bot itself. if they indeed were sent by the Bot, just return.
  3. This is Golang Data structure 101. Splits the message sent from a string into a slice separated by a whitespace. More in here
  4. this if statement will check if the first element of the slice is the string !search and if the length of the slice is more than one, so it at least contains a keyword, not just a prefix.
  5. concatenates the the rest of the elements into a single string, and assigning it to the variable gifKeyword.
  6. This is where the heavy-lifting is happening. It's kinda confusing & the syntax may be a bit hard, but it's kinda easy. We just create a new GET request with the http.NewRequest() function and add our Request Parameters, with req.URL.Query(). Create a client, and send the request with Client.Do(req), then receive and read the response with ioutil.ReadAll(res.Body) and unmarshal the JSON we got from the response according to the Struct we defined earlier. You may wanna read this again to understand the full functionality of this block of code.
  7. As a final step, we'll just send the URL we got from the response to our server, using the s.ChannelMessageSend() function.

that's it!
That took a lot more effort than it should've, but the end result is worth it.
Build your project like we did last time, and run your code.
Go back to your server and in the text-box, type !search followed by anything you want. You'll see that the Bot will respond with a GIF!

Conclusion:

There's way more amazing things you can do with a Discord bot! Even ones that uses databases and so on.
The Github repo for this tutorial can be found here.
Fore more resources, I'll leave a few links at the end of the post. be sure to check them out.

Thanks for reading. I hope this has been a helpful introduction on how to make a simple yet useful Discord bot. If it was helpful, do leave a comment or share this article around for more reach.

Resources:

https://github.com/bwmarrin/discordgo
https://github.com/joho/godotenv
https://developers.giphy.com/docs/api/endpoint
https://pkg.go.dev/strings
https://mholt.github.io/json-to-go/

Discussion (1)

Collapse
b3hniya profile image
Behniya

Good article. Hope to see more.