How to create a discord bot in Go?

svendetta

Sven

Posted on October 1, 2023

How to create a discord bot in Go?

Introduction

This is meant to be a small guide on how to build a simple discord bot using Go.

I am by no means a professional senior engineer, so please do not expect that kind of wisdom in this post. My goal here is to build in public and enable other beginners to look into this topic.

Goal

The goal of this tutorial is to build a discord bot that accepts slash commands to trigger some interaction.

We will take a look into:

  1. How to set up a bot in the discord developer portal.
  2. How to invite the bot to your server.
  3. How to build a working bot using the amazing discordgo package.

While we build, we will try to follow good practices when it gets to version control, so we keep our project clean and tidy.

So without further delay, let us start!

What do we need?

  • Access to the discord developer portal
  • A personal testing/development server within the discord application
  • Some basic Go knowledge to understand our code
  • git
  • (Optional) A github account

Creating the application in the discord developer portal

First and foremost, we need to create an application in the Discord Developer Portal. This gives us access to the Discord API. By creating an application, we will get a token that is used to authenticate our bot later on.

After logging into the portal, we should be greeted by the "Applications" view. If not, please navigate there. From here, we can create a new application.

Now we need to name our bot and accept discords TOS & Developer Policy.

Create application form

Once completed, we will be redirected to the overview of our newly created application. While it's not essential, I would recommend adding a profile picture and a short description to your bot. The profile picture makes it more lively!

Now, on to the essential steps in the Dev Portal!

Navigate to the "Bot" view from the left-side menu.
Uncheck the "Public Bot" option if you do not want other people to be able to invite the bot to their servers.

Scroll down and check the required permissions for the bot. For simplicity I just check the "Administrator" permission, so the bot is allowed to do everything in my server:

Checked administrator permission

Next we have to reset our bots token:

Bot token

This generates a token that our bot will use to authenticate to discord. Keep this token private! If the token is publicly visible, other people gain access to your bot and are able to use it for malicious actions. I obviously reset the token after creating the screenshot, so I can publicly show it.

We will copy the token in a dotenv file later, so we can safely access it in code, without pushing it into our remote repository on Github.

Lastly we have to invite the bot to our discord server. For that, navigate to the OAuth2/Url Generator.

OAuth2 view

You'll see a huge form called "Scopes". Check the "bot" option and a second form will appear. Here again, check the bots permissions. For this tutorial it will just be "Administrator".

At the very bottom of the page there will be a generated URL, copy that. Then paste it into a new browser tab. This is a discord invite for the bot. Select the server that it is supposed to join and proceed through the form. Once it's completed the bot should have joined your server. Nice!

The bot will appear offline. No worries though, it'll show up online once we run some code.

Now we are all done with the developer portal and can proceed to create our project.

Creating the project

We are going to properly use git in our project. Make sure you have it on your machine before you proceed.

So let us create a folder that will contain our bot:

mkdir discordbot && cd discordbot
Enter fullscreen mode Exit fullscreen mode

Next, initiate a new git repository:

git init .
Enter fullscreen mode Exit fullscreen mode

Optionally, if you have a remote repository on GitHub, add it as a origin. I will have a public repository specifically made for this tutorial.

git remote add origin git@github.com:<<YOUR USERNAME>>/<<YOUR REPOSITORY>>.git
Enter fullscreen mode Exit fullscreen mode

Init a new Go project:

go mod init <<YOUR PROJECT NAME>>
// In my case: go mod init github.com/Zwnow/discordbot
Enter fullscreen mode Exit fullscreen mode

The next step is to create these files and folders.
Using the cmd folder for the main Go file is general good practice. Within the internal folder we will create our commands.

discordbot
├── cmd
│   └── main.go
├── .env
├── go.mod
└── internal
    └── commands.go
Enter fullscreen mode Exit fullscreen mode

But before we create our first commit we will have to create a .gitignore file, so the .env will be excluded from the repository:

echo .env >> .gitignore && git add -A && git commit -m "Initial commit"
Enter fullscreen mode Exit fullscreen mode

