Writing a Discord bot in Go

This post was written in 2021. I don't condone the use of Discord for any purpose whatsoever. Use open platforms that don't censor their user base at will.
The article remains online for archival purposes but should not be followed for ethical reasons.

Free and open source alternatives that respect the user include, but are not limited to:

About a month ago I decided to get into Go a bit. It’s always kind of been an interesting programming language since it’s modern, simple and has quite powerful multi-threading capabilities, most of which I have yet to use. I was asked if I could program a Discord bot that would print the weekly Covid-19 incidence numbers in Germany and I thought that’s a great idea, so here we are.

You can find the source code for this bot here. I thought I’d share it since I put in a bit of work recently.

Prerequisites

For this Discord bot I’ve used the Discord library discordgo, it’s “extension” dgc to structure my code better and, most important of all, the REST API I used is rki-covid-api.
Since the API can be self-hosted easily with Docker, I decided to do exactly that. You can find this over at https://rkiapi.wiredspace.de/.

Writing the code

The API

The API is fairly easy to use. So far the only thing I’ve been implementing is the “districts” endpoint, which is well structured.

The response is structured in data. Each state has it’s own AGS (“Allgemeiner Gemeinde Schlüssel”, essentially an ID for each district), which is how it’s identified. Besides that, the districts contain information about their name, population, weekIncidence, deaths, etc.
The one I’ll be focusing on is the weekIncidence field since this was what I originally built my bot around.

I ran into a bit of trouble deserializing the JSON you got from the API since I wasn’t familiar with the Go way of doing this. The problem I had was that the fields of the data response aren’t static; they are the AGS returned by the API.
As it turns out this is easily handled. I declared the reponse I get as the following:

type DistrictResponseData struct {
	Data map[string]DistrictResponse `json:"data"`
	Meta []MetaResponse              `json:"meta"`
}

The Data field contains the districts which are identified by the AGS. Simply mapping string to the struct for the district did the job.
Deserializing the object turned out to be a bit weird, but it’s fine overall:

var drd DistrictResponseData
// initialize a (hopefully) big enough map
// api contains about 410 districts
drd.Data = make(map[string]DistrictResponse, 410)

I initialize a struct for the response and can’t call json.Unmarshal directly on that struct, I need to call it on the Data field of it. This is the only I’ve managed to get it working, maybe you can find another one that might be more elegant. This works though so I won’t complain.
After this I just query the API for a reponse and call err = json.Unmarshal(responseData, &drd) on the reponse body. This fills the drd variable with all the district data.

That’s all you should need to know about the API.

The Discord libraries

discordgo

The discordgo library is fairly easy to use. As with any other go package you can find the documentation on https://pkg.go.dev/github.com/bwmarrin/discordgo.

To use this library you create a discordgo.Session that will handle all the interaction with the Discord servers.
For basic usage on this library I recommend having a look at the examples from their GitHub repo. They teach the basics well enough for use with the other Discord library I’m using.

dgc

dgc is an extension of the discordgo library. It uses that one to offer more functionality and better usability, as I’ll show you in this section.
As usual, you can find the documentation on https://pkg.go.dev/github.com/Lukaesebrot/dgc.

With discordgo you need to register a handler and handle the incoming messages yourself. This includes argument parsing.
Obviously this gets very boring really quickly, so I started using dgc. dgc, which lets you define command handlers that get called for specific commands for which you can even set up aliases.
For basic usage, again, I recommend you to look at their examples. The basic.go example should be all you need for now.

Initializing this library is done via the dgc.Create() function. It takes a dgc.Router as an argument, which is initialized with the Prefixes, among other things.
Registering commands to this router is done via router.RegisterCmd(), which takes a dgc.Command as an argument. With a Command you can specify Name, Description, Usage, a Handler and more. The Handler will be a function with a Signature of func(*Ctx), meaning that it takes a context through which you will be able to send messages.

dgc provides a default help handler which you can register via router.RegisterDefaultHelpCommand(s, nil), where s is the discordgo.Session.
This help handler needs the reaction intent since the user will be able to flip through “pages” of the helper on discord, which is done via reactions.
The intents I assigned the bot are the following:

discord.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsGuildMessageReactions

This let’s you reply to incoming messages and react to reactions.

Sending messages is really easy. When one of the command handlers is being called they will have the Ctx available as a parameter. This Ctx presents you with 3 methods:

  • RespondText(string)
  • RespondEmbed(*discordgo.MessageEmbed)
  • RespondEmbedText(string, *discordgo.MessageEmbed)

These are fairly self-explanatory by themselves.

Creating an embedded message is pretty simple, too. You just create a discordgo.MessageEmbed struct and fill out its members. Not all members have to be assigned something:

embed := discordgo.MessageEmbed{
	Title:       "Removed districts",
	Timestamp:   time.Now().Format(time.RFC3339),
	Description: strings.Join(names, ", "),
}

This is an excerpt from my code. It defines a Title, a Timestamp and a Description for the embedded message. Note that the Timestamp needs to be in RFC3339 format. If that’s not the case you will get an error when sending the embed.
Sending it is as easy as doing ctx.RespondEmbed(&embed).
Sending messages can throw an error so you should catch that and log it somewhere.

Do you have a comment on one of my posts? Feel free to send me an E-Mail: witcher@wiredspace.de
To participate in a public discussion, use my public inbox: ~witcher/public-inbox@lists.sr.ht (Archive)
Please review the mail etiquette.

Posted on: June 08, 2021

Articles from blogs I read

I managed to stop biting my nails

watch out!Some sections of this article make nail biting sound appealing. If you're currently avoiding biting your own nails, please be careful reading in case they make you want to bite. Remove sentences from the article?For a long time, I haven'…

via Cadence's Weblog December 12, 2024

I'm daily driving Jujutsu, and maybe you should too

I’m not the first to write about how Jujutsu won me over. I’ve seen it off and on, and each time it came across my feed it was bumped a bit higher in my “list of things to look at eventually”. It finally reached the top spot, I think, when I saw Tony Finn’s …

via Drew DeVault's blog December 10, 2024

Launching the 2024 State of Rust Survey

It’s time for the 2024 State of Rust Survey! Since 2016, the Rust Project has collected valuable information and feedback from the Rust programming language community through our annual State of Rust Survey. This tool allows us to more deeply understand ho…

via Rust Blog December 5, 2024

Generated by openring