Making a Discord bot in Crystal: Intro

Our goal today is simple: Get a bot up and running via a JSON configuration file, in Crystal, that responds to a "!ping" command.

Crystal and Shards installation

In order to install Crystal, you can follow the docs for your operating system.

You can verify Crystal and Shards are installed by checking their versions:

λ shards --version
Shards 0.7.2 (2018-01-27)
λ crystal -v
Crystal 0.24.1 (2018-01-27)

LLVM: 5.0.1
Default target: x86_64-apple-macosx

Creating the project repo

Crystal provides an init TYPE NAME [DIR] command for us generate a basic file structure. You can pass app or lib for the type, and you can optionally have a different directory name than the project name.

For now, let's go ahead and do crystal init app mybot. The generated structure looks like this:

├── shard.yml
├── spec
│   ├──
│   └──
└── src
    ├── mybot
    │   └──
3 directories, 7 files

LICENSE (MIT by default) and are self explanatory. spec stores our tests, which can be run with crystal spec.

shard.yml is the YAML configuration file for shards. Running this will generate a shards.lock with a snapshot of the dependencies.

src is where our code lives. By default, will include files in src/mybot, and is used to track our project using Semantic Versioning, starting at 0.1.0.

Creating your bot

In order to run Discord::Client, we need a bot token from Discord's apps section. You'll need to create a Bot if you haven't already. Note: Make sure to click "Create a Bot User", as there's a difference. Next, click "show token" and make sure to save that value for a moment. Copy the Client ID as well.

Now that the bot has been made, we have to add it to our server. On your bot's application page, click Generate OAuth2 url. This will open a dialogue showing all of Discord's options for a server.

For now, our bot only requires Send Messages and Read Message History, but I imagine you'll want it to things later down the line such as editing channels, removing content, etc. You can request those things here, or do it manually on your Discord server by giving the bot a moderator role, etc.

Reading and Writing JSON

Let's create a file to manage our configuration file.

# src/mybot/

module MyBot
  class Config
      token: String,
      client_id: UInt64

Let's break that down. First, we create a Config class, and then JSON.mapping handily lets us describe how our class gets mapped to and from JSON. We must define the type, and token is a String. client_id is an unsigned 64bit integer.

So now we need to create the file! Let's put it under conf/bot.json.

  "token": "Bot xxxxxxxxx",
  "client_id": yyyyyyyyyyyy

Note: We have to prefix our token with "Bot ".

Now we don't want to share that token with the world… Make sure to modify your .gitignore file:

λ echo "/conf" >> .gitignore

Almost there. Now in src/, we can load up our dependencies and config file.

# src/
require "json"
require "./mybot/*" # Crystal supports wildcard matching

module MyBot
  file ="conf", "bot.json"))
  config = Config.from_json(file) 
  puts config.token

First, we need to load json from the standard library, and our new config file. File.join lets us join files and directories in a system-agnostic manner ("\" versus "/"). Once we read the file, we can use Config.from_json, which is provided automatically when we use JSON.mapping.

If everything was done correctly, the output of crystal run src/ should be your token!

Starting up Discord::Client

The discordcr library wraps Discord's API for us and provides basic functionality. To install it, add this to your shard.yml file under dependencies:

    github: meew0/discordcr

Then run shards install. This will grab the library for us and create a shard.lock file.

Now we can edit src/ again:

require "json"
require "discordcr"
require "./mybot/config"

module MyBot
  file ="conf", "bot.json"))
  config = Config.from_json(file) 

  client =
    token: config.token,
    client_id: config.client_id

If everything was done correctly, crystal run src/ should produce nothing.

Listening to input

Discord::Client comes with many instance methods. We're going to be using #on_message_create which gives us a payload of type Discord::Message.

# src/
# ...
client.on_message_create do |payload|
  if payload.content.starts_with? "!ping"
    client.create_message(payload.channel_id, "Pong!")

#on_message_create takes a block which lets us do what we want. If the payload starts with !ping, we create a message in the same channel as the payload with the text Pong!., well, runs the bot. This will block so anything after this point will not run. We'll explore asynchronous execution through fibers later.

Let's check to see if our bot works.

Hooray! Now that you have a bot running, check out Discord::Client's documentation and explore to see what you can do!

Have any observations, criticisms, or corrections? Feel free to email me. Have a nice day!

Random quote

Tags: Discord | Crystal
Categories: Programming

Note: I hate spam mail as much as you do.