Discord bot with Discordia and Luvit with a command handler

How to make a Discord bot with Lua

Prerequisites

  • Knowledge in Lua or similar
  • Discord
  • Internet

Installation

Discordia, a Lua Discord bot framework uses a Lua distribution called Luvit. To install follow the instructions for you platform

Windows Install

Install the correct version for your platform

64 Bit

https://github.com/luvit/luvi/releases/download/v2.11.0/luvi-regular-Windows-amd64.exe

32 Bit

https://github.com/luvit/luvi/releases/download/v2.11.0/luvi-regular-Windows-ia32.exe

Linux Install

This will install in the current working directory so run in your bot folder

curl -L https://github.com/luvit/lit/raw/master/get-lit.sh | sh

Mac Install

https://github.com/luvit/luvi/releases/download/v2.11.0/luvi-tiny-Darwin_x86_64

Discordia Install

Now with Luvit installed in your directory now we need to install Discordia. This can be done by running

lit install SinisterRectus/discordia

In the directory

Code Editor

Something like Notepad won’t do so use something like Notepad++, Visual Studio Code, Atom, or something more fit.

Setup

Now with the programs installed we can start. You first need to create an application in Discord. Click new application, Go to Bot and create one. Click copy token write it down. It should look like this.

NzE0NTk5ODgxOTU1ODY4NzU0.XsxBMQ.TYm5knJTJ_ex81ipVqIkpi9Ucy8 (Not real token)

Now create 2 files, main.lua and settings.lua. We will start off with the settings.lua file.

Also you should download the helper.lua file as it contains extensions that can be useful.

return {
    Prefix = ";",
    Token = "NzE0NTk5ODgxOTU1ODY4NzU0.XsxBMQ.TYm5knJTJ_ex81ipVqIkpi9Ucy8" -- Replace with yours
}

What this does is it allows us to require this file to get this information and allows for easy access. Now that we have our configurations we can make our main file.

local discordia = require('discordia') -- Call in the Discordia framework
local settings = require('settings') -- Call in the settings file we made
local helper = require('helper') -- Call in the helper file we downloaded

local client = discordia.Client() -- Create a client object

helper() -- Load the helper libraries, we'll use them later

client:on('messageCreate', function(message) -- Setup an function that is called when a message is sent
	if message.content == settings.Prefix ..'ping' then -- Check if the message equals our prefix followed by ping
		message.channel:send('pong') -- Return pong in the channel
	end
end)

client:run("Bot ".. settings.Token) -- Connect to discord with our token

Now open up a terminal (command prompt) and type luvit main.lua. If everything is fine then go into a server with the bot and type ;ping or whatever your prefix is. You should see it return pong. This is all fine but we can do better and make this cleaner for more commands. This can be done with a command handler. What this does is that it has a list of commands that we give it. When a message is sent it will go through this list and check if it equals any one of the commands. After this it will call the file which the command is in and give it the parameters. An example of a command handler is this.

function runCommand(msg)
    local args = string.split(msg.content, ' ') -- Split the command into an array by spaces

    local command = string.gsub(args[1],settings.Prefix, ''); -- Get the first item in the table and remove the prefix

    args = table.slice(args, 2) -- Get rid of the command and get only whats after
    -- i.g ;test arg1 arg2 arg3

    command = string.lower(command) -- Make the command all lowercase

    local commandObject
    
    for i,v in pairs(commandList) do -- Go through all of the commands in the command list
        if string.lower(v.name) == command or string.lower(v.alias) == command then -- Check if the command passed is a command by its name or alias
            commandObject = v -- Set our commandObject to it
        end
    end
        
    if (commandObject) then -- If there is one then run it
    --[[ Optional if you want commands to only work in dms, guilds, or anywhere
		if (commandObject.type == 'guild') then
			if (msg.channel.type == 0) then
				commandObject.run(args, msg, client, {
					commands = commandList,
                    categories = categoryList,
                    prefix = settings.Prefix
				});
				return;
			else
				return msg.channel:send('Incorrect channel used! Use ' .. commandObject.type);
            end
		end
		if (commandObject.type == 'dm') then
			if (msg.channel.type == 1) then
				commandObject.run(args, msg, client, {
					commands = commandList,
                    categories = categoryList,
                    prefix = settings.Prefix
				});
				return;
			else
				return msg.channel:send('Incorrect channel used! Use ' .. commandObject.type);
            end
        end
        
		if (commandObject.type == '' or commandObject.type == 'any') then
			commandObject.run(args, msg, client, {
				commands = commandList,
                categories = categoryList,
                prefix = settings.Prefix
			});
        end
        ]]
        commandObject.run(args, msg, client, { -- Call the function and pass the arguments, message, client, and others
			commands = commandList,
            categories = categoryList,
            prefix = settings.Prefix
		});
	end