In case of working with a remote repository we can now push as well.

That is it, we initialized a new project and can now start to code!

It is good practice to also add a README.md to a git repository, so I'd recommend doing that. I wont include it in this tutorial though.

Coding the bots main function

The main file of our bot will include a few packages that you can copy from the following codeblock:

//discordbot/cmd/main.go
package main

import (
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"

    c "github.com/Zwnow/discordbot/internal"
    "github.com/bwmarrin/discordgo"
    "github.com/joho/godotenv"
)
Enter fullscreen mode Exit fullscreen mode

For the last two imports we need to run:

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

Our main.go file will have this structure:

package main

import (
 //...
)

func init() {
    //...
}

func main() {
    //...
}
Enter fullscreen mode Exit fullscreen mode

The first two sections we already completed! Now we need to take a look at the init function.

func init() {
    err := godotenv.Load(".env")
    if err != nil {
    log.Fatalf("Error while loading the .env file: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we load our .env file, so we can access our Token. We did not fill our .env file yet so lets do that now.
The content should be a single line containing our bots token like this:
TOKEN=YOURTOKEN

Lastly we will have to create a main function that we can use to test if our bot works!

Here is the code that does exactly that:

func main() {
    // Start a new discord session
    discord, err := discordgo.New("Bot " + os.Getenv("TOKEN"))
    if err != nil {
        log.Fatalf("Error while creating a discord session: %v", err)
    }

    // Open the connection
    err = discord.Open()
    if err != nil {
        fmt.Println("error opening connection, ", err)
        return
    }

    // Properly shut down on exit signal
    fmt.Println("Bot is now running. Press CTRL-C to exit.")
    sc := make(chan os.Signal, 1)
    signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
    <-sc

    discord.Close()
}
Enter fullscreen mode Exit fullscreen mode

That's the main for now. We can test if everything works correctly by running the code!
go run cmd/main.go

If everything worked, the bot should now appear online:

Bot is online in member list of discord

The first time I came this far I had some trouble with the authentication. Make sure you followed everything exactly as I did it and it should work!

Now we need to create and register commands. But first lets commit to git as we have a breakthrough!

Creating and registering commands

This is the final step of my tutorial. The point at which our bot gets some functionality. Let's start by creating a new branch in our repository:
git checkout -b commands

This way we can safely tinker around in our already existing code without breaking anything!

In this part we will have to:

  • Edit our main() function to handle & register commands
  • Add commands in our commands.go file

The command we will be creating will create a server event of your choice. It's a little more complex than a small Ping-Pong example, but it shows a actual useful command.

The structure of our commands.go file will be as follows:

package commands

import (
    //...
)

var (
    Commands = []*discordgo.ApplicationCommand{
        //...
    }
    CommandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
        //...
    }
)
Enter fullscreen mode Exit fullscreen mode

I will have to explain the "Commands" and "CommandHandlers" variables.
Commands is a array that we will use to define our slash commands. This is where the commands metadata is registered.

A value in this array will look like this:

{
    Name: "event",
    Description: "creates a event for the server",
    Options: []*discordgo.ApplicationCommandOption{
        {
            Type: discordgo.ApplicationCommandOptionString,
            Name: "name",
            Description: "the events name",
            Required: true,
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we name our command and give it a description. The third option "Options" can be used to give a command extra parameters. This is very useful as we will see later. Just note that required parameters need to be written out first in the array and we can not use numbers or capital letters as command name.

The CommandHandlers variable is a map, that contains the function the event is supposed to trigger. As an example:

"event": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
    // some functionality
}
Enter fullscreen mode Exit fullscreen mode

So when the "event" command is triggered the bot knows what function to call.

Let's start with the actual commands.go implementation:

package commands

import (
    "fmt"
    "log"
    "time"

    "github.com/bwmarrin/discordgo"
)

var (
    // Array containing all the bots command details
    Commands = []*discordgo.ApplicationCommand{
        {
            Name:        "event",
            Description: "creates a event for the server",
            Options: []*discordgo.ApplicationCommandOption{
                {
                    Type:        discordgo.ApplicationCommandOptionString,
                    Name:        "name",
                    Description: "the events name",
                    Required:    true,
                },
                {
                    Type:         discordgo.ApplicationCommandOptionChannel,
                    Name:         "channel",
                    Description:  "the events voice channel",
                    ChannelTypes: []discordgo.ChannelType{2},
                    Required:     true,
                },
                {
                    Type:        discordgo.ApplicationCommandOptionInteger,
                    Name:        "time",
                    Description: "starting time of the event from now in minutes",
                    Required:    true,
                },
                {
                    Type:        discordgo.ApplicationCommandOptionString,
                    Name:        "description",
                    Description: "the events description",
Required: true,
                },
            },
        },
    }

    // Command handlers executing the commands logic
    CommandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
        "event": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
            // Get command option values
            options := i.ApplicationCommandData().Options
            optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
            for _, opt := range options {
                optionMap[opt.Name] = opt
            }

            // Create server event
            t := optionMap["time"].IntValue()
            startingTime := time.Now().Add(time.Duration(t) * time.Minute)
            endingTime := startingTime.Add(720 * time.Minute)

            _, err := s.GuildScheduledEventCreate(i.GuildID, &discordgo.GuildScheduledEventParams{
                Name:               optionMap["name"].StringValue(),
                Description:        optionMap["description"].StringValue(),
                ScheduledStartTime: &startingTime,
                ScheduledEndTime:   &endingTime,
                EntityType:         discordgo.GuildScheduledEventEntityTypeVoice,
                ChannelID:          optionMap["channel"].ChannelValue(s).ID,
                PrivacyLevel:       discordgo.GuildScheduledEventPrivacyLevelGuildOnly,
            })
            if err != nil {
                log.Printf("Error creating scheduled event: %v", err)

            }

            // Respond
            err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
                Type: 4,
                Data: &discordgo.InteractionResponseData{
                    Content: "Event created!",
                    Flags:   discordgo.MessageFlagsEphemeral,
                },
            })
            if err != nil {
                fmt.Printf("Failed to respond to event creation: %v", err)
            }
        },
    }
)
Enter fullscreen mode Exit fullscreen mode

