mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Merge branch 'master' into fix/less-invalidation
This commit is contained in:
commit
4697af3211
53 changed files with 2823 additions and 3012 deletions
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
|
||||
|
|
|
@ -108,8 +108,10 @@
|
|||
- 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)
|
||||
|
||||
## 2.5.1
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5,7 +5,6 @@ set(benchmark_SOURCES
|
|||
resources/bench.qrc
|
||||
|
||||
src/Emojis.cpp
|
||||
src/Highlights.cpp
|
||||
src/FormatTime.cpp
|
||||
src/Helpers.cpp
|
||||
src/LimitedQueue.cpp
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "controllers/highlights/HighlightPhrase.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "mocks/BaseApplication.hpp"
|
||||
#include "mocks/UserData.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
class BenchmarkMessageBuilder : public MessageBuilder
|
||||
{
|
||||
public:
|
||||
explicit BenchmarkMessageBuilder(
|
||||
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args)
|
||||
: MessageBuilder(_channel, _ircMessage, _args)
|
||||
{
|
||||
}
|
||||
|
||||
virtual MessagePtr build()
|
||||
{
|
||||
// PARSE
|
||||
this->parse();
|
||||
this->usernameColor_ = getRandomColor(this->ircMessage->nick());
|
||||
|
||||
// words
|
||||
// this->addWords(this->originalMessage_.split(' '));
|
||||
|
||||
this->message().messageText = this->originalMessage_;
|
||||
this->message().searchText = this->message().localizedName + " " +
|
||||
this->userName + ": " +
|
||||
this->originalMessage_;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void bench()
|
||||
{
|
||||
this->parseHighlights();
|
||||
}
|
||||
};
|
||||
|
||||
class MockApplication : public mock::BaseApplication
|
||||
{
|
||||
public:
|
||||
MockApplication()
|
||||
: highlights(this->settings, &this->accounts)
|
||||
{
|
||||
}
|
||||
|
||||
AccountController *getAccounts() override
|
||||
{
|
||||
return &this->accounts;
|
||||
}
|
||||
HighlightController *getHighlights() override
|
||||
{
|
||||
return &this->highlights;
|
||||
}
|
||||
|
||||
IUserDataController *getUserData() override
|
||||
{
|
||||
return &this->userData;
|
||||
}
|
||||
|
||||
AccountController accounts;
|
||||
HighlightController highlights;
|
||||
mock::UserDataController userData;
|
||||
};
|
||||
|
||||
static void BM_HighlightTest(benchmark::State &state)
|
||||
{
|
||||
MockApplication mockApplication;
|
||||
|
||||
std::string message =
|
||||
R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))";
|
||||
auto ircMessage = Communi::IrcMessage::fromData(message.c_str(), nullptr);
|
||||
auto privMsg = dynamic_cast<Communi::IrcPrivateMessage *>(ircMessage);
|
||||
assert(privMsg != nullptr);
|
||||
MessageParseArgs args;
|
||||
auto emptyChannel = Channel::getEmpty();
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
state.PauseTiming();
|
||||
BenchmarkMessageBuilder b(emptyChannel.get(), privMsg, args);
|
||||
|
||||
b.build();
|
||||
state.ResumeTiming();
|
||||
|
||||
b.bench();
|
||||
}
|
||||
}
|
||||
|
||||
BENCHMARK(BM_HighlightTest);
|
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)
|
||||
{
|
||||
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;
|
||||
L.plugin()->callbacks[evtType] = std::move(callback);
|
||||
}
|
||||
|
||||
int c2_later(lua_State *L)
|
||||
void c2_log(ThisPluginState L, LogLevel lvl, sol::variadic_args args)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
lua::StackGuard guard(L);
|
||||
{
|
||||
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)");
|
||||
QDebug stream = qdebugStreamForLogLevel(lvl);
|
||||
logHelper(L, L.plugin(), stream, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (!lua_isfunction(L, lua_gettop(L)))
|
||||
void c2_later(ThisPluginState L, sol::protected_function callback, int time)
|
||||
{
|
||||
if (time <= 0)
|
||||
{
|
||||
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]() {
|
||||
timer->deleteLater();
|
||||
pl->removeTimeout(timer);
|
||||
int nres{};
|
||||
lua_resume(coro, nullptr, 0, &nres);
|
||||
sol::state_view main = sol::main_thread(L);
|
||||
|
||||
lua_pushnil(coro);
|
||||
lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
||||
if (lua_gettop(coro) != 0)
|
||||
{
|
||||
stackDump(coro,
|
||||
pl->id +
|
||||
": timer returned a value, this shouldn't happen "
|
||||
"and is probably a plugin bug");
|
||||
}
|
||||
});
|
||||
stackDump(L, "before setfield");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
||||
lua_xmove(L, coro, 1); // move function to thread
|
||||
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);
|
||||
sol::protected_function_result res = cb();
|
||||
|
||||
if (res.return_count() != 0)
|
||||
{
|
||||
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;
|
||||
});
|
||||
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"] = "";
|
||||
|
||||
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
||||
for (int i = 0; i < 3; i++)
|
||||
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++)
|
||||
{
|
||||
tbremove(searchers);
|
||||
}
|
||||
searchers.add(&lua::api::searcherRelative);
|
||||
searchers.add(&lua::api::searcherAbsolute);
|
||||
}
|
||||
// set up io lib
|
||||
{
|
||||
lua_pushvalue(L, remove);
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pcall(L, 1, 0, 0);
|
||||
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
|
||||
r["_IO_input"] = sol::nil;
|
||||
r["_IO_output"] = sol::nil;
|
||||
}
|
||||
lua_pop(L, 1); // get rid of remove
|
||||
PluginController::initSol(lua, plugin);
|
||||
}
|
||||
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pushcclosure(L, lua::api::searcherRelative, 0);
|
||||
lua_seti(L, -2, 2);
|
||||
// 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);
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
|
||||
// set ourio.type = realio.type
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "type");
|
||||
lua_remove(L, -2); // remove realio
|
||||
lua_setfield(L, iolibIdx, "type");
|
||||
lua_pop(L, 1); // still have iolib on top of stack
|
||||
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_setfield(L, gtable, "io");
|
||||
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME);
|
||||
|
||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_setfield(L, -2, "io");
|
||||
|
||||
lua_pop(L, 3); // remove gtable, iolib, LOADED
|
||||
|
||||
// Don't give plugins the option to shit into our stdio
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
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{
|
||||
.query = query,
|
||||
.full_text_content = fullTextContent,
|
||||
.cursor_position = cursorPosition,
|
||||
.is_first_word = isFirstWord,
|
||||
});
|
||||
if (std::holds_alternative<int>(errOrList))
|
||||
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 (!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},
|
||||
|
||||
// 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},
|
||||
|
||||
// misc
|
||||
{"__tostring", &ChannelRef::to_string},
|
||||
|
||||
// static
|
||||
{"by_name", &ChannelRef::get_by_name},
|
||||
{"by_twitch_id", &ChannelRef::get_by_twitch_id},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
void ChannelRef::createMetatable(lua_State *L)
|
||||
ChannelRef::ChannelRef(const std::shared_ptr<Channel> &chan)
|
||||
: weak(chan)
|
||||
{
|
||||
lua::StackGuard guard(L, 1);
|
||||
|
||||
luaL_newmetatable(L, "c2.Channel");
|
||||
lua_pushstring(L, "__index");
|
||||
lua_pushvalue(L, -2); // clone metatable
|
||||
lua_settable(L, -3); // metatable.__index = metatable
|
||||
|
||||
// Generic IWeakResource stuff
|
||||
lua_pushstring(L, "__gc");
|
||||
lua_pushcfunction(
|
||||
L, (&WeakPtrUserData<UserData::Type::Channel, ChannelRef>::destroy));
|
||||
lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy
|
||||
|
||||
luaL_setfuncs(L, CHANNEL_REF_METHODS, 0);
|
||||
}
|
||||
|
||||
ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
||||
std::shared_ptr<Channel> ChannelRef::strong()
|
||||
{
|
||||
if (lua_gettop(L) < 1)
|
||||
auto c = this->weak.lock();
|
||||
if (!c)
|
||||
{
|
||||
luaL_error(L, "Called c2.Channel method without a channel object");
|
||||
return nullptr;
|
||||
throw std::runtime_error(
|
||||
"Expired c2.Channel used - use c2.Channel:is_valid() to "
|
||||
"check validity");
|
||||
}
|
||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchChannel> ChannelRef::twitch()
|
||||
{
|
||||
auto c = std::dynamic_pointer_cast<TwitchChannel>(this->weak.lock());
|
||||
if (!c)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Called c2.Channel method with a non-userdata 'self' argument");
|
||||
return nullptr;
|
||||
throw std::runtime_error(
|
||||
"Expired or non-twitch c2.Channel used - use "
|
||||
"c2.Channel:is_valid() and c2.Channe:is_twitch_channel()");
|
||||
}
|
||||
// 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)
|
||||
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)
|
||||
{
|
||||
luaL_error(L,
|
||||
"Usage of expired c2.Channel object. Underlying "
|
||||
"resource was freed. Use Channel:is_valid() to check");
|
||||
return va.get<bool>();
|
||||
}
|
||||
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)
|
||||
return false;
|
||||
}();
|
||||
text = text.replace('\n', ' ');
|
||||
auto chan = this->strong();
|
||||
if (execCommands)
|
||||
{
|
||||
luaL_error(L,
|
||||
"c2.Channel Twitch-only operation on non-Twitch channel.");
|
||||
text = getApp()->getCommands()->execCommand(text, chan, false);
|
||||
}
|
||||
return ptr;
|
||||
chan->sendMessage(text);
|
||||
}
|
||||
|
||||
int ChannelRef::is_valid(lua_State *L)
|
||||
void ChannelRef::add_system_message(QString text)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
||||
lua::push(L, that != nullptr);
|
||||
return 1;
|
||||
text = text.replace('\n', ' ');
|
||||
this->strong()->addSystemMessage(text);
|
||||
}
|
||||
|
||||
int ChannelRef::get_name(lua_State *L)
|
||||
bool ChannelRef::is_twitch_channel()
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getName());
|
||||
return 1;
|
||||
return this->strong()->isTwitchChannel();
|
||||
}
|
||||
|
||||
int ChannelRef::get_type(lua_State *L)
|
||||
sol::table ChannelRef::get_room_modes(sol::this_state state)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getType());
|
||||
return 1;
|
||||
return toTable(state.L, *this->twitch()->accessRoomModes());
|
||||
}
|
||||
|
||||
int ChannelRef::get_display_name(lua_State *L)
|
||||
sol::table ChannelRef::get_stream_status(sol::this_state state)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getDisplayName());
|
||||
return 1;
|
||||
return toTable(state.L, *this->twitch()->accessStreamStatus());
|
||||
}
|
||||
|
||||
int ChannelRef::send_message(lua_State *L)
|
||||
QString ChannelRef::get_twitch_id()
|
||||
{
|
||||
if (lua_gettop(L) != 2 && lua_gettop(L) != 3)
|
||||
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)
|
||||
{
|
||||
luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message "
|
||||
"text and optionally execute_commands flag)");
|
||||
return 0;
|
||||
return "<c2.Channel expired>";
|
||||
}
|
||||
bool execcmds = false;
|
||||
if (lua_gettop(L) == 3)
|
||||
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())
|
||||
{
|
||||
if (!lua::pop(L, &execcmds))
|
||||
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
|
||||
);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes)
|
||||
{
|
||||
auto maybe = [](int value) {
|
||||
if (value >= 0)
|
||||
{
|
||||
luaL_error(L, "cannot get execute_commands (2nd argument of "
|
||||
"Channel:send_message)");
|
||||
return 0;
|
||||
return std::optional{value};
|
||||
}
|
||||
}
|
||||
|
||||
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, "
|
||||
"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)");
|
||||
throw std::runtime_error(
|
||||
"cannot parse url (2nd argument of HTTPRequest.create, "
|
||||
"got invalid url in argument)");
|
||||
}
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (!pl->hasHTTPPermissionFor(parsedurl))
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Plugin does not have permission to send HTTP requests "
|
||||
"to this URL");
|
||||
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)
|
||||
LuaFileMode mode(strmode);
|
||||
if (!mode.error.isEmpty())
|
||||
{
|
||||
// we have a mode
|
||||
QString smode;
|
||||
if (!lua::pop(L, &smode))
|
||||
{
|
||||
return luaL_error(
|
||||
L,
|
||||
"io.open mode (2nd argument) must be a string or not present");
|
||||
}
|
||||
mode = LuaFileMode(smode);
|
||||
if (!mode.error.isEmpty())
|
||||
{
|
||||
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)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
// 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
|
||||
|
||||
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;
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
} // namespace
|
||||
|
||||
int io_input(lua_State *L)
|
||||
{
|
||||
return globalFileCommon(L, false);
|
||||
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||
sol::protected_function_result res = func();
|
||||
return res;
|
||||
}
|
||||
|
||||
int io_output(lua_State *L)
|
||||
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file)
|
||||
{
|
||||
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]["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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
int io_flush(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
sol::state_view lua(L);
|
||||
auto res = io_open(L, std::move(filename), "r");
|
||||
if (res.size() != 1)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.flush. Expected one or zero.");
|
||||
throw std::runtime_error(res.at(1).as<std::string>());
|
||||
}
|
||||
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)
|
||||
auto obj = res.at(0);
|
||||
if (obj.get_type() != sol::type::userdata)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.read. Expected one or zero.");
|
||||
throw std::runtime_error("a file must be a userdata.");
|
||||
}
|
||||
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;
|
||||
return io_input_file(L, obj);
|
||||
}
|
||||
|
||||
int io_write(lua_State *L)
|
||||
sol::variadic_results io_output_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;
|
||||
}
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
int io_popen(lua_State *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)
|
||||
{
|
||||
return luaL_error(L, "io.popen: This function is a stub!");
|
||||
}
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
int io_tmpfile(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)
|
||||
{
|
||||
return luaL_error(L, "io.tmpfile: This function is a stub!");
|
||||
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), "w");
|
||||
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("internal error: a file must be a userdata.");
|
||||
}
|
||||
return io_output_file(L, obj);
|
||||
}
|
||||
|
||||
// NOLINTEND(*vararg)
|
||||
bool io_close_argless(sol::this_state L)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
auto out = lua.registry()["_IO_output"];
|
||||
return io_close_file(L, out);
|
||||
}
|
||||
|
||||
bool io_close_file(sol::this_state L, sol::userdata file)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
return file["close"](file);
|
||||
}
|
||||
|
||||
void io_flush_argless(sol::this_state L)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
auto out = lua.registry()["_IO_output"];
|
||||
io_flush_file(L, out);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -22,6 +22,7 @@ class ScrollbarHighlight;
|
|||
|
||||
struct Message;
|
||||
using MessagePtr = std::shared_ptr<const Message>;
|
||||
using MessagePtrMut = std::shared_ptr<Message>;
|
||||
struct Message {
|
||||
Message();
|
||||
~Message();
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,8 +14,6 @@
|
|||
|
||||
#include <ctime>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
|
@ -31,6 +29,7 @@ struct AutomodUserAction;
|
|||
struct AutomodInfoAction;
|
||||
struct Message;
|
||||
using MessagePtr = std::shared_ptr<const Message>;
|
||||
using MessagePtrMut = std::shared_ptr<Message>;
|
||||
|
||||
class MessageElement;
|
||||
class TextElement;
|
||||
|
@ -68,6 +67,7 @@ struct LiveUpdatesUpdateEmoteSetMessageTag {
|
|||
struct ImageUploaderResultTag {
|
||||
};
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
const SystemMessageTag systemMessage{};
|
||||
const RaidEntryMessageTag raidEntryMessage{};
|
||||
const TimeoutMessageTag timeoutMessage{};
|
||||
|
@ -79,6 +79,7 @@ const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{};
|
|||
// This signifies that you want to construct a message containing the result of
|
||||
// a successful image upload.
|
||||
const ImageUploaderResultTag imageUploaderResultMessage{};
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
MessagePtr makeSystemMessage(const QString &text);
|
||||
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
|
||||
|
@ -90,26 +91,22 @@ struct MessageParseArgs {
|
|||
bool trimSubscriberUsername = false;
|
||||
bool isStaffOrBroadcaster = false;
|
||||
bool isSubscriptionMessage = false;
|
||||
bool allowIgnore = true;
|
||||
bool isAction = false;
|
||||
QString channelPointRewardId = "";
|
||||
};
|
||||
|
||||
struct HighlightAlert {
|
||||
QUrl customSound;
|
||||
bool playSound = false;
|
||||
bool windowAlert = false;
|
||||
};
|
||||
class MessageBuilder
|
||||
{
|
||||
public:
|
||||
/// Build a message without a base IRC message.
|
||||
MessageBuilder();
|
||||
|
||||
/// Build a message based on an incoming IRC PRIVMSG
|
||||
explicit MessageBuilder(Channel *_channel,
|
||||
const Communi::IrcPrivateMessage *_ircMessage,
|
||||
const MessageParseArgs &_args);
|
||||
|
||||
/// Build a message based on an incoming IRC message (e.g. notice)
|
||||
explicit MessageBuilder(Channel *_channel,
|
||||
const Communi::IrcMessage *_ircMessage,
|
||||
const MessageParseArgs &_args, QString content,
|
||||
bool isAction);
|
||||
|
||||
MessageBuilder(SystemMessageTag, const QString &text,
|
||||
const QTime &time = QTime::currentTime());
|
||||
MessageBuilder(RaidEntryMessageTag, const QString &text,
|
||||
|
@ -157,17 +154,10 @@ public:
|
|||
|
||||
~MessageBuilder() = default;
|
||||
|
||||
QString userName;
|
||||
|
||||
/// The Twitch Channel the message was received in
|
||||
TwitchChannel *twitchChannel = nullptr;
|
||||
/// The Twitch Channel the message was sent in, according to the Shared Chat feature
|
||||
TwitchChannel *sourceChannel = nullptr;
|
||||
|
||||
Message *operator->();
|
||||
Message &message();
|
||||
MessagePtr release();
|
||||
std::weak_ptr<Message> weakOf();
|
||||
MessagePtrMut release();
|
||||
std::weak_ptr<const Message> weakOf();
|
||||
|
||||
void append(std::unique_ptr<MessageElement> element);
|
||||
void addLink(const linkparser::Parsed &parsedLink, const QString &source);
|
||||
|
@ -184,14 +174,8 @@ public:
|
|||
return pointer;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isIgnored() const;
|
||||
bool isIgnoredReply() const;
|
||||
void triggerHighlights();
|
||||
MessagePtr build();
|
||||
|
||||
void setThread(std::shared_ptr<MessageThread> thread);
|
||||
void setParent(MessagePtr parent);
|
||||
void setMessageOffset(int offset);
|
||||
static void triggerHighlights(const Channel *channel,
|
||||
const HighlightAlert &alert);
|
||||
|
||||
void appendChannelPointRewardMessage(const ChannelPointReward &reward,
|
||||
bool isMod, bool isBroadcaster);
|
||||
|
@ -231,96 +215,124 @@ public:
|
|||
static MessagePtr makeLowTrustUpdateMessage(
|
||||
const PubSubLowTrustUsersMessage &action);
|
||||
|
||||
protected:
|
||||
void addTextOrEmoji(EmotePtr emote);
|
||||
void addTextOrEmoji(const QString &string_);
|
||||
/// @brief Builds a message out of an `ircMessage`.
|
||||
///
|
||||
/// Building a message won't cause highlights to be triggered. They will
|
||||
/// only be parsed. To trigger highlights (play sound etc.), use
|
||||
/// triggerHighlights().
|
||||
///
|
||||
/// @param channel The channel this message was sent to. Must not be
|
||||
/// `nullptr`.
|
||||
/// @param ircMessage The original message. This can be any message
|
||||
/// (PRIVMSG, USERNOTICE, etc.). Its content is not
|
||||
/// accessed through this parameter but through `content`,
|
||||
/// as the content might be inside a tag (e.g. gifts in a
|
||||
/// USERNOTICE).
|
||||
/// @param args Arguments from parsing a chat message.
|
||||
/// @param content The message text. This isn't always the entire text. In
|
||||
/// replies, the leading mention can be cut off.
|
||||
/// See `messageOffset`.
|
||||
/// @param messageOffset Starting offset to be used on index-based
|
||||
/// operations on `content` such as parsing emotes.
|
||||
/// For example:
|
||||
/// ircMessage = "@hi there"
|
||||
/// content = "there"
|
||||
/// messageOffset_ = 4
|
||||
/// The index 6 would resolve to 6 - 4 = 2 => 'e'
|
||||
/// @param thread The reply thread this message is part of. If there's no
|
||||
/// thread, this is an empty `shared_ptr`.
|
||||
/// @param parent The direct parent this message is replying to. This does
|
||||
/// 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. 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,
|
||||
QString::size_type messageOffset,
|
||||
const std::shared_ptr<MessageThread> &thread = {},
|
||||
const MessagePtr &parent = {});
|
||||
|
||||
private:
|
||||
struct TextState {
|
||||
TwitchChannel *twitchChannel = nullptr;
|
||||
bool hasBits = false;
|
||||
bool bitsStacked = false;
|
||||
int bitsLeft = 0;
|
||||
};
|
||||
void addEmoji(const EmotePtr &emote);
|
||||
void addTextOrEmote(TextState &state, QString string);
|
||||
|
||||
Outcome tryAppendCheermote(TextState &state, const QString &string);
|
||||
Outcome tryAppendEmote(TwitchChannel *twitchChannel, const EmoteName &name);
|
||||
|
||||
bool isEmpty() const;
|
||||
MessageElement &back();
|
||||
std::unique_ptr<MessageElement> releaseBack();
|
||||
|
||||
MessageColor textColor_ = MessageColor::Text;
|
||||
|
||||
// Helper method that emplaces some text stylized as system text
|
||||
// and then appends that text to the QString parameter "toUpdate".
|
||||
// Returns the TextElement that was emplaced.
|
||||
TextElement *emplaceSystemTextAndUpdate(const QString &text,
|
||||
QString &toUpdate);
|
||||
|
||||
std::shared_ptr<Message> message_;
|
||||
|
||||
void parse();
|
||||
void parseUsernameColor();
|
||||
void parseUsername();
|
||||
void parseMessageID();
|
||||
void parseRoomID();
|
||||
void parseUsernameColor(const QVariantMap &tags, const QString &userID);
|
||||
void parseUsername(const Communi::IrcMessage *ircMessage,
|
||||
TwitchChannel *twitchChannel,
|
||||
bool trimSubscriberUsername);
|
||||
void parseMessageID(const QVariantMap &tags);
|
||||
|
||||
/// Parses the room-ID this message was received in
|
||||
///
|
||||
/// @returns The room-ID
|
||||
static QString parseRoomID(const QVariantMap &tags,
|
||||
TwitchChannel *twitchChannel);
|
||||
|
||||
/// Parses the shared-chat information from this message.
|
||||
///
|
||||
/// @param tags The tags of the received message
|
||||
/// @param twitchChannel The channel this message was received in
|
||||
/// @returns The source channel - the channel this message originated from.
|
||||
/// If there's no channel currently open, @a twitchChannel is
|
||||
/// returned.
|
||||
TwitchChannel *parseSharedChatInfo(const QVariantMap &tags,
|
||||
TwitchChannel *twitchChannel);
|
||||
|
||||
// Parse & build thread information into the message
|
||||
// Will read information from thread_ or from IRC tags
|
||||
void parseThread();
|
||||
void parseThread(const QString &messageContent, const QVariantMap &tags,
|
||||
const Channel *channel,
|
||||
const std::shared_ptr<MessageThread> &thread,
|
||||
const MessagePtr &parent);
|
||||
// parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
|
||||
void parseHighlights();
|
||||
void appendChannelName();
|
||||
void appendUsername();
|
||||
HighlightAlert parseHighlights(const QVariantMap &tags,
|
||||
const QString &originalMessage,
|
||||
const MessageParseArgs &args);
|
||||
|
||||
/// Return the Twitch Channel this message originated from
|
||||
///
|
||||
/// Useful to handle messages from the "Shared Chat" feature
|
||||
///
|
||||
/// Can return nullptr
|
||||
const TwitchChannel *getSourceChannel() const;
|
||||
|
||||
std::tuple<std::optional<EmotePtr>, MessageElementFlags, bool> parseEmote(
|
||||
const EmoteName &name) const;
|
||||
Outcome tryAppendEmote(const EmoteName &name);
|
||||
void appendChannelName(const Channel *channel);
|
||||
void appendUsername(const QVariantMap &tags, const MessageParseArgs &args);
|
||||
|
||||
void addWords(const QStringList &words,
|
||||
const std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
||||
const std::vector<TwitchEmoteOccurrence> &twitchEmotes,
|
||||
TextState &state);
|
||||
|
||||
void appendTwitchBadges();
|
||||
void appendChatterinoBadges();
|
||||
void appendFfzBadges();
|
||||
void appendSeventvBadges();
|
||||
Outcome tryParseCheermote(const QString &string);
|
||||
void appendTwitchBadges(const QVariantMap &tags,
|
||||
TwitchChannel *twitchChannel);
|
||||
void appendChatterinoBadges(const QString &userID);
|
||||
void appendFfzBadges(TwitchChannel *twitchChannel, const QString &userID);
|
||||
void appendSeventvBadges(const QString &userID);
|
||||
|
||||
bool shouldAddModerationElements() const;
|
||||
[[nodiscard]] static bool isIgnored(const QString &originalMessage,
|
||||
const QString &userID,
|
||||
const Channel *channel);
|
||||
|
||||
QString roomID_;
|
||||
bool hasBits_ = false;
|
||||
QString bits;
|
||||
int bitsLeft{};
|
||||
bool bitsStacked = false;
|
||||
bool historicalMessage_ = false;
|
||||
std::shared_ptr<MessageThread> thread_;
|
||||
MessagePtr parent_;
|
||||
|
||||
/**
|
||||
* Starting offset to be used on index-based operations on `originalMessage_`.
|
||||
*
|
||||
* For example:
|
||||
* originalMessage_ = "there"
|
||||
* messageOffset_ = 4
|
||||
* (the irc message is "hey there")
|
||||
*
|
||||
* then the index 6 would resolve to 6 - 4 = 2 => 'e'
|
||||
*/
|
||||
int messageOffset_ = 0;
|
||||
|
||||
QString userId_;
|
||||
bool senderIsBroadcaster{};
|
||||
|
||||
Channel *channel = nullptr;
|
||||
const Communi::IrcMessage *ircMessage;
|
||||
MessageParseArgs args;
|
||||
const QVariantMap tags;
|
||||
QString originalMessage_;
|
||||
|
||||
const bool action_{};
|
||||
std::shared_ptr<Message> message_;
|
||||
MessageColor textColor_ = MessageColor::Text;
|
||||
|
||||
QColor usernameColor_ = {153, 153, 153};
|
||||
|
||||
bool highlightAlert_ = false;
|
||||
bool highlightSound_ = false;
|
||||
std::optional<QUrl> highlightSoundCustomUrl_{};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -508,15 +508,20 @@ std::vector<MessagePtr> parseUserNoticeMessage(Channel *channel,
|
|||
{
|
||||
MessageParseArgs args;
|
||||
args.trimSubscriberUsername = true;
|
||||
args.allowIgnore = false;
|
||||
|
||||
MessageBuilder builder(channel, message, args, content, false);
|
||||
builder->flags.set(MessageFlag::Subscription);
|
||||
builder->flags.unset(MessageFlag::Highlighted);
|
||||
if (mirrored)
|
||||
auto [built, highlight] = MessageBuilder::makeIrcMessage(
|
||||
channel, message, args, content, 0);
|
||||
if (built)
|
||||
{
|
||||
builder->flags.set(MessageFlag::SharedMessage);
|
||||
built->flags.set(MessageFlag::Subscription);
|
||||
built->flags.unset(MessageFlag::Highlighted);
|
||||
if (mirrored)
|
||||
{
|
||||
built->flags.set(MessageFlag::SharedMessage);
|
||||
}
|
||||
builtMessages.emplace_back(std::move(built));
|
||||
}
|
||||
builtMessages.emplace_back(builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -661,12 +666,13 @@ std::vector<MessagePtr> parsePrivMessage(Channel *channel,
|
|||
|
||||
std::vector<MessagePtr> builtMessages;
|
||||
MessageParseArgs args;
|
||||
MessageBuilder builder(channel, message, args, message->content(),
|
||||
message->isAction());
|
||||
if (!builder.isIgnored())
|
||||
args.isAction = message->isAction();
|
||||
auto [built, alert] = MessageBuilder::makeIrcMessage(channel, message, args,
|
||||
message->content(), 0);
|
||||
if (built)
|
||||
{
|
||||
builtMessages.emplace_back(builder.build());
|
||||
builder.triggerHighlights();
|
||||
builtMessages.emplace_back(std::move(built));
|
||||
MessageBuilder::triggerHighlights(channel, alert);
|
||||
}
|
||||
|
||||
return builtMessages;
|
||||
|
@ -709,22 +715,21 @@ std::vector<MessagePtr> IrcMessageHandler::parseMessageWithReply(
|
|||
{
|
||||
args.channelPointRewardId = it.value().toString();
|
||||
}
|
||||
MessageBuilder builder(channel, message, args, content,
|
||||
privMsg->isAction());
|
||||
builder.setMessageOffset(messageOffset);
|
||||
args.isAction = privMsg->isAction();
|
||||
|
||||
auto replyCtx = getReplyContext(tc, message, otherLoaded);
|
||||
builder.setThread(std::move(replyCtx.thread));
|
||||
builder.setParent(std::move(replyCtx.parent));
|
||||
if (replyCtx.highlight)
|
||||
{
|
||||
builder.message().flags.set(MessageFlag::SubscribedThread);
|
||||
}
|
||||
auto [built, alert] = MessageBuilder::makeIrcMessage(
|
||||
channel, message, args, content, messageOffset, replyCtx.thread,
|
||||
replyCtx.parent);
|
||||
|
||||
if (!builder.isIgnored())
|
||||
if (built)
|
||||
{
|
||||
builtMessages.emplace_back(builder.build());
|
||||
builder.triggerHighlights();
|
||||
if (replyCtx.highlight)
|
||||
{
|
||||
built->flags.set(MessageFlag::SubscribedThread);
|
||||
}
|
||||
builtMessages.emplace_back(built);
|
||||
MessageBuilder::triggerHighlights(channel, alert);
|
||||
}
|
||||
|
||||
if (message->tags().contains(u"pinned-chat-paid-amount"_s))
|
||||
|
@ -1016,20 +1021,18 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage)
|
|||
|
||||
auto *c = getApp()->getTwitch()->getWhispersChannel().get();
|
||||
|
||||
MessageBuilder builder(c, ircMessage, args,
|
||||
unescapeZeroWidthJoiner(ircMessage->parameter(1)),
|
||||
false);
|
||||
|
||||
if (builder.isIgnored())
|
||||
auto [message, alert] = MessageBuilder::makeIrcMessage(
|
||||
c, ircMessage, args, unescapeZeroWidthJoiner(ircMessage->parameter(1)),
|
||||
0);
|
||||
if (!message)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
builder->flags.set(MessageFlag::Whisper);
|
||||
MessagePtr message = builder.build();
|
||||
builder.triggerHighlights();
|
||||
message->flags.set(MessageFlag::Whisper);
|
||||
MessageBuilder::triggerHighlights(c, alert);
|
||||
|
||||
getApp()->getTwitch()->setLastUserThatWhisperedMe(builder.userName);
|
||||
getApp()->getTwitch()->setLastUserThatWhisperedMe(message->loginName);
|
||||
|
||||
if (message->flags.has(MessageFlag::ShowInMentions))
|
||||
{
|
||||
|
@ -1504,6 +1507,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
|
|||
{
|
||||
args.isStaffOrBroadcaster = true;
|
||||
}
|
||||
args.isAction = isAction;
|
||||
|
||||
auto *channel = dynamic_cast<TwitchChannel *>(chan.get());
|
||||
|
||||
|
@ -1605,24 +1609,22 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
|
|||
}
|
||||
}
|
||||
|
||||
MessageBuilder builder(channel, message, args, content, isAction);
|
||||
builder.setMessageOffset(messageOffset);
|
||||
args.allowIgnore = !isSub;
|
||||
auto [msg, alert] = MessageBuilder::makeIrcMessage(
|
||||
channel, message, args, content, messageOffset, replyCtx.thread,
|
||||
replyCtx.parent);
|
||||
|
||||
builder.setThread(std::move(replyCtx.thread));
|
||||
builder.setParent(std::move(replyCtx.parent));
|
||||
if (replyCtx.highlight)
|
||||
{
|
||||
builder.message().flags.set(MessageFlag::SubscribedThread);
|
||||
}
|
||||
|
||||
if (isSub || !builder.isIgnored())
|
||||
if (msg)
|
||||
{
|
||||
if (isSub)
|
||||
{
|
||||
builder->flags.set(MessageFlag::Subscription);
|
||||
builder->flags.unset(MessageFlag::Highlighted);
|
||||
msg->flags.set(MessageFlag::Subscription);
|
||||
msg->flags.unset(MessageFlag::Highlighted);
|
||||
}
|
||||
if (replyCtx.highlight)
|
||||
{
|
||||
msg->flags.set(MessageFlag::SubscribedThread);
|
||||
}
|
||||
auto msg = builder.build();
|
||||
|
||||
IrcMessageHandler::setSimilarityFlags(msg, chan);
|
||||
|
||||
|
@ -1630,7 +1632,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message,
|
|||
(!getSettings()->hideSimilar &&
|
||||
getSettings()->shownSimilarTriggerHighlights))
|
||||
{
|
||||
builder.triggerHighlights();
|
||||
MessageBuilder::triggerHighlights(channel, alert);
|
||||
}
|
||||
|
||||
const auto highlighted = msg->flags.has(MessageFlag::Highlighted);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -153,7 +153,7 @@
|
|||
"searchText": "mm2pl mm2pl: Kappa ",
|
||||
"serverReceivedTime": "2022-09-03T10:31:42Z",
|
||||
"timeoutUser": "",
|
||||
"usernameColor": "#ff000000"
|
||||
"usernameColor": "#ffdaa521"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@
|
|||
"searchText": "mm2pl mm2pl: Kappa ",
|
||||
"serverReceivedTime": "2022-09-03T10:31:42Z",
|
||||
"timeoutUser": "",
|
||||
"usernameColor": "#ff000000"
|
||||
"usernameColor": "#ffdaa521"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@
|
|||
"searchText": "mm2pl mm2pl: Keepo ",
|
||||
"serverReceivedTime": "2022-09-03T10:31:35Z",
|
||||
"timeoutUser": "",
|
||||
"usernameColor": "#ff000000"
|
||||
"usernameColor": "#ffdaa521"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@
|
|||
"searchText": "mm2pl mm2pl: Kappa Keepo PogChamp ",
|
||||
"serverReceivedTime": "2022-09-03T10:31:42Z",
|
||||
"timeoutUser": "",
|
||||
"usernameColor": "#ff000000"
|
||||
"usernameColor": "#ffdaa521"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -285,9 +285,9 @@ TEST_F(FiltersF, TypingContextChecks)
|
|||
|
||||
QString originalMessage = privmsg->content();
|
||||
|
||||
MessageBuilder builder(&channel, privmsg, MessageParseArgs{});
|
||||
auto [msg, alert] = MessageBuilder::makeIrcMessage(
|
||||
&channel, privmsg, MessageParseArgs{}, originalMessage, 0);
|
||||
|
||||
auto msg = builder.build();
|
||||
EXPECT_NE(msg.get(), nullptr);
|
||||
|
||||
auto contextMap = buildContextMap(msg, &channel);
|
||||
|
|
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