end

Now this may look a bit complicated but what its doing is splitting the message, getting the command and arguments, looking to see if its a real command and running it. The advantages is you can have the commands in separate areas, have arguments, and allow for easy changing. Now we need to go and add commands to our command list. So far our file looks like this. There was some checking added like seeing if they are the bot itself,other bots, automated message, or starting with a prefix. There was also an pcall around the command handler so if a command breaks, it doesn’t cause the bot to go offline.

local discordia = require('discordia') -- Call in the Discordia framework
local settings = require('settings') -- Call in the settings file we made
local helper = require('helper') -- Call in the helper file we downloaded

local client = discordia.Client() -- Create a client object

helper() -- Load the helper libraries, we'll use them later

local commandList = {} -- Create some tables for our commands

function runCommand(msg)
    local args = string.split(msg.content, ' ') -- Split the command into an array by spaces

    local command = string.gsub(args[1],settings.Prefix, ''); -- Get the first item in the table and remove the prefix

    args = table.slice(args, 2) -- Get rid of the command and get only whats after
    -- i.g ;test arg1 arg2 arg3

    command = string.lower(command) -- Make the command all lowercase

    local commandObject
    
    for i,v in pairs(commandList) do -- Go through all of the commands in the command list
        if string.lower(v.name) == command or string.lower(v.alias) == command then -- Check if the command passed is a command by its name or alias
            commandObject = v -- Set our commandObject to it
        end
    end
        
    if (commandObject) then -- If there is one then run it
        commandObject.run(args, msg, client, { -- Call the function and pass the arguments, message, client, and others
			commands = commandList,
            prefix = settings.Prefix
		});
	end
end

client:on('messageCreate', function(ms) -- Setup an function that is called when a message is sent
	if (not string.startswith(msg.content, settings.Prefix)) then -- Check if it starts with our prefix
		return;
	end
	if (not msg.author) then -- Is it an automated message
		return;
    end
	if (msg.author.id == client.user.id) then -- Did we send the message
		return;
    end
	if (msg.author.bot) then -- Is the message from a bot
		return;
    end
    local suc, err = pcall(function() -- Make sure the command doesn't nock out our bot
        runCommand(msg);
    end)

    if not suc then -- Error logging, make sure its print, if not it will end the program
        print(err)
        msg.channel:send('Something went wrong, please try again later')
    end
end)

client:run("Bot ".. settings.Token) -- Connect to discord with our token

Now we need to tell the code what our commands are. A simple solution is to give the name of the command files in settings and just use those. A more elegant solution is to use fs(file system) to go through a commands folder and get them for us. For this tutorial we will use the simple solution. Open up your settings file and add this to it.

return {
    Prefix = ";",
    Token = "NzE0NTk5ODgxOTU1ODY4NzU0.XsxBMQ.TYm5knJTJ_ex81ipVqIkpi9Ucy8", -- Replace with yours
    Commands = {
        'Ping' -- Make sure its the name of the file without .lua
    }
}