My command is a little messy, but you can hopefully see how to approach adding a command to the bot.

As final step we need to register our "event" command so we can actually call it. For this we need to add some code to our main.go file:

// discordbot/cmd/main.go
func main() {
    // Start session...
    // Already implemented.

    // Add command handlers
    discord.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
        if h, ok := c.CommandHandlers[i.ApplicationCommandData().Name]; ok {
            h(s, i)
        }
    })

    // Open connection...
    // Already implemented.

   // Register commands
    registeredCommands := make([]*discordgo.ApplicationCommand, len(c.Commands))
    for i, v := range c.Commands {
        cmd, err := d.ApplicationCommandCreate(d.State.User.ID, "YOUR_SERVER_ID", v)
        if err != nil {
            log.Panicf("Cannot create '%v' command: %v", v.Name, err)
        }
        registeredCommands[i] = cmd
    }

}
Enter fullscreen mode Exit fullscreen mode

Here we add command handlers and register the commands to the discord server.
While registering the commands you will have to replace YOUR_SERVER_ID with your actual server ID.

Once that happened it is time to test our command. To do that run the main.go file and start typing /event into the chat in discord. The event should pop up and we should be guided through the parameters.

Once we fill out the command like this:

discord command filled out with all the parameters

The bot should message you mentioning that the event was created successfully:

Success message

The last step

Once we confirmed that everything works as expected, we can merge our main branch with the commands branch:

git checkout main && git merge main commands && git branch -d commands

This command will swap to main branch, merge both of our branches and delete the commands branch as we wont need it anymore.

That's it. We created a simple discord bot that can create events for us. Unfortunately I have no idea where and how to host it so you will have to figure that out by yourself.

I hope this guide provides some value to someone.

💖 💪 🙅 🚩
svendetta
Sven

Posted on October 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

How to create a discord bot in Go?
discord How to create a discord bot in Go?

October 1, 2023