-
Notifications
You must be signed in to change notification settings - Fork 145
Executing Lua with your bot
I've seen a significant number of people attempt to execute Lua code with their Discordia bots with varying success. In order to clear up confusion about how this can be done, I will explain one method of doing this.
Note that this guide is for experienced Lua users. Do not attempt to do this if you are new to Lua or programming.
Additionally, you should restrict the use of this feature only to yourself, the bot owner. Allowing other users to execute code via your bot is a security risk. Trust no one!
This tutorial assumes that you have already parsed your message content for a specific prefix (such as !
or /
) and command (such as lua
, eval
, or exec
). Once you have the prefix and command stripped from the content, you should be left with a string of Lua code. For example:
"/lua print('hello world')"
becomes
"print('hello world')"
To help out later, we will define a helper function that will wrap text in a markdown code block:
local function code(str)
return string.format('```\n%s```', str)
end
Let's say you have a function that takes Lua code arg
to execute and the corresponding message object msg
as arguments. The first thing you want to do is filter some situations:
local function exec(arg, msg)
if not arg then return end -- make sure arg exists
if msg.author ~= msg.client.owner then return end -- restrict to owner only
end
You'll probably want to make sure arg
is valid if your command handler did not already do this. I also strongly recommend that you restrict the usage of this command to only the owner of the bot, as executing random code can be a security risk.
In Lua 5.1, load
is used for executing functions and loadstring
is used for executing strings. In Lua 5.2, loadstring
was removed and load
was changed to execute strings, and was given more features. In LuaJIT and Luvit, the 5.2 version of load
is used, and loadstring
is an alias for the same function. For simplicity, I will use load
, which is documented here.
As documented, load
returns a function if it was successful, or a syntax error if it was not. Thus, you can execute code and check for syntax errors first:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
local fn, syntaxError = load(arg) -- load the code
if not fn then return msg:reply(code(syntaxError)) end -- handle syntax errors
end
If a syntax error is discovered, it can be sent as a reply to the Discord message. If the function is created, we can execute the code that it represents:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
local fn, syntaxError = load(arg)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn) -- run the code
if not success then return msg:reply(code(runtimeError)) end -- handle runtime errors
end
Notice that I used pcall
here. If there is a runtime error, pcall
will prevent it from crashing your bot, and will return the error message instead. Like with the syntax error, this can be sent as a reply to the Discord message.
This is actually enough information to execute Lua code, but there are probably some more things that you will want to do.
Without getting into excessive detail, Lua functions have an environment that contains all of the variables that the function can "see". The default environment for a function created by load
is a sandboxed standard Lua global environment with nothing special; no Luvit or Discordia features. In order to have advanced functionality, you will need to define a custom environment table.
local sandbox = {}
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
end
Unfortunately, an empty table is not useful for doing much. You can whitelist necessary components:
local sandbox = {
math = math,
string = string,
}
If you'd like to have the global environment of your Luvit script be available, you can use a metatable to indirectly access _G
:
local sandbox = setmetatable({ }, { __index = _G })
If you'd like to blacklist certain libraries, you can set them to be an empty table. Use of a metatable prevents modification of _G
itself:
local sandbox = setmetatable({
os = { }
}, { __index = _G })
When executing the dynamic code, you might want to drop some Discordia objects into the sandbox:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
sandbox.message = msg -- add features as necessary
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
end
Now you can do things like send messages to a channel or check your guild's member count.
One thing you might have noticed is that print
will print strings to your Lua console (which is expected). What if you want to use your Discord channel as the console? The naive way to do this would be to define print as a message creator:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
sandbox.message = msg
sandbox.print = function(...) -- don't do this
msg:reply(...)
end)
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
end
The problem with this is that for every call to print
, you will create a message. This can become excessive if you have a lot of things to print, or are printing from inside of a lengthy loop. Wouldn't it be better if you can combine all of these printed lines into one message?
To do this takes some trickery. First, you need a place to collect all the printed lines as they are generated during the code execution. Let's use a table:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
local lines = {} -- this is where our printed lines will collect
sandbox.message = msg
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
end
Now you need to re-define print
so that instead of printing to the console, it dumps its lines to the lines
table. We can use a helper function to do this:
local function printLine(...)
local ret = {}
for i = 1, select('#', ...) do
local arg = tostring(select(i, ...))
table.insert(ret, arg)
end
return table.concat(ret, '\t')
end
This function simulates the behavior of Lua's print function. It takes a variable number of arguments, converts them to strings, and concatenates them with tabs. The difference is that instead of writing the line to stdout, it returns the line.
You can do the same thing for Luvit's pretty-print function:
local pp = require('pretty-print')
local function prettyLine(...)
local ret = {}
for i = 1, select('#', ...) do
local arg = pp.strip(pp.dump(select(i, ...)))
table.insert(ret, arg)
end
return table.concat(ret, '\t')
end
These can now be used to populate your lines
table when ever print
or p
are encountered in your code:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
local lines = {}
sandbox.message = msg
sandbox.print = function(...) -- intercept printed lines with this
table.insert(lines, printLine(...))
end
sandbox.p = function(...) -- intercept pretty-printed lines with this
table.insert(lines, prettyLine(...))
end
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
end
After the table has been filled, you need to concatenate the lines and send them to the channel:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
local lines = {}
sandbox.message = msg
sandbox.print = function(...)
table.insert(lines, printLine(...))
end
sandbox.p = function(...)
table.insert(lines, prettyLine(...))
end
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
lines = table.concat(lines, '\n') -- bring all the lines together
return msg:reply(code(lines)) -- and send them as a message reply
end
What if you have a lot of code to input? Sometimes it's easier to put it into a code block, but Lua cannot interpret markdown code blocks. You have to strip the markdown characters from the content before running the code:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
arg = arg:gsub('```\n?', '') -- strip markdown codeblocks
local lines = {}
sandbox.message = msg
sandbox.print = function(...)
table.insert(lines, printLine(...))
end
sandbox.p = function(...)
table.insert(lines, prettyLine(...))
end
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
lines = table.concat(lines, '\n') -- bring all the lines together
return msg:reply(code(lines)) -- and send them as a message reply
end
What if the output is greater than the default Discord message length of 2000 characters? You'll probably want to handle this, since sometimes code output can become lengthy. You can either split it into multiple messages or just truncate it like so:
local function exec(arg, msg)
if not arg then return end
if msg.author ~= msg.client.owner then return end
arg = arg:gsub('```\n?', '') -- strip markdown codeblocks
local lines = {}
sandbox.message = msg
sandbox.print = function(...)
table.insert(lines, printLine(...))
end
sandbox.p = function(...)
table.insert(lines, prettyLine(...))
end
local fn, syntaxError = load(arg, 'DiscordBot', 't', sandbox)
if not fn then return msg:reply(code(syntaxError)) end
local success, runtimeError = pcall(fn)
if not success then return msg:reply(code(runtimeError)) end
lines = table.concat(lines, '\n')
if #lines > 1990 then -- truncate long messages
lines = lines:sub(1, 1990)
end
return msg:reply(code(lines))
end
You now should have enough information to set up your own Lua code executor in your Discordia bot. Hopefully this tutorial was informative. Thanks for reading.