Adding Commands with parameters to a DiscordRB bot

Fri 11, Sep 2020

This is Part 2 (Part 1) of a series about writing a Discord bot in Ruby using DiscordRB.

The source code of the part can be found on GitHub!

Hi!

In the last part, we set up our basic file structure and added our first command, ?ping. In this part, we'll add a command to bulk delete messages, which will require one parameter. Also, we'll set up help texts and basic usage hints.


?delete [amount]

Let's start with a fresh, plain command at src/modules/commands/delete.rb:

module Bot
    module DiscordCommands
        module Delete extend Discordrb::Commands::CommandContainer
            command :delete do |event|
                'This command works!'
            end
        end
    end
end

That command doesn't do much for now - it only replies with This command works! and that's it. Not quite what a delete command should do, right?

DiscordRB passes our command an event object. This object allows us to access all different kinds of informations about the event: the user who emitted it, the channel that it was emitted in, the guild, ...

This is what we need to make this command work! Let's change our command up a bit:

            command :delete do |event, amount|
                amount = amount.to_i
                event.channel.prune(amount) if amount >= 2
                nil
            end

The delete command now takes in one parameter, amount, which will be converted to an integer right away. If the amount is greather than or equals 2, it will delete the specified amount of messages. The nil simply means that our bot won't respond with a message for this.

Now, if you send, say, ?delete 10 into some channel the bot has access to, it will delete the last 10 messages. Great!

Validation: Parameter Count

DiscordRB allows you to add a minimum amount of parameters: if the user supplies less parameters, the bot will send a message about it.

This is done by simply passing an additional parameter to command:

command :delete, min_args: 1 do |event, amount|

If we now execute ?delete without any parameters, we'll get:

Too few arguments for command delete!

Great!

Checking for user permissions

Right now, any user can delete posts. Yep, everyone. To control who can use this command, we can add required_permissions to our command call:

            command :delete, min_args: 1,
            required_permissions: [:manage_messages] do |event, amount|
                amount = amount.to_i
                event.channel.prune(amount) if amount >= 2
                nil
            end

Now only members who have the Manage Members permission are allowed to use this command. Other users will only receive a message telling them that they don't have sufficient permissions to use the command.

Adding a help text and usage hints

Our delete command still misses a help text and usage hints. Those are important for your users to know what the command will do and what parameters it needs. So do we have to add two new commands for help and usage? Nope, we only need to add more arguments (description and usage) to the command call:

            command :delete, min_args: 1,
            required_permissions: [:manage_messages],
            description: 'Deletes messages in this channel.',
            usage: 'delete [amount]' do |event, amount|
                amount = amount.to_i
                event.channel.prune(amount) if amount >= 2
                nil
            end

DiscordRB responds to help calls out of the box, so if we restart our bot and type ?help, we'll get a list of available commands. If we type ?help delete, we get the description text saying "Deletes messages in this channel." and the usage information.


Alright, that's it for part 2! Thanks for reading, I hope you enjoyed this tutorial so far! Stay tuned by subscribing to my blog's feed (RSS|Atom|JSON).

Writing a Discord Bot in Ruby using DiscordRB

Sun 30, Aug 2020

This is Part 1 of a series about writing a Discord bot in Ruby using DiscordRB.

Update: The source code of the part can be found on GitHub!

Hi!

