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