mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Compare commits
8 commits
802edeb8de
...
4af8b2f1d7
Author | SHA1 | Date | |
---|---|---|---|
4af8b2f1d7 | |||
42cb5cddd1 | |||
352a4ec132 | |||
e92ba14e64 | |||
3ca3b63f4e | |||
1f5e99cd55 | |||
55b8daf7a0 | |||
9345050868 |
2
.github/workflows/test-macos.yml
vendored
2
.github/workflows/test-macos.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
matrix:
|
||||
os: [macos-13]
|
||||
qt-version: [5.15.2, 6.7.1]
|
||||
plugins: [false]
|
||||
plugins: [true]
|
||||
fail-fast: false
|
||||
env:
|
||||
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:
|
||||
os: [windows-latest]
|
||||
qt-version: [5.15.2, 6.7.1]
|
||||
plugins: [false]
|
||||
plugins: [true]
|
||||
skip-artifact: [false]
|
||||
skip-crashpad: [false]
|
||||
fail-fast: false
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -44,3 +44,6 @@
|
|||
[submodule "lib/expected-lite"]
|
||||
path = lib/expected-lite
|
||||
url = https://github.com/martinmoene/expected-lite
|
||||
[submodule "lib/sol2"]
|
||||
path = lib/sol2
|
||||
url = https://github.com/ThePhD/sol2.git
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
- Major: Add option to show pronouns in user card. (#5442, #5583)
|
||||
- Major: Release plugins alpha. (#5288)
|
||||
- Major: Improve high-DPI support on Windows. (#4868, #5391)
|
||||
- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664)
|
||||
- Major: Added transparent overlay window (default keybind: <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>N</kbd>). (#4746, #5643, #5659)
|
||||
- Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530)
|
||||
- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625)
|
||||
|
@ -108,6 +108,7 @@
|
|||
- 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: 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: Decoupled reply parsing from `MessageBuilder`. (#5660)
|
||||
- Dev: Refactored IRC message building. (#5663)
|
||||
|
|
|
@ -212,6 +212,8 @@ endif()
|
|||
if (CHATTERINO_PLUGINS)
|
||||
set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src")
|
||||
add_subdirectory(lib/lua)
|
||||
|
||||
find_package(Sol2 REQUIRED)
|
||||
endif()
|
||||
|
||||
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".
|
||||
|
||||
c2 = {}
|
||||
---@alias c2.LogLevel integer
|
||||
---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel }
|
||||
---@alias c2.LogLevel.Debug "c2.LogLevel.Debug"
|
||||
---@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 = {}
|
||||
|
||||
---@alias c2.EventType integer
|
||||
---@type { CompletionRequested: c2.EventType }
|
||||
-- Begin src/controllers/plugins/api/EventType.hpp
|
||||
|
||||
---@alias c2.EventType.CompletionRequested "c2.EventType.CompletionRequested"
|
||||
---@alias c2.EventType c2.EventType.CompletionRequested
|
||||
---@type { CompletionRequested: c2.EventType.CompletionRequested }
|
||||
c2.EventType = {}
|
||||
|
||||
-- End src/controllers/plugins/api/EventType.hpp
|
||||
|
||||
---@class CommandContext
|
||||
---@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.
|
||||
|
@ -29,19 +38,40 @@ c2.EventType = {}
|
|||
|
||||
-- Begin src/common/Channel.hpp
|
||||
|
||||
---@alias c2.ChannelType integer
|
||||
---@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.None "c2.ChannelType.None"
|
||||
---@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 = {}
|
||||
|
||||
-- End src/common/Channel.hpp
|
||||
|
||||
-- Begin src/controllers/plugins/api/ChannelRef.hpp
|
||||
|
||||
---@alias c2.Platform integer
|
||||
--- This enum describes a platform for the purpose of searching for a channel.
|
||||
--- Currently only Twitch is supported because identifying IRC channels is tricky.
|
||||
---@type { Twitch: c2.Platform }
|
||||
c2.Platform = {}
|
||||
-- Begin src/providers/twitch/TwitchChannel.hpp
|
||||
|
||||
---@class StreamStatus
|
||||
---@field live boolean
|
||||
---@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
|
||||
c2.Channel = {}
|
||||
|
@ -72,7 +102,7 @@ function c2.Channel:get_display_name() end
|
|||
--- Note that this does not execute client-commands.
|
||||
---
|
||||
---@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
|
||||
|
||||
--- Adds a system message client-side
|
||||
|
@ -131,9 +161,8 @@ function c2.Channel:__tostring() end
|
|||
--- - /automod
|
||||
---
|
||||
---@param name string Which channel are you looking for?
|
||||
---@param platform c2.Platform Where to search for the 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.
|
||||
---
|
||||
|
@ -141,21 +170,6 @@ function c2.Channel.by_name(name, platform) end
|
|||
---@return c2.Channel?
|
||||
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
|
||||
|
||||
-- Begin src/controllers/plugins/api/HTTPResponse.hpp
|
||||
|
@ -176,6 +190,9 @@ function HTTPResponse:status() end
|
|||
---
|
||||
function HTTPResponse:error() end
|
||||
|
||||
---@return string
|
||||
function HTTPResponse:__tostring() end
|
||||
|
||||
-- End src/controllers/plugins/api/HTTPResponse.hpp
|
||||
|
||||
-- Begin src/controllers/plugins/api/HTTPRequest.hpp
|
||||
|
@ -219,6 +236,9 @@ function HTTPRequest:set_header(name, value) end
|
|||
---
|
||||
function HTTPRequest:execute() end
|
||||
|
||||
---@return string
|
||||
function HTTPRequest:__tostring() end
|
||||
|
||||
--- Creates a new HTTPRequest
|
||||
---
|
||||
---@param method HTTPMethod Method to use
|
||||
|
@ -230,8 +250,13 @@ function HTTPRequest.create(method, url) end
|
|||
|
||||
-- Begin src/common/network/NetworkCommon.hpp
|
||||
|
||||
---@alias HTTPMethod integer
|
||||
---@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod }
|
||||
---@alias HTTPMethod.Get "HTTPMethod.Get"
|
||||
---@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 = {}
|
||||
|
||||
-- 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.
|
||||
---
|
||||
---@param type "CompletionRequested"
|
||||
---@param type c2.EventType.CompletionRequested
|
||||
---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
|
||||
function c2.register_callback(type, func) end
|
||||
|
||||
|
|
|
@ -171,7 +171,7 @@ function cmd_words(ctx)
|
|||
-- ctx contains:
|
||||
-- words - table of words supplied to the command including the trigger
|
||||
-- channel - the channel the command is being run in
|
||||
channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
|
||||
ctx.channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
|
||||
end
|
||||
|
||||
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.
|
||||
- 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:
|
||||
|
||||
|
@ -207,7 +207,7 @@ function string.startswith(s, other)
|
|||
end
|
||||
|
||||
c2.register_callback(
|
||||
"CompletionRequested",
|
||||
c2.EventType.CompletionRequested,
|
||||
function(event)
|
||||
if ("!join"):startswith(event.query) then
|
||||
---@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
|
||||
|
||||
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
|
||||
`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:
|
||||
|
||||
|
@ -275,7 +266,7 @@ Some miscellaneous channels are marked as if they are specifically Twitch channe
|
|||
Example:
|
||||
|
||||
```lua
|
||||
local pajladas = c2.Channel.by_name("pajlada", c2.Platform.Twitch)
|
||||
local pajladas = c2.Channel.by_name("pajlada")
|
||||
```
|
||||
|
||||
##### `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
|
||||
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.
|
||||
|
||||
##### `Channel:get_twitch_id()`
|
||||
|
|
|
@ -1,48 +1,44 @@
|
|||
project(lua CXX)
|
||||
|
||||
#[====[
|
||||
Updating this list:
|
||||
remove all listed files
|
||||
go to line below, ^y2j4j$@" and then reindent the file names
|
||||
/LUA_SRC
|
||||
:r!ls lib/lua/src | grep '\.c' | grep -Ev 'lua\.c|onelua\.c' | sed 's#^#src/#'
|
||||
|
||||
This list contains all .c files except lua.c and onelua.c
|
||||
Use the following command from the repository root to get these file:
|
||||
perl -e 'print s/^lib\/lua\///r . "\n" for grep { /\.c$/ && !/(lua|onelua)\.c$/ } glob "lib/lua/src/*.c"'
|
||||
#]====]
|
||||
set(LUA_SRC
|
||||
"src/lapi.c"
|
||||
"src/lauxlib.c"
|
||||
"src/lbaselib.c"
|
||||
"src/lcode.c"
|
||||
"src/lcorolib.c"
|
||||
"src/lctype.c"
|
||||
"src/ldblib.c"
|
||||
"src/ldebug.c"
|
||||
"src/ldo.c"
|
||||
"src/ldump.c"
|
||||
"src/lfunc.c"
|
||||
"src/lgc.c"
|
||||
"src/linit.c"
|
||||
"src/liolib.c"
|
||||
"src/llex.c"
|
||||
"src/lmathlib.c"
|
||||
"src/lmem.c"
|
||||
"src/loadlib.c"
|
||||
"src/lobject.c"
|
||||
"src/lopcodes.c"
|
||||
"src/loslib.c"
|
||||
"src/lparser.c"
|
||||
"src/lstate.c"
|
||||
"src/lstring.c"
|
||||
"src/lstrlib.c"
|
||||
"src/ltable.c"
|
||||
"src/ltablib.c"
|
||||
"src/ltests.c"
|
||||
"src/ltm.c"
|
||||
"src/lua.c"
|
||||
"src/lundump.c"
|
||||
"src/lutf8lib.c"
|
||||
"src/lvm.c"
|
||||
"src/lzio.c"
|
||||
src/lapi.c
|
||||
src/lauxlib.c
|
||||
src/lbaselib.c
|
||||
src/lcode.c
|
||||
src/lcorolib.c
|
||||
src/lctype.c
|
||||
src/ldblib.c
|
||||
src/ldebug.c
|
||||
src/ldo.c
|
||||
src/ldump.c
|
||||
src/lfunc.c
|
||||
src/lgc.c
|
||||
src/linit.c
|
||||
src/liolib.c
|
||||
src/llex.c
|
||||
src/lmathlib.c
|
||||
src/lmem.c
|
||||
src/loadlib.c
|
||||
src/lobject.c
|
||||
src/lopcodes.c
|
||||
src/loslib.c
|
||||
src/lparser.c
|
||||
src/lstate.c
|
||||
src/lstring.c
|
||||
src/lstrlib.c
|
||||
src/ltable.c
|
||||
src/ltablib.c
|
||||
src/ltests.c
|
||||
src/ltm.c
|
||||
src/lundump.c
|
||||
src/lutf8lib.c
|
||||
src/lvm.c
|
||||
src/lzio.c
|
||||
)
|
||||
|
||||
add_library(lua STATIC ${LUA_SRC})
|
||||
|
@ -50,4 +46,14 @@ target_include_directories(lua
|
|||
PUBLIC
|
||||
${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("@"):
|
||||
out.write(f"--- {comments[0]}\n---\n")
|
||||
comments = comments[1:]
|
||||
params = []
|
||||
params: list[str] = []
|
||||
for comment in comments[:-1]:
|
||||
if not comment.startswith("@lua"):
|
||||
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]}'")
|
||||
name = comments[-1].split(" ", 1)[1]
|
||||
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")
|
||||
|
||||
|
||||
|
@ -242,13 +242,21 @@ def read_file(path: Path, out: TextIOWrapper):
|
|||
)
|
||||
name = header[0].split(" ", 1)[1]
|
||||
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:
|
||||
out.write(f"--- {header_comment}\n")
|
||||
out.write("---@type { ")
|
||||
out.write(
|
||||
", ".join(
|
||||
[f"{variant}: {name}" for variant in reader.read_enum_variants()]
|
||||
[f"{variant}: {typ}" for variant, typ in zip(variants,vtypes)]
|
||||
)
|
||||
)
|
||||
out.write(" }\n")
|
||||
|
|
|
@ -225,24 +225,28 @@ set(SOURCE_FILES
|
|||
controllers/pings/MutedChannelModel.cpp
|
||||
controllers/pings/MutedChannelModel.hpp
|
||||
|
||||
|
||||
controllers/plugins/api/ChannelRef.cpp
|
||||
controllers/plugins/api/ChannelRef.hpp
|
||||
controllers/plugins/api/IOWrapper.cpp
|
||||
controllers/plugins/api/IOWrapper.hpp
|
||||
controllers/plugins/api/EventType.hpp
|
||||
controllers/plugins/api/HTTPRequest.cpp
|
||||
controllers/plugins/api/HTTPRequest.hpp
|
||||
controllers/plugins/api/HTTPResponse.cpp
|
||||
controllers/plugins/api/HTTPResponse.hpp
|
||||
controllers/plugins/api/IOWrapper.cpp
|
||||
controllers/plugins/api/IOWrapper.hpp
|
||||
controllers/plugins/LuaAPI.cpp
|
||||
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.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/MiniaudioBackend.cpp
|
||||
|
@ -791,7 +795,7 @@ target_link_libraries(${LIBRARY_PROJECT}
|
|||
$<$<BOOL:${WIN32}>:Wtsapi32>
|
||||
)
|
||||
if (CHATTERINO_PLUGINS)
|
||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua)
|
||||
target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua sol2::sol2)
|
||||
endif()
|
||||
|
||||
if (BUILD_WITH_QTKEYCHAIN)
|
||||
|
|
|
@ -129,6 +129,10 @@
|
|||
# include <unordered_set>
|
||||
# include <vector>
|
||||
|
||||
# ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include <sol/sol.hpp>
|
||||
# endif
|
||||
|
||||
# ifndef UNUSED
|
||||
# define UNUSED(x) (void)(x)
|
||||
# endif
|
||||
|
|
|
@ -165,30 +165,3 @@ private:
|
|||
};
|
||||
|
||||
} // 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 "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp" // for lua operations on QString{,List} for CompletionList
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
}
|
||||
# include <QFileInfo>
|
||||
# include <QList>
|
||||
# include <QLoggingCategory>
|
||||
# include <QTextCodec>
|
||||
# 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 {
|
||||
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 << "[" + 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)
|
||||
|
@ -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
|
||||
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);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
return sol::state_view(L).create_table_with(
|
||||
"query", ev.query, //
|
||||
"full_text_content", ev.full_text_content, //
|
||||
"cursor_position", ev.cursor_position, //
|
||||
"is_first_word", ev.is_first_word //
|
||||
);
|
||||
}
|
||||
|
||||
int c2_log(lua_State *L)
|
||||
void c2_register_callback(ThisPluginState L, EventType evtType,
|
||||
sol::protected_function callback)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
L.plugin()->callbacks[evtType] = std::move(callback);
|
||||
}
|
||||
|
||||
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);
|
||||
logHelper(L, pl, stream, logc);
|
||||
return 0;
|
||||
logHelper(L, L.plugin(), stream, args);
|
||||
}
|
||||
}
|
||||
|
||||
int c2_later(lua_State *L)
|
||||
void c2_later(ThisPluginState L, sol::protected_function callback, int time)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
if (time <= 0)
|
||||
{
|
||||
return luaL_error(L, "c2.later: internal error: no plugin?");
|
||||
}
|
||||
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)");
|
||||
throw std::runtime_error(
|
||||
"c2.later time must be strictly greater than zero.");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
auto *timer = new QTimer();
|
||||
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 *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();
|
||||
pl->removeTimeout(timer);
|
||||
int nres{};
|
||||
lua_resume(coro, nullptr, 0, &nres);
|
||||
sol::protected_function_result res = cb();
|
||||
|
||||
lua_pushnil(coro);
|
||||
lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
||||
if (lua_gettop(coro) != 0)
|
||||
if (res.return_count() != 0)
|
||||
{
|
||||
stackDump(coro,
|
||||
stackDump(thread.lua_state(),
|
||||
pl->id +
|
||||
": timer returned a value, this shouldn't happen "
|
||||
"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();
|
||||
|
||||
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
|
||||
luaL_error(L, "load() is only usable in debug mode");
|
||||
return 0;
|
||||
(void)data;
|
||||
(void)s;
|
||||
throw std::runtime_error("load() is only usable in debug mode");
|
||||
# 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++)
|
||||
{
|
||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
||||
}
|
||||
|
||||
// fetch load and call it
|
||||
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);
|
||||
// If you're modifying this PLEASE verify it works, Sol is very annoying about serialization
|
||||
// - Mm2PL
|
||||
sol::state_view lua(s);
|
||||
auto load = lua.registry()["real_load"];
|
||||
sol::protected_function_result ret = load(data, "=(load)", "t");
|
||||
return ret;
|
||||
# endif
|
||||
}
|
||||
|
||||
|
@ -320,7 +228,7 @@ int searcherAbsolute(lua_State *L)
|
|||
int searcherRelative(lua_State *L)
|
||||
{
|
||||
lua_Debug dbg;
|
||||
lua_getstack(L, 1, &dbg);
|
||||
lua_getstack(L, 2, &dbg);
|
||||
lua_getinfo(L, "S", &dbg);
|
||||
auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen);
|
||||
if (currentFile.startsWith("@"))
|
||||
|
@ -346,22 +254,14 @@ int searcherRelative(lua_State *L)
|
|||
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
|
||||
auto stream =
|
||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
||||
.debug());
|
||||
logHelper(L, pl, stream, argc);
|
||||
return 0;
|
||||
logHelper(L, L.plugin(), stream, args);
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#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 "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include <QList>
|
||||
# include <QString>
|
||||
# include <sol/table.hpp>
|
||||
|
||||
# include <cassert>
|
||||
# include <memory>
|
||||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
namespace chatterino::lua::api {
|
||||
|
@ -30,11 +30,8 @@ namespace chatterino::lua::api {
|
|||
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||
|
||||
/**
|
||||
* @exposeenum c2.EventType
|
||||
* @includefile controllers/plugins/api/EventType.hpp
|
||||
*/
|
||||
enum class EventType {
|
||||
CompletionRequested,
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class CommandContext
|
||||
|
@ -46,10 +43,12 @@ enum class EventType {
|
|||
* @lua@class CompletionList
|
||||
*/
|
||||
struct CompletionList {
|
||||
CompletionList(const sol::table &);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
|
@ -79,6 +78,8 @@ struct CompletionEvent {
|
|||
bool is_first_word{};
|
||||
};
|
||||
|
||||
sol::table toTable(lua_State *L, const CompletionEvent &ev);
|
||||
|
||||
/**
|
||||
* @includefile common/Channel.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.
|
||||
* @exposed c2.register_command
|
||||
*/
|
||||
int c2_register_command(lua_State *L);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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.
|
||||
|
@ -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()`.
|
||||
* @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.
|
||||
|
@ -122,11 +123,11 @@ int c2_log(lua_State *L);
|
|||
* @lua@param msec number How long to wait.
|
||||
* @exposed c2.later
|
||||
*/
|
||||
int c2_later(lua_State *L);
|
||||
void c2_later(ThisPluginState L, sol::protected_function callback, int time);
|
||||
|
||||
// These ones are global
|
||||
int g_load(lua_State *L);
|
||||
int g_print(lua_State *L);
|
||||
sol::variadic_results g_load(ThisPluginState s, sol::object data);
|
||||
void g_print(ThisPluginState L, sol::variadic_args args);
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
// This is for require() exposed as an element of package.searchers
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include "common/Channel.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 <lua.h>
|
||||
}
|
||||
|
||||
# include <climits>
|
||||
# include <cstdlib>
|
||||
|
@ -79,9 +73,6 @@ QString humanErrorText(lua_State *L, int errCode)
|
|||
case LUA_ERRFILE:
|
||||
errName = "(file error)";
|
||||
break;
|
||||
case ERROR_BAD_PEEK:
|
||||
errName = "(unable to convert value to c++)";
|
||||
break;
|
||||
default:
|
||||
errName = "(unknown error type)";
|
||||
}
|
||||
|
@ -93,18 +84,6 @@ QString humanErrorText(lua_State *L, int errCode)
|
|||
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)
|
||||
{
|
||||
return lua::push(L, str.toStdString());
|
||||
|
@ -116,82 +95,6 @@ StackIdx push(lua_State *L, const std::string &str)
|
|||
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)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
|
@ -209,57 +112,6 @@ bool peek(lua_State *L, QString *out, StackIdx idx)
|
|||
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)
|
||||
{
|
||||
size_t len{};
|
||||
|
|
|
@ -2,37 +2,20 @@
|
|||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "common/QLogging.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
}
|
||||
# include <magic_enum/magic_enum.hpp>
|
||||
# include <QList>
|
||||
# include <sol/state_view.hpp>
|
||||
|
||||
# include <cassert>
|
||||
# include <optional>
|
||||
# include <string>
|
||||
# include <string_view>
|
||||
# include <type_traits>
|
||||
# include <variant>
|
||||
# include <vector>
|
||||
struct lua_State;
|
||||
class QJsonObject;
|
||||
namespace chatterino {
|
||||
struct CommandContext;
|
||||
} // namespace chatterino
|
||||
|
||||
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)
|
||||
*
|
||||
|
@ -40,6 +23,9 @@ constexpr int ERROR_BAD_PEEK = LUA_OK - 1;
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -50,33 +36,11 @@ QString humanErrorText(lua_State *L, int errCode);
|
|||
*/
|
||||
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 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?
|
||||
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, 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.
|
||||
|
@ -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.
|
||||
*
|
||||
* Values in this table may change.
|
||||
*
|
||||
* @returns stack index of newly created table
|
||||
* @returns Sol reference to the table
|
||||
*/
|
||||
template <typename T>
|
||||
StackIdx pushEnumTable(lua_State *L)
|
||||
requires std::is_enum_v<T>
|
||||
sol::table createEnumTable(sol::state_view &lua)
|
||||
{
|
||||
// std::array<T, _>
|
||||
auto values = magic_enum::enum_values<T>();
|
||||
StackIdx out = lua::pushEmptyTable(L, values.size());
|
||||
constexpr auto values = magic_enum::enum_values<T>();
|
||||
auto out = lua.create_table(0, values.size());
|
||||
for (const T v : values)
|
||||
{
|
||||
std::string_view name = magic_enum::enum_name<T>(v);
|
||||
std::string str(name);
|
||||
|
||||
lua::push(L, str);
|
||||
lua_setfield(L, out, str.c_str());
|
||||
out.raw_set(str, v);
|
||||
}
|
||||
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
|
||||
|
||||
#endif
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
# include "controllers/plugins/PluginPermission.hpp"
|
||||
# include "util/QMagicEnum.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lua.h>
|
||||
}
|
||||
# include <magic_enum/magic_enum.hpp>
|
||||
# include <QJsonArray>
|
||||
# include <QJsonObject>
|
||||
# include <QLoggingCategory>
|
||||
# include <QUrl>
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
# include <algorithm>
|
||||
# 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())
|
||||
{
|
||||
|
@ -202,7 +202,7 @@ bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
|||
{
|
||||
return false;
|
||||
}
|
||||
this->ownedCommands.insert({name, functionName});
|
||||
this->ownedCommands.emplace(name, std::move(function));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -223,14 +223,24 @@ Plugin::~Plugin()
|
|||
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
||||
timer->deleteLater();
|
||||
}
|
||||
this->httpRequests.clear();
|
||||
qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size()
|
||||
<< "timers for plugin" << this->id
|
||||
<< "while destroying the object";
|
||||
this->activeTimeouts.clear();
|
||||
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_);
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "Application.hpp"
|
||||
# include "common/network/NetworkCommon.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/api/EventType.hpp"
|
||||
# include "controllers/plugins/api/HTTPRequest.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginPermission.hpp"
|
||||
|
||||
|
@ -11,7 +11,10 @@
|
|||
# include <QString>
|
||||
# include <QUrl>
|
||||
# include <semver/semver.hpp>
|
||||
# include <sol/forward.hpp>
|
||||
|
||||
# include <memory>
|
||||
# include <optional>
|
||||
# include <unordered_map>
|
||||
# include <unordered_set>
|
||||
# include <vector>
|
||||
|
@ -56,6 +59,8 @@ struct PluginMeta {
|
|||
}
|
||||
|
||||
explicit PluginMeta(const QJsonObject &obj);
|
||||
// This is for tests
|
||||
PluginMeta() = default;
|
||||
};
|
||||
|
||||
class Plugin
|
||||
|
@ -75,13 +80,18 @@ public:
|
|||
|
||||
~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
|
||||
* @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)
|
||||
*/
|
||||
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
|
||||
|
@ -98,35 +108,19 @@ public:
|
|||
return this->loadDirectory_.absoluteFilePath("data");
|
||||
}
|
||||
|
||||
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
|
||||
using LuaCompletionCallback =
|
||||
lua::CallbackFunction<lua::api::CompletionList,
|
||||
lua::api::CompletionEvent>;
|
||||
std::optional<LuaCompletionCallback> getCompletionCallback()
|
||||
std::optional<sol::protected_function> getCompletionCallback()
|
||||
{
|
||||
if (this->state_ == nullptr || !this->error_.isNull())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
// this uses magic enum to help automatic tooling find usages
|
||||
auto typeName =
|
||||
magic_enum::enum_name(lua::api::EventType::CompletionRequested);
|
||||
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)
|
||||
auto it =
|
||||
this->callbacks.find(lua::api::EventType::CompletionRequested);
|
||||
if (it == this->callbacks.end())
|
||||
{
|
||||
lua_pop(this->state_, 1);
|
||||
return {};
|
||||
}
|
||||
|
||||
// move
|
||||
return std::make_optional<lua::CallbackFunction<
|
||||
lua::api::CompletionList, lua::api::CompletionEvent>>(
|
||||
this->state_, lua_gettop(this->state_));
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,18 +137,25 @@ public:
|
|||
bool hasFSPermissionFor(bool write, const QString &path);
|
||||
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:
|
||||
QDir loadDirectory_;
|
||||
lua_State *state_;
|
||||
|
||||
QString error_;
|
||||
|
||||
// maps command name -> function name
|
||||
std::unordered_map<QString, QString> ownedCommands;
|
||||
// maps command name -> function
|
||||
std::unordered_map<QString, sol::protected_function> ownedCommands;
|
||||
std::vector<QTimer *> activeTimeouts;
|
||||
int lastTimerId = 0;
|
||||
|
||||
friend class PluginController;
|
||||
friend class PluginControllerAccess; // this is for tests
|
||||
};
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
|
|
|
@ -13,16 +13,20 @@
|
|||
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
# include "singletons/Settings.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
}
|
||||
# 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 <utility>
|
||||
|
@ -113,10 +117,11 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
|||
return true;
|
||||
}
|
||||
|
||||
void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||
const QDir &pluginDir)
|
||||
void PluginController::openLibrariesFor(Plugin *plugin)
|
||||
{
|
||||
auto *L = plugin->state_;
|
||||
lua::StackGuard guard(L);
|
||||
sol::state_view lua(L);
|
||||
// Stuff to change, remove or hide behind a permission system:
|
||||
static const std::vector<luaL_Reg> loadedlibs = {
|
||||
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_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},
|
||||
// - fs 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));
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg c2Lib[] = {
|
||||
{"register_command", lua::api::c2_register_command},
|
||||
{"register_callback", lua::api::c2_register_callback},
|
||||
{"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");
|
||||
auto r = lua.registry();
|
||||
auto g = lua.globals();
|
||||
auto c2 = lua.create_table();
|
||||
g["c2"] = c2;
|
||||
|
||||
// ban functions
|
||||
// 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
|
||||
lua_getfield(L, gtable, "load");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||
lua.registry()["real_load"] = lua.globals()["load"];
|
||||
# endif
|
||||
// See chatterino::lua::api::g_load implementation
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg replacementFuncs[] = {
|
||||
{"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");
|
||||
g["loadfile"] = sol::nil;
|
||||
g["dofile"] = sol::nil;
|
||||
|
||||
// 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 table = lua_gettop(L);
|
||||
lua_getfield(L, -1, "remove");
|
||||
lua_remove(L, table);
|
||||
}
|
||||
auto remove = lua_gettop(L);
|
||||
auto package = g["package"];
|
||||
package["cpath"] = "";
|
||||
package["path"] = "";
|
||||
|
||||
sol::protected_function tbremove = g["table"]["remove"];
|
||||
|
||||
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
||||
sol::table searchers = package["searchers"];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
lua_pushvalue(L, remove);
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pcall(L, 1, 0, 0);
|
||||
tbremove(searchers);
|
||||
}
|
||||
lua_pop(L, 1); // get rid of remove
|
||||
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pushcclosure(L, lua::api::searcherRelative, 0);
|
||||
lua_seti(L, -2, 2);
|
||||
|
||||
lua::push(L, QString(pluginDir.absolutePath()));
|
||||
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
|
||||
lua_seti(L, -2, 3);
|
||||
lua_pop(L, 2); // remove package, package.searchers
|
||||
|
||||
// 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
|
||||
searchers.add(&lua::api::searcherRelative);
|
||||
searchers.add(&lua::api::searcherAbsolute);
|
||||
}
|
||||
// set up io lib
|
||||
{
|
||||
auto c2io = lua.create_table();
|
||||
auto realio = r[lua::api::REG_REAL_IO_NAME];
|
||||
c2io["type"] = realio["type"];
|
||||
g["io"] = c2io;
|
||||
// prevent plugins getting direct access to realio
|
||||
r[LUA_LOADED_TABLE]["io"] = c2io;
|
||||
|
||||
// Don't give plugins the option to shit into our stdio
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
||||
r["_IO_input"] = sol::nil;
|
||||
r["_IO_output"] = sol::nil;
|
||||
}
|
||||
PluginController::initSol(lua, plugin);
|
||||
}
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
// TODO: investigate if `plugin` can ever point to an invalid plugin,
|
||||
// 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,
|
||||
|
@ -314,7 +262,7 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
|||
<< " because safe mode is enabled.";
|
||||
return;
|
||||
}
|
||||
PluginController::openLibrariesFor(l, meta, pluginDir);
|
||||
PluginController::openLibrariesFor(temp);
|
||||
|
||||
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||
!getSettings()->pluginsEnabled)
|
||||
|
@ -345,17 +293,13 @@ bool PluginController::reload(const QString &id)
|
|||
{
|
||||
return false;
|
||||
}
|
||||
if (it->second->state_ != nullptr)
|
||||
{
|
||||
lua_close(it->second->state_);
|
||||
it->second->state_ = nullptr;
|
||||
}
|
||||
|
||||
for (const auto &[cmd, _] : it->second->ownedCommands)
|
||||
{
|
||||
getApp()->getCommands()->unregisterPluginCommand(cmd);
|
||||
}
|
||||
it->second->ownedCommands.clear();
|
||||
QDir loadDir = it->second->loadDirectory_;
|
||||
// Since Plugin owns the state, it will clean up everything related to it
|
||||
this->plugins_.erase(id);
|
||||
this->tryLoadFromDir(loadDir);
|
||||
return true;
|
||||
|
@ -369,27 +313,36 @@ QString PluginController::tryExecPluginCommand(const QString &commandName,
|
|||
if (auto it = plugin->ownedCommands.find(commandName);
|
||||
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_;
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str());
|
||||
lua::push(L, ctx);
|
||||
|
||||
auto res = lua_pcall(L, 1, 0, 0);
|
||||
if (res != LUA_OK)
|
||||
auto result =
|
||||
lua::tryCall<std::optional<QString>>(it->second, args);
|
||||
if (!result)
|
||||
{
|
||||
ctx.channel->addSystemMessage("Lua error: " +
|
||||
lua::humanErrorText(L, res));
|
||||
return "";
|
||||
ctx.channel->addSystemMessage(
|
||||
QStringView(
|
||||
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)
|
||||
<< "Something's seriously up, no plugin owns command" << commandName
|
||||
<< "yet a call to execute it came in";
|
||||
assert(false && "missing plugin command owner");
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
bool PluginController::isPluginEnabled(const QString &id)
|
||||
|
@ -435,32 +388,31 @@ std::pair<bool, QStringList> PluginController::updateCustomCompletions(
|
|||
continue;
|
||||
}
|
||||
|
||||
lua::StackGuard guard(pl->state_);
|
||||
|
||||
auto opt = pl->getCompletionCallback();
|
||||
if (opt)
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Processing custom completions from plugin" << name;
|
||||
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,
|
||||
.full_text_content = fullTextContent,
|
||||
.cursor_position = cursorPosition,
|
||||
.is_first_word = isFirstWord,
|
||||
});
|
||||
if (std::holds_alternative<int>(errOrList))
|
||||
}));
|
||||
if (!errOrList.has_value())
|
||||
{
|
||||
guard.handled();
|
||||
int err = std::get<int>(errOrList);
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Got error from plugin " << pl->meta.name
|
||||
<< " while refreshing tab completion: "
|
||||
<< lua::humanErrorText(pl->state_, err);
|
||||
<< errOrList.get_unexpected().error();
|
||||
continue;
|
||||
}
|
||||
|
||||
auto list = std::get<lua::api::CompletionList>(errOrList);
|
||||
auto list = lua::api::CompletionList(*errOrList);
|
||||
if (list.hideOthers)
|
||||
{
|
||||
results = QStringList(list.values.begin(), list.values.end());
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
# include <QJsonArray>
|
||||
# include <QJsonObject>
|
||||
# include <QString>
|
||||
# include <sol/forward.hpp>
|
||||
|
||||
# include <algorithm>
|
||||
# include <map>
|
||||
|
@ -66,11 +67,16 @@ private:
|
|||
const PluginMeta &meta);
|
||||
|
||||
// This function adds lua standard libraries into the state
|
||||
static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/,
|
||||
const QDir &pluginDir);
|
||||
static void openLibrariesFor(Plugin *plugin);
|
||||
|
||||
static void initSol(sol::state_view &lua, Plugin *plugin);
|
||||
|
||||
static void loadChatterinoLib(lua_State *l);
|
||||
bool tryLoadFromDir(const QDir &pluginDir);
|
||||
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
||||
|
||||
// This is for tests, pay no attention
|
||||
friend class PluginControllerAccess;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -10,6 +10,8 @@ namespace chatterino {
|
|||
|
||||
struct PluginPermission {
|
||||
explicit PluginPermission(const QJsonObject &obj);
|
||||
// This is for tests
|
||||
PluginPermission() = default;
|
||||
|
||||
enum class Type {
|
||||
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
|
||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/Channel.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "providers/twitch/TwitchChannel.hpp"
|
||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
}
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
# include <cassert>
|
||||
# include <memory>
|
||||
# include <optional>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(*vararg)
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg CHANNEL_REF_METHODS[] = {
|
||||
{"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},
|
||||
ChannelRef::ChannelRef(const std::shared_ptr<Channel> &chan)
|
||||
: weak(chan)
|
||||
{
|
||||
}
|
||||
|
||||
// Twitch
|
||||
{"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},
|
||||
std::shared_ptr<Channel> ChannelRef::strong()
|
||||
{
|
||||
auto c = this->weak.lock();
|
||||
if (!c)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Expired c2.Channel used - use c2.Channel:is_valid() to "
|
||||
"check validity");
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// misc
|
||||
{"__tostring", &ChannelRef::to_string},
|
||||
std::shared_ptr<TwitchChannel> ChannelRef::twitch()
|
||||
{
|
||||
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
|
||||
{"by_name", &ChannelRef::get_by_name},
|
||||
{"by_twitch_id", &ChannelRef::get_by_twitch_id},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
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);
|
||||
"by_name", &ChannelRef::get_by_name,
|
||||
"by_twitch_id", &ChannelRef::get_by_twitch_id
|
||||
);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
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 nullptr;
|
||||
return std::optional{value};
|
||||
}
|
||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
||||
{
|
||||
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),
|
||||
|
||||
return std::optional<int>{};
|
||||
};
|
||||
lua::push(L, modes);
|
||||
return 1;
|
||||
// clang-format off
|
||||
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);
|
||||
const auto s = tc->accessStreamStatus();
|
||||
const auto status = LuaStreamStatus{
|
||||
.live = s->live,
|
||||
.viewer_count = static_cast<int>(s->viewerCount),
|
||||
.uptime = s->uptimeSeconds,
|
||||
.title = s->title,
|
||||
.game_name = s->game,
|
||||
.game_id = s->gameId,
|
||||
};
|
||||
lua::push(L, status);
|
||||
return 1;
|
||||
// clang-format off
|
||||
return sol::table::create_with(L,
|
||||
"live", status.live,
|
||||
"viewer_count", status.viewerCount,
|
||||
"title", status.title,
|
||||
"game_name", status.game,
|
||||
"game_id", status.gameId,
|
||||
"uptime", status.uptimeSeconds
|
||||
);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
|
|
|
@ -1,48 +1,24 @@
|
|||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/Channel.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
# include <optional>
|
||||
# include <sol/forward.hpp>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
|
||||
/**
|
||||
* This enum describes a platform for the purpose of searching for a channel.
|
||||
* Currently only Twitch is supported because identifying IRC channels is tricky.
|
||||
* @exposeenum c2.Platform
|
||||
* @includefile providers/twitch/TwitchChannel.hpp
|
||||
*/
|
||||
enum class LPlatform {
|
||||
Twitch,
|
||||
//IRC,
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class c2.Channel
|
||||
*/
|
||||
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:
|
||||
ChannelRef(const std::shared_ptr<Channel> &chan);
|
||||
|
||||
/**
|
||||
* Returns true if the channel this object points to is valid.
|
||||
* If the object expired, returns false
|
||||
|
@ -51,7 +27,7 @@ public:
|
|||
* @lua@return boolean success
|
||||
* @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.
|
||||
|
@ -59,7 +35,7 @@ public:
|
|||
* @lua@return string name
|
||||
* @exposed c2.Channel:get_name
|
||||
*/
|
||||
static int get_name(lua_State *L);
|
||||
QString get_name();
|
||||
|
||||
/**
|
||||
* Gets the channel's type
|
||||
|
@ -67,7 +43,7 @@ public:
|
|||
* @lua@return c2.ChannelType
|
||||
* @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.
|
||||
|
@ -75,17 +51,17 @@ public:
|
|||
* @lua@return string 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.
|
||||
* Note that this does not execute client-commands.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
static int send_message(lua_State *L);
|
||||
void send_message(QString text, sol::variadic_args va);
|
||||
|
||||
/**
|
||||
* Adds a system message client-side
|
||||
|
@ -93,7 +69,7 @@ public:
|
|||
* @lua@param message string
|
||||
* @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.
|
||||
|
@ -103,7 +79,7 @@ public:
|
|||
* @lua@return boolean
|
||||
* @exposed c2.Channel:is_twitch_channel
|
||||
*/
|
||||
static int is_twitch_channel(lua_State *L);
|
||||
bool is_twitch_channel();
|
||||
|
||||
/**
|
||||
* Twitch Channel specific functions
|
||||
|
@ -115,7 +91,7 @@ public:
|
|||
* @lua@return RoomModes
|
||||
* @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.
|
||||
|
@ -123,7 +99,7 @@ public:
|
|||
* @lua@return StreamStatus
|
||||
* @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.
|
||||
|
@ -131,7 +107,7 @@ public:
|
|||
* @lua@return string
|
||||
* @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
|
||||
|
@ -139,7 +115,7 @@ public:
|
|||
* @lua@return boolean
|
||||
* @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
|
||||
|
@ -148,7 +124,7 @@ public:
|
|||
* @lua@return boolean
|
||||
* @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
|
||||
|
@ -157,7 +133,7 @@ public:
|
|||
* @lua@return boolean
|
||||
* @exposed c2.Channel:is_vip
|
||||
*/
|
||||
static int is_vip(lua_State *L);
|
||||
bool is_vip();
|
||||
|
||||
/**
|
||||
* Misc
|
||||
|
@ -167,7 +143,7 @@ public:
|
|||
* @lua@return string
|
||||
* @exposed c2.Channel:__tostring
|
||||
*/
|
||||
static int to_string(lua_State *L);
|
||||
QString to_string();
|
||||
|
||||
/**
|
||||
* Static functions
|
||||
|
@ -184,11 +160,10 @@ public:
|
|||
* - /automod
|
||||
*
|
||||
* @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?
|
||||
* @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.
|
||||
|
@ -197,79 +172,24 @@ public:
|
|||
* @lua@return c2.Channel?
|
||||
* @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
|
||||
/**
|
||||
* @lua@class RoomModes
|
||||
*/
|
||||
struct LuaRoomModes {
|
||||
/**
|
||||
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||
*/
|
||||
bool unique_chat = false;
|
||||
static void createUserType(sol::table &c2);
|
||||
|
||||
/**
|
||||
* @lua@field subscriber_only boolean
|
||||
*/
|
||||
bool subscriber_only = false;
|
||||
private:
|
||||
std::weak_ptr<Channel> weak;
|
||||
|
||||
/**
|
||||
* @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 emotes_only = false;
|
||||
/// Locks the weak pointer and throws if the pointer expired
|
||||
std::shared_ptr<Channel> strong();
|
||||
|
||||
/**
|
||||
* @lua@field follower_only number? Time in minutes you need to follow to chat or nil.
|
||||
*/
|
||||
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;
|
||||
/// Locks the weak pointer and throws if the pointer is invalid
|
||||
std::shared_ptr<TwitchChannel> twitch();
|
||||
};
|
||||
|
||||
// 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 {
|
||||
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
|
||||
|
|
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/NetworkResult.hpp"
|
||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "util/DebugCount.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
}
|
||||
# include <QChar>
|
||||
# include <QLoggingCategory>
|
||||
# include <QRandomGenerator>
|
||||
# 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 <vector>
|
||||
|
||||
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},
|
||||
{"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)
|
||||
void HTTPRequest::createUserType(sol::table &c2)
|
||||
{
|
||||
if (lua_gettop(L) < 1)
|
||||
{
|
||||
// The nullptr is there just to appease the compiler, luaL_error is no return
|
||||
luaL_error(L, "Called c2.HTTPRequest method without a request object");
|
||||
return nullptr;
|
||||
}
|
||||
if (lua_isuserdata(L, where) == 0)
|
||||
{
|
||||
luaL_error(
|
||||
L,
|
||||
"Called c2.HTTPRequest method with a non-userdata 'self' argument");
|
||||
return nullptr;
|
||||
}
|
||||
// luaL_checkudata is no-return if check fails
|
||||
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;
|
||||
c2.new_usertype<HTTPRequest>( //
|
||||
"HTTPRequest", sol::no_constructor, //
|
||||
sol::meta_method::to_string, &HTTPRequest::to_string, //
|
||||
|
||||
"on_success", &HTTPRequest::on_success, //
|
||||
"on_error", &HTTPRequest::on_error, //
|
||||
"finally", &HTTPRequest::finally, //
|
||||
|
||||
"set_timeout", &HTTPRequest::set_timeout, //
|
||||
"set_payload", &HTTPRequest::set_payload, //
|
||||
"set_header", &HTTPRequest::set_header, //
|
||||
"execute", &HTTPRequest::execute, //
|
||||
|
||||
"create", &HTTPRequest::create //
|
||||
);
|
||||
}
|
||||
|
||||
void HTTPRequest::createMetatable(lua_State *L)
|
||||
void HTTPRequest::on_success(sol::protected_function func)
|
||||
{
|
||||
lua::StackGuard guard(L, 1);
|
||||
|
||||
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);
|
||||
this->cbSuccess = std::make_optional(func);
|
||||
}
|
||||
|
||||
int HTTPRequest::on_success_wrap(lua_State *L)
|
||||
void HTTPRequest::on_error(sol::protected_function func)
|
||||
{
|
||||
lua::StackGuard guard(L, -2);
|
||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
||||
return ptr->on_success(L);
|
||||
this->cbError = std::make_optional(func);
|
||||
}
|
||||
|
||||
int HTTPRequest::on_success(lua_State *L)
|
||||
void HTTPRequest::set_timeout(int timeout)
|
||||
{
|
||||
auto top = lua_gettop(L);
|
||||
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;
|
||||
this->timeout_ = timeout;
|
||||
}
|
||||
|
||||
int HTTPRequest::on_error_wrap(lua_State *L)
|
||||
void HTTPRequest::finally(sol::protected_function func)
|
||||
{
|
||||
lua::StackGuard guard(L, -2);
|
||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
||||
return ptr->on_error(L);
|
||||
this->cbFinally = std::make_optional(func);
|
||||
}
|
||||
|
||||
int HTTPRequest::on_error(lua_State *L)
|
||||
void HTTPRequest::set_payload(QByteArray payload)
|
||||
{
|
||||
auto top = lua_gettop(L);
|
||||
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;
|
||||
this->req_ = std::move(this->req_).payload(payload);
|
||||
}
|
||||
|
||||
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);
|
||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
||||
return ptr->set_timeout(L);
|
||||
this->req_ = std::move(this->req_).header(name, value);
|
||||
}
|
||||
|
||||
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);
|
||||
if (!parsedurl.isValid())
|
||||
{
|
||||
return luaL_error(
|
||||
L, "cannot parse url (2nd argument of HTTPRequest.create, "
|
||||
throw std::runtime_error(
|
||||
"cannot parse url (2nd argument of HTTPRequest.create, "
|
||||
"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);
|
||||
if (!pl->hasHTTPPermissionFor(parsedurl))
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Plugin does not have permission to send HTTP requests "
|
||||
throw std::runtime_error(
|
||||
"Plugin does not have permission to send HTTP requests "
|
||||
"to this URL");
|
||||
}
|
||||
NetworkRequest r(parsedurl, method);
|
||||
lua::push(
|
||||
L, std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r)));
|
||||
return 1;
|
||||
return std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r));
|
||||
}
|
||||
|
||||
int HTTPRequest::execute_wrap(lua_State *L)
|
||||
void HTTPRequest::execute(sol::this_state L)
|
||||
{
|
||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
||||
return ptr->execute(L);
|
||||
}
|
||||
|
||||
int HTTPRequest::execute(lua_State *L)
|
||||
{
|
||||
auto shared = this->shared_from_this();
|
||||
if (this->done)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Cannot execute this c2.HTTPRequest, it was executed already!");
|
||||
}
|
||||
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_)
|
||||
.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);
|
||||
auto *thread = lua_newthread(L);
|
||||
|
||||
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
|
||||
(*self->cbSuccess)(HTTPResponse(res));
|
||||
self->cbSuccess = std::nullopt;
|
||||
})
|
||||
.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);
|
||||
auto *thread = lua_newthread(L);
|
||||
|
||||
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
|
||||
(*self->cbError)(HTTPResponse(res));
|
||||
self->cbError = std::nullopt;
|
||||
})
|
||||
.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);
|
||||
auto *thread = lua_newthread(L);
|
||||
|
||||
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();
|
||||
(*self->cbFinally)();
|
||||
self->cbFinally = std::nullopt;
|
||||
})
|
||||
.timeout(this->timeout_)
|
||||
.execute();
|
||||
return 0;
|
||||
}
|
||||
|
||||
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
|
||||
|
@ -418,34 +189,10 @@ HTTPRequest::~HTTPRequest()
|
|||
// 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())
|
||||
{
|
||||
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);
|
||||
return "<HTTPRequest>";
|
||||
}
|
||||
|
||||
// NOLINTEND(*vararg)
|
||||
} // 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
|
||||
|
|
|
@ -2,10 +2,16 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/network/NetworkRequest.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
# include <sol/forward.hpp>
|
||||
# include <sol/types.hpp>
|
||||
|
||||
# include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
class PluginController;
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
|
||||
|
@ -33,33 +39,19 @@ public:
|
|||
private:
|
||||
NetworkRequest req_;
|
||||
|
||||
static void createMetatable(lua_State *L);
|
||||
static void createUserType(sol::table &c2);
|
||||
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 might be a null QString if the request has already been executed or
|
||||
// the table wasn't created yet.
|
||||
QString privateKey;
|
||||
int timeout_ = 10'000;
|
||||
bool done = false;
|
||||
|
||||
std::optional<sol::protected_function> cbSuccess;
|
||||
std::optional<sol::protected_function> cbError;
|
||||
std::optional<sol::protected_function> cbFinally;
|
||||
|
||||
public:
|
||||
// These functions are wrapped so data can be accessed more easily. When a call from Lua comes in:
|
||||
// - the static wrapper function is called
|
||||
|
@ -72,8 +64,7 @@ public:
|
|||
* @lua@param callback HTTPCallback Function to call when the HTTP request succeeds
|
||||
* @exposed HTTPRequest:on_success
|
||||
*/
|
||||
static int on_success_wrap(lua_State *L);
|
||||
int on_success(lua_State *L);
|
||||
void on_success(sol::protected_function func);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @exposed HTTPRequest:on_error
|
||||
*/
|
||||
static int on_error_wrap(lua_State *L);
|
||||
int on_error(lua_State *L);
|
||||
void on_error(sol::protected_function func);
|
||||
|
||||
/**
|
||||
* Sets the finally callback
|
||||
|
@ -90,8 +80,7 @@ public:
|
|||
* @lua@param callback fun(): nil Function to call when the HTTP request finishes
|
||||
* @exposed HTTPRequest:finally
|
||||
*/
|
||||
static int finally_wrap(lua_State *L);
|
||||
int finally(lua_State *L);
|
||||
void finally(sol::protected_function func);
|
||||
|
||||
/**
|
||||
* Sets the timeout
|
||||
|
@ -99,8 +88,7 @@ public:
|
|||
* @lua@param timeout integer How long in milliseconds until the times out
|
||||
* @exposed HTTPRequest:set_timeout
|
||||
*/
|
||||
static int set_timeout_wrap(lua_State *L);
|
||||
int set_timeout(lua_State *L);
|
||||
void set_timeout(int timeout);
|
||||
|
||||
/**
|
||||
* Sets the request payload
|
||||
|
@ -108,8 +96,7 @@ public:
|
|||
* @lua@param data string
|
||||
* @exposed HTTPRequest:set_payload
|
||||
*/
|
||||
static int set_payload_wrap(lua_State *L);
|
||||
int set_payload(lua_State *L);
|
||||
void set_payload(QByteArray payload);
|
||||
|
||||
/**
|
||||
* Sets a header in the request
|
||||
|
@ -118,16 +105,19 @@ public:
|
|||
* @lua@param value string
|
||||
* @exposed HTTPRequest:set_header
|
||||
*/
|
||||
static int set_header_wrap(lua_State *L);
|
||||
int set_header(lua_State *L);
|
||||
void set_header(QByteArray name, QByteArray value);
|
||||
|
||||
/**
|
||||
* Executes the HTTP request
|
||||
*
|
||||
* @exposed HTTPRequest:execute
|
||||
*/
|
||||
static int execute_wrap(lua_State *L);
|
||||
int execute(lua_State *L);
|
||||
void execute(sol::this_state L);
|
||||
/**
|
||||
* @lua@return string
|
||||
* @exposed HTTPRequest:__tostring
|
||||
*/
|
||||
QString to_string();
|
||||
|
||||
/**
|
||||
* Static functions
|
||||
|
@ -142,7 +132,9 @@ public:
|
|||
* @lua@return HTTPRequest
|
||||
* @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)
|
||||
|
|
|
@ -2,77 +2,28 @@
|
|||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||
|
||||
# include "common/network/NetworkResult.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "util/DebugCount.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
}
|
||||
# include <sol/raii.hpp>
|
||||
# include <sol/types.hpp>
|
||||
|
||||
# include <utility>
|
||||
|
||||
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");
|
||||
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::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;
|
||||
"data", &HTTPResponse::data, //
|
||||
"status", &HTTPResponse::status, //
|
||||
"error", &HTTPResponse::error //
|
||||
);
|
||||
}
|
||||
|
||||
HTTPResponse::HTTPResponse(NetworkResult res)
|
||||
|
@ -85,60 +36,30 @@ HTTPResponse::~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
|
||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
||||
return ptr->data(L);
|
||||
return this->result_.getData();
|
||||
}
|
||||
|
||||
int HTTPResponse::data(lua_State *L)
|
||||
std::optional<int> HTTPResponse::status()
|
||||
{
|
||||
lua::push(L, this->result_.getData().toStdString());
|
||||
return 1;
|
||||
return this->result_.status();
|
||||
}
|
||||
|
||||
int HTTPResponse::status_wrap(lua_State *L)
|
||||
QString HTTPResponse::error()
|
||||
{
|
||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
||||
return ptr->status(L);
|
||||
return this->result_.formatError();
|
||||
}
|
||||
|
||||
int HTTPResponse::status(lua_State *L)
|
||||
QString HTTPResponse::to_string()
|
||||
{
|
||||
lua::push(L, this->result_.status());
|
||||
return 1;
|
||||
if (this->status().has_value())
|
||||
{
|
||||
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 {
|
||||
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
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/network/NetworkResult.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include <lua.h>
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
# include <memory>
|
||||
extern "C" {
|
||||
# include <lua.h>
|
||||
}
|
||||
|
||||
namespace chatterino {
|
||||
class PluginController;
|
||||
|
@ -18,7 +17,7 @@ namespace chatterino::lua::api {
|
|||
/**
|
||||
* @lua@class HTTPResponse
|
||||
*/
|
||||
class HTTPResponse : public std::enable_shared_from_this<HTTPResponse>
|
||||
class HTTPResponse
|
||||
{
|
||||
NetworkResult result_;
|
||||
|
||||
|
@ -31,20 +30,9 @@ public:
|
|||
~HTTPResponse();
|
||||
|
||||
private:
|
||||
static void createMetatable(lua_State *L);
|
||||
static void createUserType(sol::table &c2);
|
||||
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:
|
||||
/**
|
||||
* Returns the data. This is not guaranteed to be encoded using any
|
||||
|
@ -52,29 +40,28 @@ public:
|
|||
*
|
||||
* @exposed HTTPResponse:data
|
||||
*/
|
||||
static int data_wrap(lua_State *L);
|
||||
int data(lua_State *L);
|
||||
QByteArray data();
|
||||
|
||||
/**
|
||||
* Returns the status code.
|
||||
*
|
||||
* @exposed HTTPResponse:status
|
||||
*/
|
||||
static int status_wrap(lua_State *L);
|
||||
int status(lua_State *L);
|
||||
std::optional<int> status();
|
||||
|
||||
/**
|
||||
* A somewhat human readable description of an error if such happened
|
||||
* @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)
|
||||
} // namespace chatterino::lua::api
|
||||
namespace chatterino::lua {
|
||||
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPResponse> request);
|
||||
} // namespace chatterino::lua
|
||||
#endif
|
||||
|
|
|
@ -2,15 +2,17 @@
|
|||
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
}
|
||||
# include <QString>
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
# include <cerrno>
|
||||
# include <stdexcept>
|
||||
# include <utility>
|
||||
|
||||
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);
|
||||
lua::push(L, value);
|
||||
lua::push(L, errnoequiv);
|
||||
return 3;
|
||||
sol::variadic_results out;
|
||||
out.push_back(sol::nil);
|
||||
out.push_back(sol::make_object(L, value.toStdString()));
|
||||
out.push_back({L, sol::in_place_type<int>, errnoequiv});
|
||||
return out;
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(*vararg)
|
||||
int io_open(lua_State *L)
|
||||
sol::variadic_results io_open(sol::this_state L, QString filename,
|
||||
QString strmode)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
LuaFileMode mode;
|
||||
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);
|
||||
LuaFileMode mode(strmode);
|
||||
if (!mode.error.isEmpty())
|
||||
{
|
||||
return luaL_error(L, mode.error.toStdString().c_str());
|
||||
}
|
||||
}
|
||||
QString filename;
|
||||
if (!lua::pop(L, &filename))
|
||||
{
|
||||
return luaL_error(L,
|
||||
"io.open filename (1st argument) must be a string");
|
||||
throw std::runtime_error(mode.error.toStdString());
|
||||
}
|
||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||
auto abs = file.absoluteFilePath();
|
||||
|
@ -144,39 +129,35 @@ int io_open(lua_State *L)
|
|||
"Plugin does not have permissions to access given file.",
|
||||
EACCES);
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "open");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua::push(L, abs);
|
||||
lua::push(L, mode.toString());
|
||||
lua_call(L, 2, 3);
|
||||
return 3;
|
||||
|
||||
sol::state_view lua(L);
|
||||
auto open = lua.registry()[REG_REAL_IO_NAME]["open"];
|
||||
sol::protected_function_result res =
|
||||
open(abs.toStdString(), mode.toString().toStdString());
|
||||
return res;
|
||||
}
|
||||
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);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "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");
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||
auto abs = file.absoluteFilePath();
|
||||
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||
|
@ -185,191 +166,168 @@ int io_lines(lua_State *L)
|
|||
bool ok = pl->hasFSPermissionFor(false, abs);
|
||||
if (!ok)
|
||||
{
|
||||
return ioError(L,
|
||||
"Plugin does not have permissions to access given file.",
|
||||
EACCES);
|
||||
throw std::runtime_error(
|
||||
"Plugin does not have permissions to access given file.");
|
||||
}
|
||||
// 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);
|
||||
lua_getfield(L, -1, "lines");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
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);
|
||||
auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"];
|
||||
sol::protected_function_result res = lines(abs.toStdString(), args);
|
||||
return res;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// This is the code for both io.input and io.output
|
||||
int globalFileCommon(lua_State *L, bool output)
|
||||
{
|
||||
sol::variadic_results io_input_argless(sol::this_state L)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
// Three signature cases:
|
||||
// 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
|
||||
sol::state_view lua(L);
|
||||
|
||||
lua_pushvalue(L, 1); // dupe arg
|
||||
if (output)
|
||||
{
|
||||
lua_pushstring(L, "w");
|
||||
}
|
||||
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)
|
||||
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||
sol::protected_function_result res = func();
|
||||
return res;
|
||||
}
|
||||
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
int io_close(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.close. Expected one or zero.");
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
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");
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
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(
|
||||
L, "Too many arguments for io.flush. Expected one or zero.");
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
lua_getfield(L, -1, "flush");
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int io_read(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
sol::state_view lua(L);
|
||||
auto res = io_open(L, std::move(filename), "w");
|
||||
if (res.size() != 1)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.read. Expected one or zero.");
|
||||
throw std::runtime_error(res.at(1).as<std::string>());
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
||||
lua_getfield(L, -1, "read");
|
||||
lua_insert(L, 1);
|
||||
lua_insert(L, 2);
|
||||
lua_call(L, lua_gettop(L) - 1, 1);
|
||||
return 1;
|
||||
auto obj = res.at(0);
|
||||
if (obj.get_type() != sol::type::userdata)
|
||||
{
|
||||
throw std::runtime_error("internal error: a file must be a userdata.");
|
||||
}
|
||||
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");
|
||||
lua_getfield(L, -1, "write");
|
||||
lua_insert(L, 1);
|
||||
lua_insert(L, 2);
|
||||
// (input)
|
||||
// (input).read
|
||||
// args
|
||||
lua_call(L, lua_gettop(L) - 1, 1);
|
||||
return 1;
|
||||
sol::state_view lua(L);
|
||||
auto out = lua.registry()["_IO_output"];
|
||||
return io_close_file(L, out);
|
||||
}
|
||||
|
||||
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
|
||||
#endif
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include <QString>
|
||||
# include <sol/types.hpp>
|
||||
# include <sol/variadic_args.hpp>
|
||||
# include <sol/variadic_results.hpp>
|
||||
|
||||
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.
|
||||
|
||||
const char *const REG_REAL_IO_NAME = "real_lua_io_lib";
|
||||
const char *const REG_C2_IO_NAME = "c2io";
|
||||
|
||||
/**
|
||||
* 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+"
|
||||
* @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
|
||||
|
@ -32,7 +37,9 @@ int io_open(lua_State *L);
|
|||
* @lua@param ...
|
||||
* @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.
|
||||
|
@ -42,7 +49,9 @@ int io_lines(lua_State *L);
|
|||
* @lua@return nil|FILE*
|
||||
* @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
|
||||
|
@ -52,7 +61,9 @@ int io_input(lua_State *L);
|
|||
* @lua@return nil|FILE*
|
||||
* @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.
|
||||
|
@ -61,7 +72,8 @@ int io_output(lua_State *L);
|
|||
* @lua@param nil|FILE*
|
||||
* @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.
|
||||
|
@ -70,7 +82,8 @@ int io_close(lua_State *L);
|
|||
* @lua@param nil|FILE*
|
||||
* @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
|
||||
|
@ -79,7 +92,7 @@ int io_flush(lua_State *L);
|
|||
* @lua@param nil|string
|
||||
* @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
|
||||
|
@ -88,10 +101,10 @@ int io_read(lua_State *L);
|
|||
* @lua@param nil|string
|
||||
* @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);
|
||||
int io_tmpfile(lua_State *L);
|
||||
void io_popen();
|
||||
void io_tmpfile();
|
||||
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
} // namespace chatterino::lua::api
|
||||
|
|
|
@ -380,6 +380,97 @@ EmotePtr makeAutoModBadge()
|
|||
Url{"https://dashboard.twitch.tv/settings/moderation/automod"}});
|
||||
}
|
||||
|
||||
std::tuple<std::optional<EmotePtr>, MessageElementFlags, bool> parseEmote(
|
||||
TwitchChannel *twitchChannel, const EmoteName &name)
|
||||
{
|
||||
// Emote order:
|
||||
// - FrankerFaceZ Channel
|
||||
// - BetterTTV Channel
|
||||
// - 7TV Channel
|
||||
// - FrankerFaceZ Global
|
||||
// - BetterTTV Global
|
||||
// - 7TV Global
|
||||
|
||||
const auto *globalFfzEmotes = getApp()->getFfzEmotes();
|
||||
const auto *globalBttvEmotes = getApp()->getBttvEmotes();
|
||||
const auto *globalSeventvEmotes = getApp()->getSeventvEmotes();
|
||||
|
||||
std::optional<EmotePtr> emote{};
|
||||
|
||||
if (twitchChannel != nullptr)
|
||||
{
|
||||
// Check for channel emotes
|
||||
|
||||
emote = twitchChannel->ffzEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
return {
|
||||
emote,
|
||||
MessageElementFlag::FfzEmote,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
emote = twitchChannel->bttvEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
return {
|
||||
emote,
|
||||
MessageElementFlag::BttvEmote,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
emote = twitchChannel->seventvEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
return {
|
||||
emote,
|
||||
MessageElementFlag::SevenTVEmote,
|
||||
emote.value()->zeroWidth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for global emotes
|
||||
|
||||
emote = globalFfzEmotes->emote(name);
|
||||
if (emote)
|
||||
{
|
||||
return {
|
||||
emote,
|
||||
MessageElementFlag::FfzEmote,
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
emote = globalBttvEmotes->emote(name);
|
||||
if (emote)
|
||||
{
|
||||
return {
|
||||
emote,
|
||||
MessageElementFlag::BttvEmote,
|
||||
zeroWidthEmotes.contains(name.string),
|
||||
};
|
||||
}
|
||||
|
||||
emote = globalSeventvEmotes->globalEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
return {
|
||||
emote,
|
||||
MessageElementFlag::SevenTVEmote,
|
||||
emote.value()->zeroWidth,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
{},
|
||||
{},
|
||||
false,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -2235,7 +2326,7 @@ QString MessageBuilder::parseRoomID(const QVariantMap &tags,
|
|||
qCWarning(chatterinoTwitch)
|
||||
<< "The room-ID of the received message doesn't match the "
|
||||
"room-ID of the channel - received:"
|
||||
<< roomID << "chanel:" << twitchChannel->roomId();
|
||||
<< roomID << "channel:" << twitchChannel->roomId();
|
||||
}
|
||||
}
|
||||
return roomID;
|
||||
|
@ -2510,73 +2601,13 @@ void MessageBuilder::appendUsername(const QVariantMap &tags,
|
|||
Outcome MessageBuilder::tryAppendEmote(TwitchChannel *twitchChannel,
|
||||
const EmoteName &name)
|
||||
{
|
||||
auto *app = getApp();
|
||||
|
||||
auto flags = MessageElementFlags();
|
||||
auto emote = std::optional<EmotePtr>{};
|
||||
|
||||
// Emote order:
|
||||
// - FrankerFaceZ Channel
|
||||
// - BetterTTV Channel
|
||||
// - 7TV Channel
|
||||
// - FrankerFaceZ Global
|
||||
// - BetterTTV Global
|
||||
// - 7TV Global
|
||||
[&] {
|
||||
if (twitchChannel)
|
||||
{
|
||||
emote = twitchChannel->ffzEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
flags = MessageElementFlag::FfzEmote;
|
||||
return;
|
||||
}
|
||||
|
||||
emote = twitchChannel->bttvEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
flags = MessageElementFlag::BttvEmote;
|
||||
return;
|
||||
}
|
||||
|
||||
emote = twitchChannel->seventvEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
flags = MessageElementFlag::SevenTVEmote;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// check for global emotes
|
||||
emote = app->getFfzEmotes()->emote(name);
|
||||
if (emote)
|
||||
{
|
||||
flags = MessageElementFlag::FfzEmote;
|
||||
return;
|
||||
}
|
||||
|
||||
emote = app->getBttvEmotes()->emote(name);
|
||||
if (emote)
|
||||
{
|
||||
flags = MessageElementFlag::BttvEmote;
|
||||
return;
|
||||
}
|
||||
|
||||
emote = app->getSeventvEmotes()->globalEmote(name);
|
||||
if (emote)
|
||||
{
|
||||
flags = MessageElementFlag::SevenTVEmote;
|
||||
return;
|
||||
}
|
||||
}();
|
||||
auto [emote, flags, zeroWidth] = parseEmote(twitchChannel, name);
|
||||
|
||||
if (!emote)
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
|
||||
bool zeroWidth = emote.value()->zeroWidth;
|
||||
|
||||
if (zeroWidth && getSettings()->enableZeroWidthEmotes && !this->isEmpty())
|
||||
{
|
||||
// Attempt to merge current zero-width emote into any previous emotes
|
||||
|
|
|
@ -245,7 +245,9 @@ public:
|
|||
/// not need to be the `thread`s root. If this message isn't
|
||||
/// replying to anything, this is an empty `shared_ptr`.
|
||||
///
|
||||
/// @returns The built message and a highlight result.
|
||||
/// @returns The built message and a highlight result. If the message is
|
||||
/// ignored (e.g. from a blocked user), then the returned pointer
|
||||
/// will be en empty `shared_ptr`.
|
||||
static std::pair<MessagePtrMut, HighlightAlert> makeIrcMessage(
|
||||
Channel *channel, const Communi::IrcMessage *ircMessage,
|
||||
const MessageParseArgs &args, QString content,
|
||||
|
|
|
@ -64,22 +64,55 @@ const int MAX_QUEUED_REDEMPTIONS = 16;
|
|||
class TwitchChannel final : public Channel, public ChannelChatters
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @lua@class StreamStatus
|
||||
*/
|
||||
struct StreamStatus {
|
||||
/**
|
||||
* @lua@field live boolean
|
||||
*/
|
||||
bool live = false;
|
||||
bool rerun = false;
|
||||
/**
|
||||
* @lua@field viewer_count number
|
||||
*/
|
||||
unsigned viewerCount = 0;
|
||||
/**
|
||||
* @lua@field title string Stream title or last stream title
|
||||
*/
|
||||
QString title;
|
||||
/**
|
||||
* @lua@field game_name string
|
||||
*/
|
||||
QString game;
|
||||
/**
|
||||
* @lua@field game_id string
|
||||
*/
|
||||
QString gameId;
|
||||
QString uptime;
|
||||
/**
|
||||
* @lua@field uptime number Seconds since the stream started.
|
||||
*/
|
||||
int uptimeSeconds = 0;
|
||||
QString streamType;
|
||||
QString streamId;
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class RoomModes
|
||||
*/
|
||||
struct RoomModes {
|
||||
/**
|
||||
* @lua@field subscriber_only boolean
|
||||
*/
|
||||
bool submode = false;
|
||||
/**
|
||||
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -88,6 +121,8 @@ public:
|
|||
* Special cases:
|
||||
* -1 = follower mode off
|
||||
* 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;
|
||||
|
||||
|
@ -95,6 +130,8 @@ public:
|
|||
* @brief Number of seconds required to wait before typing emotes
|
||||
*
|
||||
* 0 = slow mode off
|
||||
*
|
||||
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||
**/
|
||||
int slowMode = 0;
|
||||
};
|
||||
|
|
|
@ -28,6 +28,15 @@ constexpr auto type_name()
|
|||
name.remove_prefix(prefix.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;
|
||||
}
|
||||
|
||||
|
|
|
@ -876,6 +876,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
|
|||
}
|
||||
break;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
// wait for Qt to process this message
|
||||
postToThread([] {
|
||||
getApp()->getWindows()->invalidateChannelViewBuffers();
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_NCLBUTTONDOWN:
|
||||
case WM_NCLBUTTONUP: {
|
||||
// WM_NCLBUTTON{DOWN, UP} gets called when the left mouse button
|
||||
|
|
|
@ -805,6 +805,32 @@ void SplitContainer::popup()
|
|||
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(
|
||||
const Node *currentNode) const
|
||||
{
|
||||
|
@ -814,7 +840,7 @@ NodeDescriptor SplitContainer::buildDescriptorRecursively(
|
|||
currentNode->split_->getIndirectChannel().getType();
|
||||
|
||||
SplitNodeDescriptor result;
|
||||
result.type_ = qmagicenum::enumNameString(channelType);
|
||||
result.type_ = channelTypeToString(channelType);
|
||||
result.channelName_ = currentNode->split_->getChannel()->getName();
|
||||
result.filters_ = currentNode->split_->getFilters();
|
||||
return result;
|
||||
|
|
|
@ -48,6 +48,7 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.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/IgnoreController.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/NetworkResult.hpp"
|
||||
#include "NetworkHelpers.hpp"
|
||||
#include "Test.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
@ -10,14 +11,6 @@ using namespace chatterino;
|
|||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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