mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Move plugins to Sol (#5622)
Co-authored-by: Nerixyz <nerixdev@outlook.de> Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
9345050868
commit
352a4ec132
2
.github/workflows/test-macos.yml
vendored
2
.github/workflows/test-macos.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-13]
|
os: [macos-13]
|
||||||
qt-version: [5.15.2, 6.7.1]
|
qt-version: [5.15.2, 6.7.1]
|
||||||
plugins: [false]
|
plugins: [true]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
env:
|
env:
|
||||||
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }}
|
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }}
|
||||||
|
|
2
.github/workflows/test-windows.yml
vendored
2
.github/workflows/test-windows.yml
vendored
|
@ -26,7 +26,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest]
|
os: [windows-latest]
|
||||||
qt-version: [5.15.2, 6.7.1]
|
qt-version: [5.15.2, 6.7.1]
|
||||||
plugins: [false]
|
plugins: [true]
|
||||||
skip-artifact: [false]
|
skip-artifact: [false]
|
||||||
skip-crashpad: [false]
|
skip-crashpad: [false]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -44,3 +44,6 @@
|
||||||
[submodule "lib/expected-lite"]
|
[submodule "lib/expected-lite"]
|
||||||
path = lib/expected-lite
|
path = lib/expected-lite
|
||||||
url = https://github.com/martinmoene/expected-lite
|
url = https://github.com/martinmoene/expected-lite
|
||||||
|
[submodule "lib/sol2"]
|
||||||
|
path = lib/sol2
|
||||||
|
url = https://github.com/ThePhD/sol2.git
|
||||||
|
|
|
@ -108,6 +108,7 @@
|
||||||
- Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607)
|
- Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607)
|
||||||
- Dev: `GIFTimer` is no longer initialized in tests. (#5608)
|
- Dev: `GIFTimer` is no longer initialized in tests. (#5608)
|
||||||
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
|
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
|
||||||
|
- Dev: Move plugins to Sol2. (#5622)
|
||||||
- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652)
|
- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652)
|
||||||
- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660)
|
- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660)
|
||||||
|
|
||||||
|
|
|
@ -212,6 +212,8 @@ endif()
|
||||||
if (CHATTERINO_PLUGINS)
|
if (CHATTERINO_PLUGINS)
|
||||||
set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src")
|
set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src")
|
||||||
add_subdirectory(lib/lua)
|
add_subdirectory(lib/lua)
|
||||||
|
|
||||||
|
find_package(Sol2 REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (BUILD_WITH_CRASHPAD)
|
if (BUILD_WITH_CRASHPAD)
|
||||||
|
|
21
cmake/FindSol2.cmake
Normal file
21
cmake/FindSol2.cmake
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
|
||||||
|
find_path(Sol2_INCLUDE_DIR sol/sol.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/sol2/include)
|
||||||
|
|
||||||
|
find_package_handle_standard_args(Sol2 DEFAULT_MSG Sol2_INCLUDE_DIR)
|
||||||
|
|
||||||
|
if (Sol2_FOUND)
|
||||||
|
add_library(Sol2 INTERFACE IMPORTED)
|
||||||
|
set_target_properties(Sol2 PROPERTIES
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${Sol2_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
target_compile_definitions(Sol2 INTERFACE
|
||||||
|
SOL_ALL_SAFETIES_ON=1
|
||||||
|
SOL_USING_CXX_LUA=1
|
||||||
|
SOL_NO_NIL=0
|
||||||
|
)
|
||||||
|
target_link_libraries(Sol2 INTERFACE lua)
|
||||||
|
add_library(sol2::sol2 ALIAS Sol2)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
mark_as_advanced(Sol2_INCLUDE_DIR)
|
|
@ -5,14 +5,23 @@
|
||||||
-- Add the folder this file is in to "Lua.workspace.library".
|
-- Add the folder this file is in to "Lua.workspace.library".
|
||||||
|
|
||||||
c2 = {}
|
c2 = {}
|
||||||
---@alias c2.LogLevel integer
|
---@alias c2.LogLevel.Debug "c2.LogLevel.Debug"
|
||||||
---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel }
|
---@alias c2.LogLevel.Info "c2.LogLevel.Info"
|
||||||
|
---@alias c2.LogLevel.Warning "c2.LogLevel.Warning"
|
||||||
|
---@alias c2.LogLevel.Critical "c2.LogLevel.Critical"
|
||||||
|
---@alias c2.LogLevel c2.LogLevel.Debug|c2.LogLevel.Info|c2.LogLevel.Warning|c2.LogLevel.Critical
|
||||||
|
---@type { Debug: c2.LogLevel.Debug, Info: c2.LogLevel.Info, Warning: c2.LogLevel.Warning, Critical: c2.LogLevel.Critical }
|
||||||
c2.LogLevel = {}
|
c2.LogLevel = {}
|
||||||
|
|
||||||
---@alias c2.EventType integer
|
-- Begin src/controllers/plugins/api/EventType.hpp
|
||||||
---@type { CompletionRequested: c2.EventType }
|
|
||||||
|
---@alias c2.EventType.CompletionRequested "c2.EventType.CompletionRequested"
|
||||||
|
---@alias c2.EventType c2.EventType.CompletionRequested
|
||||||
|
---@type { CompletionRequested: c2.EventType.CompletionRequested }
|
||||||
c2.EventType = {}
|
c2.EventType = {}
|
||||||
|
|
||||||
|
-- End src/controllers/plugins/api/EventType.hpp
|
||||||
|
|
||||||
---@class CommandContext
|
---@class CommandContext
|
||||||
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
|
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
|
||||||
---@field channel c2.Channel The channel the command was executed in.
|
---@field channel c2.Channel The channel the command was executed in.
|
||||||
|
@ -29,19 +38,40 @@ c2.EventType = {}
|
||||||
|
|
||||||
-- Begin src/common/Channel.hpp
|
-- Begin src/common/Channel.hpp
|
||||||
|
|
||||||
---@alias c2.ChannelType integer
|
---@alias c2.ChannelType.None "c2.ChannelType.None"
|
||||||
---@type { None: c2.ChannelType, Direct: c2.ChannelType, Twitch: c2.ChannelType, TwitchWhispers: c2.ChannelType, TwitchWatching: c2.ChannelType, TwitchMentions: c2.ChannelType, TwitchLive: c2.ChannelType, TwitchAutomod: c2.ChannelType, TwitchEnd: c2.ChannelType, Irc: c2.ChannelType, Misc: c2.ChannelType }
|
---@alias c2.ChannelType.Direct "c2.ChannelType.Direct"
|
||||||
|
---@alias c2.ChannelType.Twitch "c2.ChannelType.Twitch"
|
||||||
|
---@alias c2.ChannelType.TwitchWhispers "c2.ChannelType.TwitchWhispers"
|
||||||
|
---@alias c2.ChannelType.TwitchWatching "c2.ChannelType.TwitchWatching"
|
||||||
|
---@alias c2.ChannelType.TwitchMentions "c2.ChannelType.TwitchMentions"
|
||||||
|
---@alias c2.ChannelType.TwitchLive "c2.ChannelType.TwitchLive"
|
||||||
|
---@alias c2.ChannelType.TwitchAutomod "c2.ChannelType.TwitchAutomod"
|
||||||
|
---@alias c2.ChannelType.TwitchEnd "c2.ChannelType.TwitchEnd"
|
||||||
|
---@alias c2.ChannelType.Misc "c2.ChannelType.Misc"
|
||||||
|
---@alias c2.ChannelType c2.ChannelType.None|c2.ChannelType.Direct|c2.ChannelType.Twitch|c2.ChannelType.TwitchWhispers|c2.ChannelType.TwitchWatching|c2.ChannelType.TwitchMentions|c2.ChannelType.TwitchLive|c2.ChannelType.TwitchAutomod|c2.ChannelType.TwitchEnd|c2.ChannelType.Misc
|
||||||
|
---@type { None: c2.ChannelType.None, Direct: c2.ChannelType.Direct, Twitch: c2.ChannelType.Twitch, TwitchWhispers: c2.ChannelType.TwitchWhispers, TwitchWatching: c2.ChannelType.TwitchWatching, TwitchMentions: c2.ChannelType.TwitchMentions, TwitchLive: c2.ChannelType.TwitchLive, TwitchAutomod: c2.ChannelType.TwitchAutomod, TwitchEnd: c2.ChannelType.TwitchEnd, Misc: c2.ChannelType.Misc }
|
||||||
c2.ChannelType = {}
|
c2.ChannelType = {}
|
||||||
|
|
||||||
-- End src/common/Channel.hpp
|
-- End src/common/Channel.hpp
|
||||||
|
|
||||||
-- Begin src/controllers/plugins/api/ChannelRef.hpp
|
-- Begin src/controllers/plugins/api/ChannelRef.hpp
|
||||||
|
|
||||||
---@alias c2.Platform integer
|
-- Begin src/providers/twitch/TwitchChannel.hpp
|
||||||
--- This enum describes a platform for the purpose of searching for a channel.
|
|
||||||
--- Currently only Twitch is supported because identifying IRC channels is tricky.
|
---@class StreamStatus
|
||||||
---@type { Twitch: c2.Platform }
|
---@field live boolean
|
||||||
c2.Platform = {}
|
---@field viewer_count number
|
||||||
|
---@field title string Stream title or last stream title
|
||||||
|
---@field game_name string
|
||||||
|
---@field game_id string
|
||||||
|
---@field uptime number Seconds since the stream started.
|
||||||
|
|
||||||
|
---@class RoomModes
|
||||||
|
---@field subscriber_only boolean
|
||||||
|
---@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||||
|
---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
||||||
|
|
||||||
|
-- End src/providers/twitch/TwitchChannel.hpp
|
||||||
|
|
||||||
---@class c2.Channel
|
---@class c2.Channel
|
||||||
c2.Channel = {}
|
c2.Channel = {}
|
||||||
|
@ -72,7 +102,7 @@ function c2.Channel:get_display_name() end
|
||||||
--- Note that this does not execute client-commands.
|
--- Note that this does not execute client-commands.
|
||||||
---
|
---
|
||||||
---@param message string
|
---@param message string
|
||||||
---@param execute_commands boolean Should commands be run on the text?
|
---@param execute_commands? boolean Should commands be run on the text?
|
||||||
function c2.Channel:send_message(message, execute_commands) end
|
function c2.Channel:send_message(message, execute_commands) end
|
||||||
|
|
||||||
--- Adds a system message client-side
|
--- Adds a system message client-side
|
||||||
|
@ -131,9 +161,8 @@ function c2.Channel:__tostring() end
|
||||||
--- - /automod
|
--- - /automod
|
||||||
---
|
---
|
||||||
---@param name string Which channel are you looking for?
|
---@param name string Which channel are you looking for?
|
||||||
---@param platform c2.Platform Where to search for the channel?
|
|
||||||
---@return c2.Channel?
|
---@return c2.Channel?
|
||||||
function c2.Channel.by_name(name, platform) end
|
function c2.Channel.by_name(name) end
|
||||||
|
|
||||||
--- Finds a channel by the Twitch user ID of its owner.
|
--- Finds a channel by the Twitch user ID of its owner.
|
||||||
---
|
---
|
||||||
|
@ -141,21 +170,6 @@ function c2.Channel.by_name(name, platform) end
|
||||||
---@return c2.Channel?
|
---@return c2.Channel?
|
||||||
function c2.Channel.by_twitch_id(id) end
|
function c2.Channel.by_twitch_id(id) end
|
||||||
|
|
||||||
---@class RoomModes
|
|
||||||
---@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
|
||||||
---@field subscriber_only boolean
|
|
||||||
---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
|
||||||
---@field follower_only number? Time in minutes you need to follow to chat or nil.
|
|
||||||
---@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
|
||||||
|
|
||||||
---@class StreamStatus
|
|
||||||
---@field live boolean
|
|
||||||
---@field viewer_count number
|
|
||||||
---@field uptime number Seconds since the stream started.
|
|
||||||
---@field title string Stream title or last stream title
|
|
||||||
---@field game_name string
|
|
||||||
---@field game_id string
|
|
||||||
|
|
||||||
-- End src/controllers/plugins/api/ChannelRef.hpp
|
-- End src/controllers/plugins/api/ChannelRef.hpp
|
||||||
|
|
||||||
-- Begin src/controllers/plugins/api/HTTPResponse.hpp
|
-- Begin src/controllers/plugins/api/HTTPResponse.hpp
|
||||||
|
@ -176,6 +190,9 @@ function HTTPResponse:status() end
|
||||||
---
|
---
|
||||||
function HTTPResponse:error() end
|
function HTTPResponse:error() end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function HTTPResponse:__tostring() end
|
||||||
|
|
||||||
-- End src/controllers/plugins/api/HTTPResponse.hpp
|
-- End src/controllers/plugins/api/HTTPResponse.hpp
|
||||||
|
|
||||||
-- Begin src/controllers/plugins/api/HTTPRequest.hpp
|
-- Begin src/controllers/plugins/api/HTTPRequest.hpp
|
||||||
|
@ -219,6 +236,9 @@ function HTTPRequest:set_header(name, value) end
|
||||||
---
|
---
|
||||||
function HTTPRequest:execute() end
|
function HTTPRequest:execute() end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function HTTPRequest:__tostring() end
|
||||||
|
|
||||||
--- Creates a new HTTPRequest
|
--- Creates a new HTTPRequest
|
||||||
---
|
---
|
||||||
---@param method HTTPMethod Method to use
|
---@param method HTTPMethod Method to use
|
||||||
|
@ -230,8 +250,13 @@ function HTTPRequest.create(method, url) end
|
||||||
|
|
||||||
-- Begin src/common/network/NetworkCommon.hpp
|
-- Begin src/common/network/NetworkCommon.hpp
|
||||||
|
|
||||||
---@alias HTTPMethod integer
|
---@alias HTTPMethod.Get "HTTPMethod.Get"
|
||||||
---@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod }
|
---@alias HTTPMethod.Post "HTTPMethod.Post"
|
||||||
|
---@alias HTTPMethod.Put "HTTPMethod.Put"
|
||||||
|
---@alias HTTPMethod.Delete "HTTPMethod.Delete"
|
||||||
|
---@alias HTTPMethod.Patch "HTTPMethod.Patch"
|
||||||
|
---@alias HTTPMethod HTTPMethod.Get|HTTPMethod.Post|HTTPMethod.Put|HTTPMethod.Delete|HTTPMethod.Patch
|
||||||
|
---@type { Get: HTTPMethod.Get, Post: HTTPMethod.Post, Put: HTTPMethod.Put, Delete: HTTPMethod.Delete, Patch: HTTPMethod.Patch }
|
||||||
HTTPMethod = {}
|
HTTPMethod = {}
|
||||||
|
|
||||||
-- End src/common/network/NetworkCommon.hpp
|
-- End src/common/network/NetworkCommon.hpp
|
||||||
|
@ -245,7 +270,7 @@ function c2.register_command(name, handler) end
|
||||||
|
|
||||||
--- Registers a callback to be invoked when completions for a term are requested.
|
--- Registers a callback to be invoked when completions for a term are requested.
|
||||||
---
|
---
|
||||||
---@param type "CompletionRequested"
|
---@param type c2.EventType.CompletionRequested
|
||||||
---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
||||||
function c2.register_callback(type, func) end
|
function c2.register_callback(type, func) end
|
||||||
|
|
||||||
|
|
|
@ -171,7 +171,7 @@ function cmd_words(ctx)
|
||||||
-- ctx contains:
|
-- ctx contains:
|
||||||
-- words - table of words supplied to the command including the trigger
|
-- words - table of words supplied to the command including the trigger
|
||||||
-- channel - the channel the command is being run in
|
-- channel - the channel the command is being run in
|
||||||
channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
|
ctx.channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
|
||||||
end
|
end
|
||||||
|
|
||||||
c2.register_command("/words", cmd_words)
|
c2.register_command("/words", cmd_words)
|
||||||
|
@ -183,7 +183,7 @@ Limitations/known issues:
|
||||||
rebuilding the window content caused by reloading another plugin will solve this.
|
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).
|
- Spaces in command names aren't handled very well (https://github.com/Chatterino/chatterino2/issues/1517).
|
||||||
|
|
||||||
#### `register_callback("CompletionRequested", handler)`
|
#### `register_callback(c2.EventType.CompletionRequested, handler)`
|
||||||
|
|
||||||
Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries:
|
Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries:
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ function string.startswith(s, other)
|
||||||
end
|
end
|
||||||
|
|
||||||
c2.register_callback(
|
c2.register_callback(
|
||||||
"CompletionRequested",
|
c2.EventType.CompletionRequested,
|
||||||
function(event)
|
function(event)
|
||||||
if ("!join"):startswith(event.query) then
|
if ("!join"):startswith(event.query) then
|
||||||
---@type CompletionList
|
---@type CompletionList
|
||||||
|
@ -219,15 +219,6 @@ c2.register_callback(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `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
|
#### `ChannelType` enum
|
||||||
|
|
||||||
This table describes channel types Chatterino supports. The values behind the
|
This table describes channel types Chatterino supports. The values behind the
|
||||||
|
@ -260,9 +251,9 @@ used on non-Twitch channels. Special channels while marked as
|
||||||
is an actual Twitch chatroom use `Channel:get_type()` instead of
|
is an actual Twitch chatroom use `Channel:get_type()` instead of
|
||||||
`Channel:is_twitch_channel()`.
|
`Channel:is_twitch_channel()`.
|
||||||
|
|
||||||
##### `Channel:by_name(name, platform)`
|
##### `Channel:by_name(name)`
|
||||||
|
|
||||||
Finds a channel given by `name` on `platform` (see [`Platform` enum](#Platform-enum)). Returns the channel or `nil` if not open.
|
Finds a channel given by `name`. Returns the channel or `nil` if not open.
|
||||||
|
|
||||||
Some miscellaneous channels are marked as if they are specifically Twitch channels:
|
Some miscellaneous channels are marked as if they are specifically Twitch channels:
|
||||||
|
|
||||||
|
@ -275,7 +266,7 @@ Some miscellaneous channels are marked as if they are specifically Twitch channe
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local pajladas = c2.Channel.by_name("pajlada", c2.Platform.Twitch)
|
local pajladas = c2.Channel.by_name("pajlada")
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `Channel:by_twitch_id(id)`
|
##### `Channel:by_twitch_id(id)`
|
||||||
|
@ -363,7 +354,7 @@ pajladas:add_system_message("Hello, world!")
|
||||||
|
|
||||||
Returns `true` if the channel is a Twitch channel, that is its type name has
|
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.
|
the `Twitch` prefix. This returns `true` for special channels like Mentions.
|
||||||
You might want `Channel:get_type() == "Twitch"` if you want to use
|
You might want `Channel:get_type() == c2.ChannelType.Twitch` if you want to use
|
||||||
Twitch-specific functions.
|
Twitch-specific functions.
|
||||||
|
|
||||||
##### `Channel:get_twitch_id()`
|
##### `Channel:get_twitch_id()`
|
||||||
|
|
|
@ -1,48 +1,44 @@
|
||||||
project(lua CXX)
|
project(lua CXX)
|
||||||
|
|
||||||
#[====[
|
#[====[
|
||||||
Updating this list:
|
This list contains all .c files except lua.c and onelua.c
|
||||||
remove all listed files
|
Use the following command from the repository root to get these file:
|
||||||
go to line below, ^y2j4j$@" and then reindent the file names
|
perl -e 'print s/^lib\/lua\///r . "\n" for grep { /\.c$/ && !/(lua|onelua)\.c$/ } glob "lib/lua/src/*.c"'
|
||||||
/LUA_SRC
|
|
||||||
:r!ls lib/lua/src | grep '\.c' | grep -Ev 'lua\.c|onelua\.c' | sed 's#^#src/#'
|
|
||||||
|
|
||||||
#]====]
|
#]====]
|
||||||
set(LUA_SRC
|
set(LUA_SRC
|
||||||
"src/lapi.c"
|
src/lapi.c
|
||||||
"src/lauxlib.c"
|
src/lauxlib.c
|
||||||
"src/lbaselib.c"
|
src/lbaselib.c
|
||||||
"src/lcode.c"
|
src/lcode.c
|
||||||
"src/lcorolib.c"
|
src/lcorolib.c
|
||||||
"src/lctype.c"
|
src/lctype.c
|
||||||
"src/ldblib.c"
|
src/ldblib.c
|
||||||
"src/ldebug.c"
|
src/ldebug.c
|
||||||
"src/ldo.c"
|
src/ldo.c
|
||||||
"src/ldump.c"
|
src/ldump.c
|
||||||
"src/lfunc.c"
|
src/lfunc.c
|
||||||
"src/lgc.c"
|
src/lgc.c
|
||||||
"src/linit.c"
|
src/linit.c
|
||||||
"src/liolib.c"
|
src/liolib.c
|
||||||
"src/llex.c"
|
src/llex.c
|
||||||
"src/lmathlib.c"
|
src/lmathlib.c
|
||||||
"src/lmem.c"
|
src/lmem.c
|
||||||
"src/loadlib.c"
|
src/loadlib.c
|
||||||
"src/lobject.c"
|
src/lobject.c
|
||||||
"src/lopcodes.c"
|
src/lopcodes.c
|
||||||
"src/loslib.c"
|
src/loslib.c
|
||||||
"src/lparser.c"
|
src/lparser.c
|
||||||
"src/lstate.c"
|
src/lstate.c
|
||||||
"src/lstring.c"
|
src/lstring.c
|
||||||
"src/lstrlib.c"
|
src/lstrlib.c
|
||||||
"src/ltable.c"
|
src/ltable.c
|
||||||
"src/ltablib.c"
|
src/ltablib.c
|
||||||
"src/ltests.c"
|
src/ltests.c
|
||||||
"src/ltm.c"
|
src/ltm.c
|
||||||
"src/lua.c"
|
src/lundump.c
|
||||||
"src/lundump.c"
|
src/lutf8lib.c
|
||||||
"src/lutf8lib.c"
|
src/lvm.c
|
||||||
"src/lvm.c"
|
src/lzio.c
|
||||||
"src/lzio.c"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(lua STATIC ${LUA_SRC})
|
add_library(lua STATIC ${LUA_SRC})
|
||||||
|
@ -50,4 +46,14 @@ target_include_directories(lua
|
||||||
PUBLIC
|
PUBLIC
|
||||||
${LUA_INCLUDE_DIRS}
|
${LUA_INCLUDE_DIRS}
|
||||||
)
|
)
|
||||||
set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE C)
|
set_target_properties(${liblua} PROPERTIES
|
||||||
|
LANGUAGE CXX
|
||||||
|
LINKER_LANGUAGE CXX
|
||||||
|
CXX_STANDARD 98
|
||||||
|
CXX_EXTENSIONS TRUE
|
||||||
|
)
|
||||||
|
target_compile_options(lua PRIVATE
|
||||||
|
-w # this makes clang shut up about c-as-c++
|
||||||
|
$<$<AND:$<BOOL:${MSVC}>,$<CXX_COMPILER_ID:Clang>>:/EHsc> # enable exceptions in clang-cl
|
||||||
|
)
|
||||||
|
set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE CXX)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60
|
Subproject commit 1ab3208a1fceb12fca8f24ba57d6e13c5bff15e3
|
1
lib/sol2
Submodule
1
lib/sol2
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 2b0d2fe8ba0074e16b499940c4f3126b9c7d3471
|
|
@ -196,7 +196,7 @@ def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper):
|
||||||
if not comments[0].startswith("@"):
|
if not comments[0].startswith("@"):
|
||||||
out.write(f"--- {comments[0]}\n---\n")
|
out.write(f"--- {comments[0]}\n---\n")
|
||||||
comments = comments[1:]
|
comments = comments[1:]
|
||||||
params = []
|
params: list[str] = []
|
||||||
for comment in comments[:-1]:
|
for comment in comments[:-1]:
|
||||||
if not comment.startswith("@lua"):
|
if not comment.startswith("@lua"):
|
||||||
panic(path, line, f"Invalid function specification - got '{comment}'")
|
panic(path, line, f"Invalid function specification - got '{comment}'")
|
||||||
|
@ -209,7 +209,7 @@ def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper):
|
||||||
panic(path, line, f"Invalid function exposure - got '{comments[-1]}'")
|
panic(path, line, f"Invalid function exposure - got '{comments[-1]}'")
|
||||||
name = comments[-1].split(" ", 1)[1]
|
name = comments[-1].split(" ", 1)[1]
|
||||||
printmsg(path, line, f"function {name}")
|
printmsg(path, line, f"function {name}")
|
||||||
lua_params = ", ".join(params)
|
lua_params = ", ".join(p.removesuffix("?") for p in params)
|
||||||
out.write(f"function {name}({lua_params}) end\n\n")
|
out.write(f"function {name}({lua_params}) end\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
@ -242,13 +242,21 @@ def read_file(path: Path, out: TextIOWrapper):
|
||||||
)
|
)
|
||||||
name = header[0].split(" ", 1)[1]
|
name = header[0].split(" ", 1)[1]
|
||||||
printmsg(path, reader.line_no(), f"enum {name}")
|
printmsg(path, reader.line_no(), f"enum {name}")
|
||||||
out.write(f"---@alias {name} integer\n")
|
variants = reader.read_enum_variants()
|
||||||
|
|
||||||
|
vtypes = []
|
||||||
|
for variant in variants:
|
||||||
|
vtype = f'{name}.{variant}'
|
||||||
|
vtypes.append(vtype)
|
||||||
|
out.write(f'---@alias {vtype} "{vtype}"\n')
|
||||||
|
|
||||||
|
out.write(f"---@alias {name} {'|'.join(vtypes)}\n")
|
||||||
if header_comment:
|
if header_comment:
|
||||||
out.write(f"--- {header_comment}\n")
|
out.write(f"--- {header_comment}\n")
|
||||||
out.write("---@type { ")
|
out.write("---@type { ")
|
||||||
out.write(
|
out.write(
|
||||||
", ".join(
|
", ".join(
|
||||||
[f"{variant}: {name}" for variant in reader.read_enum_variants()]
|
[f"{variant}: {typ}" for variant, typ in zip(variants,vtypes)]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
out.write(" }\n")
|
out.write(" }\n")
|
||||||
|
|
|
@ -225,24 +225,28 @@ set(SOURCE_FILES
|
||||||
controllers/pings/MutedChannelModel.cpp
|
controllers/pings/MutedChannelModel.cpp
|
||||||
controllers/pings/MutedChannelModel.hpp
|
controllers/pings/MutedChannelModel.hpp
|
||||||
|
|
||||||
|
|
||||||
controllers/plugins/api/ChannelRef.cpp
|
controllers/plugins/api/ChannelRef.cpp
|
||||||
controllers/plugins/api/ChannelRef.hpp
|
controllers/plugins/api/ChannelRef.hpp
|
||||||
controllers/plugins/api/IOWrapper.cpp
|
controllers/plugins/api/EventType.hpp
|
||||||
controllers/plugins/api/IOWrapper.hpp
|
|
||||||
controllers/plugins/api/HTTPRequest.cpp
|
controllers/plugins/api/HTTPRequest.cpp
|
||||||
controllers/plugins/api/HTTPRequest.hpp
|
controllers/plugins/api/HTTPRequest.hpp
|
||||||
controllers/plugins/api/HTTPResponse.cpp
|
controllers/plugins/api/HTTPResponse.cpp
|
||||||
controllers/plugins/api/HTTPResponse.hpp
|
controllers/plugins/api/HTTPResponse.hpp
|
||||||
|
controllers/plugins/api/IOWrapper.cpp
|
||||||
|
controllers/plugins/api/IOWrapper.hpp
|
||||||
controllers/plugins/LuaAPI.cpp
|
controllers/plugins/LuaAPI.cpp
|
||||||
controllers/plugins/LuaAPI.hpp
|
controllers/plugins/LuaAPI.hpp
|
||||||
controllers/plugins/PluginPermission.cpp
|
|
||||||
controllers/plugins/PluginPermission.hpp
|
|
||||||
controllers/plugins/Plugin.cpp
|
|
||||||
controllers/plugins/Plugin.hpp
|
|
||||||
controllers/plugins/PluginController.hpp
|
|
||||||
controllers/plugins/PluginController.cpp
|
|
||||||
controllers/plugins/LuaUtilities.cpp
|
controllers/plugins/LuaUtilities.cpp
|
||||||
controllers/plugins/LuaUtilities.hpp
|
controllers/plugins/LuaUtilities.hpp
|
||||||
|
controllers/plugins/PluginController.cpp
|
||||||
|
controllers/plugins/PluginController.hpp
|
||||||
|
controllers/plugins/Plugin.cpp
|
||||||
|
controllers/plugins/Plugin.hpp
|
||||||
|
controllers/plugins/PluginPermission.cpp
|
||||||
|
controllers/plugins/PluginPermission.hpp
|
||||||
|
controllers/plugins/SolTypes.cpp
|
||||||
|
controllers/plugins/SolTypes.hpp
|
||||||
|
|
||||||
controllers/sound/ISoundController.hpp
|
controllers/sound/ISoundController.hpp
|
||||||
controllers/sound/MiniaudioBackend.cpp
|
controllers/sound/MiniaudioBackend.cpp
|
||||||
|
@ -791,7 +795,7 @@ target_link_libraries(${LIBRARY_PROJECT}
|
||||||
$<$<BOOL:${WIN32}>:Wtsapi32>
|
$<$<BOOL:${WIN32}>:Wtsapi32>
|
||||||
)
|
)
|
||||||
if (CHATTERINO_PLUGINS)
|
if (CHATTERINO_PLUGINS)
|
||||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua)
|
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (BUILD_WITH_QTKEYCHAIN)
|
if (BUILD_WITH_QTKEYCHAIN)
|
||||||
|
|
|
@ -129,6 +129,10 @@
|
||||||
# include <unordered_set>
|
# include <unordered_set>
|
||||||
# include <vector>
|
# include <vector>
|
||||||
|
|
||||||
|
# ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
# endif
|
||||||
|
|
||||||
# ifndef UNUSED
|
# ifndef UNUSED
|
||||||
# define UNUSED(x) (void)(x)
|
# define UNUSED(x) (void)(x)
|
||||||
# endif
|
# endif
|
||||||
|
|
|
@ -165,30 +165,3 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
||||||
template <>
|
|
||||||
constexpr magic_enum::customize::customize_t
|
|
||||||
magic_enum::customize::enum_name<chatterino::Channel::Type>(
|
|
||||||
chatterino::Channel::Type value) noexcept
|
|
||||||
{
|
|
||||||
using Type = chatterino::Channel::Type;
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case Type::Twitch:
|
|
||||||
return "twitch";
|
|
||||||
case Type::TwitchWhispers:
|
|
||||||
return "whispers";
|
|
||||||
case Type::TwitchWatching:
|
|
||||||
return "watching";
|
|
||||||
case Type::TwitchMentions:
|
|
||||||
return "mentions";
|
|
||||||
case Type::TwitchLive:
|
|
||||||
return "live";
|
|
||||||
case Type::TwitchAutomod:
|
|
||||||
return "automod";
|
|
||||||
case Type::Misc:
|
|
||||||
return "misc";
|
|
||||||
default:
|
|
||||||
return default_tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,34 +3,45 @@
|
||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/commands/CommandController.hpp"
|
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
# include "messages/MessageBuilder.hpp"
|
# include "controllers/plugins/SolTypes.hpp" // for lua operations on QString{,List} for CompletionList
|
||||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
# include <lualib.h>
|
# include <lualib.h>
|
||||||
}
|
|
||||||
# include <QFileInfo>
|
# include <QFileInfo>
|
||||||
|
# include <QList>
|
||||||
# include <QLoggingCategory>
|
# include <QLoggingCategory>
|
||||||
# include <QTextCodec>
|
# include <QTextCodec>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
# include <sol/protected_function_result.hpp>
|
||||||
|
# include <sol/reference.hpp>
|
||||||
|
# include <sol/stack.hpp>
|
||||||
|
# include <sol/state_view.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
# include <sol/variadic_args.hpp>
|
||||||
|
# include <sol/variadic_results.hpp>
|
||||||
|
|
||||||
|
# include <stdexcept>
|
||||||
|
# include <string>
|
||||||
|
# include <utility>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using namespace chatterino;
|
using namespace chatterino;
|
||||||
|
|
||||||
void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc)
|
void logHelper(lua_State *L, Plugin *pl, QDebug stream,
|
||||||
|
const sol::variadic_args &args)
|
||||||
{
|
{
|
||||||
stream.noquote();
|
stream.noquote();
|
||||||
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
||||||
for (int i = 1; i <= argc; i++)
|
for (const auto &arg : args)
|
||||||
{
|
{
|
||||||
stream << lua::toString(L, i);
|
stream << lua::toString(L, arg.stack_index());
|
||||||
|
// Remove this from our stack
|
||||||
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
lua_pop(L, argc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
||||||
|
@ -63,195 +74,92 @@ QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
||||||
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
|
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
|
|
||||||
int c2_register_command(lua_State *L)
|
CompletionList::CompletionList(const sol::table &table)
|
||||||
|
: values(table.get<QStringList>("values"))
|
||||||
|
, hideOthers(table["hide_others"])
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "internal error: no plugin");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString name;
|
|
||||||
if (!lua::peek(L, &name, 1))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get command name (1st arg of register_command, "
|
|
||||||
"expected a string)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (lua_isnoneornil(L, 2))
|
|
||||||
{
|
|
||||||
luaL_error(L, "missing argument for register_command: function "
|
|
||||||
"\"pointer\"");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto callbackSavedName = QString("c2commandcb-%1").arg(name);
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str());
|
|
||||||
auto ok = pl->registerCommand(name, callbackSavedName);
|
|
||||||
|
|
||||||
// delete both name and callback
|
|
||||||
lua_pop(L, 2);
|
|
||||||
|
|
||||||
lua::push(L, ok);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int c2_register_callback(lua_State *L)
|
sol::table toTable(lua_State *L, const CompletionEvent &ev)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
return sol::state_view(L).create_table_with(
|
||||||
if (pl == nullptr)
|
"query", ev.query, //
|
||||||
{
|
"full_text_content", ev.full_text_content, //
|
||||||
luaL_error(L, "internal error: no plugin");
|
"cursor_position", ev.cursor_position, //
|
||||||
return 0;
|
"is_first_word", ev.is_first_word //
|
||||||
}
|
);
|
||||||
EventType evtType{};
|
|
||||||
if (!lua::peek(L, &evtType, 1))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get event name (1st arg of register_callback, "
|
|
||||||
"expected a string)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (lua_isnoneornil(L, 2))
|
|
||||||
{
|
|
||||||
luaL_error(L, "missing argument for register_callback: function "
|
|
||||||
"\"pointer\"");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto typeName = magic_enum::enum_name(evtType);
|
|
||||||
std::string callbackSavedName;
|
|
||||||
callbackSavedName.reserve(5 + typeName.size());
|
|
||||||
callbackSavedName += "c2cb-";
|
|
||||||
callbackSavedName += typeName;
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str());
|
|
||||||
|
|
||||||
lua_pop(L, 2);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int c2_log(lua_State *L)
|
void c2_register_callback(ThisPluginState L, EventType evtType,
|
||||||
|
sol::protected_function callback)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
L.plugin()->callbacks[evtType] = std::move(callback);
|
||||||
if (pl == nullptr)
|
}
|
||||||
|
|
||||||
|
void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args)
|
||||||
|
{
|
||||||
|
lua::StackGuard guard(L);
|
||||||
{
|
{
|
||||||
luaL_error(L, "c2_log: internal error: no plugin?");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
auto logc = lua_gettop(L) - 1;
|
|
||||||
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
|
||||||
LogLevel lvl{};
|
|
||||||
if (!lua::pop(L, &lvl, 1))
|
|
||||||
{
|
|
||||||
luaL_error(L, "Invalid log level, use one from c2.LogLevel.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
QDebug stream = qdebugStreamForLogLevel(lvl);
|
QDebug stream = qdebugStreamForLogLevel(lvl);
|
||||||
logHelper(L, pl, stream, logc);
|
logHelper(L, L.plugin(), stream, args);
|
||||||
return 0;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int c2_later(lua_State *L)
|
void c2_later(ThisPluginState L, sol::protected_function callback, int time)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
if (time <= 0)
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
{
|
||||||
return luaL_error(L, "c2.later: internal error: no plugin?");
|
throw std::runtime_error(
|
||||||
}
|
"c2.later time must be strictly greater than zero.");
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "c2.later expects two arguments (a callback that takes no "
|
|
||||||
"arguments and returns nothing and a number the time in "
|
|
||||||
"milliseconds to wait)\n");
|
|
||||||
}
|
|
||||||
int time{};
|
|
||||||
if (!lua::pop(L, &time))
|
|
||||||
{
|
|
||||||
return luaL_error(L, "cannot get time (2nd arg of c2.later, "
|
|
||||||
"expected a number)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lua_isfunction(L, lua_gettop(L)))
|
|
||||||
{
|
|
||||||
return luaL_error(L, "cannot get callback (1st arg of c2.later, "
|
|
||||||
"expected a function)");
|
|
||||||
}
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
|
|
||||||
auto *timer = new QTimer();
|
auto *timer = new QTimer();
|
||||||
timer->setInterval(time);
|
timer->setInterval(time);
|
||||||
auto id = pl->addTimeout(timer);
|
timer->setSingleShot(true);
|
||||||
|
auto id = L.plugin()->addTimeout(timer);
|
||||||
auto name = QString("timeout_%1").arg(id);
|
auto name = QString("timeout_%1").arg(id);
|
||||||
auto *coro = lua_newthread(L);
|
|
||||||
|
|
||||||
QObject::connect(timer, &QTimer::timeout, [pl, coro, name, timer]() {
|
sol::state_view main = sol::main_thread(L);
|
||||||
|
|
||||||
|
sol::thread thread = sol::thread::create(main);
|
||||||
|
sol::protected_function cb(thread.state(), callback);
|
||||||
|
main.registry()[name.toStdString()] = thread;
|
||||||
|
|
||||||
|
QObject::connect(
|
||||||
|
timer, &QTimer::timeout,
|
||||||
|
[pl = L.plugin(), name, timer, cb, thread, main]() {
|
||||||
timer->deleteLater();
|
timer->deleteLater();
|
||||||
pl->removeTimeout(timer);
|
pl->removeTimeout(timer);
|
||||||
int nres{};
|
sol::protected_function_result res = cb();
|
||||||
lua_resume(coro, nullptr, 0, &nres);
|
|
||||||
|
|
||||||
lua_pushnil(coro);
|
if (res.return_count() != 0)
|
||||||
lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
|
||||||
if (lua_gettop(coro) != 0)
|
|
||||||
{
|
{
|
||||||
stackDump(coro,
|
stackDump(thread.lua_state(),
|
||||||
pl->id +
|
pl->id +
|
||||||
": timer returned a value, this shouldn't happen "
|
": timer returned a value, this shouldn't happen "
|
||||||
"and is probably a plugin bug");
|
"and is probably a plugin bug");
|
||||||
}
|
}
|
||||||
|
main.registry()[name.toStdString()] = sol::nil;
|
||||||
});
|
});
|
||||||
stackDump(L, "before setfield");
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
|
||||||
lua_xmove(L, coro, 1); // move function to thread
|
|
||||||
timer->start();
|
timer->start();
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int g_load(lua_State *L)
|
// TODO: Add tests for this once we run tests in debug mode
|
||||||
|
sol::variadic_results g_load(ThisPluginState s, sol::object data)
|
||||||
{
|
{
|
||||||
# ifdef NDEBUG
|
# ifdef NDEBUG
|
||||||
luaL_error(L, "load() is only usable in debug mode");
|
(void)data;
|
||||||
return 0;
|
(void)s;
|
||||||
|
throw std::runtime_error("load() is only usable in debug mode");
|
||||||
# else
|
# else
|
||||||
auto countArgs = lua_gettop(L);
|
|
||||||
QByteArray data;
|
|
||||||
if (lua::peek(L, &data, 1))
|
|
||||||
{
|
|
||||||
auto *utf8 = QTextCodec::codecForName("UTF-8");
|
|
||||||
QTextCodec::ConverterState state;
|
|
||||||
utf8->toUnicode(data.constData(), data.size(), &state);
|
|
||||||
if (state.invalidChars != 0)
|
|
||||||
{
|
|
||||||
luaL_error(L, "invalid utf-8 in load() is not allowed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
luaL_error(L, "using reader function in load() is not allowed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < countArgs; i++)
|
// If you're modifying this PLEASE verify it works, Sol is very annoying about serialization
|
||||||
{
|
// - Mm2PL
|
||||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
sol::state_view lua(s);
|
||||||
}
|
auto load = lua.registry()["real_load"];
|
||||||
|
sol::protected_function_result ret = load(data, "=(load)", "t");
|
||||||
// fetch load and call it
|
return ret;
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "real_load");
|
|
||||||
|
|
||||||
for (int i = 0; i < countArgs; i++)
|
|
||||||
{
|
|
||||||
lua_geti(L, LUA_REGISTRYINDEX, i);
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
lua_call(L, countArgs, LUA_MULTRET);
|
|
||||||
|
|
||||||
return lua_gettop(L);
|
|
||||||
# endif
|
# endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,7 +228,7 @@ int searcherAbsolute(lua_State *L)
|
||||||
int searcherRelative(lua_State *L)
|
int searcherRelative(lua_State *L)
|
||||||
{
|
{
|
||||||
lua_Debug dbg;
|
lua_Debug dbg;
|
||||||
lua_getstack(L, 1, &dbg);
|
lua_getstack(L, 2, &dbg);
|
||||||
lua_getinfo(L, "S", &dbg);
|
lua_getinfo(L, "S", &dbg);
|
||||||
auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen);
|
auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen);
|
||||||
if (currentFile.startsWith("@"))
|
if (currentFile.startsWith("@"))
|
||||||
|
@ -346,22 +254,14 @@ int searcherRelative(lua_State *L)
|
||||||
return loadfile(L, filename);
|
return loadfile(L, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
int g_print(lua_State *L)
|
void g_print(ThisPluginState L, sol::variadic_args args)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
|
||||||
if (pl == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "c2_print: internal error: no plugin?");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
auto argc = lua_gettop(L);
|
|
||||||
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
||||||
auto stream =
|
auto stream =
|
||||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
||||||
.debug());
|
.debug());
|
||||||
logHelper(L, pl, stream, argc);
|
logHelper(L, L.plugin(), stream, args);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||||
|
# include "controllers/plugins/Plugin.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
# include <QList>
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
|
|
||||||
# include <QString>
|
# include <QString>
|
||||||
|
# include <sol/table.hpp>
|
||||||
|
|
||||||
# include <cassert>
|
# include <cassert>
|
||||||
# include <memory>
|
# include <memory>
|
||||||
# include <vector>
|
|
||||||
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
|
@ -30,11 +30,8 @@ namespace chatterino::lua::api {
|
||||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exposeenum c2.EventType
|
* @includefile controllers/plugins/api/EventType.hpp
|
||||||
*/
|
*/
|
||||||
enum class EventType {
|
|
||||||
CompletionRequested,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@class CommandContext
|
* @lua@class CommandContext
|
||||||
|
@ -46,10 +43,12 @@ enum class EventType {
|
||||||
* @lua@class CompletionList
|
* @lua@class CompletionList
|
||||||
*/
|
*/
|
||||||
struct CompletionList {
|
struct CompletionList {
|
||||||
|
CompletionList(const sol::table &);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@field values string[] The completions
|
* @lua@field values string[] The completions
|
||||||
*/
|
*/
|
||||||
std::vector<QString> values{};
|
QStringList values;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
* @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
|
||||||
|
@ -79,6 +78,8 @@ struct CompletionEvent {
|
||||||
bool is_first_word{};
|
bool is_first_word{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sol::table toTable(lua_State *L, const CompletionEvent &ev);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @includefile common/Channel.hpp
|
* @includefile common/Channel.hpp
|
||||||
* @includefile controllers/plugins/api/ChannelRef.hpp
|
* @includefile controllers/plugins/api/ChannelRef.hpp
|
||||||
|
@ -95,16 +96,16 @@ struct CompletionEvent {
|
||||||
* @lua@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
|
* @lua@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
|
||||||
* @exposed c2.register_command
|
* @exposed c2.register_command
|
||||||
*/
|
*/
|
||||||
int c2_register_command(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a callback to be invoked when completions for a term are requested.
|
* Registers a callback to be invoked when completions for a term are requested.
|
||||||
*
|
*
|
||||||
* @lua@param type "CompletionRequested"
|
* @lua@param type c2.EventType.CompletionRequested
|
||||||
* @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
* @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
||||||
* @exposed c2.register_callback
|
* @exposed c2.register_callback
|
||||||
*/
|
*/
|
||||||
int c2_register_callback(lua_State *L);
|
void c2_register_callback(ThisPluginState L, EventType evtType,
|
||||||
|
sol::protected_function callback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a message to the Chatterino log.
|
* Writes a message to the Chatterino log.
|
||||||
|
@ -113,7 +114,7 @@ int c2_register_callback(lua_State *L);
|
||||||
* @lua@param ... any Values to log. Should be convertible to a string with `tostring()`.
|
* @lua@param ... any Values to log. Should be convertible to a string with `tostring()`.
|
||||||
* @exposed c2.log
|
* @exposed c2.log
|
||||||
*/
|
*/
|
||||||
int c2_log(lua_State *L);
|
void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls callback around msec milliseconds later. Does not freeze Chatterino.
|
* Calls callback around msec milliseconds later. Does not freeze Chatterino.
|
||||||
|
@ -122,11 +123,11 @@ int c2_log(lua_State *L);
|
||||||
* @lua@param msec number How long to wait.
|
* @lua@param msec number How long to wait.
|
||||||
* @exposed c2.later
|
* @exposed c2.later
|
||||||
*/
|
*/
|
||||||
int c2_later(lua_State *L);
|
void c2_later(ThisPluginState L, sol::protected_function callback, int time);
|
||||||
|
|
||||||
// These ones are global
|
// These ones are global
|
||||||
int g_load(lua_State *L);
|
sol::variadic_results g_load(ThisPluginState s, sol::object data);
|
||||||
int g_print(lua_State *L);
|
void g_print(ThisPluginState L, sol::variadic_args args);
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
// This is for require() exposed as an element of package.searchers
|
// This is for require() exposed as an element of package.searchers
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
|
||||||
# include "common/Channel.hpp"
|
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/commands/CommandContext.hpp"
|
|
||||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
|
||||||
|
|
||||||
# include <climits>
|
# include <climits>
|
||||||
# include <cstdlib>
|
# include <cstdlib>
|
||||||
|
@ -79,9 +73,6 @@ QString humanErrorText(lua_State *L, int errCode)
|
||||||
case LUA_ERRFILE:
|
case LUA_ERRFILE:
|
||||||
errName = "(file error)";
|
errName = "(file error)";
|
||||||
break;
|
break;
|
||||||
case ERROR_BAD_PEEK:
|
|
||||||
errName = "(unable to convert value to c++)";
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
errName = "(unknown error type)";
|
errName = "(unknown error type)";
|
||||||
}
|
}
|
||||||
|
@ -93,18 +84,6 @@ QString humanErrorText(lua_State *L, int errCode)
|
||||||
return errName;
|
return errName;
|
||||||
}
|
}
|
||||||
|
|
||||||
StackIdx pushEmptyArray(lua_State *L, int countArray)
|
|
||||||
{
|
|
||||||
lua_createtable(L, countArray, 0);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx pushEmptyTable(lua_State *L, int countProperties)
|
|
||||||
{
|
|
||||||
lua_createtable(L, 0, countProperties);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const QString &str)
|
StackIdx push(lua_State *L, const QString &str)
|
||||||
{
|
{
|
||||||
return lua::push(L, str.toStdString());
|
return lua::push(L, str.toStdString());
|
||||||
|
@ -116,82 +95,6 @@ StackIdx push(lua_State *L, const std::string &str)
|
||||||
return lua_gettop(L);
|
return lua_gettop(L);
|
||||||
}
|
}
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const CommandContext &ctx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L, 1);
|
|
||||||
auto outIdx = pushEmptyTable(L, 2);
|
|
||||||
|
|
||||||
push(L, ctx.words);
|
|
||||||
lua_setfield(L, outIdx, "words");
|
|
||||||
|
|
||||||
push(L, ctx.channel);
|
|
||||||
lua_setfield(L, outIdx, "channel");
|
|
||||||
|
|
||||||
return outIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const bool &b)
|
|
||||||
{
|
|
||||||
lua_pushboolean(L, int(b));
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const int &b)
|
|
||||||
{
|
|
||||||
lua_pushinteger(L, b);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const api::CompletionEvent &ev)
|
|
||||||
{
|
|
||||||
auto idx = pushEmptyTable(L, 4);
|
|
||||||
# define PUSH(field) \
|
|
||||||
lua::push(L, ev.field); \
|
|
||||||
lua_setfield(L, idx, #field)
|
|
||||||
PUSH(query);
|
|
||||||
PUSH(full_text_content);
|
|
||||||
PUSH(cursor_position);
|
|
||||||
PUSH(is_first_word);
|
|
||||||
# undef PUSH
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, int *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
if (lua_isnumber(L, idx) == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out = lua_tointeger(L, idx);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, bool *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
if (!lua_isboolean(L, idx))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out = bool(lua_toboolean(L, idx));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, double *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
int ok{0};
|
|
||||||
auto v = lua_tonumberx(L, idx, &ok);
|
|
||||||
if (ok != 0)
|
|
||||||
{
|
|
||||||
*out = v;
|
|
||||||
}
|
|
||||||
return ok != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, QString *out, StackIdx idx)
|
bool peek(lua_State *L, QString *out, StackIdx idx)
|
||||||
{
|
{
|
||||||
StackGuard guard(L);
|
StackGuard guard(L);
|
||||||
|
@ -209,57 +112,6 @@ bool peek(lua_State *L, QString *out, StackIdx idx)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
size_t len{0};
|
|
||||||
const char *str = lua_tolstring(L, idx, &len);
|
|
||||||
if (str == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (len >= INT_MAX)
|
|
||||||
{
|
|
||||||
assert(false && "string longer than INT_MAX, shit's fucked, yo");
|
|
||||||
}
|
|
||||||
*out = QByteArray(str, int(len));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, std::string *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
size_t len{0};
|
|
||||||
const char *str = lua_tolstring(L, idx, &len);
|
|
||||||
if (str == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (len >= INT_MAX)
|
|
||||||
{
|
|
||||||
assert(false && "string longer than INT_MAX, shit's fucked, yo");
|
|
||||||
}
|
|
||||||
*out = std::string(str, len);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool peek(lua_State *L, api::CompletionList *out, StackIdx idx)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
int typ = lua_getfield(L, idx, "values");
|
|
||||||
if (typ != LUA_TTABLE)
|
|
||||||
{
|
|
||||||
lua_pop(L, 1);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!lua::pop(L, &out->values, -1))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
lua_getfield(L, idx, "hide_others");
|
|
||||||
return lua::pop(L, &out->hideOthers);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString toString(lua_State *L, StackIdx idx)
|
QString toString(lua_State *L, StackIdx idx)
|
||||||
{
|
{
|
||||||
size_t len{};
|
size_t len{};
|
||||||
|
|
|
@ -2,37 +2,20 @@
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
# include "common/QLogging.hpp"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
# include <lualib.h>
|
# include <lualib.h>
|
||||||
}
|
|
||||||
# include <magic_enum/magic_enum.hpp>
|
# include <magic_enum/magic_enum.hpp>
|
||||||
# include <QList>
|
# include <QList>
|
||||||
|
# include <sol/state_view.hpp>
|
||||||
|
|
||||||
# include <cassert>
|
# include <cassert>
|
||||||
# include <optional>
|
|
||||||
# include <string>
|
# include <string>
|
||||||
# include <string_view>
|
# include <string_view>
|
||||||
# include <type_traits>
|
# include <type_traits>
|
||||||
# include <variant>
|
|
||||||
# include <vector>
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
class QJsonObject;
|
|
||||||
namespace chatterino {
|
|
||||||
struct CommandContext;
|
|
||||||
} // namespace chatterino
|
|
||||||
|
|
||||||
namespace chatterino::lua {
|
namespace chatterino::lua {
|
||||||
|
|
||||||
namespace api {
|
|
||||||
struct CompletionList;
|
|
||||||
struct CompletionEvent;
|
|
||||||
} // namespace api
|
|
||||||
|
|
||||||
constexpr int ERROR_BAD_PEEK = LUA_OK - 1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Dumps the Lua stack into qCDebug(chatterinoLua)
|
* @brief Dumps the Lua stack into qCDebug(chatterinoLua)
|
||||||
*
|
*
|
||||||
|
@ -40,6 +23,9 @@ constexpr int ERROR_BAD_PEEK = LUA_OK - 1;
|
||||||
*/
|
*/
|
||||||
void stackDump(lua_State *L, const QString &tag);
|
void stackDump(lua_State *L, const QString &tag);
|
||||||
|
|
||||||
|
// This is for calling stackDump out of gdb as it's not easy to create a QString there
|
||||||
|
const QString GDB_DUMMY = "GDB_DUMMY";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Converts a lua error code and potentially string on top of the stack into a human readable message
|
* @brief Converts a lua error code and potentially string on top of the stack into a human readable message
|
||||||
*/
|
*/
|
||||||
|
@ -50,33 +36,11 @@ QString humanErrorText(lua_State *L, int errCode);
|
||||||
*/
|
*/
|
||||||
using StackIdx = int;
|
using StackIdx = int;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Creates a table with countArray array properties on the Lua stack
|
|
||||||
* @return stack index of the newly created table
|
|
||||||
*/
|
|
||||||
StackIdx pushEmptyArray(lua_State *L, int countArray);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Creates a table with countProperties named properties on the Lua stack
|
|
||||||
* @return stack index of the newly created table
|
|
||||||
*/
|
|
||||||
StackIdx pushEmptyTable(lua_State *L, int countProperties);
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const CommandContext &ctx);
|
|
||||||
StackIdx push(lua_State *L, const QString &str);
|
StackIdx push(lua_State *L, const QString &str);
|
||||||
StackIdx push(lua_State *L, const std::string &str);
|
StackIdx push(lua_State *L, const std::string &str);
|
||||||
StackIdx push(lua_State *L, const bool &b);
|
|
||||||
StackIdx push(lua_State *L, const int &b);
|
|
||||||
StackIdx push(lua_State *L, const api::CompletionEvent &ev);
|
|
||||||
|
|
||||||
// returns OK?
|
// returns OK?
|
||||||
bool peek(lua_State *L, int *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, bool *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, double *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
||||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, std::string *out, StackIdx idx = -1);
|
|
||||||
bool peek(lua_State *L, api::CompletionList *out, StackIdx idx = -1);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Converts Lua object at stack index idx to a string.
|
* @brief Converts Lua object at stack index idx to a string.
|
||||||
|
@ -140,246 +104,29 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// TEMPLATES
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
StackIdx push(lua_State *L, std::optional<T> val)
|
|
||||||
{
|
|
||||||
if (val.has_value())
|
|
||||||
{
|
|
||||||
return lua::push(L, *val);
|
|
||||||
}
|
|
||||||
lua_pushnil(L);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
bool peek(lua_State *L, std::optional<T> *out, StackIdx idx = -1)
|
|
||||||
{
|
|
||||||
if (lua_isnil(L, idx))
|
|
||||||
{
|
|
||||||
*out = std::nullopt;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out = T();
|
|
||||||
return peek(L, out->operator->(), idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
bool peek(lua_State *L, std::vector<T> *vec, StackIdx idx = -1)
|
|
||||||
{
|
|
||||||
StackGuard guard(L);
|
|
||||||
|
|
||||||
if (!lua_istable(L, idx))
|
|
||||||
{
|
|
||||||
lua::stackDump(L, "!table");
|
|
||||||
qCDebug(chatterinoLua)
|
|
||||||
<< "value is not a table, type is" << lua_type(L, idx);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
auto len = lua_rawlen(L, idx);
|
|
||||||
if (len == 0)
|
|
||||||
{
|
|
||||||
qCDebug(chatterinoLua) << "value has 0 length";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (len > 1'000'000)
|
|
||||||
{
|
|
||||||
qCDebug(chatterinoLua) << "value is too long";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// count like lua
|
|
||||||
for (int i = 1; i <= len; i++)
|
|
||||||
{
|
|
||||||
lua_geti(L, idx, i);
|
|
||||||
std::optional<T> obj;
|
|
||||||
if (!lua::peek(L, &obj))
|
|
||||||
{
|
|
||||||
//lua_seti(L, LUA_REGISTRYINDEX, 1); // lazy
|
|
||||||
qCDebug(chatterinoLua)
|
|
||||||
<< "Failed to convert lua object into c++: at array index " << i
|
|
||||||
<< ":";
|
|
||||||
stackDump(L, "bad conversion into string");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
lua_pop(L, 1);
|
|
||||||
vec->push_back(obj.value());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts object at stack index idx to enum given by template parameter T
|
|
||||||
*/
|
|
||||||
template <typename T,
|
|
||||||
typename std::enable_if<std::is_enum_v<T>, bool>::type = true>
|
|
||||||
bool peek(lua_State *L, T *out, StackIdx idx = -1)
|
|
||||||
{
|
|
||||||
std::string tmp;
|
|
||||||
if (!lua::peek(L, &tmp, idx))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::optional<T> opt = magic_enum::enum_cast<T>(tmp);
|
|
||||||
if (opt.has_value())
|
|
||||||
{
|
|
||||||
*out = opt.value();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a vector<T> to Lua and pushes it onto the stack.
|
|
||||||
*
|
|
||||||
* Needs StackIdx push(lua_State*, T); to work.
|
|
||||||
*
|
|
||||||
* @return Stack index of newly created table.
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
StackIdx push(lua_State *L, std::vector<T> vec)
|
|
||||||
{
|
|
||||||
auto out = pushEmptyArray(L, vec.size());
|
|
||||||
int i = 1;
|
|
||||||
for (const auto &el : vec)
|
|
||||||
{
|
|
||||||
push(L, el);
|
|
||||||
lua_seti(L, out, i);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a QList<T> to Lua and pushes it onto the stack.
|
|
||||||
*
|
|
||||||
* Needs StackIdx push(lua_State*, T); to work.
|
|
||||||
*
|
|
||||||
* @return Stack index of newly created table.
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
StackIdx push(lua_State *L, QList<T> vec)
|
|
||||||
{
|
|
||||||
auto out = pushEmptyArray(L, vec.size());
|
|
||||||
int i = 1;
|
|
||||||
for (const auto &el : vec)
|
|
||||||
{
|
|
||||||
push(L, el);
|
|
||||||
lua_seti(L, out, i);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts an enum given by T to Lua (into a string) and pushes it onto the stack.
|
|
||||||
*
|
|
||||||
* @return Stack index of newly created string.
|
|
||||||
*/
|
|
||||||
template <typename T, typename std::enable_if_t<std::is_enum_v<T>, bool> = true>
|
|
||||||
StackIdx push(lua_State *L, T inp)
|
|
||||||
{
|
|
||||||
std::string_view name = magic_enum::enum_name<T>(inp);
|
|
||||||
return lua::push(L, std::string(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a Lua object into c++ and removes it from the stack.
|
|
||||||
* If peek fails, the object is still removed from the stack.
|
|
||||||
*
|
|
||||||
* Relies on bool peek(lua_State*, T*, StackIdx) existing.
|
|
||||||
*/
|
|
||||||
template <typename T>
|
|
||||||
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
|
||||||
{
|
|
||||||
StackGuard guard(L, -1);
|
|
||||||
auto ok = peek(L, out, idx);
|
|
||||||
if (idx < 0)
|
|
||||||
{
|
|
||||||
idx = lua_gettop(L) + idx + 1;
|
|
||||||
}
|
|
||||||
lua_remove(L, idx);
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates a table mapping enum names to unique values.
|
* @brief Creates a table mapping enum names to unique values.
|
||||||
*
|
*
|
||||||
* Values in this table may change.
|
* Values in this table may change.
|
||||||
*
|
*
|
||||||
* @returns stack index of newly created table
|
* @returns Sol reference to the table
|
||||||
*/
|
*/
|
||||||
template <typename T>
|
template <typename T>
|
||||||
StackIdx pushEnumTable(lua_State *L)
|
requires std::is_enum_v<T>
|
||||||
|
sol::table createEnumTable(sol::state_view &lua)
|
||||||
{
|
{
|
||||||
// std::array<T, _>
|
constexpr auto values = magic_enum::enum_values<T>();
|
||||||
auto values = magic_enum::enum_values<T>();
|
auto out = lua.create_table(0, values.size());
|
||||||
StackIdx out = lua::pushEmptyTable(L, values.size());
|
|
||||||
for (const T v : values)
|
for (const T v : values)
|
||||||
{
|
{
|
||||||
std::string_view name = magic_enum::enum_name<T>(v);
|
std::string_view name = magic_enum::enum_name<T>(v);
|
||||||
std::string str(name);
|
std::string str(name);
|
||||||
|
|
||||||
lua::push(L, str);
|
out.raw_set(str, v);
|
||||||
lua_setfield(L, out, str.c_str());
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents a Lua function on the stack
|
|
||||||
template <typename ReturnType, typename... Args>
|
|
||||||
class CallbackFunction
|
|
||||||
{
|
|
||||||
StackIdx stackIdx_;
|
|
||||||
lua_State *L;
|
|
||||||
|
|
||||||
public:
|
|
||||||
CallbackFunction(lua_State *L, StackIdx stackIdx)
|
|
||||||
: stackIdx_(stackIdx)
|
|
||||||
, L(L)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// this type owns the stackidx, it must not be trivially copiable
|
|
||||||
CallbackFunction operator=(CallbackFunction &) = delete;
|
|
||||||
CallbackFunction(CallbackFunction &) = delete;
|
|
||||||
|
|
||||||
// Permit only move
|
|
||||||
CallbackFunction &operator=(CallbackFunction &&) = default;
|
|
||||||
CallbackFunction(CallbackFunction &&) = default;
|
|
||||||
|
|
||||||
~CallbackFunction()
|
|
||||||
{
|
|
||||||
lua_remove(L, this->stackIdx_);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::variant<int, ReturnType> operator()(Args... arguments)
|
|
||||||
{
|
|
||||||
lua_pushvalue(this->L, this->stackIdx_);
|
|
||||||
( // apparently this calls lua::push() for every Arg
|
|
||||||
[this, &arguments] {
|
|
||||||
lua::push(this->L, arguments);
|
|
||||||
}(),
|
|
||||||
...);
|
|
||||||
|
|
||||||
int res = lua_pcall(L, sizeof...(Args), 1, 0);
|
|
||||||
if (res != LUA_OK)
|
|
||||||
{
|
|
||||||
qCDebug(chatterinoLua) << "error is: " << res;
|
|
||||||
return {res};
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnType val;
|
|
||||||
if (!lua::pop(L, &val))
|
|
||||||
{
|
|
||||||
return {ERROR_BAD_PEEK};
|
|
||||||
}
|
|
||||||
return {val};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace chatterino::lua
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -7,14 +7,13 @@
|
||||||
# include "controllers/plugins/PluginPermission.hpp"
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
# include "util/QMagicEnum.hpp"
|
# include "util/QMagicEnum.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
|
||||||
# include <magic_enum/magic_enum.hpp>
|
# include <magic_enum/magic_enum.hpp>
|
||||||
# include <QJsonArray>
|
# include <QJsonArray>
|
||||||
# include <QJsonObject>
|
# include <QJsonObject>
|
||||||
# include <QLoggingCategory>
|
# include <QLoggingCategory>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
# include <algorithm>
|
# include <algorithm>
|
||||||
# include <unordered_map>
|
# include <unordered_map>
|
||||||
|
@ -190,7 +189,8 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
bool Plugin::registerCommand(const QString &name,
|
||||||
|
sol::protected_function function)
|
||||||
{
|
{
|
||||||
if (this->ownedCommands.find(name) != this->ownedCommands.end())
|
if (this->ownedCommands.find(name) != this->ownedCommands.end())
|
||||||
{
|
{
|
||||||
|
@ -202,7 +202,7 @@ bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this->ownedCommands.insert({name, functionName});
|
this->ownedCommands.emplace(name, std::move(function));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,14 +223,24 @@ Plugin::~Plugin()
|
||||||
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
||||||
timer->deleteLater();
|
timer->deleteLater();
|
||||||
}
|
}
|
||||||
|
this->httpRequests.clear();
|
||||||
qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size()
|
qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size()
|
||||||
<< "timers for plugin" << this->id
|
<< "timers for plugin" << this->id
|
||||||
<< "while destroying the object";
|
<< "while destroying the object";
|
||||||
this->activeTimeouts.clear();
|
this->activeTimeouts.clear();
|
||||||
if (this->state_ != nullptr)
|
if (this->state_ != nullptr)
|
||||||
{
|
{
|
||||||
|
// clearing this after the state is gone is not safe to do
|
||||||
|
this->ownedCommands.clear();
|
||||||
|
this->callbacks.clear();
|
||||||
lua_close(this->state_);
|
lua_close(this->state_);
|
||||||
}
|
}
|
||||||
|
assert(this->ownedCommands.empty() &&
|
||||||
|
"This must be empty or destructor of sol::protected_function would "
|
||||||
|
"explode malloc structures later");
|
||||||
|
assert(this->callbacks.empty() &&
|
||||||
|
"This must be empty or destructor of sol::protected_function would "
|
||||||
|
"explode malloc structures later");
|
||||||
}
|
}
|
||||||
int Plugin::addTimeout(QTimer *timer)
|
int Plugin::addTimeout(QTimer *timer)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "common/network/NetworkCommon.hpp"
|
# include "controllers/plugins/api/EventType.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/api/HTTPRequest.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginPermission.hpp"
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
|
|
||||||
|
@ -11,7 +11,10 @@
|
||||||
# include <QString>
|
# include <QString>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
# include <semver/semver.hpp>
|
# include <semver/semver.hpp>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
|
# include <memory>
|
||||||
|
# include <optional>
|
||||||
# include <unordered_map>
|
# include <unordered_map>
|
||||||
# include <unordered_set>
|
# include <unordered_set>
|
||||||
# include <vector>
|
# include <vector>
|
||||||
|
@ -56,6 +59,8 @@ struct PluginMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
explicit PluginMeta(const QJsonObject &obj);
|
explicit PluginMeta(const QJsonObject &obj);
|
||||||
|
// This is for tests
|
||||||
|
PluginMeta() = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Plugin
|
class Plugin
|
||||||
|
@ -75,13 +80,18 @@ public:
|
||||||
|
|
||||||
~Plugin();
|
~Plugin();
|
||||||
|
|
||||||
|
Plugin(const Plugin &) = delete;
|
||||||
|
Plugin(Plugin &&) = delete;
|
||||||
|
Plugin &operator=(const Plugin &) = delete;
|
||||||
|
Plugin &operator=(Plugin &&) = delete;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Perform all necessary tasks to bind a command name to this plugin
|
* @brief Perform all necessary tasks to bind a command name to this plugin
|
||||||
* @param name name of the command to create
|
* @param name name of the command to create
|
||||||
* @param functionName name of the function that should be called when the command is executed
|
* @param function the function that should be called when the command is executed
|
||||||
* @return true if addition succeeded, false otherwise (for example because the command name is already taken)
|
* @return true if addition succeeded, false otherwise (for example because the command name is already taken)
|
||||||
*/
|
*/
|
||||||
bool registerCommand(const QString &name, const QString &functionName);
|
bool registerCommand(const QString &name, sol::protected_function function);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get names of all commands belonging to this plugin
|
* @brief Get names of all commands belonging to this plugin
|
||||||
|
@ -98,35 +108,19 @@ public:
|
||||||
return this->loadDirectory_.absoluteFilePath("data");
|
return this->loadDirectory_.absoluteFilePath("data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
|
std::optional<sol::protected_function> getCompletionCallback()
|
||||||
using LuaCompletionCallback =
|
|
||||||
lua::CallbackFunction<lua::api::CompletionList,
|
|
||||||
lua::api::CompletionEvent>;
|
|
||||||
std::optional<LuaCompletionCallback> getCompletionCallback()
|
|
||||||
{
|
{
|
||||||
if (this->state_ == nullptr || !this->error_.isNull())
|
if (this->state_ == nullptr || !this->error_.isNull())
|
||||||
{
|
{
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
// this uses magic enum to help automatic tooling find usages
|
auto it =
|
||||||
auto typeName =
|
this->callbacks.find(lua::api::EventType::CompletionRequested);
|
||||||
magic_enum::enum_name(lua::api::EventType::CompletionRequested);
|
if (it == this->callbacks.end())
|
||||||
std::string cbName;
|
|
||||||
cbName.reserve(5 + typeName.size());
|
|
||||||
cbName += "c2cb-";
|
|
||||||
cbName += typeName;
|
|
||||||
auto typ =
|
|
||||||
lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str());
|
|
||||||
if (typ != LUA_TFUNCTION)
|
|
||||||
{
|
{
|
||||||
lua_pop(this->state_, 1);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
return it->second;
|
||||||
// move
|
|
||||||
return std::make_optional<lua::CallbackFunction<
|
|
||||||
lua::api::CompletionList, lua::api::CompletionEvent>>(
|
|
||||||
this->state_, lua_gettop(this->state_));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,18 +137,25 @@ public:
|
||||||
bool hasFSPermissionFor(bool write, const QString &path);
|
bool hasFSPermissionFor(bool write, const QString &path);
|
||||||
bool hasHTTPPermissionFor(const QUrl &url);
|
bool hasHTTPPermissionFor(const QUrl &url);
|
||||||
|
|
||||||
|
std::map<lua::api::EventType, sol::protected_function> callbacks;
|
||||||
|
|
||||||
|
// In-flight HTTP Requests
|
||||||
|
// This is a lifetime hack to ensure they get deleted with the plugin. This relies on the Plugin getting deleted on reload!
|
||||||
|
std::vector<std::shared_ptr<lua::api::HTTPRequest>> httpRequests;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QDir loadDirectory_;
|
QDir loadDirectory_;
|
||||||
lua_State *state_;
|
lua_State *state_;
|
||||||
|
|
||||||
QString error_;
|
QString error_;
|
||||||
|
|
||||||
// maps command name -> function name
|
// maps command name -> function
|
||||||
std::unordered_map<QString, QString> ownedCommands;
|
std::unordered_map<QString, sol::protected_function> ownedCommands;
|
||||||
std::vector<QTimer *> activeTimeouts;
|
std::vector<QTimer *> activeTimeouts;
|
||||||
int lastTimerId = 0;
|
int lastTimerId = 0;
|
||||||
|
|
||||||
friend class PluginController;
|
friend class PluginController;
|
||||||
|
friend class PluginControllerAccess; // this is for tests
|
||||||
};
|
};
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -13,16 +13,20 @@
|
||||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "messages/MessageBuilder.hpp"
|
# include "messages/MessageBuilder.hpp"
|
||||||
# include "singletons/Paths.hpp"
|
# include "singletons/Paths.hpp"
|
||||||
# include "singletons/Settings.hpp"
|
# include "singletons/Settings.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
# include <lualib.h>
|
# include <lualib.h>
|
||||||
}
|
|
||||||
# include <QJsonDocument>
|
# include <QJsonDocument>
|
||||||
|
# include <sol/overload.hpp>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
# include <sol/variadic_args.hpp>
|
||||||
|
# include <sol/variadic_results.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
# include <utility>
|
# include <utility>
|
||||||
|
@ -113,10 +117,11 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
void PluginController::openLibrariesFor(Plugin *plugin)
|
||||||
const QDir &pluginDir)
|
|
||||||
{
|
{
|
||||||
|
auto *L = plugin->state_;
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
|
sol::state_view lua(L);
|
||||||
// Stuff to change, remove or hide behind a permission system:
|
// Stuff to change, remove or hide behind a permission system:
|
||||||
static const std::vector<luaL_Reg> loadedlibs = {
|
static const std::vector<luaL_Reg> loadedlibs = {
|
||||||
luaL_Reg{LUA_GNAME, luaopen_base},
|
luaL_Reg{LUA_GNAME, luaopen_base},
|
||||||
|
@ -124,8 +129,6 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||||
|
|
||||||
luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
||||||
luaL_Reg{LUA_TABLIBNAME, luaopen_table},
|
luaL_Reg{LUA_TABLIBNAME, luaopen_table},
|
||||||
// luaL_Reg{LUA_IOLIBNAME, luaopen_io},
|
|
||||||
// - explicit fs access, needs wrapper with permissions, no usage ideas yet
|
|
||||||
// luaL_Reg{LUA_OSLIBNAME, luaopen_os},
|
// luaL_Reg{LUA_OSLIBNAME, luaopen_os},
|
||||||
// - fs access
|
// - fs access
|
||||||
// - environ access
|
// - environ access
|
||||||
|
@ -147,155 +150,100 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||||
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false));
|
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false));
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
auto r = lua.registry();
|
||||||
static const luaL_Reg c2Lib[] = {
|
auto g = lua.globals();
|
||||||
{"register_command", lua::api::c2_register_command},
|
auto c2 = lua.create_table();
|
||||||
{"register_callback", lua::api::c2_register_callback},
|
g["c2"] = c2;
|
||||||
{"log", lua::api::c2_log},
|
|
||||||
{"later", lua::api::c2_later},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
lua_pushglobaltable(L);
|
|
||||||
auto gtable = lua_gettop(L);
|
|
||||||
|
|
||||||
// count of elements in C2LIB + LogLevel + EventType
|
|
||||||
auto c2libIdx = lua::pushEmptyTable(L, 8);
|
|
||||||
|
|
||||||
luaL_setfuncs(L, c2Lib, 0);
|
|
||||||
|
|
||||||
lua::pushEnumTable<lua::api::LogLevel>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "LogLevel");
|
|
||||||
|
|
||||||
lua::pushEnumTable<lua::api::EventType>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "EventType");
|
|
||||||
|
|
||||||
lua::pushEnumTable<lua::api::LPlatform>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "Platform");
|
|
||||||
|
|
||||||
lua::pushEnumTable<Channel::Type>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "ChannelType");
|
|
||||||
|
|
||||||
lua::pushEnumTable<NetworkRequestType>(L);
|
|
||||||
lua_setfield(L, c2libIdx, "HTTPMethod");
|
|
||||||
|
|
||||||
// Initialize metatables for objects
|
|
||||||
lua::api::ChannelRef::createMetatable(L);
|
|
||||||
lua_setfield(L, c2libIdx, "Channel");
|
|
||||||
|
|
||||||
lua::api::HTTPRequest::createMetatable(L);
|
|
||||||
lua_setfield(L, c2libIdx, "HTTPRequest");
|
|
||||||
|
|
||||||
lua::api::HTTPResponse::createMetatable(L);
|
|
||||||
lua_setfield(L, c2libIdx, "HTTPResponse");
|
|
||||||
|
|
||||||
lua_setfield(L, gtable, "c2");
|
|
||||||
|
|
||||||
// ban functions
|
// ban functions
|
||||||
// Note: this might not be fully secure? some kind of metatable fuckery might come up?
|
// Note: this might not be fully secure? some kind of metatable fuckery might come up?
|
||||||
|
|
||||||
// possibly randomize this name at runtime to prevent some attacks?
|
|
||||||
|
|
||||||
# ifndef NDEBUG
|
# ifndef NDEBUG
|
||||||
lua_getfield(L, gtable, "load");
|
lua.registry()["real_load"] = lua.globals()["load"];
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
|
||||||
# endif
|
# endif
|
||||||
|
// See chatterino::lua::api::g_load implementation
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
g["loadfile"] = sol::nil;
|
||||||
static const luaL_Reg replacementFuncs[] = {
|
g["dofile"] = sol::nil;
|
||||||
{"load", lua::api::g_load},
|
|
||||||
{"print", lua::api::g_print},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
luaL_setfuncs(L, replacementFuncs, 0);
|
|
||||||
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_setfield(L, gtable, "loadfile");
|
|
||||||
|
|
||||||
lua_pushnil(L);
|
|
||||||
lua_setfield(L, gtable, "dofile");
|
|
||||||
|
|
||||||
// set up package lib
|
// set up package lib
|
||||||
lua_getfield(L, gtable, "package");
|
|
||||||
|
|
||||||
auto package = lua_gettop(L);
|
|
||||||
lua_pushstring(L, "");
|
|
||||||
lua_setfield(L, package, "cpath");
|
|
||||||
|
|
||||||
// we don't use path
|
|
||||||
lua_pushstring(L, "");
|
|
||||||
lua_setfield(L, package, "path");
|
|
||||||
|
|
||||||
{
|
{
|
||||||
lua_getfield(L, gtable, "table");
|
auto package = g["package"];
|
||||||
auto table = lua_gettop(L);
|
package["cpath"] = "";
|
||||||
lua_getfield(L, -1, "remove");
|
package["path"] = "";
|
||||||
lua_remove(L, table);
|
|
||||||
}
|
sol::protected_function tbremove = g["table"]["remove"];
|
||||||
auto remove = lua_gettop(L);
|
|
||||||
|
|
||||||
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
||||||
|
sol::table searchers = package["searchers"];
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
lua_pushvalue(L, remove);
|
tbremove(searchers);
|
||||||
lua_getfield(L, package, "searchers");
|
|
||||||
lua_pcall(L, 1, 0, 0);
|
|
||||||
}
|
}
|
||||||
lua_pop(L, 1); // get rid of remove
|
searchers.add(&lua::api::searcherRelative);
|
||||||
|
searchers.add(&lua::api::searcherAbsolute);
|
||||||
lua_getfield(L, package, "searchers");
|
}
|
||||||
lua_pushcclosure(L, lua::api::searcherRelative, 0);
|
// set up io lib
|
||||||
lua_seti(L, -2, 2);
|
{
|
||||||
|
auto c2io = lua.create_table();
|
||||||
lua::push(L, QString(pluginDir.absolutePath()));
|
auto realio = r[lua::api::REG_REAL_IO_NAME];
|
||||||
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
|
c2io["type"] = realio["type"];
|
||||||
lua_seti(L, -2, 3);
|
g["io"] = c2io;
|
||||||
lua_pop(L, 2); // remove package, package.searchers
|
// prevent plugins getting direct access to realio
|
||||||
|
r[LUA_LOADED_TABLE]["io"] = c2io;
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
|
||||||
static const luaL_Reg ioLib[] = {
|
|
||||||
{"close", lua::api::io_close},
|
|
||||||
{"flush", lua::api::io_flush},
|
|
||||||
{"input", lua::api::io_input},
|
|
||||||
{"lines", lua::api::io_lines},
|
|
||||||
{"open", lua::api::io_open},
|
|
||||||
{"output", lua::api::io_output},
|
|
||||||
{"popen", lua::api::io_popen}, // stub
|
|
||||||
{"read", lua::api::io_read},
|
|
||||||
{"tmpfile", lua::api::io_tmpfile}, // stub
|
|
||||||
{"write", lua::api::io_write},
|
|
||||||
// type = realio.type
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
// TODO: io.popen stub
|
|
||||||
auto iolibIdx = lua::pushEmptyTable(L, 1);
|
|
||||||
luaL_setfuncs(L, ioLib, 0);
|
|
||||||
|
|
||||||
// set ourio.type = realio.type
|
|
||||||
lua_pushvalue(L, iolibIdx);
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
|
||||||
lua_getfield(L, -1, "type");
|
|
||||||
lua_remove(L, -2); // remove realio
|
|
||||||
lua_setfield(L, iolibIdx, "type");
|
|
||||||
lua_pop(L, 1); // still have iolib on top of stack
|
|
||||||
|
|
||||||
lua_pushvalue(L, iolibIdx);
|
|
||||||
lua_setfield(L, gtable, "io");
|
|
||||||
|
|
||||||
lua_pushvalue(L, iolibIdx);
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME);
|
|
||||||
|
|
||||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
|
|
||||||
lua_pushvalue(L, iolibIdx);
|
|
||||||
lua_setfield(L, -2, "io");
|
|
||||||
|
|
||||||
lua_pop(L, 3); // remove gtable, iolib, LOADED
|
|
||||||
|
|
||||||
// Don't give plugins the option to shit into our stdio
|
// Don't give plugins the option to shit into our stdio
|
||||||
lua_pushnil(L);
|
r["_IO_input"] = sol::nil;
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
r["_IO_output"] = sol::nil;
|
||||||
|
}
|
||||||
|
PluginController::initSol(lua, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
lua_pushnil(L);
|
// TODO: investigate if `plugin` can ever point to an invalid plugin,
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
// especially in cases when the plugin is errored.
|
||||||
|
void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
|
||||||
|
{
|
||||||
|
auto g = lua.globals();
|
||||||
|
// Do not capture plugin->state_ in lambdas, this makes the functions unusable in callbacks
|
||||||
|
g.set_function("print", &lua::api::g_print);
|
||||||
|
g.set_function("load", &lua::api::g_load);
|
||||||
|
|
||||||
|
sol::table c2 = g["c2"];
|
||||||
|
c2.set_function("register_command",
|
||||||
|
[plugin](const QString &name, sol::protected_function cb) {
|
||||||
|
return plugin->registerCommand(name, std::move(cb));
|
||||||
|
});
|
||||||
|
c2.set_function("register_callback", &lua::api::c2_register_callback);
|
||||||
|
c2.set_function("log", &lua::api::c2_log);
|
||||||
|
c2.set_function("later", &lua::api::c2_later);
|
||||||
|
|
||||||
|
lua::api::ChannelRef::createUserType(c2);
|
||||||
|
lua::api::HTTPResponse::createUserType(c2);
|
||||||
|
lua::api::HTTPRequest::createUserType(c2);
|
||||||
|
c2["ChannelType"] = lua::createEnumTable<Channel::Type>(lua);
|
||||||
|
c2["HTTPMethod"] = lua::createEnumTable<NetworkRequestType>(lua);
|
||||||
|
c2["EventType"] = lua::createEnumTable<lua::api::EventType>(lua);
|
||||||
|
c2["LogLevel"] = lua::createEnumTable<lua::api::LogLevel>(lua);
|
||||||
|
|
||||||
|
sol::table io = g["io"];
|
||||||
|
io.set_function(
|
||||||
|
"open", sol::overload(&lua::api::io_open, &lua::api::io_open_modeless));
|
||||||
|
io.set_function("lines", sol::overload(&lua::api::io_lines,
|
||||||
|
&lua::api::io_lines_noargs));
|
||||||
|
io.set_function("input", sol::overload(&lua::api::io_input_argless,
|
||||||
|
&lua::api::io_input_name,
|
||||||
|
&lua::api::io_input_file));
|
||||||
|
io.set_function("output", sol::overload(&lua::api::io_output_argless,
|
||||||
|
&lua::api::io_output_name,
|
||||||
|
&lua::api::io_output_file));
|
||||||
|
io.set_function("close", sol::overload(&lua::api::io_close_argless,
|
||||||
|
&lua::api::io_close_file));
|
||||||
|
io.set_function("flush", sol::overload(&lua::api::io_flush_argless,
|
||||||
|
&lua::api::io_flush_file));
|
||||||
|
io.set_function("read", &lua::api::io_read);
|
||||||
|
io.set_function("write", &lua::api::io_write);
|
||||||
|
io.set_function("popen", &lua::api::io_popen);
|
||||||
|
io.set_function("tmpfile", &lua::api::io_tmpfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||||
|
@ -314,7 +262,7 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||||
<< " because safe mode is enabled.";
|
<< " because safe mode is enabled.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PluginController::openLibrariesFor(l, meta, pluginDir);
|
PluginController::openLibrariesFor(temp);
|
||||||
|
|
||||||
if (!PluginController::isPluginEnabled(pluginName) ||
|
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||||
!getSettings()->pluginsEnabled)
|
!getSettings()->pluginsEnabled)
|
||||||
|
@ -345,17 +293,13 @@ bool PluginController::reload(const QString &id)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (it->second->state_ != nullptr)
|
|
||||||
{
|
|
||||||
lua_close(it->second->state_);
|
|
||||||
it->second->state_ = nullptr;
|
|
||||||
}
|
|
||||||
for (const auto &[cmd, _] : it->second->ownedCommands)
|
for (const auto &[cmd, _] : it->second->ownedCommands)
|
||||||
{
|
{
|
||||||
getApp()->getCommands()->unregisterPluginCommand(cmd);
|
getApp()->getCommands()->unregisterPluginCommand(cmd);
|
||||||
}
|
}
|
||||||
it->second->ownedCommands.clear();
|
|
||||||
QDir loadDir = it->second->loadDirectory_;
|
QDir loadDir = it->second->loadDirectory_;
|
||||||
|
// Since Plugin owns the state, it will clean up everything related to it
|
||||||
this->plugins_.erase(id);
|
this->plugins_.erase(id);
|
||||||
this->tryLoadFromDir(loadDir);
|
this->tryLoadFromDir(loadDir);
|
||||||
return true;
|
return true;
|
||||||
|
@ -369,27 +313,36 @@ QString PluginController::tryExecPluginCommand(const QString &commandName,
|
||||||
if (auto it = plugin->ownedCommands.find(commandName);
|
if (auto it = plugin->ownedCommands.find(commandName);
|
||||||
it != plugin->ownedCommands.end())
|
it != plugin->ownedCommands.end())
|
||||||
{
|
{
|
||||||
const auto &funcName = it->second;
|
sol::state_view lua(plugin->state_);
|
||||||
|
sol::table args = lua.create_table_with(
|
||||||
|
"words", ctx.words, //
|
||||||
|
"channel", lua::api::ChannelRef(ctx.channel) //
|
||||||
|
);
|
||||||
|
|
||||||
auto *L = plugin->state_;
|
auto result =
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str());
|
lua::tryCall<std::optional<QString>>(it->second, args);
|
||||||
lua::push(L, ctx);
|
if (!result)
|
||||||
|
|
||||||
auto res = lua_pcall(L, 1, 0, 0);
|
|
||||||
if (res != LUA_OK)
|
|
||||||
{
|
{
|
||||||
ctx.channel->addSystemMessage("Lua error: " +
|
ctx.channel->addSystemMessage(
|
||||||
lua::humanErrorText(L, res));
|
QStringView(
|
||||||
return "";
|
u"Failed to evaluate command from plugin %1: %2")
|
||||||
|
.arg(plugin->meta.name, result.error()));
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
|
auto opt = result.value();
|
||||||
|
if (!opt)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return *opt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
qCCritical(chatterinoLua)
|
qCCritical(chatterinoLua)
|
||||||
<< "Something's seriously up, no plugin owns command" << commandName
|
<< "Something's seriously up, no plugin owns command" << commandName
|
||||||
<< "yet a call to execute it came in";
|
<< "yet a call to execute it came in";
|
||||||
assert(false && "missing plugin command owner");
|
assert(false && "missing plugin command owner");
|
||||||
return "";
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PluginController::isPluginEnabled(const QString &id)
|
bool PluginController::isPluginEnabled(const QString &id)
|
||||||
|
@ -435,32 +388,31 @@ std::pair<bool, QStringList> PluginController::updateCustomCompletions(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
lua::StackGuard guard(pl->state_);
|
|
||||||
|
|
||||||
auto opt = pl->getCompletionCallback();
|
auto opt = pl->getCompletionCallback();
|
||||||
if (opt)
|
if (opt)
|
||||||
{
|
{
|
||||||
qCDebug(chatterinoLua)
|
qCDebug(chatterinoLua)
|
||||||
<< "Processing custom completions from plugin" << name;
|
<< "Processing custom completions from plugin" << name;
|
||||||
auto &cb = *opt;
|
auto &cb = *opt;
|
||||||
auto errOrList = cb(lua::api::CompletionEvent{
|
sol::state_view view(pl->state_);
|
||||||
|
auto errOrList = lua::tryCall<sol::table>(
|
||||||
|
cb,
|
||||||
|
toTable(pl->state_, lua::api::CompletionEvent{
|
||||||
.query = query,
|
.query = query,
|
||||||
.full_text_content = fullTextContent,
|
.full_text_content = fullTextContent,
|
||||||
.cursor_position = cursorPosition,
|
.cursor_position = cursorPosition,
|
||||||
.is_first_word = isFirstWord,
|
.is_first_word = isFirstWord,
|
||||||
});
|
}));
|
||||||
if (std::holds_alternative<int>(errOrList))
|
if (!errOrList.has_value())
|
||||||
{
|
{
|
||||||
guard.handled();
|
|
||||||
int err = std::get<int>(errOrList);
|
|
||||||
qCDebug(chatterinoLua)
|
qCDebug(chatterinoLua)
|
||||||
<< "Got error from plugin " << pl->meta.name
|
<< "Got error from plugin " << pl->meta.name
|
||||||
<< " while refreshing tab completion: "
|
<< " while refreshing tab completion: "
|
||||||
<< lua::humanErrorText(pl->state_, err);
|
<< errOrList.get_unexpected().error();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto list = std::get<lua::api::CompletionList>(errOrList);
|
auto list = lua::api::CompletionList(*errOrList);
|
||||||
if (list.hideOthers)
|
if (list.hideOthers)
|
||||||
{
|
{
|
||||||
results = QStringList(list.values.begin(), list.values.end());
|
results = QStringList(list.values.begin(), list.values.end());
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
# include <QJsonArray>
|
# include <QJsonArray>
|
||||||
# include <QJsonObject>
|
# include <QJsonObject>
|
||||||
# include <QString>
|
# include <QString>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
# include <algorithm>
|
# include <algorithm>
|
||||||
# include <map>
|
# include <map>
|
||||||
|
@ -66,11 +67,16 @@ private:
|
||||||
const PluginMeta &meta);
|
const PluginMeta &meta);
|
||||||
|
|
||||||
// This function adds lua standard libraries into the state
|
// This function adds lua standard libraries into the state
|
||||||
static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/,
|
static void openLibrariesFor(Plugin *plugin);
|
||||||
const QDir &pluginDir);
|
|
||||||
|
static void initSol(sol::state_view &lua, Plugin *plugin);
|
||||||
|
|
||||||
static void loadChatterinoLib(lua_State *l);
|
static void loadChatterinoLib(lua_State *l);
|
||||||
bool tryLoadFromDir(const QDir &pluginDir);
|
bool tryLoadFromDir(const QDir &pluginDir);
|
||||||
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
||||||
|
|
||||||
|
// This is for tests, pay no attention
|
||||||
|
friend class PluginControllerAccess;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -10,6 +10,8 @@ namespace chatterino {
|
||||||
|
|
||||||
struct PluginPermission {
|
struct PluginPermission {
|
||||||
explicit PluginPermission(const QJsonObject &obj);
|
explicit PluginPermission(const QJsonObject &obj);
|
||||||
|
// This is for tests
|
||||||
|
PluginPermission() = default;
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
FilesystemRead,
|
FilesystemRead,
|
||||||
|
|
131
src/controllers/plugins/SolTypes.cpp
Normal file
131
src/controllers/plugins/SolTypes.cpp
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
|
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
|
||||||
|
# include <QObject>
|
||||||
|
# include <sol/thread.hpp>
|
||||||
|
namespace chatterino::lua {
|
||||||
|
|
||||||
|
Plugin *ThisPluginState::plugin()
|
||||||
|
{
|
||||||
|
if (this->plugptr_ != nullptr)
|
||||||
|
{
|
||||||
|
return this->plugptr_;
|
||||||
|
}
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(this->state_);
|
||||||
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("internal error: missing plugin");
|
||||||
|
}
|
||||||
|
this->plugptr_ = pl;
|
||||||
|
return pl;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
// NOLINTBEGIN(readability-named-parameter)
|
||||||
|
// QString
|
||||||
|
bool sol_lua_check(sol::types<QString>, lua_State *L, int index,
|
||||||
|
std::function<sol::check_handler_type> handler,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
return sol::stack::check<const char *>(L, index, handler, tracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString sol_lua_get(sol::types<QString>, lua_State *L, int index,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
auto str = sol::stack::get<std::string_view>(L, index, tracking);
|
||||||
|
return QString::fromUtf8(str.data(), static_cast<qsizetype>(str.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<QString>, lua_State *L, const QString &value)
|
||||||
|
{
|
||||||
|
return sol::stack::push(L, value.toUtf8().data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// QStringList
|
||||||
|
bool sol_lua_check(sol::types<QStringList>, lua_State *L, int index,
|
||||||
|
std::function<sol::check_handler_type> handler,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
return sol::stack::check<sol::table>(L, index, handler, tracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList sol_lua_get(sol::types<QStringList>, lua_State *L, int index,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
sol::table table = sol::stack::get<sol::table>(L, index, tracking);
|
||||||
|
QStringList result;
|
||||||
|
result.reserve(static_cast<qsizetype>(table.size()));
|
||||||
|
for (size_t i = 1; i < table.size() + 1; i++)
|
||||||
|
{
|
||||||
|
result.append(table.get<QString>(i));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<QStringList>, lua_State *L,
|
||||||
|
const QStringList &value)
|
||||||
|
{
|
||||||
|
sol::table table = sol::table::create(L, static_cast<int>(value.size()));
|
||||||
|
for (const QString &str : value)
|
||||||
|
{
|
||||||
|
table.add(str);
|
||||||
|
}
|
||||||
|
return sol::stack::push(L, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
// QByteArray
|
||||||
|
bool sol_lua_check(sol::types<QByteArray>, lua_State *L, int index,
|
||||||
|
std::function<sol::check_handler_type> handler,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
return sol::stack::check<const char *>(L, index, handler, tracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray sol_lua_get(sol::types<QByteArray>, lua_State *L, int index,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
auto str = sol::stack::get<std::string_view>(L, index, tracking);
|
||||||
|
return QByteArray::fromRawData(str.data(), str.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<QByteArray>, lua_State *L, const QByteArray &value)
|
||||||
|
{
|
||||||
|
return sol::stack::push(L,
|
||||||
|
std::string_view(value.constData(), value.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace chatterino::lua {
|
||||||
|
|
||||||
|
// ThisPluginState
|
||||||
|
|
||||||
|
bool sol_lua_check(sol::types<chatterino::lua::ThisPluginState>,
|
||||||
|
lua_State * /*L*/, int /* index*/,
|
||||||
|
std::function<sol::check_handler_type> /* handler*/,
|
||||||
|
sol::stack::record & /*tracking*/)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatterino::lua::ThisPluginState sol_lua_get(
|
||||||
|
sol::types<chatterino::lua::ThisPluginState>, lua_State *L, int /*index*/,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
tracking.use(0);
|
||||||
|
return {L};
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<chatterino::lua::ThisPluginState>, lua_State *L,
|
||||||
|
const chatterino::lua::ThisPluginState &value)
|
||||||
|
{
|
||||||
|
return sol::stack::push(L, sol::thread(L, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
// NOLINTEND(readability-named-parameter)
|
||||||
|
|
||||||
|
#endif
|
170
src/controllers/plugins/SolTypes.hpp
Normal file
170
src/controllers/plugins/SolTypes.hpp
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
#pragma once
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "util/QMagicEnum.hpp"
|
||||||
|
# include "util/TypeName.hpp"
|
||||||
|
|
||||||
|
# include <nonstd/expected.hpp>
|
||||||
|
# include <QObject>
|
||||||
|
# include <QString>
|
||||||
|
# include <QStringBuilder>
|
||||||
|
# include <QStringList>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
|
namespace chatterino::detail {
|
||||||
|
|
||||||
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
template <typename T>
|
||||||
|
constexpr bool IsOptional = false;
|
||||||
|
template <typename T>
|
||||||
|
constexpr bool IsOptional<std::optional<T>> = true;
|
||||||
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
|
} // namespace chatterino::detail
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class Plugin;
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
namespace chatterino::lua {
|
||||||
|
|
||||||
|
class ThisPluginState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ThisPluginState(lua_State *Ls)
|
||||||
|
: plugptr_(nullptr)
|
||||||
|
, state_(Ls)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
operator lua_State *() const noexcept
|
||||||
|
{
|
||||||
|
return this->state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_State *operator->() const noexcept
|
||||||
|
{
|
||||||
|
return this->state_;
|
||||||
|
}
|
||||||
|
lua_State *state() const noexcept
|
||||||
|
{
|
||||||
|
return this->state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin *plugin();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Plugin *plugptr_;
|
||||||
|
lua_State *state_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Attempts to call @a function with @a args
|
||||||
|
///
|
||||||
|
/// @a T is expected to be returned.
|
||||||
|
/// If `void` is specified, the returned values
|
||||||
|
/// are ignored.
|
||||||
|
/// `std::optional<T>` means nil|LuaEquiv<T> (or zero returns)
|
||||||
|
/// A return type that doesn't match returns an error
|
||||||
|
template <typename T, typename... Args>
|
||||||
|
inline nonstd::expected_lite::expected<T, QString> tryCall(
|
||||||
|
const sol::protected_function &function, Args &&...args)
|
||||||
|
{
|
||||||
|
sol::protected_function_result result =
|
||||||
|
function(std::forward<Args>(args)...);
|
||||||
|
if (!result.valid())
|
||||||
|
{
|
||||||
|
sol::error err = result;
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
QString::fromUtf8(err.what()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, void>)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if constexpr (detail::IsOptional<T>)
|
||||||
|
{
|
||||||
|
if (result.return_count() == 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.return_count() > 1)
|
||||||
|
{
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
u"Expected one value to be returned but " %
|
||||||
|
QString::number(result.return_count()) %
|
||||||
|
u" values were returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if constexpr (detail::IsOptional<T>)
|
||||||
|
{
|
||||||
|
// we want to error on anything that is not nil|T,
|
||||||
|
// std::optional<T> in sol means "give me a T or if it does not match nullopt"
|
||||||
|
if (result.get_type() == sol::type::nil)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto ret = result.get<T>();
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
{
|
||||||
|
auto t = type_name<T>();
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
u"Expected " % QLatin1String(t.data(), t.size()) %
|
||||||
|
u" to be returned but " %
|
||||||
|
qmagicenum::enumName(result.get_type()) %
|
||||||
|
u" was returned");
|
||||||
|
}
|
||||||
|
return *ret;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto ret = result.get<std::optional<T>>();
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
{
|
||||||
|
auto t = type_name<T>();
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
u"Expected " % QLatin1String(t.data(), t.size()) %
|
||||||
|
u" to be returned but " %
|
||||||
|
qmagicenum::enumName(result.get_type()) %
|
||||||
|
u" was returned");
|
||||||
|
}
|
||||||
|
return *ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (std::runtime_error &e)
|
||||||
|
{
|
||||||
|
return nonstd::expected_lite::make_unexpected(
|
||||||
|
QString::fromUtf8(e.what()));
|
||||||
|
}
|
||||||
|
// non other exceptions we let it explode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||||
|
# define SOL_STACK_FUNCTIONS(TYPE) \
|
||||||
|
bool sol_lua_check(sol::types<TYPE>, lua_State *L, int index, \
|
||||||
|
std::function<sol::check_handler_type> handler, \
|
||||||
|
sol::stack::record &tracking); \
|
||||||
|
TYPE sol_lua_get(sol::types<TYPE>, lua_State *L, int index, \
|
||||||
|
sol::stack::record &tracking); \
|
||||||
|
int sol_lua_push(sol::types<TYPE>, lua_State *L, const TYPE &value);
|
||||||
|
|
||||||
|
SOL_STACK_FUNCTIONS(chatterino::lua::ThisPluginState)
|
||||||
|
|
||||||
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
SOL_STACK_FUNCTIONS(QString)
|
||||||
|
SOL_STACK_FUNCTIONS(QStringList)
|
||||||
|
SOL_STACK_FUNCTIONS(QByteArray)
|
||||||
|
|
||||||
|
# undef SOL_STACK_FUNCTIONS
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,397 +1,224 @@
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||||
|
|
||||||
|
# include "Application.hpp"
|
||||||
# include "common/Channel.hpp"
|
# include "common/Channel.hpp"
|
||||||
# include "controllers/commands/CommandController.hpp"
|
# include "controllers/commands/CommandController.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
# include "messages/MessageBuilder.hpp"
|
|
||||||
# include "providers/twitch/TwitchChannel.hpp"
|
# include "providers/twitch/TwitchChannel.hpp"
|
||||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
|
|
||||||
extern "C" {
|
# include <sol/sol.hpp>
|
||||||
# include <lauxlib.h>
|
|
||||||
# include <lua.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
# include <cassert>
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
# include <optional>
|
# include <optional>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(*vararg)
|
|
||||||
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
ChannelRef::ChannelRef(const std::shared_ptr<Channel> &chan)
|
||||||
static const luaL_Reg CHANNEL_REF_METHODS[] = {
|
: weak(chan)
|
||||||
{"is_valid", &ChannelRef::is_valid},
|
{
|
||||||
{"get_name", &ChannelRef::get_name},
|
}
|
||||||
{"get_type", &ChannelRef::get_type},
|
|
||||||
{"get_display_name", &ChannelRef::get_display_name},
|
|
||||||
{"send_message", &ChannelRef::send_message},
|
|
||||||
{"add_system_message", &ChannelRef::add_system_message},
|
|
||||||
{"is_twitch_channel", &ChannelRef::is_twitch_channel},
|
|
||||||
|
|
||||||
// Twitch
|
std::shared_ptr<Channel> ChannelRef::strong()
|
||||||
{"get_room_modes", &ChannelRef::get_room_modes},
|
{
|
||||||
{"get_stream_status", &ChannelRef::get_stream_status},
|
auto c = this->weak.lock();
|
||||||
{"get_twitch_id", &ChannelRef::get_twitch_id},
|
if (!c)
|
||||||
{"is_broadcaster", &ChannelRef::is_broadcaster},
|
{
|
||||||
{"is_mod", &ChannelRef::is_mod},
|
throw std::runtime_error(
|
||||||
{"is_vip", &ChannelRef::is_vip},
|
"Expired c2.Channel used - use c2.Channel:is_valid() to "
|
||||||
|
"check validity");
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
// misc
|
std::shared_ptr<TwitchChannel> ChannelRef::twitch()
|
||||||
{"__tostring", &ChannelRef::to_string},
|
{
|
||||||
|
auto c = std::dynamic_pointer_cast<TwitchChannel>(this->weak.lock());
|
||||||
|
if (!c)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Expired or non-twitch c2.Channel used - use "
|
||||||
|
"c2.Channel:is_valid() and c2.Channe:is_twitch_channel()");
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_valid()
|
||||||
|
{
|
||||||
|
return !this->weak.expired();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChannelRef::get_name()
|
||||||
|
{
|
||||||
|
return this->strong()->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
Channel::Type ChannelRef::get_type()
|
||||||
|
{
|
||||||
|
return this->strong()->getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChannelRef::get_display_name()
|
||||||
|
{
|
||||||
|
return this->strong()->getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChannelRef::send_message(QString text, sol::variadic_args va)
|
||||||
|
{
|
||||||
|
bool execCommands = [&] {
|
||||||
|
if (va.size() >= 1)
|
||||||
|
{
|
||||||
|
return va.get<bool>();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}();
|
||||||
|
text = text.replace('\n', ' ');
|
||||||
|
auto chan = this->strong();
|
||||||
|
if (execCommands)
|
||||||
|
{
|
||||||
|
text = getApp()->getCommands()->execCommand(text, chan, false);
|
||||||
|
}
|
||||||
|
chan->sendMessage(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChannelRef::add_system_message(QString text)
|
||||||
|
{
|
||||||
|
text = text.replace('\n', ' ');
|
||||||
|
this->strong()->addSystemMessage(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_twitch_channel()
|
||||||
|
{
|
||||||
|
return this->strong()->isTwitchChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::table ChannelRef::get_room_modes(sol::this_state state)
|
||||||
|
{
|
||||||
|
return toTable(state.L, *this->twitch()->accessRoomModes());
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::table ChannelRef::get_stream_status(sol::this_state state)
|
||||||
|
{
|
||||||
|
return toTable(state.L, *this->twitch()->accessStreamStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChannelRef::get_twitch_id()
|
||||||
|
{
|
||||||
|
return this->twitch()->roomId();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_broadcaster()
|
||||||
|
{
|
||||||
|
return this->twitch()->isBroadcaster();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_mod()
|
||||||
|
{
|
||||||
|
return this->twitch()->isMod();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChannelRef::is_vip()
|
||||||
|
{
|
||||||
|
return this->twitch()->isVip();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChannelRef::to_string()
|
||||||
|
{
|
||||||
|
auto chan = this->weak.lock();
|
||||||
|
if (!chan)
|
||||||
|
{
|
||||||
|
return "<c2.Channel expired>";
|
||||||
|
}
|
||||||
|
return QStringView(u"<c2.Channel %1>").arg(chan->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ChannelRef> ChannelRef::get_by_name(const QString &name)
|
||||||
|
{
|
||||||
|
auto chan = getApp()->getTwitch()->getChannelOrEmpty(name);
|
||||||
|
if (chan->isEmpty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return chan;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ChannelRef> ChannelRef::get_by_twitch_id(const QString &id)
|
||||||
|
{
|
||||||
|
auto chan = getApp()->getTwitch()->getChannelOrEmptyByID(id);
|
||||||
|
if (chan->isEmpty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return chan;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChannelRef::createUserType(sol::table &c2)
|
||||||
|
{
|
||||||
|
// clang-format off
|
||||||
|
c2.new_usertype<ChannelRef>(
|
||||||
|
"Channel", sol::no_constructor,
|
||||||
|
// meta methods
|
||||||
|
sol::meta_method::to_string, &ChannelRef::to_string,
|
||||||
|
|
||||||
|
// Channel
|
||||||
|
"is_valid", &ChannelRef::is_valid,
|
||||||
|
"get_name",&ChannelRef::get_name,
|
||||||
|
"get_type", &ChannelRef::get_type,
|
||||||
|
"get_display_name", &ChannelRef::get_display_name,
|
||||||
|
"send_message", &ChannelRef::send_message,
|
||||||
|
"add_system_message", &ChannelRef::add_system_message,
|
||||||
|
"is_twitch_channel", &ChannelRef::is_twitch_channel,
|
||||||
|
|
||||||
|
// TwitchChannel
|
||||||
|
"get_room_modes", &ChannelRef::get_room_modes,
|
||||||
|
"get_stream_status", &ChannelRef::get_stream_status,
|
||||||
|
"get_twitch_id", &ChannelRef::get_twitch_id,
|
||||||
|
"is_broadcaster", &ChannelRef::is_broadcaster,
|
||||||
|
"is_mod", &ChannelRef::is_mod,
|
||||||
|
"is_vip", &ChannelRef::is_vip,
|
||||||
|
|
||||||
// static
|
// static
|
||||||
{"by_name", &ChannelRef::get_by_name},
|
"by_name", &ChannelRef::get_by_name,
|
||||||
{"by_twitch_id", &ChannelRef::get_by_twitch_id},
|
"by_twitch_id", &ChannelRef::get_by_twitch_id
|
||||||
{nullptr, nullptr},
|
);
|
||||||
};
|
// clang-format on
|
||||||
|
|
||||||
void ChannelRef::createMetatable(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, 1);
|
|
||||||
|
|
||||||
luaL_newmetatable(L, "c2.Channel");
|
|
||||||
lua_pushstring(L, "__index");
|
|
||||||
lua_pushvalue(L, -2); // clone metatable
|
|
||||||
lua_settable(L, -3); // metatable.__index = metatable
|
|
||||||
|
|
||||||
// Generic IWeakResource stuff
|
|
||||||
lua_pushstring(L, "__gc");
|
|
||||||
lua_pushcfunction(
|
|
||||||
L, (&WeakPtrUserData<UserData::Type::Channel, ChannelRef>::destroy));
|
|
||||||
lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy
|
|
||||||
|
|
||||||
luaL_setfuncs(L, CHANNEL_REF_METHODS, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes)
|
||||||
{
|
{
|
||||||
if (lua_gettop(L) < 1)
|
auto maybe = [](int value) {
|
||||||
|
if (value >= 0)
|
||||||
{
|
{
|
||||||
luaL_error(L, "Called c2.Channel method without a channel object");
|
return std::optional{value};
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
return std::optional<int>{};
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "Called c2.Channel method with a non-userdata 'self' argument");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
// luaL_checkudata is no-return if check fails
|
|
||||||
auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel");
|
|
||||||
auto *data =
|
|
||||||
WeakPtrUserData<UserData::Type::Channel, Channel>::from(checked);
|
|
||||||
if (data == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L,
|
|
||||||
"Called c2.Channel method with an invalid channel pointer");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
lua_pop(L, 1);
|
|
||||||
if (data->target.expired())
|
|
||||||
{
|
|
||||||
if (!expiredOk)
|
|
||||||
{
|
|
||||||
luaL_error(L,
|
|
||||||
"Usage of expired c2.Channel object. Underlying "
|
|
||||||
"resource was freed. Use Channel:is_valid() to check");
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return data->target.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<TwitchChannel> ChannelRef::getTwitchOrError(lua_State *L)
|
|
||||||
{
|
|
||||||
auto ref = ChannelRef::getOrError(L);
|
|
||||||
auto ptr = dynamic_pointer_cast<TwitchChannel>(ref);
|
|
||||||
if (ptr == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L,
|
|
||||||
"c2.Channel Twitch-only operation on non-Twitch channel.");
|
|
||||||
}
|
|
||||||
return ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_valid(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
|
||||||
lua::push(L, that != nullptr);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_name(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
lua::push(L, that->getName());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_type(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
lua::push(L, that->getType());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_display_name(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
lua::push(L, that->getDisplayName());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::send_message(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) != 2 && lua_gettop(L) != 3)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message "
|
|
||||||
"text and optionally execute_commands flag)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
bool execcmds = false;
|
|
||||||
if (lua_gettop(L) == 3)
|
|
||||||
{
|
|
||||||
if (!lua::pop(L, &execcmds))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get execute_commands (2nd argument of "
|
|
||||||
"Channel:send_message)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString text;
|
|
||||||
if (!lua::pop(L, &text))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get text (1st argument of Channel:send_message)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
|
|
||||||
text = text.replace('\n', ' ');
|
|
||||||
if (execcmds)
|
|
||||||
{
|
|
||||||
text = getApp()->getCommands()->execCommand(text, that, false);
|
|
||||||
}
|
|
||||||
that->sendMessage(text);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::add_system_message(lua_State *L)
|
|
||||||
{
|
|
||||||
// needs to account for the hidden self argument
|
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "Channel:add_system_message needs exactly 1 argument (message "
|
|
||||||
"text)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString text;
|
|
||||||
if (!lua::pop(L, &text))
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "cannot get text (1st argument of Channel:add_system_message)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
text = text.replace('\n', ' ');
|
|
||||||
that->addSystemMessage(text);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_twitch_channel(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L);
|
|
||||||
lua::push(L, that->isTwitchChannel());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_room_modes(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
const auto m = tc->accessRoomModes();
|
|
||||||
const auto modes = LuaRoomModes{
|
|
||||||
.unique_chat = m->r9k,
|
|
||||||
.subscriber_only = m->submode,
|
|
||||||
.emotes_only = m->emoteOnly,
|
|
||||||
.follower_only = (m->followerOnly == -1)
|
|
||||||
? std::nullopt
|
|
||||||
: std::optional(m->followerOnly),
|
|
||||||
.slow_mode =
|
|
||||||
(m->slowMode == 0) ? std::nullopt : std::optional(m->slowMode),
|
|
||||||
|
|
||||||
};
|
};
|
||||||
lua::push(L, modes);
|
// clang-format off
|
||||||
return 1;
|
return sol::table::create_with(L,
|
||||||
|
"subscriber_only", modes.submode,
|
||||||
|
"unique_chat", modes.r9k,
|
||||||
|
"emotes_only", modes.emoteOnly,
|
||||||
|
"follower_only", maybe(modes.followerOnly),
|
||||||
|
"slow_mode", maybe(modes.slowMode)
|
||||||
|
);
|
||||||
|
// clang-format on
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::get_stream_status(lua_State *L)
|
sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status)
|
||||||
{
|
{
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
// clang-format off
|
||||||
const auto s = tc->accessStreamStatus();
|
return sol::table::create_with(L,
|
||||||
const auto status = LuaStreamStatus{
|
"live", status.live,
|
||||||
.live = s->live,
|
"viewer_count", status.viewerCount,
|
||||||
.viewer_count = static_cast<int>(s->viewerCount),
|
"title", status.title,
|
||||||
.uptime = s->uptimeSeconds,
|
"game_name", status.game,
|
||||||
.title = s->title,
|
"game_id", status.gameId,
|
||||||
.game_name = s->game,
|
"uptime", status.uptimeSeconds
|
||||||
.game_id = s->gameId,
|
);
|
||||||
};
|
// clang-format on
|
||||||
lua::push(L, status);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChannelRef::get_twitch_id(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->roomId());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_broadcaster(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->isBroadcaster());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_mod(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->isMod());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::is_vip(lua_State *L)
|
|
||||||
{
|
|
||||||
auto tc = ChannelRef::getTwitchOrError(L);
|
|
||||||
lua::push(L, tc->isVip());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_by_name(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Channel.by_name needs exactly 2 arguments (channel "
|
|
||||||
"name and platform)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
LPlatform platform{};
|
|
||||||
if (!lua::pop(L, &platform))
|
|
||||||
{
|
|
||||||
luaL_error(L, "cannot get platform (2nd argument of Channel.by_name, "
|
|
||||||
"expected a string)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString name;
|
|
||||||
if (!lua::pop(L, &name))
|
|
||||||
{
|
|
||||||
luaL_error(L,
|
|
||||||
"cannot get channel name (1st argument of Channel.by_name, "
|
|
||||||
"expected a string)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
auto chn = getApp()->getTwitch()->getChannelOrEmpty(name);
|
|
||||||
lua::push(L, chn);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::get_by_twitch_id(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) != 1)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "Channel.by_twitch_id needs exactly 1 arguments (channel owner "
|
|
||||||
"id)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString id;
|
|
||||||
if (!lua::pop(L, &id))
|
|
||||||
{
|
|
||||||
luaL_error(L,
|
|
||||||
"cannot get channel name (1st argument of Channel.by_name, "
|
|
||||||
"expected a string)");
|
|
||||||
lua_pushnil(L);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
auto chn = getApp()->getTwitch()->getChannelOrEmptyByID(id);
|
|
||||||
|
|
||||||
lua::push(L, chn);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChannelRef::to_string(lua_State *L)
|
|
||||||
{
|
|
||||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
|
||||||
if (that == nullptr)
|
|
||||||
{
|
|
||||||
lua_pushstring(L, "<c2.Channel expired>");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString formated = QString("<c2.Channel %1>").arg(that->getName());
|
|
||||||
lua::push(L, formated);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
// NOLINTEND(*vararg)
|
|
||||||
//
|
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes)
|
|
||||||
{
|
|
||||||
auto out = lua::pushEmptyTable(L, 6);
|
|
||||||
# define PUSH(field) \
|
|
||||||
lua::push(L, modes.field); \
|
|
||||||
lua_setfield(L, out, #field)
|
|
||||||
PUSH(unique_chat);
|
|
||||||
PUSH(subscriber_only);
|
|
||||||
PUSH(emotes_only);
|
|
||||||
PUSH(follower_only);
|
|
||||||
PUSH(slow_mode);
|
|
||||||
# undef PUSH
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status)
|
|
||||||
{
|
|
||||||
auto out = lua::pushEmptyTable(L, 6);
|
|
||||||
# define PUSH(field) \
|
|
||||||
lua::push(L, status.field); \
|
|
||||||
lua_setfield(L, out, #field)
|
|
||||||
PUSH(live);
|
|
||||||
PUSH(viewer_count);
|
|
||||||
PUSH(uptime);
|
|
||||||
PUSH(title);
|
|
||||||
PUSH(game_name);
|
|
||||||
PUSH(game_id);
|
|
||||||
# undef PUSH
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, ChannelPtr chn)
|
|
||||||
{
|
|
||||||
using namespace chatterino::lua::api;
|
|
||||||
|
|
||||||
if (chn->isEmpty())
|
|
||||||
{
|
|
||||||
lua_pushnil(L);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
WeakPtrUserData<UserData::Type::Channel, Channel>::create(
|
|
||||||
L, chn->weak_from_this());
|
|
||||||
luaL_getmetatable(L, "c2.Channel");
|
|
||||||
lua_setmetatable(L, -2);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,48 +1,24 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "common/Channel.hpp"
|
# include "common/Channel.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
|
||||||
# include "providers/twitch/TwitchChannel.hpp"
|
# include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
|
||||||
# include <optional>
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(readability-identifier-naming)
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This enum describes a platform for the purpose of searching for a channel.
|
* @includefile providers/twitch/TwitchChannel.hpp
|
||||||
* Currently only Twitch is supported because identifying IRC channels is tricky.
|
|
||||||
* @exposeenum c2.Platform
|
|
||||||
*/
|
*/
|
||||||
enum class LPlatform {
|
|
||||||
Twitch,
|
|
||||||
//IRC,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @lua@class c2.Channel
|
* @lua@class c2.Channel
|
||||||
*/
|
*/
|
||||||
struct ChannelRef {
|
struct ChannelRef {
|
||||||
static void createMetatable(lua_State *L);
|
|
||||||
friend class chatterino::PluginController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the content of the top object on Lua stack, usually first argument to function as a ChannelPtr.
|
|
||||||
* If the object given is not a userdatum or the pointer inside that
|
|
||||||
* userdatum doesn't point to a Channel, a lua error is thrown.
|
|
||||||
*
|
|
||||||
* @param expiredOk Should an expired return nullptr instead of erroring
|
|
||||||
*/
|
|
||||||
static ChannelPtr getOrError(lua_State *L, bool expiredOk = false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Casts the result of getOrError to std::shared_ptr<TwitchChannel>
|
|
||||||
* if that fails thows a lua error.
|
|
||||||
*/
|
|
||||||
static std::shared_ptr<TwitchChannel> getTwitchOrError(lua_State *L);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
ChannelRef(const std::shared_ptr<Channel> &chan);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel this object points to is valid.
|
* Returns true if the channel this object points to is valid.
|
||||||
* If the object expired, returns false
|
* If the object expired, returns false
|
||||||
|
@ -51,7 +27,7 @@ public:
|
||||||
* @lua@return boolean success
|
* @lua@return boolean success
|
||||||
* @exposed c2.Channel:is_valid
|
* @exposed c2.Channel:is_valid
|
||||||
*/
|
*/
|
||||||
static int is_valid(lua_State *L);
|
bool is_valid();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the channel's name. This is the lowercase login name.
|
* Gets the channel's name. This is the lowercase login name.
|
||||||
|
@ -59,7 +35,7 @@ public:
|
||||||
* @lua@return string name
|
* @lua@return string name
|
||||||
* @exposed c2.Channel:get_name
|
* @exposed c2.Channel:get_name
|
||||||
*/
|
*/
|
||||||
static int get_name(lua_State *L);
|
QString get_name();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the channel's type
|
* Gets the channel's type
|
||||||
|
@ -67,7 +43,7 @@ public:
|
||||||
* @lua@return c2.ChannelType
|
* @lua@return c2.ChannelType
|
||||||
* @exposed c2.Channel:get_type
|
* @exposed c2.Channel:get_type
|
||||||
*/
|
*/
|
||||||
static int get_type(lua_State *L);
|
Channel::Type get_type();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
* Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
||||||
|
@ -75,17 +51,17 @@ public:
|
||||||
* @lua@return string name
|
* @lua@return string name
|
||||||
* @exposed c2.Channel:get_display_name
|
* @exposed c2.Channel:get_display_name
|
||||||
*/
|
*/
|
||||||
static int get_display_name(lua_State *L);
|
QString get_display_name();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to the target channel.
|
* Sends a message to the target channel.
|
||||||
* Note that this does not execute client-commands.
|
* Note that this does not execute client-commands.
|
||||||
*
|
*
|
||||||
* @lua@param message string
|
* @lua@param message string
|
||||||
* @lua@param execute_commands boolean Should commands be run on the text?
|
* @lua@param execute_commands? boolean Should commands be run on the text?
|
||||||
* @exposed c2.Channel:send_message
|
* @exposed c2.Channel:send_message
|
||||||
*/
|
*/
|
||||||
static int send_message(lua_State *L);
|
void send_message(QString text, sol::variadic_args va);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a system message client-side
|
* Adds a system message client-side
|
||||||
|
@ -93,7 +69,7 @@ public:
|
||||||
* @lua@param message string
|
* @lua@param message string
|
||||||
* @exposed c2.Channel:add_system_message
|
* @exposed c2.Channel:add_system_message
|
||||||
*/
|
*/
|
||||||
static int add_system_message(lua_State *L);
|
void add_system_message(QString text);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true for twitch channels.
|
* Returns true for twitch channels.
|
||||||
|
@ -103,7 +79,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_twitch_channel
|
* @exposed c2.Channel:is_twitch_channel
|
||||||
*/
|
*/
|
||||||
static int is_twitch_channel(lua_State *L);
|
bool is_twitch_channel();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Twitch Channel specific functions
|
* Twitch Channel specific functions
|
||||||
|
@ -115,7 +91,7 @@ public:
|
||||||
* @lua@return RoomModes
|
* @lua@return RoomModes
|
||||||
* @exposed c2.Channel:get_room_modes
|
* @exposed c2.Channel:get_room_modes
|
||||||
*/
|
*/
|
||||||
static int get_room_modes(lua_State *L);
|
sol::table get_room_modes(sol::this_state state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the stream status.
|
* Returns a copy of the stream status.
|
||||||
|
@ -123,7 +99,7 @@ public:
|
||||||
* @lua@return StreamStatus
|
* @lua@return StreamStatus
|
||||||
* @exposed c2.Channel:get_stream_status
|
* @exposed c2.Channel:get_stream_status
|
||||||
*/
|
*/
|
||||||
static int get_stream_status(lua_State *L);
|
sol::table get_stream_status(sol::this_state state);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Twitch user ID of the owner of the channel.
|
* Returns the Twitch user ID of the owner of the channel.
|
||||||
|
@ -131,7 +107,7 @@ public:
|
||||||
* @lua@return string
|
* @lua@return string
|
||||||
* @exposed c2.Channel:get_twitch_id
|
* @exposed c2.Channel:get_twitch_id
|
||||||
*/
|
*/
|
||||||
static int get_twitch_id(lua_State *L);
|
QString get_twitch_id();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel is a Twitch channel and the user owns it
|
* Returns true if the channel is a Twitch channel and the user owns it
|
||||||
|
@ -139,7 +115,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_broadcaster
|
* @exposed c2.Channel:is_broadcaster
|
||||||
*/
|
*/
|
||||||
static int is_broadcaster(lua_State *L);
|
bool is_broadcaster();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
* Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
||||||
|
@ -148,7 +124,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_mod
|
* @exposed c2.Channel:is_mod
|
||||||
*/
|
*/
|
||||||
static int is_mod(lua_State *L);
|
bool is_mod();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
* Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
||||||
|
@ -157,7 +133,7 @@ public:
|
||||||
* @lua@return boolean
|
* @lua@return boolean
|
||||||
* @exposed c2.Channel:is_vip
|
* @exposed c2.Channel:is_vip
|
||||||
*/
|
*/
|
||||||
static int is_vip(lua_State *L);
|
bool is_vip();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Misc
|
* Misc
|
||||||
|
@ -167,7 +143,7 @@ public:
|
||||||
* @lua@return string
|
* @lua@return string
|
||||||
* @exposed c2.Channel:__tostring
|
* @exposed c2.Channel:__tostring
|
||||||
*/
|
*/
|
||||||
static int to_string(lua_State *L);
|
QString to_string();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static functions
|
* Static functions
|
||||||
|
@ -184,11 +160,10 @@ public:
|
||||||
* - /automod
|
* - /automod
|
||||||
*
|
*
|
||||||
* @lua@param name string Which channel are you looking for?
|
* @lua@param name string Which channel are you looking for?
|
||||||
* @lua@param platform c2.Platform Where to search for the channel?
|
|
||||||
* @lua@return c2.Channel?
|
* @lua@return c2.Channel?
|
||||||
* @exposed c2.Channel.by_name
|
* @exposed c2.Channel.by_name
|
||||||
*/
|
*/
|
||||||
static int get_by_name(lua_State *L);
|
static std::optional<ChannelRef> get_by_name(const QString &name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a channel by the Twitch user ID of its owner.
|
* Finds a channel by the Twitch user ID of its owner.
|
||||||
|
@ -197,79 +172,24 @@ public:
|
||||||
* @lua@return c2.Channel?
|
* @lua@return c2.Channel?
|
||||||
* @exposed c2.Channel.by_twitch_id
|
* @exposed c2.Channel.by_twitch_id
|
||||||
*/
|
*/
|
||||||
static int get_by_twitch_id(lua_State *L);
|
static std::optional<ChannelRef> get_by_twitch_id(const QString &id);
|
||||||
};
|
|
||||||
|
|
||||||
// This is a copy of the TwitchChannel::RoomModes structure, except it uses nicer optionals
|
static void createUserType(sol::table &c2);
|
||||||
/**
|
|
||||||
* @lua@class RoomModes
|
|
||||||
*/
|
|
||||||
struct LuaRoomModes {
|
|
||||||
/**
|
|
||||||
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
|
||||||
*/
|
|
||||||
bool unique_chat = false;
|
|
||||||
|
|
||||||
/**
|
private:
|
||||||
* @lua@field subscriber_only boolean
|
std::weak_ptr<Channel> weak;
|
||||||
*/
|
|
||||||
bool subscriber_only = false;
|
|
||||||
|
|
||||||
/**
|
/// Locks the weak pointer and throws if the pointer expired
|
||||||
* @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
std::shared_ptr<Channel> strong();
|
||||||
*/
|
|
||||||
bool emotes_only = false;
|
|
||||||
|
|
||||||
/**
|
/// Locks the weak pointer and throws if the pointer is invalid
|
||||||
* @lua@field follower_only number? Time in minutes you need to follow to chat or nil.
|
std::shared_ptr<TwitchChannel> twitch();
|
||||||
*/
|
|
||||||
std::optional<int> follower_only;
|
|
||||||
/**
|
|
||||||
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
|
||||||
*/
|
|
||||||
std::optional<int> slow_mode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@class StreamStatus
|
|
||||||
*/
|
|
||||||
struct LuaStreamStatus {
|
|
||||||
/**
|
|
||||||
* @lua@field live boolean
|
|
||||||
*/
|
|
||||||
bool live = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field viewer_count number
|
|
||||||
*/
|
|
||||||
int viewer_count = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field uptime number Seconds since the stream started.
|
|
||||||
*/
|
|
||||||
int uptime = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field title string Stream title or last stream title
|
|
||||||
*/
|
|
||||||
QString title;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field game_name string
|
|
||||||
*/
|
|
||||||
QString game_name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @lua@field game_id string
|
|
||||||
*/
|
|
||||||
QString game_id;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
||||||
|
sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes);
|
||||||
|
sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status);
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes);
|
|
||||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status);
|
|
||||||
StackIdx push(lua_State *L, ChannelPtr chn);
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
14
src/controllers/plugins/api/EventType.hpp
Normal file
14
src/controllers/plugins/api/EventType.hpp
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
|
||||||
|
namespace chatterino::lua::api {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exposeenum c2.EventType
|
||||||
|
*/
|
||||||
|
enum class EventType {
|
||||||
|
CompletionRequested,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino::lua::api
|
||||||
|
#endif
|
|
@ -6,402 +6,173 @@
|
||||||
# include "common/network/NetworkRequest.hpp"
|
# include "common/network/NetworkRequest.hpp"
|
||||||
# include "common/network/NetworkResult.hpp"
|
# include "common/network/NetworkResult.hpp"
|
||||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "util/DebugCount.hpp"
|
# include "util/DebugCount.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
# include <QChar>
|
||||||
|
# include <QLoggingCategory>
|
||||||
# include <QRandomGenerator>
|
# include <QRandomGenerator>
|
||||||
# include <QUrl>
|
# include <QUrl>
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
# include <sol/raii.hpp>
|
||||||
|
# include <sol/state_view.hpp>
|
||||||
|
# include <sol/table.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <optional>
|
||||||
|
# include <stdexcept>
|
||||||
# include <utility>
|
# include <utility>
|
||||||
|
# include <vector>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(*vararg)
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
|
||||||
static const luaL_Reg HTTP_REQUEST_METHODS[] = {
|
|
||||||
{"on_success", &HTTPRequest::on_success_wrap},
|
|
||||||
{"on_error", &HTTPRequest::on_error_wrap},
|
|
||||||
{"finally", &HTTPRequest::finally_wrap},
|
|
||||||
|
|
||||||
{"execute", &HTTPRequest::execute_wrap},
|
void HTTPRequest::createUserType(sol::table &c2)
|
||||||
{"set_timeout", &HTTPRequest::set_timeout_wrap},
|
|
||||||
{"set_payload", &HTTPRequest::set_payload_wrap},
|
|
||||||
{"set_header", &HTTPRequest::set_header_wrap},
|
|
||||||
// static
|
|
||||||
{"create", &HTTPRequest::create},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
std::shared_ptr<HTTPRequest> HTTPRequest::getOrError(lua_State *L,
|
|
||||||
StackIdx where)
|
|
||||||
{
|
{
|
||||||
if (lua_gettop(L) < 1)
|
c2.new_usertype<HTTPRequest>( //
|
||||||
{
|
"HTTPRequest", sol::no_constructor, //
|
||||||
// The nullptr is there just to appease the compiler, luaL_error is no return
|
sol::meta_method::to_string, &HTTPRequest::to_string, //
|
||||||
luaL_error(L, "Called c2.HTTPRequest method without a request object");
|
|
||||||
return nullptr;
|
"on_success", &HTTPRequest::on_success, //
|
||||||
}
|
"on_error", &HTTPRequest::on_error, //
|
||||||
if (lua_isuserdata(L, where) == 0)
|
"finally", &HTTPRequest::finally, //
|
||||||
{
|
|
||||||
luaL_error(
|
"set_timeout", &HTTPRequest::set_timeout, //
|
||||||
L,
|
"set_payload", &HTTPRequest::set_payload, //
|
||||||
"Called c2.HTTPRequest method with a non-userdata 'self' argument");
|
"set_header", &HTTPRequest::set_header, //
|
||||||
return nullptr;
|
"execute", &HTTPRequest::execute, //
|
||||||
}
|
|
||||||
// luaL_checkudata is no-return if check fails
|
"create", &HTTPRequest::create //
|
||||||
auto *checked = luaL_checkudata(L, where, "c2.HTTPRequest");
|
);
|
||||||
auto *data =
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPRequest, HTTPRequest>::from(
|
|
||||||
checked);
|
|
||||||
if (data == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Called c2.HTTPRequest method with an invalid pointer");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
lua_remove(L, where);
|
|
||||||
if (data->target == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L, "Internal error: SharedPtrUserData<UserData::Type::HTTPRequest, "
|
|
||||||
"HTTPRequest>::target was null. This is a Chatterino bug!");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (data->target->done)
|
|
||||||
{
|
|
||||||
luaL_error(L, "This c2.HTTPRequest has already been executed!");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return data->target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPRequest::createMetatable(lua_State *L)
|
void HTTPRequest::on_success(sol::protected_function func)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 1);
|
this->cbSuccess = std::make_optional(func);
|
||||||
|
|
||||||
luaL_newmetatable(L, "c2.HTTPRequest");
|
|
||||||
lua_pushstring(L, "__index");
|
|
||||||
lua_pushvalue(L, -2); // clone metatable
|
|
||||||
lua_settable(L, -3); // metatable.__index = metatable
|
|
||||||
|
|
||||||
// Generic ISharedResource stuff
|
|
||||||
lua_pushstring(L, "__gc");
|
|
||||||
lua_pushcfunction(L, (&SharedPtrUserData<UserData::Type::HTTPRequest,
|
|
||||||
HTTPRequest>::destroy));
|
|
||||||
lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy
|
|
||||||
|
|
||||||
luaL_setfuncs(L, HTTP_REQUEST_METHODS, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_success_wrap(lua_State *L)
|
void HTTPRequest::on_error(sol::protected_function func)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, -2);
|
this->cbError = std::make_optional(func);
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->on_success(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_success(lua_State *L)
|
void HTTPRequest::set_timeout(int timeout)
|
||||||
{
|
{
|
||||||
auto top = lua_gettop(L);
|
this->timeout_ = timeout;
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_success needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
if (!lua_isfunction(L, top))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_success needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
auto shared = this->pushPrivate(L);
|
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_setfield(L, shared, "success"); // this deletes the function copy
|
|
||||||
lua_pop(L, 2); // delete the table and function original
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_error_wrap(lua_State *L)
|
void HTTPRequest::finally(sol::protected_function func)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, -2);
|
this->cbFinally = std::make_optional(func);
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->on_error(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::on_error(lua_State *L)
|
void HTTPRequest::set_payload(QByteArray payload)
|
||||||
{
|
{
|
||||||
auto top = lua_gettop(L);
|
this->req_ = std::move(this->req_).payload(payload);
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_error needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
if (!lua_isfunction(L, top))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:on_error needs 1 argument (a callback "
|
|
||||||
"that takes an HTTPResult and doesn't return anything)");
|
|
||||||
}
|
|
||||||
auto shared = this->pushPrivate(L);
|
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_setfield(L, shared, "error"); // this deletes the function copy
|
|
||||||
lua_pop(L, 2); // delete the table and function original
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::set_timeout_wrap(lua_State *L)
|
// name and value may be random bytes
|
||||||
|
void HTTPRequest::set_header(QByteArray name, QByteArray value)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, -2);
|
this->req_ = std::move(this->req_).header(name, value);
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->set_timeout(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::set_timeout(lua_State *L)
|
std::shared_ptr<HTTPRequest> HTTPRequest::create(sol::this_state L,
|
||||||
|
NetworkRequestType method,
|
||||||
|
QString url)
|
||||||
{
|
{
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_timeout needs 1 argument (a number of "
|
|
||||||
"milliseconds after which the request will time out)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int temporary = -1;
|
|
||||||
if (!lua::pop(L, &temporary))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_timeout failed to get timeout, expected a "
|
|
||||||
"positive integer");
|
|
||||||
}
|
|
||||||
if (temporary <= 0)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_timeout failed to get timeout, expected a "
|
|
||||||
"positive integer");
|
|
||||||
}
|
|
||||||
this->timeout_ = temporary;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::finally_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -2);
|
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->finally(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::finally(lua_State *L)
|
|
||||||
{
|
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback "
|
|
||||||
"that takes nothing and doesn't return anything)");
|
|
||||||
}
|
|
||||||
if (!lua_isfunction(L, top))
|
|
||||||
{
|
|
||||||
return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback "
|
|
||||||
"that takes nothing and doesn't return anything)");
|
|
||||||
}
|
|
||||||
auto shared = this->pushPrivate(L);
|
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_setfield(L, shared, "finally"); // this deletes the function copy
|
|
||||||
lua_pop(L, 2); // delete the table and function original
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_payload_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -2);
|
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->set_payload(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_payload(lua_State *L)
|
|
||||||
{
|
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_payload needs 1 argument (a string payload)");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string temporary;
|
|
||||||
if (!lua::pop(L, &temporary))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_payload failed to get payload, expected a "
|
|
||||||
"string");
|
|
||||||
}
|
|
||||||
this->req_ =
|
|
||||||
std::move(this->req_).payload(QByteArray::fromStdString(temporary));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_header_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -3);
|
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
|
||||||
return ptr->set_header(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::set_header(lua_State *L)
|
|
||||||
{
|
|
||||||
auto top = lua_gettop(L);
|
|
||||||
if (top != 2)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest:set_header needs 2 arguments (a header name "
|
|
||||||
"and a value)");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string value;
|
|
||||||
if (!lua::pop(L, &value))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "cannot get value (2nd argument of HTTPRequest:set_header)");
|
|
||||||
}
|
|
||||||
std::string name;
|
|
||||||
if (!lua::pop(L, &name))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "cannot get name (1st argument of HTTPRequest:set_header)");
|
|
||||||
}
|
|
||||||
this->req_ = std::move(this->req_)
|
|
||||||
.header(QByteArray::fromStdString(name),
|
|
||||||
QByteArray::fromStdString(value));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPRequest::create(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, -1);
|
|
||||||
if (lua_gettop(L) != 2)
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "HTTPRequest.create needs exactly 2 arguments (method "
|
|
||||||
"and url)");
|
|
||||||
}
|
|
||||||
QString url;
|
|
||||||
if (!lua::pop(L, &url))
|
|
||||||
{
|
|
||||||
return luaL_error(L,
|
|
||||||
"cannot get url (2nd argument of HTTPRequest.create, "
|
|
||||||
"expected a string)");
|
|
||||||
}
|
|
||||||
auto parsedurl = QUrl(url);
|
auto parsedurl = QUrl(url);
|
||||||
if (!parsedurl.isValid())
|
if (!parsedurl.isValid())
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error(
|
||||||
L, "cannot parse url (2nd argument of HTTPRequest.create, "
|
"cannot parse url (2nd argument of HTTPRequest.create, "
|
||||||
"got invalid url in argument)");
|
"got invalid url in argument)");
|
||||||
}
|
}
|
||||||
NetworkRequestType method{};
|
|
||||||
if (!lua::pop(L, &method))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L, "cannot get method (1st argument of HTTPRequest.create, "
|
|
||||||
"expected a string)");
|
|
||||||
}
|
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
if (!pl->hasHTTPPermissionFor(parsedurl))
|
if (!pl->hasHTTPPermissionFor(parsedurl))
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error(
|
||||||
L, "Plugin does not have permission to send HTTP requests "
|
"Plugin does not have permission to send HTTP requests "
|
||||||
"to this URL");
|
"to this URL");
|
||||||
}
|
}
|
||||||
NetworkRequest r(parsedurl, method);
|
NetworkRequest r(parsedurl, method);
|
||||||
lua::push(
|
return std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r));
|
||||||
L, std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r)));
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPRequest::execute_wrap(lua_State *L)
|
void HTTPRequest::execute(sol::this_state L)
|
||||||
{
|
{
|
||||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
if (this->done)
|
||||||
return ptr->execute(L);
|
{
|
||||||
}
|
throw std::runtime_error(
|
||||||
|
"Cannot execute this c2.HTTPRequest, it was executed already!");
|
||||||
int HTTPRequest::execute(lua_State *L)
|
}
|
||||||
{
|
|
||||||
auto shared = this->shared_from_this();
|
|
||||||
this->done = true;
|
this->done = true;
|
||||||
|
|
||||||
|
// this keeps the object alive even if Lua were to forget about it,
|
||||||
|
auto hack = this->weak_from_this();
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
pl->httpRequests.push_back(this->shared_from_this());
|
||||||
|
|
||||||
std::move(this->req_)
|
std::move(this->req_)
|
||||||
.onSuccess([shared, L](const NetworkResult &res) {
|
.onSuccess([L, hack](const NetworkResult &res) {
|
||||||
|
auto self = hack.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!self->cbSuccess.has_value())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
auto *thread = lua_newthread(L);
|
(*self->cbSuccess)(HTTPResponse(res));
|
||||||
|
self->cbSuccess = std::nullopt;
|
||||||
auto priv = shared->pushPrivate(thread);
|
|
||||||
lua_getfield(thread, priv, "success");
|
|
||||||
auto cb = lua_gettop(thread);
|
|
||||||
if (lua_isfunction(thread, cb))
|
|
||||||
{
|
|
||||||
lua::push(thread, std::make_shared<HTTPResponse>(res));
|
|
||||||
// one arg, no return, no msgh
|
|
||||||
lua_pcall(thread, 1, 0, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pop(thread, 1); // remove callback
|
|
||||||
}
|
|
||||||
lua_closethread(thread, nullptr);
|
|
||||||
lua_pop(L, 1); // remove thread from L
|
|
||||||
})
|
})
|
||||||
.onError([shared, L](const NetworkResult &res) {
|
.onError([L, hack](const NetworkResult &res) {
|
||||||
|
auto self = hack.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!self->cbError.has_value())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
auto *thread = lua_newthread(L);
|
(*self->cbError)(HTTPResponse(res));
|
||||||
|
self->cbError = std::nullopt;
|
||||||
auto priv = shared->pushPrivate(thread);
|
|
||||||
lua_getfield(thread, priv, "error");
|
|
||||||
auto cb = lua_gettop(thread);
|
|
||||||
if (lua_isfunction(thread, cb))
|
|
||||||
{
|
|
||||||
lua::push(thread, std::make_shared<HTTPResponse>(res));
|
|
||||||
// one arg, no return, no msgh
|
|
||||||
lua_pcall(thread, 1, 0, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pop(thread, 1); // remove callback
|
|
||||||
}
|
|
||||||
lua_closethread(thread, nullptr);
|
|
||||||
lua_pop(L, 1); // remove thread from L
|
|
||||||
})
|
})
|
||||||
.finally([shared, L]() {
|
.finally([L, hack]() {
|
||||||
|
auto self = hack.lock();
|
||||||
|
if (!self)
|
||||||
|
{
|
||||||
|
// this could happen if the plugin was deleted
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
for (auto it = pl->httpRequests.begin();
|
||||||
|
it < pl->httpRequests.end(); it++)
|
||||||
|
{
|
||||||
|
if (*it == self)
|
||||||
|
{
|
||||||
|
pl->httpRequests.erase(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->cbFinally.has_value())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
lua::StackGuard guard(L);
|
lua::StackGuard guard(L);
|
||||||
auto *thread = lua_newthread(L);
|
(*self->cbFinally)();
|
||||||
|
self->cbFinally = std::nullopt;
|
||||||
auto priv = shared->pushPrivate(thread);
|
|
||||||
lua_getfield(thread, priv, "finally");
|
|
||||||
auto cb = lua_gettop(thread);
|
|
||||||
if (lua_isfunction(thread, cb))
|
|
||||||
{
|
|
||||||
// no args, no return, no msgh
|
|
||||||
lua_pcall(thread, 0, 0, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pop(thread, 1); // remove callback
|
|
||||||
}
|
|
||||||
// remove our private data
|
|
||||||
lua_pushnil(thread);
|
|
||||||
lua_setfield(thread, LUA_REGISTRYINDEX,
|
|
||||||
shared->privateKey.toStdString().c_str());
|
|
||||||
lua_closethread(thread, nullptr);
|
|
||||||
lua_pop(L, 1); // remove thread from L
|
|
||||||
|
|
||||||
// we removed our private table, forget the key for it
|
|
||||||
shared->privateKey = QString();
|
|
||||||
})
|
})
|
||||||
.timeout(this->timeout_)
|
.timeout(this->timeout_)
|
||||||
.execute();
|
.execute();
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
|
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
|
||||||
|
@ -418,34 +189,10 @@ HTTPRequest::~HTTPRequest()
|
||||||
// but that's better than accessing a possibly invalid lua_State pointer.
|
// but that's better than accessing a possibly invalid lua_State pointer.
|
||||||
}
|
}
|
||||||
|
|
||||||
StackIdx HTTPRequest::pushPrivate(lua_State *L)
|
QString HTTPRequest::to_string()
|
||||||
{
|
{
|
||||||
if (this->privateKey.isEmpty())
|
return "<HTTPRequest>";
|
||||||
{
|
|
||||||
this->privateKey = QString("HTTPRequestPrivate%1")
|
|
||||||
.arg(QRandomGenerator::system()->generate());
|
|
||||||
pushEmptyTable(L, 4);
|
|
||||||
lua_setfield(L, LUA_REGISTRYINDEX,
|
|
||||||
this->privateKey.toStdString().c_str());
|
|
||||||
}
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, this->privateKey.toStdString().c_str());
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTEND(*vararg)
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
namespace chatterino::lua {
|
|
||||||
|
|
||||||
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPRequest> request)
|
|
||||||
{
|
|
||||||
using namespace chatterino::lua::api;
|
|
||||||
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPRequest, HTTPRequest>::create(
|
|
||||||
L, std::move(request));
|
|
||||||
luaL_getmetatable(L, "c2.HTTPRequest");
|
|
||||||
lua_setmetatable(L, -2);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,10 +2,16 @@
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "common/network/NetworkRequest.hpp"
|
# include "common/network/NetworkRequest.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
|
||||||
|
# include <sol/forward.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
class PluginController;
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(readability-identifier-naming)
|
// NOLINTBEGIN(readability-identifier-naming)
|
||||||
|
|
||||||
|
@ -33,33 +39,19 @@ public:
|
||||||
private:
|
private:
|
||||||
NetworkRequest req_;
|
NetworkRequest req_;
|
||||||
|
|
||||||
static void createMetatable(lua_State *L);
|
static void createUserType(sol::table &c2);
|
||||||
friend class chatterino::PluginController;
|
friend class chatterino::PluginController;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPRequest
|
|
||||||
*
|
|
||||||
* If the object given is not a userdatum or the pointer inside that
|
|
||||||
* userdatum doesn't point to a HTTPRequest, a lua error is thrown.
|
|
||||||
*
|
|
||||||
* This function always returns a non-null pointer.
|
|
||||||
*/
|
|
||||||
static std::shared_ptr<HTTPRequest> getOrError(lua_State *L,
|
|
||||||
StackIdx where = -1);
|
|
||||||
/**
|
|
||||||
* Pushes the private table onto the lua stack.
|
|
||||||
*
|
|
||||||
* This might create it if it doesn't exist.
|
|
||||||
*/
|
|
||||||
StackIdx pushPrivate(lua_State *L);
|
|
||||||
|
|
||||||
// This is the key in the registry the private table it held at (if it exists)
|
// This is the key in the registry the private table it held at (if it exists)
|
||||||
// This might be a null QString if the request has already been executed or
|
// This might be a null QString if the request has already been executed or
|
||||||
// the table wasn't created yet.
|
// the table wasn't created yet.
|
||||||
QString privateKey;
|
|
||||||
int timeout_ = 10'000;
|
int timeout_ = 10'000;
|
||||||
bool done = false;
|
bool done = false;
|
||||||
|
|
||||||
|
std::optional<sol::protected_function> cbSuccess;
|
||||||
|
std::optional<sol::protected_function> cbError;
|
||||||
|
std::optional<sol::protected_function> cbFinally;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// These functions are wrapped so data can be accessed more easily. When a call from Lua comes in:
|
// These functions are wrapped so data can be accessed more easily. When a call from Lua comes in:
|
||||||
// - the static wrapper function is called
|
// - the static wrapper function is called
|
||||||
|
@ -72,8 +64,7 @@ public:
|
||||||
* @lua@param callback HTTPCallback Function to call when the HTTP request succeeds
|
* @lua@param callback HTTPCallback Function to call when the HTTP request succeeds
|
||||||
* @exposed HTTPRequest:on_success
|
* @exposed HTTPRequest:on_success
|
||||||
*/
|
*/
|
||||||
static int on_success_wrap(lua_State *L);
|
void on_success(sol::protected_function func);
|
||||||
int on_success(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the failure callback
|
* Sets the failure callback
|
||||||
|
@ -81,8 +72,7 @@ public:
|
||||||
* @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
|
* @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
|
||||||
* @exposed HTTPRequest:on_error
|
* @exposed HTTPRequest:on_error
|
||||||
*/
|
*/
|
||||||
static int on_error_wrap(lua_State *L);
|
void on_error(sol::protected_function func);
|
||||||
int on_error(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the finally callback
|
* Sets the finally callback
|
||||||
|
@ -90,8 +80,7 @@ public:
|
||||||
* @lua@param callback fun(): nil Function to call when the HTTP request finishes
|
* @lua@param callback fun(): nil Function to call when the HTTP request finishes
|
||||||
* @exposed HTTPRequest:finally
|
* @exposed HTTPRequest:finally
|
||||||
*/
|
*/
|
||||||
static int finally_wrap(lua_State *L);
|
void finally(sol::protected_function func);
|
||||||
int finally(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the timeout
|
* Sets the timeout
|
||||||
|
@ -99,8 +88,7 @@ public:
|
||||||
* @lua@param timeout integer How long in milliseconds until the times out
|
* @lua@param timeout integer How long in milliseconds until the times out
|
||||||
* @exposed HTTPRequest:set_timeout
|
* @exposed HTTPRequest:set_timeout
|
||||||
*/
|
*/
|
||||||
static int set_timeout_wrap(lua_State *L);
|
void set_timeout(int timeout);
|
||||||
int set_timeout(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the request payload
|
* Sets the request payload
|
||||||
|
@ -108,8 +96,7 @@ public:
|
||||||
* @lua@param data string
|
* @lua@param data string
|
||||||
* @exposed HTTPRequest:set_payload
|
* @exposed HTTPRequest:set_payload
|
||||||
*/
|
*/
|
||||||
static int set_payload_wrap(lua_State *L);
|
void set_payload(QByteArray payload);
|
||||||
int set_payload(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a header in the request
|
* Sets a header in the request
|
||||||
|
@ -118,16 +105,19 @@ public:
|
||||||
* @lua@param value string
|
* @lua@param value string
|
||||||
* @exposed HTTPRequest:set_header
|
* @exposed HTTPRequest:set_header
|
||||||
*/
|
*/
|
||||||
static int set_header_wrap(lua_State *L);
|
void set_header(QByteArray name, QByteArray value);
|
||||||
int set_header(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the HTTP request
|
* Executes the HTTP request
|
||||||
*
|
*
|
||||||
* @exposed HTTPRequest:execute
|
* @exposed HTTPRequest:execute
|
||||||
*/
|
*/
|
||||||
static int execute_wrap(lua_State *L);
|
void execute(sol::this_state L);
|
||||||
int execute(lua_State *L);
|
/**
|
||||||
|
* @lua@return string
|
||||||
|
* @exposed HTTPRequest:__tostring
|
||||||
|
*/
|
||||||
|
QString to_string();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static functions
|
* Static functions
|
||||||
|
@ -142,7 +132,9 @@ public:
|
||||||
* @lua@return HTTPRequest
|
* @lua@return HTTPRequest
|
||||||
* @exposed HTTPRequest.create
|
* @exposed HTTPRequest.create
|
||||||
*/
|
*/
|
||||||
static int create(lua_State *L);
|
static std::shared_ptr<HTTPRequest> create(sol::this_state L,
|
||||||
|
NetworkRequestType method,
|
||||||
|
QString url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
|
|
|
@ -2,77 +2,28 @@
|
||||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||||
|
|
||||||
# include "common/network/NetworkResult.hpp"
|
# include "common/network/NetworkResult.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "util/DebugCount.hpp"
|
# include "util/DebugCount.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
}
|
# include <sol/raii.hpp>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
|
||||||
# include <utility>
|
# include <utility>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
// NOLINTBEGIN(*vararg)
|
|
||||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
|
||||||
static const luaL_Reg HTTP_RESPONSE_METHODS[] = {
|
|
||||||
{"data", &HTTPResponse::data_wrap},
|
|
||||||
{"status", &HTTPResponse::status_wrap},
|
|
||||||
{"error", &HTTPResponse::error_wrap},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
void HTTPResponse::createMetatable(lua_State *L)
|
void HTTPResponse::createUserType(sol::table &c2)
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 1);
|
c2.new_usertype<HTTPResponse>( //
|
||||||
|
"HTTPResponse", sol::no_constructor,
|
||||||
|
// metamethods
|
||||||
|
sol::meta_method::to_string, &HTTPResponse::to_string, //
|
||||||
|
|
||||||
luaL_newmetatable(L, "c2.HTTPResponse");
|
"data", &HTTPResponse::data, //
|
||||||
lua_pushstring(L, "__index");
|
"status", &HTTPResponse::status, //
|
||||||
lua_pushvalue(L, -2); // clone metatable
|
"error", &HTTPResponse::error //
|
||||||
lua_settable(L, -3); // metatable.__index = metatable
|
);
|
||||||
|
|
||||||
// Generic ISharedResource stuff
|
|
||||||
lua_pushstring(L, "__gc");
|
|
||||||
lua_pushcfunction(L, (&SharedPtrUserData<UserData::Type::HTTPResponse,
|
|
||||||
HTTPResponse>::destroy));
|
|
||||||
lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy
|
|
||||||
|
|
||||||
luaL_setfuncs(L, HTTP_RESPONSE_METHODS, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<HTTPResponse> HTTPResponse::getOrError(lua_State *L,
|
|
||||||
StackIdx where)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) < 1)
|
|
||||||
{
|
|
||||||
// The nullptr is there just to appease the compiler, luaL_error is no return
|
|
||||||
luaL_error(L, "Called c2.HTTPResponse method without a request object");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (lua_isuserdata(L, where) == 0)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Called c2.HTTPResponse method with a non-userdata "
|
|
||||||
"'self' argument");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
// luaL_checkudata is no-return if check fails
|
|
||||||
auto *checked = luaL_checkudata(L, where, "c2.HTTPResponse");
|
|
||||||
auto *data =
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPResponse, HTTPResponse>::from(
|
|
||||||
checked);
|
|
||||||
if (data == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(L, "Called c2.HTTPResponse method with an invalid pointer");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
lua_remove(L, where);
|
|
||||||
if (data->target == nullptr)
|
|
||||||
{
|
|
||||||
luaL_error(
|
|
||||||
L,
|
|
||||||
"Internal error: SharedPtrUserData<UserData::Type::HTTPResponse, "
|
|
||||||
"HTTPResponse>::target was null. This is a Chatterino bug!");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return data->target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPResponse::HTTPResponse(NetworkResult res)
|
HTTPResponse::HTTPResponse(NetworkResult res)
|
||||||
|
@ -85,60 +36,30 @@ HTTPResponse::~HTTPResponse()
|
||||||
DebugCount::decrease("lua::api::HTTPResponse");
|
DebugCount::decrease("lua::api::HTTPResponse");
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::data_wrap(lua_State *L)
|
QByteArray HTTPResponse::data()
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
return this->result_.getData();
|
||||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
|
||||||
return ptr->data(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::data(lua_State *L)
|
std::optional<int> HTTPResponse::status()
|
||||||
{
|
{
|
||||||
lua::push(L, this->result_.getData().toStdString());
|
return this->result_.status();
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::status_wrap(lua_State *L)
|
QString HTTPResponse::error()
|
||||||
{
|
{
|
||||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
return this->result_.formatError();
|
||||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
|
||||||
return ptr->status(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::status(lua_State *L)
|
QString HTTPResponse::to_string()
|
||||||
{
|
{
|
||||||
lua::push(L, this->result_.status());
|
if (this->status().has_value())
|
||||||
return 1;
|
{
|
||||||
|
return QStringView(u"<c2.HTTPResponse status %1>")
|
||||||
|
.arg(QString::number(*this->status()));
|
||||||
|
}
|
||||||
|
return "<c2.HTTPResponse no status>";
|
||||||
}
|
}
|
||||||
|
|
||||||
int HTTPResponse::error_wrap(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
|
||||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
|
||||||
return ptr->error(L);
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPResponse::error(lua_State *L)
|
|
||||||
{
|
|
||||||
lua::push(L, this->result_.formatError());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOLINTEND(*vararg)
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPResponse> request)
|
|
||||||
{
|
|
||||||
using namespace chatterino::lua::api;
|
|
||||||
|
|
||||||
// Prepare table
|
|
||||||
SharedPtrUserData<UserData::Type::HTTPResponse, HTTPResponse>::create(
|
|
||||||
L, std::move(request));
|
|
||||||
luaL_getmetatable(L, "c2.HTTPResponse");
|
|
||||||
lua_setmetatable(L, -2);
|
|
||||||
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
# include "common/network/NetworkResult.hpp"
|
# include "common/network/NetworkResult.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
|
||||||
|
# include <lua.h>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
# include <memory>
|
# include <memory>
|
||||||
extern "C" {
|
|
||||||
# include <lua.h>
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
class PluginController;
|
class PluginController;
|
||||||
|
@ -18,7 +17,7 @@ namespace chatterino::lua::api {
|
||||||
/**
|
/**
|
||||||
* @lua@class HTTPResponse
|
* @lua@class HTTPResponse
|
||||||
*/
|
*/
|
||||||
class HTTPResponse : public std::enable_shared_from_this<HTTPResponse>
|
class HTTPResponse
|
||||||
{
|
{
|
||||||
NetworkResult result_;
|
NetworkResult result_;
|
||||||
|
|
||||||
|
@ -31,20 +30,9 @@ public:
|
||||||
~HTTPResponse();
|
~HTTPResponse();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void createMetatable(lua_State *L);
|
static void createUserType(sol::table &c2);
|
||||||
friend class chatterino::PluginController;
|
friend class chatterino::PluginController;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPResponse
|
|
||||||
*
|
|
||||||
* If the object given is not a userdatum or the pointer inside that
|
|
||||||
* userdatum doesn't point to a HTTPResponse, a lua error is thrown.
|
|
||||||
*
|
|
||||||
* This function always returns a non-null pointer.
|
|
||||||
*/
|
|
||||||
static std::shared_ptr<HTTPResponse> getOrError(lua_State *L,
|
|
||||||
StackIdx where = -1);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* Returns the data. This is not guaranteed to be encoded using any
|
* Returns the data. This is not guaranteed to be encoded using any
|
||||||
|
@ -52,29 +40,28 @@ public:
|
||||||
*
|
*
|
||||||
* @exposed HTTPResponse:data
|
* @exposed HTTPResponse:data
|
||||||
*/
|
*/
|
||||||
static int data_wrap(lua_State *L);
|
QByteArray data();
|
||||||
int data(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status code.
|
* Returns the status code.
|
||||||
*
|
*
|
||||||
* @exposed HTTPResponse:status
|
* @exposed HTTPResponse:status
|
||||||
*/
|
*/
|
||||||
static int status_wrap(lua_State *L);
|
std::optional<int> status();
|
||||||
int status(lua_State *L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A somewhat human readable description of an error if such happened
|
* A somewhat human readable description of an error if such happened
|
||||||
* @exposed HTTPResponse:error
|
* @exposed HTTPResponse:error
|
||||||
*/
|
*/
|
||||||
|
QString error();
|
||||||
|
|
||||||
static int error_wrap(lua_State *L);
|
/**
|
||||||
int error(lua_State *L);
|
* @lua@return string
|
||||||
|
* @exposed HTTPResponse:__tostring
|
||||||
|
*/
|
||||||
|
QString to_string();
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
namespace chatterino::lua {
|
|
||||||
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPResponse> request);
|
|
||||||
} // namespace chatterino::lua
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,15 +2,17 @@
|
||||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "controllers/plugins/LuaUtilities.hpp"
|
# include "common/QLogging.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
# include <lauxlib.h>
|
# include <lauxlib.h>
|
||||||
# include <lua.h>
|
# include <lua.h>
|
||||||
}
|
# include <QString>
|
||||||
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
# include <cerrno>
|
# include <cerrno>
|
||||||
|
# include <stdexcept>
|
||||||
|
# include <utility>
|
||||||
|
|
||||||
namespace chatterino::lua::api {
|
namespace chatterino::lua::api {
|
||||||
|
|
||||||
|
@ -91,45 +93,28 @@ struct LuaFileMode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
int ioError(lua_State *L, const QString &value, int errnoequiv)
|
sol::variadic_results ioError(lua_State *L, const QString &value,
|
||||||
|
int errnoequiv)
|
||||||
{
|
{
|
||||||
lua_pushnil(L);
|
sol::variadic_results out;
|
||||||
lua::push(L, value);
|
out.push_back(sol::nil);
|
||||||
lua::push(L, errnoequiv);
|
out.push_back(sol::make_object(L, value.toStdString()));
|
||||||
return 3;
|
out.push_back({L, sol::in_place_type<int>, errnoequiv});
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTBEGIN(*vararg)
|
sol::variadic_results io_open(sol::this_state L, QString filename,
|
||||||
int io_open(lua_State *L)
|
QString strmode)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
if (pl == nullptr)
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
luaL_error(L, "internal error: no plugin");
|
throw std::runtime_error("internal error: no plugin");
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
LuaFileMode mode;
|
LuaFileMode mode(strmode);
|
||||||
if (lua_gettop(L) == 2)
|
|
||||||
{
|
|
||||||
// we have a mode
|
|
||||||
QString smode;
|
|
||||||
if (!lua::pop(L, &smode))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L,
|
|
||||||
"io.open mode (2nd argument) must be a string or not present");
|
|
||||||
}
|
|
||||||
mode = LuaFileMode(smode);
|
|
||||||
if (!mode.error.isEmpty())
|
if (!mode.error.isEmpty())
|
||||||
{
|
{
|
||||||
return luaL_error(L, mode.error.toStdString().c_str());
|
throw std::runtime_error(mode.error.toStdString());
|
||||||
}
|
|
||||||
}
|
|
||||||
QString filename;
|
|
||||||
if (!lua::pop(L, &filename))
|
|
||||||
{
|
|
||||||
return luaL_error(L,
|
|
||||||
"io.open filename (1st argument) must be a string");
|
|
||||||
}
|
}
|
||||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||||
auto abs = file.absoluteFilePath();
|
auto abs = file.absoluteFilePath();
|
||||||
|
@ -144,39 +129,35 @@ int io_open(lua_State *L)
|
||||||
"Plugin does not have permissions to access given file.",
|
"Plugin does not have permissions to access given file.",
|
||||||
EACCES);
|
EACCES);
|
||||||
}
|
}
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
lua_getfield(L, -1, "open");
|
sol::state_view lua(L);
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
auto open = lua.registry()[REG_REAL_IO_NAME]["open"];
|
||||||
lua::push(L, abs);
|
sol::protected_function_result res =
|
||||||
lua::push(L, mode.toString());
|
open(abs.toStdString(), mode.toString().toStdString());
|
||||||
lua_call(L, 2, 3);
|
return res;
|
||||||
return 3;
|
}
|
||||||
|
sol::variadic_results io_open_modeless(sol::this_state L, QString filename)
|
||||||
|
{
|
||||||
|
return io_open(L, std::move(filename), "r");
|
||||||
}
|
}
|
||||||
|
|
||||||
int io_lines(lua_State *L)
|
sol::variadic_results io_lines_noargs(sol::this_state L)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"];
|
||||||
|
sol::protected_function_result res = lines();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::variadic_results io_lines(sol::this_state L, QString filename,
|
||||||
|
sol::variadic_args args)
|
||||||
{
|
{
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
if (pl == nullptr)
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
luaL_error(L, "internal error: no plugin");
|
throw std::runtime_error("internal error: no plugin");
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (lua_gettop(L) == 0)
|
|
||||||
{
|
|
||||||
// io.lines() case, just call realio.lines
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
lua_getfield(L, -1, "lines");
|
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
|
||||||
lua_call(L, 0, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
QString filename;
|
|
||||||
if (!lua::pop(L, &filename))
|
|
||||||
{
|
|
||||||
return luaL_error(
|
|
||||||
L,
|
|
||||||
"io.lines filename (1st argument) must be a string or not present");
|
|
||||||
}
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||||
auto abs = file.absoluteFilePath();
|
auto abs = file.absoluteFilePath();
|
||||||
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||||
|
@ -185,191 +166,168 @@ int io_lines(lua_State *L)
|
||||||
bool ok = pl->hasFSPermissionFor(false, abs);
|
bool ok = pl->hasFSPermissionFor(false, abs);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
{
|
{
|
||||||
return ioError(L,
|
throw std::runtime_error(
|
||||||
"Plugin does not have permissions to access given file.",
|
"Plugin does not have permissions to access given file.");
|
||||||
EACCES);
|
|
||||||
}
|
}
|
||||||
// Our stack looks like this:
|
|
||||||
// - {...}[1]
|
|
||||||
// - {...}[2]
|
|
||||||
// ...
|
|
||||||
// We want:
|
|
||||||
// - REG[REG_REAL_IO_NAME].lines
|
|
||||||
// - absolute file path
|
|
||||||
// - {...}[1]
|
|
||||||
// - {...}[2]
|
|
||||||
// ...
|
|
||||||
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"];
|
||||||
lua_getfield(L, -1, "lines");
|
sol::protected_function_result res = lines(abs.toStdString(), args);
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
return res;
|
||||||
lua_insert(L, 1); // move function to start of stack
|
|
||||||
lua::push(L, abs);
|
|
||||||
lua_insert(L, 2); // move file name just after the function
|
|
||||||
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
|
|
||||||
return lua_gettop(L);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
sol::variadic_results io_input_argless(sol::this_state L)
|
||||||
|
{
|
||||||
// This is the code for both io.input and io.output
|
|
||||||
int globalFileCommon(lua_State *L, bool output)
|
|
||||||
{
|
|
||||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
if (pl == nullptr)
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
luaL_error(L, "internal error: no plugin");
|
throw std::runtime_error("internal error: no plugin");
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
// Three signature cases:
|
sol::state_view lua(L);
|
||||||
// io.input()
|
|
||||||
// io.input(file)
|
|
||||||
// io.input(name)
|
|
||||||
if (lua_gettop(L) == 0)
|
|
||||||
{
|
|
||||||
// We have no arguments, call realio.input()
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
if (output)
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "output");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "input");
|
|
||||||
}
|
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
|
||||||
lua_call(L, 0, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (lua_gettop(L) != 1)
|
|
||||||
{
|
|
||||||
return luaL_error(L, "Too many arguments given to io.input().");
|
|
||||||
}
|
|
||||||
// Now check if we have a file or name
|
|
||||||
auto *p = luaL_testudata(L, 1, LUA_FILEHANDLE);
|
|
||||||
if (p == nullptr)
|
|
||||||
{
|
|
||||||
// this is not a file handle, send it to open
|
|
||||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, REG_C2_IO_NAME);
|
|
||||||
lua_getfield(L, -1, "open");
|
|
||||||
lua_remove(L, -2); // remove io
|
|
||||||
|
|
||||||
lua_pushvalue(L, 1); // dupe arg
|
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||||
if (output)
|
sol::protected_function_result res = func();
|
||||||
{
|
return res;
|
||||||
lua_pushstring(L, "w");
|
}
|
||||||
}
|
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file)
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_pushstring(L, "r");
|
|
||||||
}
|
|
||||||
lua_call(L, 2, 1); // call ourio.open(arg1, 'r'|'w')
|
|
||||||
// if this isn't a string ourio.open errors
|
|
||||||
|
|
||||||
// this leaves us with:
|
|
||||||
// 1. arg
|
|
||||||
// 2. new_file
|
|
||||||
lua_remove(L, 1); // remove arg, replacing it with new_file
|
|
||||||
}
|
|
||||||
|
|
||||||
// file handle, pass it off to realio.input
|
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
|
||||||
if (output)
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "output");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lua_getfield(L, -1, "input");
|
|
||||||
}
|
|
||||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
|
||||||
lua_pushvalue(L, 1); // duplicate arg
|
|
||||||
lua_call(L, 1, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int io_input(lua_State *L)
|
|
||||||
{
|
{
|
||||||
return globalFileCommon(L, false);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("internal error: no plugin");
|
||||||
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
|
|
||||||
|
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||||
|
sol::protected_function_result res = func(file);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
sol::variadic_results io_input_name(sol::this_state L, QString filename)
|
||||||
|
{
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("internal error: no plugin");
|
||||||
|
}
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto res = io_open(L, std::move(filename), "r");
|
||||||
|
if (res.size() != 1)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(res.at(1).as<std::string>());
|
||||||
|
}
|
||||||
|
auto obj = res.at(0);
|
||||||
|
if (obj.get_type() != sol::type::userdata)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("a file must be a userdata.");
|
||||||
|
}
|
||||||
|
return io_input_file(L, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
int io_output(lua_State *L)
|
sol::variadic_results io_output_argless(sol::this_state L)
|
||||||
{
|
{
|
||||||
return globalFileCommon(L, true);
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
}
|
if (pl == nullptr)
|
||||||
|
|
||||||
int io_close(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) > 1)
|
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error("internal error: no plugin");
|
||||||
L, "Too many arguments for io.close. Expected one or zero.");
|
|
||||||
}
|
}
|
||||||
if (lua_gettop(L) == 0)
|
sol::state_view lua(L);
|
||||||
|
|
||||||
|
auto func = lua.registry()[REG_REAL_IO_NAME]["output"];
|
||||||
|
sol::protected_function_result res = func();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
sol::variadic_results io_output_file(sol::this_state L, sol::userdata file)
|
||||||
|
{
|
||||||
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
throw std::runtime_error("internal error: no plugin");
|
||||||
}
|
}
|
||||||
lua_getfield(L, -1, "close");
|
sol::state_view lua(L);
|
||||||
lua_pushvalue(L, -2);
|
|
||||||
lua_call(L, 1, 0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int io_flush(lua_State *L)
|
auto func = lua.registry()[REG_REAL_IO_NAME]["output"];
|
||||||
|
sol::protected_function_result res = func(file);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
sol::variadic_results io_output_name(sol::this_state L, QString filename)
|
||||||
{
|
{
|
||||||
if (lua_gettop(L) > 1)
|
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||||
|
if (pl == nullptr)
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error("internal error: no plugin");
|
||||||
L, "Too many arguments for io.flush. Expected one or zero.");
|
|
||||||
}
|
}
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
sol::state_view lua(L);
|
||||||
lua_getfield(L, -1, "flush");
|
auto res = io_open(L, std::move(filename), "w");
|
||||||
lua_pushvalue(L, -2);
|
if (res.size() != 1)
|
||||||
lua_call(L, 1, 0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int io_read(lua_State *L)
|
|
||||||
{
|
|
||||||
if (lua_gettop(L) > 1)
|
|
||||||
{
|
{
|
||||||
return luaL_error(
|
throw std::runtime_error(res.at(1).as<std::string>());
|
||||||
L, "Too many arguments for io.read. Expected one or zero.");
|
|
||||||
}
|
}
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
auto obj = res.at(0);
|
||||||
lua_getfield(L, -1, "read");
|
if (obj.get_type() != sol::type::userdata)
|
||||||
lua_insert(L, 1);
|
{
|
||||||
lua_insert(L, 2);
|
throw std::runtime_error("internal error: a file must be a userdata.");
|
||||||
lua_call(L, lua_gettop(L) - 1, 1);
|
}
|
||||||
return 1;
|
return io_output_file(L, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
int io_write(lua_State *L)
|
bool io_close_argless(sol::this_state L)
|
||||||
{
|
{
|
||||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
sol::state_view lua(L);
|
||||||
lua_getfield(L, -1, "write");
|
auto out = lua.registry()["_IO_output"];
|
||||||
lua_insert(L, 1);
|
return io_close_file(L, out);
|
||||||
lua_insert(L, 2);
|
|
||||||
// (input)
|
|
||||||
// (input).read
|
|
||||||
// args
|
|
||||||
lua_call(L, lua_gettop(L) - 1, 1);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int io_popen(lua_State *L)
|
bool io_close_file(sol::this_state L, sol::userdata file)
|
||||||
{
|
{
|
||||||
return luaL_error(L, "io.popen: This function is a stub!");
|
sol::state_view lua(L);
|
||||||
|
return file["close"](file);
|
||||||
}
|
}
|
||||||
|
|
||||||
int io_tmpfile(lua_State *L)
|
void io_flush_argless(sol::this_state L)
|
||||||
{
|
{
|
||||||
return luaL_error(L, "io.tmpfile: This function is a stub!");
|
sol::state_view lua(L);
|
||||||
|
auto out = lua.registry()["_IO_output"];
|
||||||
|
io_flush_file(L, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTEND(*vararg)
|
void io_flush_file(sol::this_state L, sol::userdata file)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
file["flush"](file);
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::variadic_results io_read(sol::this_state L, sol::variadic_args args)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto inp = lua.registry()["_IO_input"];
|
||||||
|
if (!inp.is<sol::userdata>())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Input not set to a file");
|
||||||
|
}
|
||||||
|
sol::protected_function read = inp["read"];
|
||||||
|
return read(inp, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::variadic_results io_write(sol::this_state L, sol::variadic_args args)
|
||||||
|
{
|
||||||
|
sol::state_view lua(L);
|
||||||
|
auto out = lua.registry()["_IO_output"];
|
||||||
|
if (!out.is<sol::userdata>())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Output not set to a file");
|
||||||
|
}
|
||||||
|
sol::protected_function write = out["write"];
|
||||||
|
return write(out, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
void io_popen()
|
||||||
|
{
|
||||||
|
throw std::runtime_error("io.popen: This function is a stub!");
|
||||||
|
}
|
||||||
|
|
||||||
|
void io_tmpfile()
|
||||||
|
{
|
||||||
|
throw std::runtime_error("io.tmpfile: This function is a stub!");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include <QString>
|
||||||
|
# include <sol/types.hpp>
|
||||||
|
# include <sol/variadic_args.hpp>
|
||||||
|
# include <sol/variadic_results.hpp>
|
||||||
|
|
||||||
struct lua_State;
|
struct lua_State;
|
||||||
|
|
||||||
|
@ -8,7 +12,6 @@ namespace chatterino::lua::api {
|
||||||
// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality.
|
// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality.
|
||||||
|
|
||||||
const char *const REG_REAL_IO_NAME = "real_lua_io_lib";
|
const char *const REG_REAL_IO_NAME = "real_lua_io_lib";
|
||||||
const char *const REG_C2_IO_NAME = "c2io";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file.
|
* Opens a file.
|
||||||
|
@ -20,7 +23,9 @@ const char *const REG_C2_IO_NAME = "c2io";
|
||||||
* @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+"
|
* @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+"
|
||||||
* @exposed io.open
|
* @exposed io.open
|
||||||
*/
|
*/
|
||||||
int io_open(lua_State *L);
|
sol::variadic_results io_open(sol::this_state L, QString filename,
|
||||||
|
QString strmode);
|
||||||
|
sol::variadic_results io_open_modeless(sol::this_state L, QString filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Equivalent to io.input():lines("l") or a specific iterator over given file
|
* Equivalent to io.input():lines("l") or a specific iterator over given file
|
||||||
|
@ -32,7 +37,9 @@ int io_open(lua_State *L);
|
||||||
* @lua@param ...
|
* @lua@param ...
|
||||||
* @exposed io.lines
|
* @exposed io.lines
|
||||||
*/
|
*/
|
||||||
int io_lines(lua_State *L);
|
sol::variadic_results io_lines(sol::this_state L, QString filename,
|
||||||
|
sol::variadic_args args);
|
||||||
|
sol::variadic_results io_lines_noargs(sol::this_state L);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file and sets it as default input or if given no arguments returns the default input.
|
* Opens a file and sets it as default input or if given no arguments returns the default input.
|
||||||
|
@ -42,7 +49,9 @@ int io_lines(lua_State *L);
|
||||||
* @lua@return nil|FILE*
|
* @lua@return nil|FILE*
|
||||||
* @exposed io.input
|
* @exposed io.input
|
||||||
*/
|
*/
|
||||||
int io_input(lua_State *L);
|
sol::variadic_results io_input_argless(sol::this_state L);
|
||||||
|
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file);
|
||||||
|
sol::variadic_results io_input_name(sol::this_state L, QString filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file and sets it as default output or if given no arguments returns the default output
|
* Opens a file and sets it as default output or if given no arguments returns the default output
|
||||||
|
@ -52,7 +61,9 @@ int io_input(lua_State *L);
|
||||||
* @lua@return nil|FILE*
|
* @lua@return nil|FILE*
|
||||||
* @exposed io.output
|
* @exposed io.output
|
||||||
*/
|
*/
|
||||||
int io_output(lua_State *L);
|
sol::variadic_results io_output_argless(sol::this_state L);
|
||||||
|
sol::variadic_results io_output_file(sol::this_state L, sol::userdata file);
|
||||||
|
sol::variadic_results io_output_name(sol::this_state L, QString filename);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes given file or io.output() if not given.
|
* Closes given file or io.output() if not given.
|
||||||
|
@ -61,7 +72,8 @@ int io_output(lua_State *L);
|
||||||
* @lua@param nil|FILE*
|
* @lua@param nil|FILE*
|
||||||
* @exposed io.close
|
* @exposed io.close
|
||||||
*/
|
*/
|
||||||
int io_close(lua_State *L);
|
bool io_close_argless(sol::this_state L);
|
||||||
|
bool io_close_file(sol::this_state L, sol::userdata file);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flushes the buffer for given file or io.output() if not given.
|
* Flushes the buffer for given file or io.output() if not given.
|
||||||
|
@ -70,7 +82,8 @@ int io_close(lua_State *L);
|
||||||
* @lua@param nil|FILE*
|
* @lua@param nil|FILE*
|
||||||
* @exposed io.flush
|
* @exposed io.flush
|
||||||
*/
|
*/
|
||||||
int io_flush(lua_State *L);
|
void io_flush_argless(sol::this_state L);
|
||||||
|
void io_flush_file(sol::this_state L, sol::userdata file);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads some data from the default input file
|
* Reads some data from the default input file
|
||||||
|
@ -79,7 +92,7 @@ int io_flush(lua_State *L);
|
||||||
* @lua@param nil|string
|
* @lua@param nil|string
|
||||||
* @exposed io.read
|
* @exposed io.read
|
||||||
*/
|
*/
|
||||||
int io_read(lua_State *L);
|
sol::variadic_results io_read(sol::this_state L, sol::variadic_args args);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes some data to the default output file
|
* Writes some data to the default output file
|
||||||
|
@ -88,10 +101,10 @@ int io_read(lua_State *L);
|
||||||
* @lua@param nil|string
|
* @lua@param nil|string
|
||||||
* @exposed io.write
|
* @exposed io.write
|
||||||
*/
|
*/
|
||||||
int io_write(lua_State *L);
|
sol::variadic_results io_write(sol::this_state L, sol::variadic_args args);
|
||||||
|
|
||||||
int io_popen(lua_State *L);
|
void io_popen();
|
||||||
int io_tmpfile(lua_State *L);
|
void io_tmpfile();
|
||||||
|
|
||||||
// NOLINTEND(readability-identifier-naming)
|
// NOLINTEND(readability-identifier-naming)
|
||||||
} // namespace chatterino::lua::api
|
} // namespace chatterino::lua::api
|
||||||
|
|
|
@ -64,22 +64,55 @@ const int MAX_QUEUED_REDEMPTIONS = 16;
|
||||||
class TwitchChannel final : public Channel, public ChannelChatters
|
class TwitchChannel final : public Channel, public ChannelChatters
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @lua@class StreamStatus
|
||||||
|
*/
|
||||||
struct StreamStatus {
|
struct StreamStatus {
|
||||||
|
/**
|
||||||
|
* @lua@field live boolean
|
||||||
|
*/
|
||||||
bool live = false;
|
bool live = false;
|
||||||
bool rerun = false;
|
bool rerun = false;
|
||||||
|
/**
|
||||||
|
* @lua@field viewer_count number
|
||||||
|
*/
|
||||||
unsigned viewerCount = 0;
|
unsigned viewerCount = 0;
|
||||||
|
/**
|
||||||
|
* @lua@field title string Stream title or last stream title
|
||||||
|
*/
|
||||||
QString title;
|
QString title;
|
||||||
|
/**
|
||||||
|
* @lua@field game_name string
|
||||||
|
*/
|
||||||
QString game;
|
QString game;
|
||||||
|
/**
|
||||||
|
* @lua@field game_id string
|
||||||
|
*/
|
||||||
QString gameId;
|
QString gameId;
|
||||||
QString uptime;
|
QString uptime;
|
||||||
|
/**
|
||||||
|
* @lua@field uptime number Seconds since the stream started.
|
||||||
|
*/
|
||||||
int uptimeSeconds = 0;
|
int uptimeSeconds = 0;
|
||||||
QString streamType;
|
QString streamType;
|
||||||
QString streamId;
|
QString streamId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @lua@class RoomModes
|
||||||
|
*/
|
||||||
struct RoomModes {
|
struct RoomModes {
|
||||||
|
/**
|
||||||
|
* @lua@field subscriber_only boolean
|
||||||
|
*/
|
||||||
bool submode = false;
|
bool submode = false;
|
||||||
|
/**
|
||||||
|
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||||
|
*/
|
||||||
bool r9k = false;
|
bool r9k = false;
|
||||||
|
/**
|
||||||
|
* @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
||||||
|
*/
|
||||||
bool emoteOnly = false;
|
bool emoteOnly = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +121,8 @@ public:
|
||||||
* Special cases:
|
* Special cases:
|
||||||
* -1 = follower mode off
|
* -1 = follower mode off
|
||||||
* 0 = follower mode on, no time requirement
|
* 0 = follower mode on, no time requirement
|
||||||
|
*
|
||||||
|
* @lua@field follower_only number? Time in minutes you need to follow to chat or nil.
|
||||||
**/
|
**/
|
||||||
int followerOnly = -1;
|
int followerOnly = -1;
|
||||||
|
|
||||||
|
@ -95,6 +130,8 @@ public:
|
||||||
* @brief Number of seconds required to wait before typing emotes
|
* @brief Number of seconds required to wait before typing emotes
|
||||||
*
|
*
|
||||||
* 0 = slow mode off
|
* 0 = slow mode off
|
||||||
|
*
|
||||||
|
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||||
**/
|
**/
|
||||||
int slowMode = 0;
|
int slowMode = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,15 @@ constexpr auto type_name()
|
||||||
name.remove_prefix(prefix.size());
|
name.remove_prefix(prefix.size());
|
||||||
name.remove_suffix(suffix.size());
|
name.remove_suffix(suffix.size());
|
||||||
|
|
||||||
|
if (name.starts_with("class "))
|
||||||
|
{
|
||||||
|
name.remove_prefix(6);
|
||||||
|
}
|
||||||
|
if (name.starts_with("struct "))
|
||||||
|
{
|
||||||
|
name.remove_prefix(7);
|
||||||
|
}
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -805,6 +805,32 @@ void SplitContainer::popup()
|
||||||
window.show();
|
window.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString channelTypeToString(Channel::Type value) noexcept
|
||||||
|
{
|
||||||
|
using Type = chatterino::Channel::Type;
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
assert(false && "value cannot be serialized");
|
||||||
|
return "never";
|
||||||
|
|
||||||
|
case Type::Twitch:
|
||||||
|
return "twitch";
|
||||||
|
case Type::TwitchWhispers:
|
||||||
|
return "whispers";
|
||||||
|
case Type::TwitchWatching:
|
||||||
|
return "watching";
|
||||||
|
case Type::TwitchMentions:
|
||||||
|
return "mentions";
|
||||||
|
case Type::TwitchLive:
|
||||||
|
return "live";
|
||||||
|
case Type::TwitchAutomod:
|
||||||
|
return "automod";
|
||||||
|
case Type::Misc:
|
||||||
|
return "misc";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NodeDescriptor SplitContainer::buildDescriptorRecursively(
|
NodeDescriptor SplitContainer::buildDescriptorRecursively(
|
||||||
const Node *currentNode) const
|
const Node *currentNode) const
|
||||||
{
|
{
|
||||||
|
@ -814,7 +840,7 @@ NodeDescriptor SplitContainer::buildDescriptorRecursively(
|
||||||
currentNode->split_->getIndirectChannel().getType();
|
currentNode->split_->getIndirectChannel().getType();
|
||||||
|
|
||||||
SplitNodeDescriptor result;
|
SplitNodeDescriptor result;
|
||||||
result.type_ = qmagicenum::enumNameString(channelType);
|
result.type_ = channelTypeToString(channelType);
|
||||||
result.channelName_ = currentNode->split_->getChannel()->getName();
|
result.channelName_ = currentNode->split_->getChannel()->getName();
|
||||||
result.filters_ = currentNode->split_->getFilters();
|
result.filters_ = currentNode->split_->getFilters();
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -48,6 +48,7 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/Plugins.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/TwitchIrc.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/TwitchIrc.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
|
||||||
|
|
55
tests/src/NetworkHelpers.hpp
Normal file
55
tests/src/NetworkHelpers.hpp
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Test.hpp"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
#ifdef CHATTERINO_TEST_USE_PUBLIC_HTTPBIN
|
||||||
|
// Using our self-hosted version of httpbox https://github.com/kevinastone/httpbox
|
||||||
|
const char *const HTTPBIN_BASE_URL = "https://braize.pajlada.com/httpbox";
|
||||||
|
#else
|
||||||
|
const char *const HTTPBIN_BASE_URL = "http://127.0.0.1:9051";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class RequestWaiter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void requestDone()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::unique_lock lck(this->mutex_);
|
||||||
|
ASSERT_FALSE(this->requestDone_);
|
||||||
|
this->requestDone_ = true;
|
||||||
|
}
|
||||||
|
this->condition_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitForRequest()
|
||||||
|
{
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::unique_lock lck(this->mutex_);
|
||||||
|
bool done = this->condition_.wait_for(lck, 10ms, [this] {
|
||||||
|
return this->requestDone_;
|
||||||
|
});
|
||||||
|
if (done)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QCoreApplication::processEvents(QEventLoop::AllEvents);
|
||||||
|
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE(this->requestDone_);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::condition_variable condition_;
|
||||||
|
bool requestDone_ = false;
|
||||||
|
};
|
||||||
|
} // namespace chatterino
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "common/network/NetworkManager.hpp"
|
#include "common/network/NetworkManager.hpp"
|
||||||
#include "common/network/NetworkResult.hpp"
|
#include "common/network/NetworkResult.hpp"
|
||||||
|
#include "NetworkHelpers.hpp"
|
||||||
#include "Test.hpp"
|
#include "Test.hpp"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
@ -10,14 +11,6 @@ using namespace chatterino;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
#ifdef CHATTERINO_TEST_USE_PUBLIC_HTTPBIN
|
|
||||||
// Not using httpbin.org, since it can be really slow and cause timeouts.
|
|
||||||
// postman-echo has the same API.
|
|
||||||
const char *const HTTPBIN_BASE_URL = "https://postman-echo.com";
|
|
||||||
#else
|
|
||||||
const char *const HTTPBIN_BASE_URL = "http://127.0.0.1:9051";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
QString getStatusURL(int code)
|
QString getStatusURL(int code)
|
||||||
{
|
{
|
||||||
return QString("%1/status/%2").arg(HTTPBIN_BASE_URL).arg(code);
|
return QString("%1/status/%2").arg(HTTPBIN_BASE_URL).arg(code);
|
||||||
|
@ -28,46 +21,6 @@ QString getDelayURL(int delay)
|
||||||
return QString("%1/delay/%2").arg(HTTPBIN_BASE_URL).arg(delay);
|
return QString("%1/delay/%2").arg(HTTPBIN_BASE_URL).arg(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestWaiter
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void requestDone()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
std::unique_lock lck(this->mutex_);
|
|
||||||
ASSERT_FALSE(this->requestDone_);
|
|
||||||
this->requestDone_ = true;
|
|
||||||
}
|
|
||||||
this->condition_.notify_one();
|
|
||||||
}
|
|
||||||
|
|
||||||
void waitForRequest()
|
|
||||||
{
|
|
||||||
using namespace std::chrono_literals;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
std::unique_lock lck(this->mutex_);
|
|
||||||
bool done = this->condition_.wait_for(lck, 10ms, [this] {
|
|
||||||
return this->requestDone_;
|
|
||||||
});
|
|
||||||
if (done)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QCoreApplication::processEvents(QEventLoop::AllEvents);
|
|
||||||
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::mutex mutex_;
|
|
||||||
std::condition_variable condition_;
|
|
||||||
bool requestDone_ = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(NetworkRequest, Success)
|
TEST(NetworkRequest, Success)
|
||||||
|
|
641
tests/src/Plugins.cpp
Normal file
641
tests/src/Plugins.cpp
Normal file
|
@ -0,0 +1,641 @@
|
||||||
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "Application.hpp"
|
||||||
|
# include "common/Channel.hpp"
|
||||||
|
# include "common/network/NetworkCommon.hpp"
|
||||||
|
# include "controllers/commands/Command.hpp" // IWYU pragma: keep
|
||||||
|
# include "controllers/commands/CommandController.hpp"
|
||||||
|
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||||
|
# include "controllers/plugins/Plugin.hpp"
|
||||||
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
# include "controllers/plugins/PluginPermission.hpp"
|
||||||
|
# include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep
|
||||||
|
# include "mocks/BaseApplication.hpp"
|
||||||
|
# include "mocks/Channel.hpp"
|
||||||
|
# include "mocks/Emotes.hpp"
|
||||||
|
# include "mocks/Logging.hpp"
|
||||||
|
# include "mocks/TwitchIrcServer.hpp"
|
||||||
|
# include "NetworkHelpers.hpp"
|
||||||
|
# include "singletons/Logging.hpp"
|
||||||
|
# include "Test.hpp"
|
||||||
|
|
||||||
|
# include <lauxlib.h>
|
||||||
|
# include <sol/state_view.hpp>
|
||||||
|
# include <sol/table.hpp>
|
||||||
|
|
||||||
|
# include <memory>
|
||||||
|
# include <optional>
|
||||||
|
# include <utility>
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
using chatterino::mock::MockChannel;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const QString TEST_SETTINGS = R"(
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"supportEnabled": true,
|
||||||
|
"enabledPlugins": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
class MockTwitch : public mock::MockTwitchIrcServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ChannelPtr mm2pl = std::make_shared<MockChannel>("mm2pl");
|
||||||
|
|
||||||
|
ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override
|
||||||
|
{
|
||||||
|
if (dirtyChannelName == "mm2pl")
|
||||||
|
{
|
||||||
|
return this->mm2pl;
|
||||||
|
}
|
||||||
|
return Channel::getEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Channel> getChannelOrEmptyByID(
|
||||||
|
const QString &channelID) override
|
||||||
|
{
|
||||||
|
if (channelID == "117691339")
|
||||||
|
{
|
||||||
|
return this->mm2pl;
|
||||||
|
}
|
||||||
|
return Channel::getEmpty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MockApplication : public mock::BaseApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MockApplication()
|
||||||
|
: mock::BaseApplication(TEST_SETTINGS)
|
||||||
|
, plugins(this->paths_)
|
||||||
|
, commands(this->paths_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginController *getPlugins() override
|
||||||
|
{
|
||||||
|
return &this->plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandController *getCommands() override
|
||||||
|
{
|
||||||
|
return &this->commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEmotes *getEmotes() override
|
||||||
|
{
|
||||||
|
return &this->emotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock::MockTwitchIrcServer *getTwitch() override
|
||||||
|
{
|
||||||
|
return &this->twitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
ILogging *getChatLogger() override
|
||||||
|
{
|
||||||
|
return &this->logging;
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginController plugins;
|
||||||
|
mock::EmptyLogging logging;
|
||||||
|
CommandController commands;
|
||||||
|
mock::Emotes emotes;
|
||||||
|
MockTwitch twitch;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class PluginControllerAccess
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static bool tryLoadFromDir(const QDir &pluginDir)
|
||||||
|
{
|
||||||
|
return getApp()->getPlugins()->tryLoadFromDir(pluginDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void openLibrariesFor(Plugin *plugin)
|
||||||
|
{
|
||||||
|
return PluginController::openLibrariesFor(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::map<QString, std::unique_ptr<Plugin>> &plugins()
|
||||||
|
{
|
||||||
|
return getApp()->getPlugins()->plugins_;
|
||||||
|
}
|
||||||
|
|
||||||
|
static lua_State *state(Plugin *pl)
|
||||||
|
{
|
||||||
|
return pl->state_;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
class PluginTest : public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void configure(std::vector<PluginPermission> permissions = {})
|
||||||
|
{
|
||||||
|
this->app = std::make_unique<MockApplication>();
|
||||||
|
|
||||||
|
auto &plugins = PluginControllerAccess::plugins();
|
||||||
|
{
|
||||||
|
PluginMeta meta;
|
||||||
|
meta.name = "Test";
|
||||||
|
meta.license = "MIT";
|
||||||
|
meta.homepage = "https://github.com/Chatterino/chatterino2";
|
||||||
|
meta.description = "Plugin for tests";
|
||||||
|
meta.permissions = std::move(permissions);
|
||||||
|
|
||||||
|
QDir plugindir =
|
||||||
|
QDir(app->paths_.pluginsDirectory).absoluteFilePath("test");
|
||||||
|
|
||||||
|
plugindir.mkpath(".");
|
||||||
|
auto temp = std::make_unique<Plugin>("test", luaL_newstate(), meta,
|
||||||
|
plugindir);
|
||||||
|
this->rawpl = temp.get();
|
||||||
|
plugins.insert({"test", std::move(temp)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: this skips PluginController::load()
|
||||||
|
PluginControllerAccess::openLibrariesFor(rawpl);
|
||||||
|
this->lua = new sol::state_view(PluginControllerAccess::state(rawpl));
|
||||||
|
|
||||||
|
this->channel = app->twitch.mm2pl;
|
||||||
|
this->rawpl->dataDirectory().mkpath(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override
|
||||||
|
{
|
||||||
|
// perform safe destruction of the plugin
|
||||||
|
delete this->lua;
|
||||||
|
this->lua = nullptr;
|
||||||
|
PluginControllerAccess::plugins().clear();
|
||||||
|
this->rawpl = nullptr;
|
||||||
|
this->app.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin *rawpl = nullptr;
|
||||||
|
std::unique_ptr<MockApplication> app;
|
||||||
|
sol::state_view *lua;
|
||||||
|
ChannelPtr channel;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(PluginTest, testCommands)
|
||||||
|
{
|
||||||
|
configure();
|
||||||
|
|
||||||
|
lua->script(R"lua(
|
||||||
|
_G.called = false
|
||||||
|
_G.words = nil
|
||||||
|
_G.channel = nil
|
||||||
|
c2.register_command("/test", function(ctx)
|
||||||
|
_G.called = true
|
||||||
|
_G.words = ctx.words
|
||||||
|
_G.channel = ctx.channel
|
||||||
|
end)
|
||||||
|
)lua");
|
||||||
|
|
||||||
|
EXPECT_EQ(app->commands.pluginCommands(), QStringList{"/test"});
|
||||||
|
app->commands.execCommand("/test with arguments", channel, false);
|
||||||
|
bool called = (*lua)["called"];
|
||||||
|
EXPECT_EQ(called, true);
|
||||||
|
|
||||||
|
EXPECT_NE((*lua)["words"], sol::nil);
|
||||||
|
{
|
||||||
|
sol::table tbl = (*lua)["words"];
|
||||||
|
std::vector<std::string> words;
|
||||||
|
for (auto &o : tbl)
|
||||||
|
{
|
||||||
|
words.push_back(o.second.as<std::string>());
|
||||||
|
}
|
||||||
|
EXPECT_EQ(words,
|
||||||
|
std::vector<std::string>({"/test", "with", "arguments"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::object chnobj = (*lua)["channel"];
|
||||||
|
EXPECT_EQ(chnobj.get_type(), sol::type::userdata);
|
||||||
|
lua::api::ChannelRef ref = chnobj.as<lua::api::ChannelRef>();
|
||||||
|
EXPECT_EQ(ref.get_name(), channel->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PluginTest, testCompletion)
|
||||||
|
{
|
||||||
|
configure();
|
||||||
|
|
||||||
|
lua->script(R"lua(
|
||||||
|
_G.called = false
|
||||||
|
_G.query = nil
|
||||||
|
_G.full_text_content = nil
|
||||||
|
_G.cursor_position = nil
|
||||||
|
_G.is_first_word = nil
|
||||||
|
|
||||||
|
c2.register_callback(
|
||||||
|
c2.EventType.CompletionRequested,
|
||||||
|
function(ev)
|
||||||
|
_G.called = true
|
||||||
|
_G.query = ev.query
|
||||||
|
_G.full_text_content = ev.full_text_content
|
||||||
|
_G.cursor_position = ev.cursor_position
|
||||||
|
_G.is_first_word = ev.is_first_word
|
||||||
|
if ev.query == "exclusive" then
|
||||||
|
return {
|
||||||
|
hide_others = true,
|
||||||
|
values = {"Completion1", "Completion2"}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
hide_others = false,
|
||||||
|
values = {"Completion"},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
)lua");
|
||||||
|
|
||||||
|
bool done{};
|
||||||
|
QStringList results;
|
||||||
|
std::tie(done, results) =
|
||||||
|
app->plugins.updateCustomCompletions("foo", "foo", 3, true);
|
||||||
|
ASSERT_EQ(done, false);
|
||||||
|
ASSERT_EQ(results, QStringList{"Completion"});
|
||||||
|
|
||||||
|
ASSERT_EQ((*lua).get<std::string>("query"), "foo");
|
||||||
|
ASSERT_EQ((*lua).get<std::string>("full_text_content"), "foo");
|
||||||
|
ASSERT_EQ((*lua).get<int>("cursor_position"), 3);
|
||||||
|
ASSERT_EQ((*lua).get<bool>("is_first_word"), true);
|
||||||
|
|
||||||
|
std::tie(done, results) = app->plugins.updateCustomCompletions(
|
||||||
|
"exclusive", "foo exclusive", 13, false);
|
||||||
|
ASSERT_EQ(done, true);
|
||||||
|
ASSERT_EQ(results, QStringList({"Completion1", "Completion2"}));
|
||||||
|
|
||||||
|
ASSERT_EQ((*lua).get<std::string>("query"), "exclusive");
|
||||||
|
ASSERT_EQ((*lua).get<std::string>("full_text_content"), "foo exclusive");
|
||||||
|
ASSERT_EQ((*lua).get<int>("cursor_position"), 13);
|
||||||
|
ASSERT_EQ((*lua).get<bool>("is_first_word"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PluginTest, testChannel)
|
||||||
|
{
|
||||||
|
configure();
|
||||||
|
lua->script(R"lua(
|
||||||
|
chn = c2.Channel.by_name("mm2pl")
|
||||||
|
)lua");
|
||||||
|
|
||||||
|
ASSERT_EQ(lua->script(R"lua( return chn:get_name() )lua").get<QString>(0),
|
||||||
|
"mm2pl");
|
||||||
|
ASSERT_EQ(
|
||||||
|
lua->script(R"lua( return chn:get_type() )lua").get<Channel::Type>(0),
|
||||||
|
Channel::Type::Twitch);
|
||||||
|
ASSERT_EQ(
|
||||||
|
lua->script(R"lua( return chn:get_display_name() )lua").get<QString>(0),
|
||||||
|
"mm2pl");
|
||||||
|
// TODO: send_message, add_system_message
|
||||||
|
|
||||||
|
ASSERT_EQ(
|
||||||
|
lua->script(R"lua( return chn:is_twitch_channel() )lua").get<bool>(0),
|
||||||
|
true);
|
||||||
|
|
||||||
|
// this is not a TwitchChannel
|
||||||
|
const auto *shouldThrow1 = R"lua(
|
||||||
|
return chn:is_broadcaster()
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow1));
|
||||||
|
const auto *shouldThrow2 = R"lua(
|
||||||
|
return chn:is_mod()
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow2));
|
||||||
|
const auto *shouldThrow3 = R"lua(
|
||||||
|
return chn:is_vip()
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow3));
|
||||||
|
const auto *shouldThrow4 = R"lua(
|
||||||
|
return chn:get_twitch_id()
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow4));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PluginTest, testHttp)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
PluginPermission net;
|
||||||
|
net.type = PluginPermission::Type::Network;
|
||||||
|
configure({net});
|
||||||
|
}
|
||||||
|
|
||||||
|
lua->script(R"lua(
|
||||||
|
function DoReq(url, postdata)
|
||||||
|
r = c2.HTTPRequest.create(method, url)
|
||||||
|
r:on_success(function(res)
|
||||||
|
status = res:status()
|
||||||
|
data = res:data()
|
||||||
|
error = res:error()
|
||||||
|
success = true
|
||||||
|
end)
|
||||||
|
r:on_error(function(res)
|
||||||
|
status = res:status()
|
||||||
|
data = res:data()
|
||||||
|
error = res:error()
|
||||||
|
failure = true
|
||||||
|
end)
|
||||||
|
r:finally(function()
|
||||||
|
finally = true
|
||||||
|
done()
|
||||||
|
end)
|
||||||
|
if postdata ~= "" then
|
||||||
|
r:set_payload(postdata)
|
||||||
|
r:set_header("Content-Type", "text/plain")
|
||||||
|
end
|
||||||
|
r:set_timeout(1000)
|
||||||
|
r:execute()
|
||||||
|
end
|
||||||
|
)lua");
|
||||||
|
|
||||||
|
struct RequestCase {
|
||||||
|
QString url;
|
||||||
|
bool success;
|
||||||
|
bool failure;
|
||||||
|
|
||||||
|
int status;
|
||||||
|
QString error;
|
||||||
|
|
||||||
|
NetworkRequestType meth = NetworkRequestType::Get;
|
||||||
|
QByteArray data; // null means do not check
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<RequestCase> cases{
|
||||||
|
{"/status/200", true, false, 200, "200"},
|
||||||
|
{"/delay/2", false, true, 0, "TimeoutError"},
|
||||||
|
{"/post", true, false, 200, "200", NetworkRequestType::Post,
|
||||||
|
"Example data"},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &c : cases)
|
||||||
|
{
|
||||||
|
lua->script(R"lua(
|
||||||
|
success = false
|
||||||
|
failure = false
|
||||||
|
finally = false
|
||||||
|
|
||||||
|
status = nil
|
||||||
|
data = nil
|
||||||
|
error = nil
|
||||||
|
)lua");
|
||||||
|
RequestWaiter waiter;
|
||||||
|
(*lua)["method"] = c.meth;
|
||||||
|
(*lua)["done"] = [&waiter]() {
|
||||||
|
waiter.requestDone();
|
||||||
|
};
|
||||||
|
|
||||||
|
(*lua)["DoReq"](HTTPBIN_BASE_URL + c.url, c.data);
|
||||||
|
waiter.waitForRequest();
|
||||||
|
|
||||||
|
EXPECT_EQ(lua->get<bool>("success"), c.success);
|
||||||
|
EXPECT_EQ(lua->get<bool>("failure"), c.failure);
|
||||||
|
EXPECT_EQ(lua->get<bool>("finally"), true);
|
||||||
|
|
||||||
|
if (c.status != 0)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(lua->get<int>("status"), c.status);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EXPECT_EQ((*lua)["status"], sol::nil);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(lua->get<QString>("error"), c.error);
|
||||||
|
if (!c.data.isNull())
|
||||||
|
{
|
||||||
|
EXPECT_EQ(lua->get<QByteArray>("data"), c.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray TEST_FILE_DATA = "Test file data\nWith a new line.\n";
|
||||||
|
|
||||||
|
TEST_F(PluginTest, ioTest)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
PluginPermission ioread;
|
||||||
|
PluginPermission iowrite;
|
||||||
|
ioread.type = PluginPermission::Type::FilesystemRead;
|
||||||
|
iowrite.type = PluginPermission::Type::FilesystemWrite;
|
||||||
|
configure({ioread, iowrite});
|
||||||
|
}
|
||||||
|
|
||||||
|
lua->set("TEST_DATA", TEST_FILE_DATA);
|
||||||
|
|
||||||
|
lua->script(R"lua(
|
||||||
|
f, err = io.open("testfile", "w")
|
||||||
|
print(f, err)
|
||||||
|
f:write(TEST_DATA)
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
f, err = io.open("testfile", "r")
|
||||||
|
out = f:read("a")
|
||||||
|
f:close()
|
||||||
|
)lua");
|
||||||
|
EXPECT_EQ(lua->get<QByteArray>("out"), TEST_FILE_DATA);
|
||||||
|
|
||||||
|
lua->script(R"lua(
|
||||||
|
io.input("testfile")
|
||||||
|
out = io.read("a")
|
||||||
|
)lua");
|
||||||
|
EXPECT_EQ(lua->get<QByteArray>("out"), TEST_FILE_DATA);
|
||||||
|
|
||||||
|
const auto *shouldThrow1 = R"lua(
|
||||||
|
io.popen("/bin/sh", "-c", "notify-send \"This should not execute.\"")
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow1));
|
||||||
|
const auto *shouldThrow2 = R"lua(
|
||||||
|
io.tmpfile()
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow2));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PluginTest, ioNoPerms)
|
||||||
|
{
|
||||||
|
configure();
|
||||||
|
auto file = rawpl->dataDirectory().filePath("testfile");
|
||||||
|
QFile f(file);
|
||||||
|
f.open(QFile::WriteOnly);
|
||||||
|
f.write(TEST_FILE_DATA);
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
// clang-format off
|
||||||
|
lua->script(R"lua(
|
||||||
|
f, err = io.open("testfile", "r")
|
||||||
|
return err
|
||||||
|
)lua").get<QString>(0),
|
||||||
|
"Plugin does not have permissions to access given file."
|
||||||
|
// clang-format on
|
||||||
|
);
|
||||||
|
|
||||||
|
const auto *shouldThrow1 = R"lua(
|
||||||
|
io.input("testfile")
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow1));
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
// clang-format off
|
||||||
|
lua->script(R"lua(
|
||||||
|
f, err = io.open("testfile", "w")
|
||||||
|
return err
|
||||||
|
)lua").get<QString>(0),
|
||||||
|
"Plugin does not have permissions to access given file."
|
||||||
|
// clang-format on
|
||||||
|
);
|
||||||
|
|
||||||
|
const auto *shouldThrow2 = R"lua(
|
||||||
|
io.output("testfile")
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow2));
|
||||||
|
|
||||||
|
const auto *shouldThrow3 = R"lua(
|
||||||
|
io.lines("testfile")
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow3));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PluginTest, requireNoData)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
PluginPermission ioread;
|
||||||
|
PluginPermission iowrite;
|
||||||
|
ioread.type = PluginPermission::Type::FilesystemRead;
|
||||||
|
iowrite.type = PluginPermission::Type::FilesystemWrite;
|
||||||
|
configure({ioread, iowrite});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto file = rawpl->dataDirectory().filePath("thisiscode.lua");
|
||||||
|
QFile f(file);
|
||||||
|
f.open(QFile::WriteOnly);
|
||||||
|
f.write(R"lua(print("Data was executed"))lua");
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
const auto *shouldThrow1 = R"lua(
|
||||||
|
require("data.thisiscode")
|
||||||
|
)lua";
|
||||||
|
EXPECT_ANY_THROW(lua->script(shouldThrow1));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PluginTest, testTimerRec)
|
||||||
|
{
|
||||||
|
configure();
|
||||||
|
|
||||||
|
RequestWaiter waiter;
|
||||||
|
lua->set("done", [&] {
|
||||||
|
waiter.requestDone();
|
||||||
|
});
|
||||||
|
|
||||||
|
sol::protected_function fn = lua->script(R"lua(
|
||||||
|
local i = 0
|
||||||
|
f = function()
|
||||||
|
i = i + 1
|
||||||
|
c2.log(c2.LogLevel.Info, "cb", i)
|
||||||
|
if i < 1024 then
|
||||||
|
c2.later(f, 1)
|
||||||
|
else
|
||||||
|
done()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
c2.later(f, 1)
|
||||||
|
)lua");
|
||||||
|
waiter.waitForRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PluginTest, tryCallTest)
|
||||||
|
{
|
||||||
|
configure();
|
||||||
|
lua->script(R"lua(
|
||||||
|
function return_table()
|
||||||
|
return {
|
||||||
|
a="b"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
function return_nothing()
|
||||||
|
end
|
||||||
|
function return_nil()
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
function return_nothing_and_error()
|
||||||
|
error("I failed :)")
|
||||||
|
end
|
||||||
|
)lua");
|
||||||
|
|
||||||
|
using func = sol::protected_function;
|
||||||
|
|
||||||
|
func returnTable = lua->get<func>("return_table");
|
||||||
|
func returnNil = lua->get<func>("return_nil");
|
||||||
|
func returnNothing = lua->get<func>("return_nothing");
|
||||||
|
func returnNothingAndError = lua->get<func>("return_nothing_and_error");
|
||||||
|
|
||||||
|
// happy paths
|
||||||
|
{
|
||||||
|
auto res = lua::tryCall<sol::table>(returnTable);
|
||||||
|
EXPECT_TRUE(res.has_value());
|
||||||
|
auto t = res.value();
|
||||||
|
EXPECT_EQ(t.get<QString>("a"), "b");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// valid void return
|
||||||
|
auto res = lua::tryCall<void>(returnNil);
|
||||||
|
EXPECT_TRUE(res.has_value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// valid void return
|
||||||
|
auto res = lua::tryCall<void>(returnNothing);
|
||||||
|
EXPECT_TRUE(res.has_value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto res = lua::tryCall<sol::table>(returnNothingAndError);
|
||||||
|
EXPECT_FALSE(res.has_value());
|
||||||
|
EXPECT_EQ(res.error(), "[string \"...\"]:13: I failed :)");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto res = lua::tryCall<std::optional<int>>(returnNil);
|
||||||
|
EXPECT_TRUE(res.has_value()); // no error
|
||||||
|
auto opt = *res;
|
||||||
|
EXPECT_FALSE(opt.has_value()); // but also no false
|
||||||
|
}
|
||||||
|
|
||||||
|
// unhappy paths
|
||||||
|
{
|
||||||
|
// wrong return type
|
||||||
|
auto res = lua::tryCall<int>(returnTable);
|
||||||
|
EXPECT_FALSE(res.has_value());
|
||||||
|
EXPECT_EQ(res.error(),
|
||||||
|
"Expected int to be returned but table was returned");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// optional but bad return type
|
||||||
|
auto res = lua::tryCall<std::optional<int>>(returnTable);
|
||||||
|
EXPECT_FALSE(res.has_value());
|
||||||
|
EXPECT_EQ(res.error(), "Expected std::optional<int> to be returned but "
|
||||||
|
"table was returned");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// no return
|
||||||
|
auto res = lua::tryCall<int>(returnNothing);
|
||||||
|
EXPECT_FALSE(res.has_value());
|
||||||
|
EXPECT_EQ(res.error(),
|
||||||
|
"Expected int to be returned but none was returned");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// nil return
|
||||||
|
auto res = lua::tryCall<int>(returnNil);
|
||||||
|
EXPECT_FALSE(res.has_value());
|
||||||
|
EXPECT_EQ(res.error(),
|
||||||
|
"Expected int to be returned but lua_nil was returned");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in a new issue