If you've been on Discord for quite some time, you've probably noticed how important Discord bots are (in case you haven't: trust me, they really are!)

Unfortunately, many tutorials on Discord bots only show you how to create the very minimum required for your bot to answer with "Pong!" when someone types "!ping".

I wanna go a little further with this tutorial and show you how to create an actually useful Discord bot in Ruby using DiscordRB.

Please note that this tutorial expects you to already have some knowledge about Ruby. You don't need to be an expert, but you should have at least some understanding of the language itself!


Registering your bot

Before we can actually start coding, we need to tell Discord about our bot. Navigate to Discord's Developer Portal and click on New Application, then enter your app's name. Note that this name can be, but does not have to be, your bot's username!

After your application has been successfully created, go to the Bot tab and click on Add Bot to create the actual bot user. In order for your bot to login to Discord using the bot account, you'll need its token. Click on Copy in the Token section. Make sure to keep that token private! Anyone can login with your bot account if you accidentally show it to someone!

Setting up the project

Now, that we have our bot user account, we can start to create our project's files and directories! Create a new project directory where you'll save your bot's files.

In that directory, we'll need to create a basic file structure:

The file structure

Now that's a lot of files and directories for just a simple hello, world! project. However, this is a really good starting point from which you can grow your bot easily!

Let's take a look at what we have:

This architecture as well as src/bot.rb are taken from z64's Gemstone template for DiscordRB bots

So, now that all of our files and directories have been set up, let's start writing something into those files! Let's start with our Gemfile: we'll define a single dependency for now, discordrb:

gem 'discordrb'

That's all there is to that file for now. Run bundle install to install our one, single dependecy (this requires Bundler; it should be packaged by default with your Ruby installation. If it is not, install it!)

As I mentioned earlier, run.rb will only be a shortcut to src/bot.rb. So the only thing we need to write into run.rb is this one, single line:

require_relative 'src/bot'

Easy, eh?

The next file we have to edit is src/bot.rb:

require 'discordrb'
require 'ostruct'
require 'yaml'

module Bot
    if File.file? 'data/config.yaml'
        CONFIG = OpenStruct.new YAML.load_file 'data/config.yaml'
    else
        puts 'Can\'t load config file!'
        exit!
    end

    BOT = Discordrb::Commands::CommandBot.new(client_id: CONFIG.client_id,
                                            token: CONFIG.token,
                                            prefix: CONFIG.prefix)

    # Integrations
    module Integrations; end
    Dir['src/integrations/*.rb'].each { |mod| load mod }

    # Discord Commands
    module DiscordCommands; end
    Dir['src/modules/commands/*.rb'].each { |mod| load mod }
    DiscordCommands.constants.each do |mod|
        BOT.include! DiscordCommands.const_get mod
    end

    # Discord Events
    module DiscordEvents; end
    Dir['src/modules/events/*.rb'].each { |mod| load mod }
    DiscordEvents.constants.each do |mod|
        BOT.include! DiscordEvents.const_get mod
    end

    BOT.run
end

Phew, so that's quite a bit more code. Let's go throught this snippet-by-snippet:

require 'discordrb'
require 'ostruct'
require 'yaml'

This imports all our dependencies. Note that ostruct and yaml come pre-installed, so no need to add them to our Gemfile.

module Bot

Here we define our Bot module. This module will contain all our bot's logic.

    if File.file? 'data/config.yaml'
        CONFIG = OpenStruct.new YAML.load_file 'data/config.yaml'
    else
        puts 'Can\'t load config file!'
        exit!
    end

    BOT = Discordrb::Commands::CommandBot.new(client_id: CONFIG.client_id,
                                            token: CONFIG.token,
                                            prefix: CONFIG.prefix)

This is where we load our configuration file (data/config.yaml) and initiate the bot. We check whether the config file exists. If it does, we load our config using OpenStruct (ostruct). If it does not, we simply exit with an error message.

The last 3 lines are where we create our CommandBot instance.

    # Integrations
    module Integrations; end
    Dir['src/integrations/*.rb'].each { |mod| load mod }

    # Discord Commands
    module DiscordCommands; end
    Dir['src/modules/commands/*.rb'].each { |mod| load mod }
    DiscordCommands.constants.each do |mod|
        BOT.include! DiscordCommands.const_get mod
    end

    # Discord Events
    module DiscordEvents; end
    Dir['src/modules/events/*.rb'].each { |mod| load mod }
    DiscordEvents.constants.each do |mod|
        BOT.include! DiscordEvents.const_get mod
    end

Here we load and register all of our commands, event handlers and integrations. Think of it as an autoloading mechanism.

All that's left to do to see our bot in action for the very first time is fill in the config file and create our first command, ping.

Here's the config file, data/config.yaml:

client_id: 
token: ''
prefix: '?'

You'll need to put in your bot user's client ID and token here. Simply copy&paste them from Discord's Developer Portal (the client ID is on the General tab, the token is on the Bot tab, simply click on the Copy button right under Token to copy it).

If you did it right, your config file should look something like this now:

client_id: 000000000000000000
token: 'YMNI%fAQ$vRwpEsTouFiJaJV0YoERrbkeuErhTCSFYOqcligcF8D3X9JKSD'
prefix: '?'

(Note that the credentials in the above example are not real)

Creating our first command

Finally, we have everything set up and can start writing our actual, own code! Open up src/modules/commands/ping.rb and put the following contents into it:

module Bot
    module DiscordCommands
        module Ping extend Discordrb::Commands::CommandContainer
            command :ping do |event|
                'Pong!'
            end
        end
    end
end

The first 3 lines only tell Ruby that we want our code to be placed into modules. However, on the 4th line, we define the actual command:

command :ping do |event|
    'Pong!'
 end

This tells DiscordRB that we add a command called ping and all it does is respond with Pong!.

If you now start up your bot by executing ruby run.rb in your project's root directory, you should get a message similar to this one:

[INFO : websocket @ 2020-08-30 19:02:19.378] Discord using gateway protocol version: 6, requested: 6

Congratulations, your bot works! 🥳

Now, how do you get it to join your Discord guild, though? That's where event handlers come in handy!

Events and inviting the bot to your guild

Create a new file at src/modules/events/online.rb. In this file, we'll define what our bot will do when it connects to the Discord API.

module Bot
    module DiscordEvents
        module Online extend Discordrb::EventContainer
            ready do |event|
                puts "Logged in as #{event.bot.profile.distinct}"
                puts "Invite URL: #{event.bot.invite_url}"
            end
        end
    end
end

This will print your bot's username and tag as well as a full invite URL to the console. So, if you now run ruby run.rb again, you should see a Discord invite URL in your console. Open it in your browser, select your guild and boom, your bot joins it!

Now you can finally test it out: type ?ping into a channel that the bot user can read and write to. If the permissions are set up correctly, it will respond with Pong!. If not, you'll see an error telling you what went wrong in the console. Try adjusting the bot's permissions if your permissions weren't setup correctly.


This was Part 1 of what will be a series on creating a Discord bot with DiscordRB. Thanks for reading, I hope you enjoyed this tutorial so far! Stay tuned by subscribing to my blog's feed (RSS|Atom|JSON).

Setting up a new Cisco SG300-28

Tue 25, Aug 2020

Hi! Just set up my new (used) Switch, a Cisco SG300-28 two days ago!

This is my first L3 switch and I have yet to explore all of its many features. The only thing I really did yet was setting up VLANs. I also bought a used StarTech 6U rack, as this switch (which is fanless!) sits right under my desk and the previous 2U rack I had there was too small to fit another switch.

Named arguments in PHP 8

Thu 23, Jul 2020

Hi!

One of the coolest new features of PHP 8 are named arguments (parameters). I'm really happy that they made it into PHP 8, as they're a huge improvement syntax-wise, I think. But first, take a look at what they look like:

class Post {

    public function __construct(
        string $title,
        string $body,
        string $author
    ) {}

}

# ...

$post = new Post(
    title: 'Some interesting post',
    body: 'Bla bla body text here',
    author: 'John'
);

Why?

While this feature was highly debated, I think it comes with far more advantages than disadvantages. It allows for cleaner and easier to understand code (think (min: 0, max: 86400) instead of (0, 86400)). It also allows us to skip default values. Say we have the following function:

function cacheText(
    string $key,
    string $value,
    int $expires = 86400,
    string $directory = 'cache/'
) {}

If we now wanted to call this function with a different directory, but keep $expires with its default value, we can now write this

cacheText(
    key: 'content-home',
    value: 'something bla bla',
    directory: 'someOtherDir/'
);

instead of that:

cacheText(
    'content-home',
    'something bla bla',
    86400,
    'someOtherDir/'
);

Named arguments can also be used together with ordered arguments. That means that you could, for example, write this:

$post = new Post(
    'Some interesting post',
    body: 'Bla bla body text here',
    author: 'John'
);

What would not work, however, is doing it this way:

$post = new Post(
    title: 'Some interesting post',
    'Bla bla body text here',
    author: 'John'
);

You can only use ordered arguments together with named ones at the beginning of the argument list. If you're using ordered arguments between named ones, PHP throws an error.

Named arguments are also supported in annotations/attributes, by the way!

In case you want some more info on that, view this article and the RFC!

uBlock Origin: Re-enable allow rules

Wed 22, Jul 2020

Hi!

So a few days ago I noticed how I can't set any allow rules in uBlock Origin anymore. Those were useful to me, because I have some custom filters, which sometimes block actually wanted content. Using the allow rules, I could simply temporarily override them.

Now with uBlock Origin 1.28.0, they have been removed from the UI. However, there is a workaround to (temporarily) re-enable them: press Ctrl twice in the popup. Now you can set allow rules again.

Notice that this is temporary, so when you open the popup the next time, you will have to do it again.

Put a README on your GitHub profile

Fri 10, Jul 2020

Hi!

I discovered a really nice easter egg in GitHub today! If you create a repository that's named exactly like your GitHub username and put a README.md in there, the rendered README will show up on your profile!

My profile with the README

How?

  1. Create a new repository named as your GitHub username (if your name is Example, name the repository Example); GitHub shows you a green easter egg alert right under the name input if the name is correct
  2. Check that Initialize this repository with a README is on
  3. Click Create repository

Now edit the newly created README.md with the contents you want to show on your GitHub profile.

Editing the README.md

PHP 8 Attribute Syntax

Thu 02, Jul 2020

So @@ reaches the quota and has been elected as the final attribute syntax for PHP 8.

@@ will be the attribute syntax for PHP 8:

@@SomeAttribute
class Something {}

I personally think that choosing @@ was the right decision.

If you don't really know what's going on with those attributes, read this!

Source

Blogs without feeds

Mon 01, Jun 2020

Just visited yet another interesting tech blog without a feed - no RSS, Atom, JSON.

I don't get why some people don't put a feed on their blogs; That way I could easily subscribe to it and read their upcoming posts. Now I would have to bookmark it and re-visit it every few days.

Pro tip: tag feeds on maxbits.net

Thu 14, May 2020

Just a quick pro tip for you: if you want to get the feed of only a specific tag of my blog, use the following URL scheme:

https://www.maxbits.net/feed/{atom|rss|json}/{tag}/

Example: If you wanted to only see webdev-related posts in Atom format:

https://www.maxbits.net/feed/atom/webdev/

This feature has been around for quite some time now, but most people probably don't know it

¯_(ツ)_/¯

My Git Mirror

Mon 11, May 2020

The Git mirror is no longer available. I'm looking forward to a new solution, though.

I just mirrored my GitHub repositories, just in case they go down, at offline. I'm using GitList for now.

You are on page 1 right now.


Back to top