Now we can make the loop code. We will use the settings file to get the commands and require it from a folder. Create a folder named commands or whatever you want it to be. Make a file called Ping.lua for this example. The code for this should be.

for i,v in pairs(settings.Commands) do -- Loop through all the commands
    local command = require('./commands/' .. v) -- Require the command
    table.insert(commandList,{ -- Add to our list the name, alias, description, and the run function
        name = command.name,
        alias = command.alias,
        description = command.description,
        run = command.runCommand,
        --type = command.type, Only if your using the optional command handler part
    });
end

This adds our commands to the list which now the command handler can use. Our main.lua file should now look like.

local discordia = require('discordia') -- Call in the Discordia framework
local settings = require('settings') -- Call in the settings file we made
local helper = require('helper') -- Call in the helper file we downloaded

local client = discordia.Client() -- Create a client object

helper() -- Load the helper libraries, we'll use them later

local commandList = {} -- Create some tables for our commands

for i,v in pairs(settings.Commands) do -- Loop through all the commands
    local command = require('./commands/' .. v) -- Require the command
    table.insert(commandList,{ -- Add to our list the name, alias, description, and the run function
        name = command.name,
        alias = command.alias,
        description = command.description,
        run = command.runCommand,
        --type = command.type, Only if your using the optional command handler part
    });
end

function runCommand(msg)
    local args = string.split(msg.content, ' ') -- Split the command into an array by spaces

    local command = string.gsub(args[1],settings.Prefix, ''); -- Get the first item in the table and remove the prefix

    args = table.slice(args, 2) -- Get rid of the command and get only whats after
    -- i.g ;test arg1 arg2 arg3

    command = string.lower(command) -- Make the command all lowercase

    local commandObject
    
    for i,v in pairs(commandList) do -- Go through all of the commands in the command list
        if string.lower(v.name) == command or string.lower(v.alias) == command then -- Check if the command passed is a command by its name or alias
            commandObject = v -- Set our commandObject to it
        end
    end
        
    if (commandObject) then -- If there is one then run it
        commandObject.run(args, msg, client, { -- Call the function and pass the arguments, message, client, and others
			commands = commandList,
            prefix = settings.Prefix
		});
	end
end

client:on('messageCreate', function(msg) -- Setup an function that is called when a message is sent
	if (not string.startswith(msg.content, settings.Prefix)) then -- Check if it starts with our prefix
		return;
	end
	if (not msg.author) then -- Is it an automated message
		return;
    end
	if (msg.author.id == client.user.id) then -- Did we send the message
		return;
    end
	if (msg.author.bot) then -- Is the message from a bot
		return;
    end
    local suc, err = pcall(function() -- Make sure the command doesn't nock out our bot
        runCommand(msg);
    end)

    if not suc then -- Error logging, make sure its print or warn, if not it will end the program
        print(err)
        msg.channel:send('Something went wrong, please try again later')
    end
end)

client:run("Bot ".. settings.Token) -- Connect to discord with our token

Now we need to create our Ping.lua file that will work with our command handler and command loader. By looking at the code you can see we need a name, alias, description, and a run function. That can be done by simply using this format.

local Discord = require('discordia')

return {
	name = 'name', -- Name of the command
	alias = 'alias', -- Alias of the command
	description = 'description', -- Description of the command
	--type = '', Optional unless you have the optional part
    runCommand = function(args, msg,client,rest) -- The run function
	end
};

You should safe this format in a file like example.lua. Now since we are only doing ping, we don’t need args, client, or rest. Set args to _, and delete client and rest.We also don’t need Discord so you can delete that as well . In our first example we just had message.channel:send('pong'). Well we can just copy that and replace message with msg. Our Ping.lua file should look like this now.

return {
	name = 'ping', -- Name of the command
	alias = 'pong', -- Alias of the command
	description = 'Ping with the bot', -- Description of the command
	--type = '', Optional unless you have the optional part
    runCommand = function(_, msg) -- The run function
        msg.channel:send('pong') -- Send pong in the channel
	end
}

