mirror-chatterino2/docs/wip-plugins.md
Mm2PL a5ac06124d
Update plugin documentation document (#5185)
* Add blurb about LuaLS type definitions

* Add c2.Channel documentation replacing c2.send_msg and c2.system_msg

* Use snake_case for command names in examples

to align to Lua naming convention

* Unrelated: add missing ChannelType
2024-02-18 21:09:19 +00:00

11 KiB

Plugins

If Chatterino is compiled with the CHATTERINO_PLUGINS CMake option, it can load and execute Lua files. Note that while there are attempts at making this decently safe, we cannot guarantee safety.

Plugin structure

Chatterino searches for plugins in the Plugins directory in the app data, right next to Settings and Logs.

Each plugin should have its own directory.

Chatterino Plugins dir/
└── plugin_name/
    ├── init.lua
    └── info.json

init.lua will be the file loaded when the plugin is enabled. You may load other files using require global function.

info.json contains metadata about the plugin, like its name, description, authors, homepage link, tags, version, license name. The version field must be semver 2.0 compliant. The general idea of info.json will not change however the exact contents probably will, for example with permission system ideas. Example file:

{
  "$schema": "https://raw.githubusercontent.com/Chatterino/chatterino2/master/docs/plugin-info.schema.json",
  "name": "Test plugin",
  "description": "This plugin is for testing stuff.",
  "authors": ["Mm2PL"],
  "homepage": "https://github.com/Chatterino/Chatterino2",
  "tags": ["test"],
  "version": "0.0.0",
  "license": "MIT"
}

An example plugin is available at https://github.com/Mm2PL/Chatterino-test-plugin

Plugins with Typescript

If you prefer, you may use TypescriptToLua to typecheck your plugins. There is a chatterino.d.ts file describing the API in this directory. However, this has several drawbacks like harder debugging at runtime.

LuaLS type definitions

Type definitions for LuaLS are available in the /plugin-meta.lua file. These are generated from the C++ headers of Chatterino using a script.

API

The following parts of the Lua standard library are loaded:

  • _G (most globals)
  • table
  • string
  • math
  • utf8

The official manual for them is available here.

Chatterino API

All Chatterino functions are exposed in a global table called c2. The following members are available:

log(level, args...)

Writes a message to the Chatterino log. The level argument should be a LogLevel member. All args should be convertible to a string with tostring().

Example:

c2.log(c2.LogLevel.Warning, "Hello, this should show up in the Chatterino log by default")

c2.log(c2.LogLevel.Debug, "Hello world")
-- Equivalent to doing qCDebug(chatterinoLua) << "[pluginDirectory:Plugin Name]" << "Hello, world"; from C++

LogLevel enum

This table describes log levels available to Lua Plugins. The values behind the names may change, do not count on them. It has the following keys:

  • Debug
  • Info
  • Warning
  • Critical

register_command(name, handler)

Registers a new command called name which when executed will call handler. Returns true if everything went ok, false if there already exists another command with this name.

Example:

function cmd_words(ctx)
    -- ctx contains:
    -- words - table of words supplied to the command including the trigger
    -- channel - the channel the command is being run in
    channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
end

c2.register_command("/words", cmd_words)

Limitations/known issues:

  • Commands registered in functions, not in the global scope might not show up in the settings UI, rebuilding the window content caused by reloading another plugin will solve this.
  • Spaces in command names aren't handled very well (https://github.com/Chatterino/chatterino2/issues/1517).

register_callback("CompletionRequested", handler)

Registers a callback (handler) to process completions. The callback gets the following parameters:

  • query: The queried word.
  • full_text_content: The whole input.
  • cursor_position: The position of the cursor in the input.
  • is_first_word: Flag whether query is the first word in the input.

Example:

Input query full_text_content cursor_position is_first_word
foo│ foo foo 3 true
fo│o fo foo 2 true
foo bar│ bar foo bar 7 false
foo │bar foo foo bar 4 false
function string.startswith(s, other)
    return string.sub(s, 1, string.len(other)) == other
end

c2.register_callback(
    "CompletionRequested",
    function(query, full_text_content, cursor_position, is_first_word)
        if ("!join"):startswith(query) then
            ---@type CompletionList
            return { hide_others = true, values = { "!join" } }
        end
        ---@type CompletionList
        return { hide_others = false, values = {} }
    end
)

Platform enum

This table describes platforms that can be accessed. Chatterino supports IRC however plugins do not yet have explicit access to get IRC channels objects. The values behind the names may change, do not count on them. It has the following keys:

  • Twitch

ChannelType enum

This table describes channel types Chatterino supports. The values behind the names may change, do not count on them. It has the following keys:

  • None
  • Direct
  • Twitch
  • TwitchWhispers
  • TwitchWatching
  • TwitchMentions
  • TwitchLive
  • TwitchAutomod
  • TwitchEnd
  • Irc
  • Misc

Channel

This is a type that represents a channel. Existence of this object doesn't force Chatterino to hold the channel open. Should the user close the last split holding this channel open, your Channel object will expire. You can check for this using the Channel:is_valid() function. Using any other function on an expired Channel yields an error. Using any Channel member function on a non-Channel table also yields an error.

Some functions make sense only for Twitch channel, these yield an error when used on non-Twitch channels. Special channels while marked as is_twitch_channel() = true do not have these functions. To check if a channel is an actual Twitch chatroom use Channel:get_type() instead of Channel:is_twitch_channel().

Channel:by_name(name, platform)

Finds a channel given by name on platform (see Platform enum). Returns the channel or nil if not open.

Some miscellaneous channels are marked as if they are specifically Twitch channels:

  • /whispers
  • /mentions
  • /watching
  • /live
  • /automod

Example:

local pajladas = c2.Channel.by_name("pajlada", c2.Platform.Twitch)
Channel:by_twitch_id(id)

Finds a channel given by the string representation of the owner's Twitch user ID. Returns the channel or nil if not open.

Example:

local pajladas = c2.Channel.by_twitch_id("11148817")
Channel:get_name()

On Twitch returns the lowercase login name of the channel owner. On IRC returns the normalized channel name.

Example:

-- Note: if the channel is not open this errors
pajladas:get_name()  -- "pajlada"
Channel:get_type()

Returns the channel's type. See ChannelType enum.

Channel:get_display_name()

Returns the channel owner's display name. This can contain characters that are not lowercase and even non-ASCII.

Example:

local saddummys = c2.Channel.by_name("saddummy")
saddummys:get_display_name() -- "서새봄냥"
Channel:send_message(message[, execute_commands])

Sends a message to the channel with the given text. If execute_commands is not present or false commands will not be executed client-side, this affects all user commands and all Twitch commands except /me.

Examples:

-- times out @Mm2PL
pajladas:send_message("/timeout mm2pl 1s test", true)

-- results in a "Unknown command" error from Twitch
pajladas:send_message("/timeout mm2pl 1s test")

-- Given a user command "hello":
-- this will execute it
pajladas:send_message("hello", true)
-- this will send "hello" literally, bypassing commands
pajladas:send_message("hello")

function cmd_shout(ctx)
    table.remove(ctx.words, 1)
    local output = table.concat(ctx.words, " ")
    ctx.channel:send_message(string.upper(output))
end
c2.register_command("/shout", cmd_shout)

Limitations/Known issues:

  • It is possible to trigger your own Lua command with this causing a potentially infinite loop.
Channel:add_system_message(message)

Shows a system message in the channel with the given text.

Example:

pajladas:add_system_message("Hello, world!")
Channel:is_twitch_channel()

Returns true if the channel is a Twitch channel, that is its type name has the Twitch prefix. This returns true for special channels like Mentions. You might want Channel:get_type() == "Twitch" if you want to use Twitch-specific functions.

Channel:get_twitch_id()

Returns the string form of the channel owner's Twitch user ID.

Example:

pajladas:get_twitch_id() -- "11148817"
Channel:is_broadcaster()

Returns true if the channel is owned by the current user.

Channel:is_mod()

Returns true if the channel can be moderated by the current user.

Channel:is_vip()

Returns true if the current user is a VIP in the channel.

Changed globals

load(chunk [, chunkname [, mode [, env]]])

This function is only available if Chatterino is compiled in debug mode. It is meant for debugging with little exception. This function behaves really similarity to Lua's load, however it does not allow for bytecode to be executed. It achieves this by forcing all inputs to be encoded with UTF-8.

See official documentation

require(modname)

This is Lua's require() function. However, the searcher and load configuration is notably different from the default:

  • Lua's built-in dynamic library searcher is removed,
  • package.path is not used, in its place are two searchers,
  • when require() is used, first a file relative to the currently executing file will be checked, then a file relative to the plugin directory,
  • binary chunks are never loaded

As in normal Lua, dots are converted to the path separators ('/' on Linux and Mac, '\' on Windows).

Example:

require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua
require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua
require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file

print(Args...)

The print global function is equivalent to calling c2.log(c2.LogLevel.Debug, Args...)