====== Lua scripting in Project Tinybot ======
Since April 9, 2025, Project Tinybot allows you to run custom scripts written in [[https://lua.org|Lua]] in Twitch chats. The scripts are run in the sandbox. This is a safe environment that prevents malicious code from doing its bad things by removing access to certain libraries.
Also, there are execution limits for scripts - 2MB RAM and 500 milliseconds wait time. These limits are made for cases where you may have forgotten "while-true loop" and so it doesn't run indefinitely until the bot shuts down, the loop will stop after 500 milliseconds. Additionally, it's a test to see if you know how to write optimized code :)
The bot provides [[#API calls|API calls]] for your scripts, which will be expanded in the future, and [[#Commands|commands]] that allow you to run Lua scripts.
If you are interested in any examples, there are [[https://tnd.quest/milkE.lua|a mini-game about drinking 'not milk']] and [[https://tnd.quest/testE.lua|test counter]].
Have fun!
===== Commands =====
==== Lua script in the chat - !lua ====
Syntax - ''!lua [Valid Lua code...]''
This command acts as a [[https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop REPL|(Read-eval-print loop)]]. It's useful for small scripts, such as ''!lua 2+2'' or ''!lua function hello(name) return "hello, " .. name end return hello("chat")''. It must always return something, otherwise it will return **Empty or unsupported response**. As you can see, in the first example, we didn't explicitly use the ''return'' keyword; it will be automatically added by the backend. However, in the second example, the bot doesn't know what the script should return, so we need to add the ''return'' keyword at the end.
If the script has more than 1 line of code, then you'll probably like the [[#Remote Lua script - !luaimport|!luaimport]] command.
==== Remote Lua script - !luaimport ====
Syntax - ''!luaimport [URL]''
This command downloads a Lua script from the specified URL. Make sure the URL returns one of the allowed content types: ''text/plain'', ''text/plain; charset=utf-8'', ''text/x-lua'' or ''text/x-lua; charset=utf-8''. The script must use the ''return'' keyword, which won't be added automatically.
By the way, scripts executed within ''!luaimport'' have access to the [[#Storage API|Storage API]].
In previous versions, you had to use something like ''!luaimport pastebin:Ejdrqmb7'' or ''!luaimport pastea:testE''. As mentioned, the full URL must be specified. So, if you're using [[https://pastebin.com|Pastebin]], your command would look like ''!luaimport https://pastebin.com/raw/Ejdrqmb7''. If you're using [[https://tnd.quest|Pastea]], the command will look like this: ''!luaimport https://tnd.quest/~/testE.lua''.
===== API calls =====
Not all APIs are available to user scripts. Chat/remote Lua scripts can only use the base APIs: [[#Bot metadata API|Bot metadata API]], [[#Time API|Time API]], [[#JSON API|JSON API]], [[#Network API|Network API]], [[#Localization API|Localization API]], [[#String API|String API]], [[#Array API|Array API]]. From the built-in Lua library, you can use ''type'', ''pairs'', ''table'', ''math'', ''string'' (without ''dump'' function), ''table'', ''tonumber'', ''tostring''.
==== Bot metadata API ====
=== bot_get_compiler_version() ===
Returns ''Python X.X.X''.
=== bot_get_uptime() ===
Returns the bot uptime in seconds.
=== bot_get_memory_usage() ===
Returns the bot memory usage in bytes.
=== bot_get_compile_time() ===
Returns the bot compile time in seconds.
=== bot_get_version() ===
Returns the bot version. If bot doesn't have active tag, then it will return short commit SHA hash.
=== bot_config() ===
Returns bot configuration. See [[#Bot configuration structure|Bot configuration structure]].
==== Time API ====
=== time_current() ===
Get current UTC time in seconds.
=== time_humanize(number) ===
Converts UNIX timestamp into humanized timestamp, e.g. ''1488'' will be ''25m48s'' (25 minutes and 48 seconds)
=== time_format(timestamp: Number, format: String) ===
Converts timestamp //(in seconds)// into a beautiful, formatted human-readable datetime. For example, ''timestamp = 1758462814'' and ''format = "%Y-%m-%d %H:%M:%S"'' will return ''2025-09-21 13:53:34''.
=== time_parse(datetime: String, format: String) ===
This function is a reverse for ''time_format()''. You specify the datetime (e.g. 2025-09-21 13:53:34) and its format (e.g. %Y-%m-%d %H:%M:%S) and it will return the timestamp in seconds (e.g. 1758462814).
==== JSON API ====
=== json_parse(String) ===
Convert stringified JSON to valid Lua value.
=== json_stringify(value) ===
Convert Lua value to stringified JSON.
=== json_get_value(body: LuaTable, path: String) ===
Retrieves a value from body based on the path. Path should be formatted like **X.Y.Z** (e.g. //data.keys.cert_key//).
==== Network API ====
=== net_get(url) ===
Sends a HTTP GET request to the specified URL and returns a [[#Network response structure|network response structure]].
=== net_get_with_headers(url, headers) ===
Sends a HTTP GET request with headers to the specified URL and returns a [[#Network response structure|network response structure]]. Headers must be a key-value table (e.g. ''{ "Header-Name" = "Header-Value", "Accept" = "application/json" }'').
=== net_post_multipart_with_headers(url, body, headers) ===
Sends a HTTP POST multipart/form-data request with headers to the specified URL and returns a [[#Network response structure|network response structure]]. Headers must be a key-value table (e.g. ''{ "Header-Name" = "Header-Value", "Accept" = "application/json" }''). Also, request body must be a key-value table. **Files are not supported!**
==== Localization API ====
=== l10n_line_request(request, lines, line_id, parameters) ===
Returns a string from the specified ''lines'' table. Firstly, it looks for the localization table based on the channel language. If it doesn't exist, then the function will use the first localization table. Secondly, the function searches for line ID, and then substitute ''{}'' with the parameters. Also, there are defined tokens: ''{sender.alias_name}'', ''{source.alias_name}'', ''{default.prefix}'', ''{channel.prefix}''. See [[#Localization table structure|localization table structure]].
=== l10n_get_localization_names() ===
Returns an array of available language IDs.
==== String API ====
=== str_split(string, separator) ===
Split a string by the separator.
=== str_make_parts(base, values, prefix, separator, max_length) ===
Returns an array of strings that have the following structure: ''[base] · [prefix][value 1][separator][prefix][value 2][separator]...''. If the message exceeds the maximum length, it is wrapped to the next line. Used in massping and events.
=== event_type_to_str(val) ===
Converts the ''EventType'' into a string.
=== str_to_event_type(val) ===
Converts the string into an ''EventType''.
==== Array API ====
=== array_contains(arr, val) ===
Returns ''true'' if the array has the value, otherwise ''false''.
==== Storage API ====
=== storage_get() ===
Get user's storage cell. Returns an empty string if it was just created.
=== storage_put(String) ===
Put string to user's storage cell. Returns true if it was successful.
=== storage_channel_get() ===
Get channel's storage cell. Returns an empty string if it was just created.
=== storage_channel_put(String) ===
Put string to channel's storage cell. Returns true if it was successful.
===== Structures =====
==== Bot configuration structure ====
{
"twitch" = {
["username"] = "String"
},
"commands" = {
["join_allowed"] = "Boolean",
["join_allow_from_other_chats"] = "Boolean",
["rpost_path"] = "String",
["rpost_url"] = "String",
["paste_path"] = "String",
["paste_body_name"] = "String",
["paste_title_name"] = "String",
["paste_url"] = "String",
["help_url"] = "String"
},
"owner" = {
["id"] = "Integer",
["name"] = "String"
},
}
==== Network response structure ====
{
"code" = "Integer",
"text" = "String"
}
==== Localization table structure ====
{
"language_id" = {
["line_id"] = "String"
},
"english" = {
["line_id"] = "Hi {sender.alias_name}! This is an example. This is the first parameter -> {} and this is the second parameter -> {}."
},
"russian" = {
["line_id"] = "Привет {sender.alias_name}! Это пример. Это первый параметр -> {}, а это второй параметр -> {}."
}
}