Now if everything works when you type ;ping you should get pong back from the bot. Now you may wonder what the rest part is for. Its the information for a help command. You can see where we are going. We are going to use the template to make the help command. What the help command needs to do is use the data from the template and display all commands. You should also add Help to the command list in settings. Make a new file called Help.lua in commands and add this.

return {
	name = 'help', -- Name of the command
	alias = 'holp', -- Alias of the command
	description = 'Get the commands of the bot', -- Description of the command
	--type = '', Optional unless you have the optional part
    runCommand = function(args, msg, _, rest) -- The run function
        local commands = rest.commands;
        local categories = rest.categories;
        local prefix = rest.prefix;
        local list = {};

        if (args[1]) then
            for _, v in pairs(commands) do
                if string.lower(v.name) == string.lower(args[1]) or
                    string.lower(v.alias) == string.lower(args[1]) then
                    command = v
                end
            end

            if (not command) then
                msg.channel:send('No command/category found');
            else
                local type = '';
                local alias = '';
                if (command.type == '' or command.type == 'any') then
                    type = 'dms or guilds';
                else
                    type = command.type;
                end
                if (command.alias == '') then
                    alias = 'None';
                else
                    alias = command.alias;
                end
                msg.channel:send('Name: ' .. command.name .. '\nAlias: ' .. alias ..
                '\nDescription: ' .. command.description);
            end
        else
            for _, v in pairs(commands) do table.insert(list, '`' .. v.name .. '`') end

            local string = ""

            for _, v in pairs(list) do string = string .. v .. ',' end

            msg.channel:send('For more information use ' .. prefix ..
            'help [command_name]\n\n' .. string);
        end
	end
}

Now the help doesn’t look the best it could. This is since we aren’t using embeds. In the helper module is a embed builder since making an embed using Discordia is more, complicated. All we have to do is replace the channel send statements. The require the helper module in the helper.lua file by doing local helper = require('helper'). Declare the embed by also doing local RichEmbed = helper.embed. Replace the first message send with

local embed = RichEmbed:new()
    :setTitle('Help')
    :setDescription(
        'Name: ' .. command.name .. '\nAlias: ' ..alias .. '\nDescription: ' .. command.description .. '
    )
    :setColor(0xd234eb);
msg.channel:send(embed:getTable());

And the second message send with

local embed = RichEmbed:new()
    :setTitle('Help')
    :setDescription(
        'For more information use ' .. prefix .. 'help [command_name]\n\n' .. string
    )
    :setColor(0xd234eb);
msg.channel:send(embed:getTable());

And our bot should be doing well now. We have a simple command and a help command which uses embeds. We recommend you use the code from the github as it has been more checked then the one on the tutorial page.

Common Mistakes

  • Using . instead of : to call functions, this messes with self
  • Capitalization
  • You can only send an http call in a coroutine. This is a mistake since the timer module is asynchronous so in a timer you need to make a coroutine.
  • Variables may not match up

More Information

22 Likes

This is very useful, I as a programmer find the discordia documentation very confusing but this really clears up my mind, keep it up!

3 Likes

I recommend using Javascript and discord.js for discord bots as discord.js documentation is easy to understand.

Still, I wanna see what Discordia could do as a library and not just go to javascript. I made this due to the tutorials not going as deep as a simple one should go.

1 Like

Lua is a easy to understand language so it makes since that you can make a discord bot.

Currently Im seeing if I could add a database like Mongodb so the bot is less shallow.

1 Like

For coding a discord bot, i agree with RAMMgamming, i really recommend to code a discord bot on discord.js (Nodejs) and Python since it’s easy to understand and learn.

I see what you mean, but that is used so often. Lua is probably the next best thing. I’ve seen so many bots use C#, JavaScript, Python, etc. but I have never seen a bot use lua.

Where exactly should I download helper.lua?