diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml
index 44f9da5c1..d382e972c 100644
--- a/.github/workflows/clang-tidy.yml
+++ b/.github/workflows/clang-tidy.yml
@@ -10,6 +10,7 @@ concurrency:
jobs:
review:
+ if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-clang-tidy') }}
name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})"
runs-on: ${{ matrix.os }}
strategy:
diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml
index 00e0bf10e..3eb1ef633 100644
--- a/.github/workflows/test-macos.yml
+++ b/.github/workflows/test-macos.yml
@@ -8,6 +8,7 @@ on:
env:
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
+ HTTPBOX_TAG: v0.2.1
QT_QPA_PLATFORM: minimal
HOMEBREW_NO_AUTO_UPDATE: 1
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
@@ -24,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' }}
@@ -58,6 +59,13 @@ jobs:
run: |
brew install boost openssl rapidjson p7zip create-dmg cmake
+ - name: Install httpbox
+ run: |
+ curl -L -o httpbox.tar.xz "https://github.com/Chatterino/httpbox/releases/download/${{ env.HTTPBOX_TAG }}/httpbox-x86_64-apple-darwin.tar.xz"
+ tar -xJf httpbox.tar.xz
+ mv ./httpbox-x86_64-apple-darwin/httpbox /usr/local/bin
+ working-directory: /tmp
+
- name: Build
run: |
mkdir build-test
@@ -83,10 +91,6 @@ jobs:
curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key"
cd ..
- - name: Cargo Install httpbox
- run: |
- cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f
-
- name: Test
timeout-minutes: 30
run: |
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index a81c69891..b6b4c6014 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -8,6 +8,7 @@ on:
env:
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
+ HTTPBOX_TAG: v0.2.1
QT_QPA_PLATFORM: minimal
# Last known good conan version
# 2.0.3 has a bug on Windows (conan-io/conan#13606)
@@ -25,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
@@ -111,6 +112,13 @@ jobs:
mkdir -Force build-test/bin
cp "$((ls $Env:VCToolsRedistDir/onecore/x64 -Filter '*.CRT')[0].FullName)/*" build-test/bin
+ - name: Install httpbox
+ run: |
+ mkdir httpbox
+ Invoke-WebRequest -Uri "https://github.com/Chatterino/httpbox/releases/download/${{ env.HTTPBOX_TAG }}/httpbox-x86_64-pc-windows-msvc.zip" -outfile "httpbox.zip"
+ Expand-Archive httpbox.zip -DestinationPath httpbox
+ rm httpbox.zip
+
- name: Build
run: |
cmake `
@@ -139,14 +147,10 @@ jobs:
Invoke-WebRequest -Uri "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" -outfile "server.key"
cd ..
- - name: Cargo Install httpbox
- run: |
- cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f
-
- name: Test
timeout-minutes: 30
run: |
- httpbox --port 9051 &
+ ..\httpbox\httpbox.exe --port 9051 &
cd ..\pubsub-server-test
.\server.exe 127.0.0.1:9050 &
cd ..\build-test
diff --git a/.gitmodules b/.gitmodules
index e58a5bbd4..e15a27575 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -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
diff --git a/.prettierignore b/.prettierignore
index 89270b789..b763c2ddd 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,6 +2,7 @@
resources/*.json
benchmarks/resources/*.json
tests/resources/*.json
+tests/snapshots/**/*.json
# ...themes should be prettified for readability.
!resources/themes/*.json
diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md
index 2449a329d..7af9b9a9a 100644
--- a/BUILDING_ON_WINDOWS.md
+++ b/BUILDING_ON_WINDOWS.md
@@ -170,9 +170,10 @@ To automatically format your code, do the following:
1. Download [LLVM 16.0.6](https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.6/LLVM-16.0.6-win64.exe)
2. During the installation, make sure to add it to your path
-3. In Qt Creator, Select `Tools` > `Options` > `Beautifier`
-4. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save`
-5. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None`
+3. Enable Beautifier under `Extensions` on the left (check "Load on start" and restart)
+4. In Qt Creator, Select `Tools` > `Options` > `Beautifier`
+5. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save`
+6. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None`
### Building on MSVC with AddressSanitizer
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5d81f9c7..1eea64131 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,10 @@
- Major: Add option to show pronouns in user card. (#5442, #5583)
- Major: Release plugins alpha. (#5288)
-- Major: Improve high-DPI support on Windows. (#4868, #5391)
-- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746)
+- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666)
+- Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659)
- Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530)
-- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625)
+- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625, #5661)
- Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530)
- Minor: Add option to customise Moderation buttons with images. (#5369)
- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300)
@@ -30,10 +30,16 @@
- Minor: Links can now have prefixes and suffixes such as parentheses. (#5486, #5515)
- Minor: Added support for scrolling in splits with touchscreen panning gestures. (#5524)
- Minor: Removed experimental IRC support. (#5547)
+- Minor: Remember last popup size for next popup. (#5635)
- Minor: Moderators can now see which mods start and cancel raids. (#5563)
- Minor: The emote popup now reloads when Twitch emotes are reloaded. (#5580)
- Minor: Added `--login ` CLI argument to specify which account to start logged in as. (#5626)
- Minor: When blocking a channel, Chatterino will now warn you about that action. (#5615)
+- Minor: Indicate when subscriptions and resubscriptions are for multiple months. (#5642)
+- Minor: Proxy URL information is now included in the `/debug-env` command. (#5648)
+- Minor: Make raid entry message usernames clickable. (#5651)
+- Minor: Tabs unhighlight when their content is read in other tabs. (#5649)
+- Minor: Made usernames in bits and sub messages clickable. (#5686)
- Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426, #5612)
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
- Bugfix: Fixed restricted users usernames not being clickable. (#5405)
@@ -45,12 +51,14 @@
- Bugfix: Fixed tooltips and input completion popups not working after moving a split. (#5541, #5576)
- Bugfix: Fixed rare issue on shutdown where the client would hang. (#5557)
- Bugfix: Fixed `/clearmessages` not working with more than one window. (#5489)
-- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504)
+- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504, #5637)
- Bugfix: Links with invalid characters in the domain are no longer detected. (#5509)
- Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525)
-- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582)
+- Bugfix: Fixed event emotes not showing up in autocomplete and popups. (#5239, #5580, #5582, #5632)
- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530)
- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558)
+- Bugfix: Fixed a crash that could occur when handling the quick switcher popup really quickly. (#5687)
+- Bugfix: Fixed 7TV badges being inadvertently animated. (#5674)
- Bugfix: Fixed some tooltips not being readable. (#5578)
- Bugfix: Fixed log files being locked longer than needed. (#5592)
- Bugfix: Fixed global badges not showing in anonymous mode. (#5599)
@@ -58,6 +66,8 @@
- Bugfix: Fixed incorrect message being disabled in some cases upon approving or denying an automod caught message. (#5611)
- Bugfix: Fixed double-click selection not working when clicking outside a message. (#5617)
- Bugfix: Fixed emotes starting with ":" not tab-completing. (#5603)
+- Bugfix: Fixed 7TV emotes messing with Qt's HTML. (#5677)
+- Bugfix: Fixed incorrect messages getting replaced visually. (#5683)
- Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420)
- Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422)
- Dev: Unsingletonize `ISoundController`. (#5462)
@@ -70,7 +80,7 @@
- Dev: Removed unused timegate settings. (#5361)
- Dev: Add `Channel::addSystemMessage` helper function, allowing us to avoid the common `channel->addMessage(makeSystemMessage(...));` pattern. (#5500)
- Dev: Unsingletonize `Resources2`. (#5460)
-- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385)
+- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385, #5682)
- Dev: Images are now loaded in worker threads. (#5431)
- Dev: Fixed broken `SignalVector::operator[]` implementation. (#5556)
- Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305)
@@ -101,9 +111,17 @@
- Dev: Added more tests for input completion. (#5604)
- Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594)
- Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600)
+- Dev: Added more tests for message building. (#5598, #5654, #5656, #5671)
- 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, #5682)
+- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652)
+- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668)
+- Dev: Refactored IRC message building. (#5663)
+- Dev: Fixed some compiler warnings. (#5672)
+- Dev: Unified parsing of historic and live IRC messages. (#5678)
+- Dev: 7TV's `entitlement.reset` is now explicitly ignored. (#5685)
## 2.5.1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6fb323286..603c9c42b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,9 @@
cmake_minimum_required(VERSION 3.15)
cmake_policy(SET CMP0087 NEW) # evaluates generator expressions in `install(CODE/SCRIPT)`
cmake_policy(SET CMP0091 NEW) # select MSVC runtime library through `CMAKE_MSVC_RUNTIME_LIBRARY`
+if (POLICY CMP0167)
+ cmake_policy(SET CMP0167 NEW) # find Boost's own CMake config file
+endif ()
include(FeatureSummary)
list(APPEND CMAKE_MODULE_PATH
@@ -212,6 +215,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)
diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt
index a0e73332e..2677365ed 100644
--- a/benchmarks/CMakeLists.txt
+++ b/benchmarks/CMakeLists.txt
@@ -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
diff --git a/benchmarks/src/Highlights.cpp b/benchmarks/src/Highlights.cpp
deleted file mode 100644
index 69c69db49..000000000
--- a/benchmarks/src/Highlights.cpp
+++ /dev/null
@@ -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
-#include
-#include
-#include
-
-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(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);
diff --git a/cmake/FindSol2.cmake b/cmake/FindSol2.cmake
new file mode 100644
index 000000000..be64d000c
--- /dev/null
+++ b/cmake/FindSol2.cmake
@@ -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)
diff --git a/cmake/sanitizers-cmake b/cmake/sanitizers-cmake
index 3f0542e4e..0573e2ea8 160000
--- a/cmake/sanitizers-cmake
+++ b/cmake/sanitizers-cmake
@@ -1 +1 @@
-Subproject commit 3f0542e4e034aab417c51b2b22c94f83355dee15
+Subproject commit 0573e2ea8651b9bb3083f193c41eb086497cc80a
diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua
index d4b1ac25b..27cdf8786 100644
--- a/docs/plugin-meta.lua
+++ b/docs/plugin-meta.lua
@@ -5,13 +5,22 @@
-- 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 }
-c2.LogLevel = {}
+---@enum c2.LogLevel
+c2.LogLevel = {
+ Debug = {}, ---@type c2.LogLevel.Debug
+ Info = {}, ---@type c2.LogLevel.Info
+ Warning = {}, ---@type c2.LogLevel.Warning
+ Critical = {}, ---@type c2.LogLevel.Critical
+}
----@alias c2.EventType integer
----@type { CompletionRequested: c2.EventType }
-c2.EventType = {}
+-- Begin src/controllers/plugins/api/EventType.hpp
+
+---@enum c2.EventType
+c2.EventType = {
+ CompletionRequested = {}, ---@type c2.EventType.CompletionRequested
+}
+
+-- 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"}`.
@@ -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 }
-c2.ChannelType = {}
+---@enum c2.ChannelType
+c2.ChannelType = {
+ None = {}, ---@type c2.ChannelType.None
+ Direct = {}, ---@type c2.ChannelType.Direct
+ Twitch = {}, ---@type c2.ChannelType.Twitch
+ TwitchWhispers = {}, ---@type c2.ChannelType.TwitchWhispers
+ TwitchWatching = {}, ---@type c2.ChannelType.TwitchWatching
+ TwitchMentions = {}, ---@type c2.ChannelType.TwitchMentions
+ TwitchLive = {}, ---@type c2.ChannelType.TwitchLive
+ TwitchAutomod = {}, ---@type c2.ChannelType.TwitchAutomod
+ TwitchEnd = {}, ---@type c2.ChannelType.TwitchEnd
+ Misc = {}, ---@type c2.ChannelType.Misc
+}
-- 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,98 +170,101 @@ 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
----@class HTTPResponse
-HTTPResponse = {}
+---@class c2.HTTPResponse
+c2.HTTPResponse = {}
--- Returns the data. This is not guaranteed to be encoded using any
--- particular encoding scheme. It's just the bytes the server returned.
---
-function HTTPResponse:data() end
+---@return string
+---@nodiscard
+function c2.HTTPResponse:data() end
--- Returns the status code.
---
-function HTTPResponse:status() end
+---@return number|nil
+---@nodiscard
+function c2.HTTPResponse:status() end
--- A somewhat human readable description of an error if such happened
---
-function HTTPResponse:error() end
+---@return string
+---@nodiscard
+function c2.HTTPResponse:error() end
+
+---@return string
+---@nodiscard
+function c2.HTTPResponse:__tostring() end
-- End src/controllers/plugins/api/HTTPResponse.hpp
-- Begin src/controllers/plugins/api/HTTPRequest.hpp
----@alias HTTPCallback fun(result: HTTPResponse): nil
----@class HTTPRequest
-HTTPRequest = {}
+---@alias c2.HTTPCallback fun(result: c2.HTTPResponse): nil
+---@class c2.HTTPRequest
+c2.HTTPRequest = {}
--- Sets the success callback
---
----@param callback HTTPCallback Function to call when the HTTP request succeeds
-function HTTPRequest:on_success(callback) end
+---@param callback c2.HTTPCallback Function to call when the HTTP request succeeds
+function c2.HTTPRequest:on_success(callback) end
--- Sets the failure callback
---
----@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
-function HTTPRequest:on_error(callback) end
+---@param callback c2.HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
+function c2.HTTPRequest:on_error(callback) end
--- Sets the finally callback
---
---@param callback fun(): nil Function to call when the HTTP request finishes
-function HTTPRequest:finally(callback) end
+function c2.HTTPRequest:finally(callback) end
--- Sets the timeout
---
---@param timeout integer How long in milliseconds until the times out
-function HTTPRequest:set_timeout(timeout) end
+function c2.HTTPRequest:set_timeout(timeout) end
--- Sets the request payload
---
---@param data string
-function HTTPRequest:set_payload(data) end
+function c2.HTTPRequest:set_payload(data) end
--- Sets a header in the request
---
---@param name string
---@param value string
-function HTTPRequest:set_header(name, value) end
+function c2.HTTPRequest:set_header(name, value) end
--- Executes the HTTP request
---
-function HTTPRequest:execute() end
+function c2.HTTPRequest:execute() end
+
+---@return string
+function c2.HTTPRequest:__tostring() end
--- Creates a new HTTPRequest
---
----@param method HTTPMethod Method to use
+---@param method c2.HTTPMethod Method to use
---@param url string Where to send the request to
----@return HTTPRequest
-function HTTPRequest.create(method, url) end
+---@return c2.HTTPRequest
+function c2.HTTPRequest.create(method, url) end
-- End src/controllers/plugins/api/HTTPRequest.hpp
-- Begin src/common/network/NetworkCommon.hpp
----@alias HTTPMethod integer
----@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod }
-HTTPMethod = {}
+---@enum c2.HTTPMethod
+c2.HTTPMethod = {
+ Get = {}, ---@type c2.HTTPMethod.Get
+ Post = {}, ---@type c2.HTTPMethod.Post
+ Put = {}, ---@type c2.HTTPMethod.Put
+ Delete = {}, ---@type c2.HTTPMethod.Delete
+ Patch = {}, ---@type c2.HTTPMethod.Patch
+}
-- End src/common/network/NetworkCommon.hpp
@@ -245,7 +277,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
diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md
index cd38fa18c..2fffb7429 100644
--- a/docs/wip-plugins.md
+++ b/docs/wip-plugins.md
@@ -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()`
diff --git a/lib/expected-lite b/lib/expected-lite
index f339d2f73..5b5caad7c 160000
--- a/lib/expected-lite
+++ b/lib/expected-lite
@@ -1 +1 @@
-Subproject commit f339d2f73730f8fee4412f5e4938717866ecef48
+Subproject commit 5b5caad7cd57d5ba3ca796bf1521b131d73ca405
diff --git a/lib/lua/CMakeLists.txt b/lib/lua/CMakeLists.txt
index cf2fad9bd..45824fdfa 100644
--- a/lib/lua/CMakeLists.txt
+++ b/lib/lua/CMakeLists.txt
@@ -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++
+ $<$,$>:/EHsc> # enable exceptions in clang-cl
+)
+set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE CXX)
diff --git a/lib/lua/src b/lib/lua/src
index 0897c0a42..1ab3208a1 160000
--- a/lib/lua/src
+++ b/lib/lua/src
@@ -1 +1 @@
-Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60
+Subproject commit 1ab3208a1fceb12fca8f24ba57d6e13c5bff15e3
diff --git a/lib/settings b/lib/settings
index c58874c1a..4a0a1e599 160000
--- a/lib/settings
+++ b/lib/settings
@@ -1 +1 @@
-Subproject commit c58874c1aa5d0619df2c975bcb87433941b46920
+Subproject commit 4a0a1e599377cdcdc91b0fbbefc312936b48730c
diff --git a/lib/sol2 b/lib/sol2
new file mode 160000
index 000000000..2b0d2fe8b
--- /dev/null
+++ b/lib/sol2
@@ -0,0 +1 @@
+Subproject commit 2b0d2fe8ba0074e16b499940c4f3126b9c7d3471
diff --git a/mocks/include/mocks/BaseApplication.hpp b/mocks/include/mocks/BaseApplication.hpp
index 2ba9f949c..619203afc 100644
--- a/mocks/include/mocks/BaseApplication.hpp
+++ b/mocks/include/mocks/BaseApplication.hpp
@@ -3,6 +3,7 @@
#include "common/Args.hpp"
#include "mocks/DisabledStreamerMode.hpp"
#include "mocks/EmptyApplication.hpp"
+#include "mocks/TwitchUsers.hpp"
#include "providers/bttv/BttvLiveUpdates.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Settings.hpp"
@@ -55,6 +56,11 @@ public:
return &this->fonts;
}
+ ITwitchUsers *getTwitchUsers() override
+ {
+ return &this->twitchUsers;
+ }
+
BttvLiveUpdates *getBttvLiveUpdates() override
{
return nullptr;
@@ -71,6 +77,7 @@ public:
DisabledStreamerMode streamerMode;
Theme theme;
Fonts fonts;
+ TwitchUsers twitchUsers;
};
} // namespace chatterino::mock
diff --git a/mocks/include/mocks/ChatterinoBadges.hpp b/mocks/include/mocks/ChatterinoBadges.hpp
index 9070a7d7e..2f8279bc5 100644
--- a/mocks/include/mocks/ChatterinoBadges.hpp
+++ b/mocks/include/mocks/ChatterinoBadges.hpp
@@ -2,6 +2,8 @@
#include "providers/chatterino/ChatterinoBadges.hpp"
+#include
+
namespace chatterino::mock {
class ChatterinoBadges : public IChatterinoBadges
@@ -9,9 +11,21 @@ class ChatterinoBadges : public IChatterinoBadges
public:
std::optional getBadge(const UserId &id) override
{
- (void)id;
+ auto it = this->users.find(id);
+ if (it != this->users.end())
+ {
+ return it->second;
+ }
return std::nullopt;
}
+
+ void setBadge(UserId id, EmotePtr emote)
+ {
+ this->users.emplace(std::move(id), std::move(emote));
+ }
+
+private:
+ std::unordered_map users;
};
} // namespace chatterino::mock
diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp
index fe309f0f8..02913efed 100644
--- a/mocks/include/mocks/Helix.hpp
+++ b/mocks/include/mocks/Helix.hpp
@@ -350,7 +350,7 @@ public:
// contains a comma
MOCK_METHOD(
void, getChatters,
- (QString broadcasterID, QString moderatorID, int maxChattersToFetch,
+ (QString broadcasterID, QString moderatorID, size_t maxChattersToFetch,
ResultCallback successCallback,
(FailureCallback failureCallback)),
(override)); // getChatters
diff --git a/mocks/include/mocks/TwitchIrcServer.hpp b/mocks/include/mocks/TwitchIrcServer.hpp
index bbeba8ca4..d218192b3 100644
--- a/mocks/include/mocks/TwitchIrcServer.hpp
+++ b/mocks/include/mocks/TwitchIrcServer.hpp
@@ -7,8 +7,11 @@
#include "providers/seventv/eventapi/Dispatch.hpp"
#include "providers/seventv/eventapi/Message.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
+#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
+#include
+
namespace chatterino::mock {
class MockTwitchIrcServer : public ITwitchIrcServer
@@ -67,7 +70,30 @@ public:
std::shared_ptr getChannelOrEmptyByID(
const QString &channelID) override
{
- return {};
+ // XXX: this is the same as in TwitchIrcServer::getChannelOrEmptyByID
+ for (const auto &[name, weakChannel] : this->mockChannels)
+ {
+ auto channel = weakChannel.lock();
+ if (!channel)
+ {
+ continue;
+ }
+
+ auto twitchChannel =
+ std::dynamic_pointer_cast(channel);
+ if (!twitchChannel)
+ {
+ continue;
+ }
+
+ if (twitchChannel->roomId() == channelID &&
+ twitchChannel->getName().count(':') < 2)
+ {
+ return channel;
+ }
+ }
+
+ return Channel::getEmpty();
}
void dropSeventvChannel(const QString &userID,
@@ -123,6 +149,8 @@ public:
ChannelPtr liveChannel;
ChannelPtr automodChannel;
QString lastUserThatWhisperedMe{"forsen"};
+
+ std::unordered_map> mockChannels;
};
} // namespace chatterino::mock
diff --git a/mocks/include/mocks/TwitchUsers.hpp b/mocks/include/mocks/TwitchUsers.hpp
new file mode 100644
index 000000000..14f6bf7e6
--- /dev/null
+++ b/mocks/include/mocks/TwitchUsers.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "providers/twitch/TwitchUser.hpp"
+#include "providers/twitch/TwitchUsers.hpp"
+
+namespace chatterino::mock {
+
+class TwitchUsers : public ITwitchUsers
+{
+public:
+ TwitchUsers() = default;
+
+ std::shared_ptr resolveID(const UserId &id)
+ {
+ TwitchUser u = {
+ .id = id.string,
+ .name = {},
+ .displayName = {},
+ };
+ return std::make_shared(u);
+ }
+};
+
+} // namespace chatterino::mock
diff --git a/mocks/include/mocks/UserData.hpp b/mocks/include/mocks/UserData.hpp
index 62159a19f..bf53ea4ab 100644
--- a/mocks/include/mocks/UserData.hpp
+++ b/mocks/include/mocks/UserData.hpp
@@ -2,6 +2,8 @@
#include "controllers/userdata/UserDataController.hpp"
+#include
+
namespace chatterino::mock {
class UserDataController : public IUserDataController
@@ -13,6 +15,11 @@ public:
// If the user does not have any extra data, return none
std::optional getUser(const QString &userID) const override
{
+ auto it = this->userMap.find(userID);
+ if (it != this->userMap.end())
+ {
+ return it->second;
+ }
return std::nullopt;
}
@@ -20,8 +27,21 @@ public:
void setUserColor(const QString &userID,
const QString &colorString) override
{
- // do nothing
+ auto it = this->userMap.find(userID);
+ if (it != this->userMap.end())
+ {
+ it->second.color = QColor(colorString);
+ }
+ else
+ {
+ this->userMap.emplace(userID, UserData{
+ .color = QColor(colorString),
+ });
+ }
}
+
+private:
+ std::unordered_map userMap;
};
} // namespace chatterino::mock
diff --git a/resources/avatars/maliByatzes.png b/resources/avatars/maliByatzes.png
new file mode 100644
index 000000000..693d84e05
Binary files /dev/null and b/resources/avatars/maliByatzes.png differ
diff --git a/resources/contributors.txt b/resources/contributors.txt
index 6267887e3..d6eaa1a7d 100644
--- a/resources/contributors.txt
+++ b/resources/contributors.txt
@@ -35,7 +35,7 @@ Confuseh | https://github.com/Confuseh |
ch-ems | https://github.com/ch-ems |
Bur0k | https://github.com/Bur0k |
nuuls | https://github.com/nuuls |
-Chronophylos | https://github.com/Chronophylos |
+Chronophylos | https://github.com/Chronophylos |
Ckath | https://github.com/Ckath |
matijakevic | https://github.com/matijakevic |
nforro | https://github.com/nforro |
@@ -79,6 +79,7 @@ KleberPF | https://github.com/KleberPF |
nealxm | https://github.com/nealxm | :/avatars/nealxm.png
Niller2005 | https://github.com/Niller2005 | :/avatars/niller2005.png
JakeRYW | https://github.com/JakeRYW | :/avatars/jakeryw.png
+maliByatzes | https://github.com/maliByatzes | :/avatars/maliByatzes.png
# If you are a contributor add yourself above this line
diff --git a/resources/twitch/sharedChat.png b/resources/twitch/sharedChat.png
new file mode 100644
index 000000000..f9a66b17c
Binary files /dev/null and b/resources/twitch/sharedChat.png differ
diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py
old mode 100644
new mode 100755
index f9f44e7ed..e1dafe496
--- a/scripts/make_luals_meta.py
+++ b/scripts/make_luals_meta.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python3
"""
This script generates docs/plugin-meta.lua. It accepts no arguments
@@ -196,7 +197,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 +210,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,17 +243,19 @@ 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")
if header_comment:
out.write(f"--- {header_comment}\n")
- out.write("---@type { ")
+ out.write(f"---@enum {name}\n")
+ out.write(f"{name} = {{\n")
out.write(
- ", ".join(
- [f"{variant}: {name}" for variant in reader.read_enum_variants()]
+ "\n".join(
+ [
+ f" {variant} = {{}}, ---@type {name}.{variant}"
+ for variant in reader.read_enum_variants()
+ ]
)
)
- out.write(" }\n")
- out.write(f"{name} = {{}}\n\n")
+ out.write("\n}\n\n")
continue
# class
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d24a0155e..631239533 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -2,6 +2,7 @@ set(LIBRARY_PROJECT "${PROJECT_NAME}-lib")
set(VERSION_PROJECT "${LIBRARY_PROJECT}-version")
set(EXECUTABLE_PROJECT "${PROJECT_NAME}")
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00)
+add_compile_definitions(QT_WARN_DEPRECATED_UP_TO=0x050F00)
# registers the native messageing host
option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF)
@@ -39,6 +40,7 @@ set(SOURCE_FILES
common/WindowDescriptors.cpp
common/WindowDescriptors.hpp
+ common/enums/MessageContext.hpp
common/enums/MessageOverflow.hpp
common/network/NetworkCommon.cpp
@@ -225,24 +227,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
@@ -277,6 +283,9 @@ set(SOURCE_FILES
messages/MessageElement.cpp
messages/MessageElement.hpp
messages/MessageFlag.hpp
+ messages/MessageSimilarity.cpp
+ messages/MessageSimilarity.hpp
+ messages/MessageSink.hpp
messages/MessageThread.cpp
messages/MessageThread.hpp
@@ -409,6 +418,8 @@ set(SOURCE_FILES
providers/twitch/TwitchEmotes.hpp
providers/twitch/TwitchHelpers.cpp
providers/twitch/TwitchHelpers.hpp
+ providers/twitch/TwitchIrc.cpp
+ providers/twitch/TwitchIrc.hpp
providers/twitch/TwitchIrcServer.cpp
providers/twitch/TwitchIrcServer.hpp
providers/twitch/TwitchUser.cpp
@@ -497,6 +508,8 @@ set(SOURCE_FILES
util/InitUpdateButton.hpp
util/IpcQueue.cpp
util/IpcQueue.hpp
+ util/IrcHelpers.cpp
+ util/IrcHelpers.hpp
util/LayoutHelper.cpp
util/LayoutHelper.hpp
util/LoadPixmap.cpp
@@ -518,6 +531,8 @@ set(SOURCE_FILES
util/Twitch.hpp
util/TypeName.hpp
util/Variant.hpp
+ util/VectorMessageSink.cpp
+ util/VectorMessageSink.hpp
util/WidgetHelpers.cpp
util/WidgetHelpers.hpp
util/WindowsHelper.cpp
@@ -787,7 +802,7 @@ target_link_libraries(${LIBRARY_PROJECT}
$<$: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)
@@ -969,6 +984,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
IRC_STATIC
IRC_NAMESPACE=Communi
$<$:_WIN32_WINNT=0x0A00> # Windows 10
+ $<$:CHATTERINO_WITH_TESTS>
)
if (USE_SYSTEM_QTKEYCHAIN)
diff --git a/src/PrecompiledHeader.hpp b/src/PrecompiledHeader.hpp
index d7c289bca..a12265950 100644
--- a/src/PrecompiledHeader.hpp
+++ b/src/PrecompiledHeader.hpp
@@ -129,6 +129,10 @@
# include
# include
+# ifdef CHATTERINO_HAVE_PLUGINS
+# include
+# endif
+
# ifndef UNUSED
# define UNUSED(x) (void)(x)
# endif
diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp
index ef778bad1..6396ef00c 100644
--- a/src/common/Channel.cpp
+++ b/src/common/Channel.cpp
@@ -3,7 +3,9 @@
#include "Application.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
+#include "messages/MessageSimilarity.hpp"
#include "providers/twitch/IrcMessageHandler.hpp"
+#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Logging.hpp"
#include "singletons/Settings.hpp"
@@ -121,10 +123,10 @@ void Channel::addSystemMessage(const QString &contents)
this->addMessage(msg, MessageContext::Original);
}
-void Channel::addOrReplaceTimeout(MessagePtr message)
+void Channel::addOrReplaceTimeout(MessagePtr message, QTime now)
{
addOrReplaceChannelTimeout(
- this->getMessageSnapshot(), std::move(message), QTime::currentTime(),
+ this->getMessageSnapshot(), std::move(message), now,
[this](auto /*idx*/, auto msg, auto replacement) {
this->replaceMessage(msg, replacement);
},
@@ -253,21 +255,33 @@ void Channel::fillInMissingMessages(const std::vector &messages)
}
}
-void Channel::replaceMessage(MessagePtr message, MessagePtr replacement)
+void Channel::replaceMessage(const MessagePtr &message,
+ const MessagePtr &replacement)
{
int index = this->messages_.replaceItem(message, replacement);
if (index >= 0)
{
- this->messageReplaced.invoke((size_t)index, replacement);
+ this->messageReplaced.invoke((size_t)index, message, replacement);
}
}
-void Channel::replaceMessage(size_t index, MessagePtr replacement)
+void Channel::replaceMessage(size_t index, const MessagePtr &replacement)
{
- if (this->messages_.replaceItem(index, replacement))
+ MessagePtr prev;
+ if (this->messages_.replaceItem(index, replacement, &prev))
{
- this->messageReplaced.invoke(index, replacement);
+ this->messageReplaced.invoke(index, prev, replacement);
+ }
+}
+
+void Channel::replaceMessage(size_t hint, const MessagePtr &message,
+ const MessagePtr &replacement)
+{
+ auto index = this->messages_.replaceItem(hint, message, replacement);
+ if (index >= 0)
+ {
+ this->messageReplaced.invoke(hint, message, replacement);
}
}
@@ -287,10 +301,15 @@ void Channel::clearMessages()
}
MessagePtr Channel::findMessage(QString messageID)
+{
+ return this->findMessageByID(messageID);
+}
+
+MessagePtr Channel::findMessageByID(QStringView messageID)
{
MessagePtr res;
- if (auto msg = this->messages_.rfind([&messageID](const MessagePtr &msg) {
+ if (auto msg = this->messages_.rfind([messageID](const MessagePtr &msg) {
return msg->id == messageID;
});
msg)
@@ -301,6 +320,19 @@ MessagePtr Channel::findMessage(QString messageID)
return res;
}
+void Channel::applySimilarityFilters(const MessagePtr &message) const
+{
+ setSimilarityFlags(message, this->messages_.getSnapshot());
+}
+
+MessageSinkTraits Channel::sinkTraits() const
+{
+ return {
+ MessageSinkTrait::AddMentionsToGlobalChannel,
+ MessageSinkTrait::RequiresKnownChannelPointReward,
+ };
+}
+
bool Channel::canSendMessage() const
{
return false;
diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp
index 554327622..c7d006f1b 100644
--- a/src/common/Channel.hpp
+++ b/src/common/Channel.hpp
@@ -1,8 +1,10 @@
#pragma once
+#include "common/enums/MessageContext.hpp"
#include "controllers/completion/TabCompletionModel.hpp"
#include "messages/LimitedQueue.hpp"
#include "messages/MessageFlag.hpp"
+#include "messages/MessageSink.hpp"
#include
#include
@@ -26,15 +28,7 @@ enum class TimeoutStackStyle : int {
Default = DontStackBeyondUserMessage,
};
-/// Context of the message being added to a channel
-enum class MessageContext {
- /// This message is the original
- Original,
- /// This message is a repost of a message that has already been added in a channel
- Repost,
-};
-
-class Channel : public std::enable_shared_from_this
+class Channel : public std::enable_shared_from_this, public MessageSink
{
public:
// This is for Lua. See scripts/make_luals_meta.py
@@ -55,7 +49,7 @@ public:
};
explicit Channel(const QString &name, Type type);
- virtual ~Channel();
+ ~Channel() override;
// SIGNALS
pajlada::Signals::Signal
@@ -66,7 +60,9 @@ public:
pajlada::Signals::Signal>
messageAppended;
pajlada::Signals::Signal &> messagesAddedAtStart;
- pajlada::Signals::Signal messageReplaced;
+ /// (index, prev-message, replacement)
+ pajlada::Signals::Signal
+ messageReplaced;
/// Invoked when some number of messages were filled in using time received
pajlada::Signals::Signal &> filledInMessages;
pajlada::Signals::NoArgSignal destroyed;
@@ -85,8 +81,9 @@ public:
// overridingFlags can be filled in with flags that should be used instead
// of the message's flags. This is useful in case a flag is specific to a
// type of split
- void addMessage(MessagePtr message, MessageContext context,
- std::optional overridingFlags = std::nullopt);
+ void addMessage(
+ MessagePtr message, MessageContext context,
+ std::optional overridingFlags = std::nullopt) final;
void addMessagesAtStart(const std::vector &messages_);
void addSystemMessage(const QString &contents);
@@ -94,19 +91,28 @@ public:
/// Inserts the given messages in order by Message::serverReceivedTime.
void fillInMissingMessages(const std::vector &messages);
- void addOrReplaceTimeout(MessagePtr message);
- void disableAllMessages();
- void replaceMessage(MessagePtr message, MessagePtr replacement);
- void replaceMessage(size_t index, MessagePtr replacement);
+ void addOrReplaceTimeout(MessagePtr message, QTime now) final;
+ void disableAllMessages() final;
+ void replaceMessage(const MessagePtr &message,
+ const MessagePtr &replacement);
+ void replaceMessage(size_t index, const MessagePtr &replacement);
+ void replaceMessage(size_t hint, const MessagePtr &message,
+ const MessagePtr &replacement);
void deleteMessage(QString messageID);
/// Removes all messages from this channel and invokes #messagesCleared
void clearMessages();
- MessagePtr findMessage(QString messageID);
+ [[deprecated("Use findMessageByID instead")]] MessagePtr findMessage(
+ QString messageID);
+ MessagePtr findMessageByID(QStringView messageID) final;
bool hasMessages() const;
+ void applySimilarityFilters(const MessagePtr &message) const final;
+
+ MessageSinkTraits sinkTraits() const final;
+
// CHANNEL INFO
virtual bool canSendMessage() const;
virtual bool isWritable() const; // whether split input will be usable
@@ -165,30 +171,3 @@ private:
};
} // namespace chatterino
-
-template <>
-constexpr magic_enum::customize::customize_t
- magic_enum::customize::enum_name(
- 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;
- }
-}
diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp
index 6dab1a0c6..f2006e6d7 100644
--- a/src/common/ChatterinoSetting.hpp
+++ b/src/common/ChatterinoSetting.hpp
@@ -3,6 +3,7 @@
#include "util/QMagicEnum.hpp"
#include
+#include
#include
namespace chatterino {
@@ -55,6 +56,7 @@ using DoubleSetting = ChatterinoSetting;
using IntSetting = ChatterinoSetting;
using StringSetting = ChatterinoSetting;
using QStringSetting = ChatterinoSetting;
+using QSizeSetting = ChatterinoSetting;
template
class EnumSetting
@@ -71,7 +73,6 @@ public:
_registerSetting(this->getData());
}
- template
EnumSetting &operator=(Enum newValue)
{
this->setValue(Underlying(newValue));
diff --git a/src/common/ProviderId.hpp b/src/common/ProviderId.hpp
index 18ccf8803..ccf997476 100644
--- a/src/common/ProviderId.hpp
+++ b/src/common/ProviderId.hpp
@@ -2,6 +2,8 @@
namespace chatterino {
-enum class ProviderId { Twitch, Irc };
+enum class ProviderId { // NOLINT(performance-enum-size)
+ Twitch,
+};
//
} // namespace chatterino
diff --git a/src/common/SignalVector.hpp b/src/common/SignalVector.hpp
index e01ebd5aa..47d536cbe 100644
--- a/src/common/SignalVector.hpp
+++ b/src/common/SignalVector.hpp
@@ -87,7 +87,8 @@ public:
}
else
{
- assert(index >= 0 && index <= this->items_.size());
+ assert(index >= 0 &&
+ index <= static_cast(this->items_.size()));
}
this->items_.insert(this->items_.begin() + index, item);
@@ -116,7 +117,7 @@ public:
void removeAt(int index, void *caller = nullptr)
{
assertInGuiThread();
- assert(index >= 0 && index < int(this->items_.size()));
+ assert(index >= 0 && index < static_cast(this->items_.size()));
T item = this->items_[index];
this->items_.erase(this->items_.begin() + index);
@@ -132,13 +133,14 @@ public:
{
assertInGuiThread();
- for (int index = 0; index < this->items_.size(); ++index)
+ for (size_t index = 0; index < this->items_.size(); ++index)
{
T item = this->items_[index];
if (matcher(item))
{
this->items_.erase(this->items_.begin() + index);
- SignalVectorItemEvent args{item, index, caller};
+ SignalVectorItemEvent args{item, static_cast(index),
+ caller};
this->itemRemoved.invoke(args);
this->itemsChanged_();
return true;
diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp
index 620ca452d..f7983288b 100644
--- a/src/common/SignalVectorModel.hpp
+++ b/src/common/SignalVectorModel.hpp
@@ -43,7 +43,7 @@ public:
}
// get row index
int index = this->getModelIndexFromVectorIndex(args.index);
- assert(index >= 0 && index <= this->rows_.size());
+ assert(index >= 0 && index <= static_cast(this->rows_.size()));
// get row items
std::vector row = this->createRow();
@@ -75,7 +75,7 @@ public:
}
int row = this->getModelIndexFromVectorIndex(args.index);
- assert(row >= 0 && row <= this->rows_.size());
+ assert(row >= 0 && row <= static_cast(this->rows_.size()));
// remove row
std::vector items = this->rows_[row].items;
@@ -130,7 +130,8 @@ public:
{
int row = index.row();
int column = index.column();
- if (row < 0 || column < 0 || row >= this->rows_.size() ||
+ if (row < 0 || column < 0 ||
+ row >= static_cast(this->rows_.size()) ||
column >= this->columnCount_)
{
return QVariant();
@@ -144,7 +145,8 @@ public:
{
int row = index.row();
int column = index.column();
- if (row < 0 || column < 0 || row >= this->rows_.size() ||
+ if (row < 0 || column < 0 ||
+ row >= static_cast(this->rows_.size()) ||
column >= this->columnCount_)
{
return false;
@@ -152,7 +154,7 @@ public:
Row &rowItem = this->rows_[row];
- assert(this->columnCount_ == rowItem.items.size());
+ assert(this->columnCount_ == static_cast(rowItem.items.size()));
auto &cell = rowItem.items[column];
@@ -167,7 +169,7 @@ public:
int vecRow = this->getVectorIndexFromModelIndex(row);
// TODO: This is only a safety-thing for when we modify data that's being modified right now.
// It should not be necessary, but it would require some rethinking about this surrounding logic
- if (vecRow >= this->vector_->readOnly()->size())
+ if (vecRow >= static_cast(this->vector_->readOnly()->size()))
{
return false;
}
@@ -224,18 +226,19 @@ public:
{
int row = index.row(), column = index.column();
- if (row < 0 || column < 0 || row >= this->rows_.size() ||
+ if (row < 0 || column < 0 ||
+ row >= static_cast(this->rows_.size()) ||
column >= this->columnCount_)
{
return Qt::NoItemFlags;
}
- assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
- column < this->columnCount_);
+ assert(row >= 0 && row < static_cast(this->rows_.size()) &&
+ column >= 0 && column < this->columnCount_);
const auto &rowItem = this->rows_[row];
- assert(this->columnCount_ == rowItem.items.size());
+ assert(this->columnCount_ == static_cast(rowItem.items.size()));
return rowItem.items[column]->flags();
}
@@ -267,7 +270,8 @@ public:
return false;
}
- assert(sourceRow >= 0 && sourceRow < this->rows_.size());
+ assert(sourceRow >= 0 &&
+ sourceRow < static_cast(this->rows_.size()));
int signalVectorRow = this->getVectorIndexFromModelIndex(sourceRow);
this->beginMoveRows(sourceParent, sourceRow, sourceRow,
@@ -294,7 +298,7 @@ public:
return false;
}
- assert(row >= 0 && row < this->rows_.size());
+ assert(row >= 0 && row < static_cast(this->rows_.size()));
int signalVectorRow = this->getVectorIndexFromModelIndex(row);
this->vector_->removeAt(signalVectorRow);
@@ -337,8 +341,10 @@ public:
int from = data->data("chatterino_row_id").toInt();
int to = parent.row();
- int vectorFrom = this->getVectorIndexFromModelIndex(from);
- int vectorTo = this->getVectorIndexFromModelIndex(to);
+ auto vectorFrom =
+ static_cast(this->getVectorIndexFromModelIndex(from));
+ auto vectorTo =
+ static_cast(this->getVectorIndexFromModelIndex(to));
if (vectorFrom < 0 || vectorFrom > this->vector_->raw().size() ||
vectorTo < 0 || vectorTo > this->vector_->raw().size())
@@ -402,7 +408,7 @@ protected:
void insertCustomRow(std::vector row, int index)
{
- assert(index >= 0 && index <= this->rows_.size());
+ assert(index >= 0 && index <= static_cast(this->rows_.size()));
this->beginInsertRows(QModelIndex(), index, index);
this->rows_.insert(this->rows_.begin() + index,
@@ -412,7 +418,7 @@ protected:
void removeCustomRow(int index)
{
- assert(index >= 0 && index <= this->rows_.size());
+ assert(index >= 0 && index <= static_cast(this->rows_.size()));
assert(this->rows_[index].isCustomRow);
this->beginRemoveRows(QModelIndex(), index, index);
diff --git a/src/common/enums/MessageContext.hpp b/src/common/enums/MessageContext.hpp
new file mode 100644
index 000000000..669e55315
--- /dev/null
+++ b/src/common/enums/MessageContext.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+namespace chatterino {
+
+/// Context of the message being added to a channel
+enum class MessageContext {
+ /// This message is the original
+ Original,
+ /// This message is a repost of a message that has already been added in a channel
+ Repost,
+};
+
+} // namespace chatterino
diff --git a/src/common/network/NetworkCommon.hpp b/src/common/network/NetworkCommon.hpp
index 215b828a7..8efd66620 100644
--- a/src/common/network/NetworkCommon.hpp
+++ b/src/common/network/NetworkCommon.hpp
@@ -16,7 +16,7 @@ using NetworkErrorCallback = std::function;
using NetworkFinallyCallback = std::function;
/**
- * @exposeenum HTTPMethod
+ * @exposeenum c2.HTTPMethod
*/
enum class NetworkRequestType {
Get,
diff --git a/src/controllers/commands/builtin/chatterino/Debugging.cpp b/src/controllers/commands/builtin/chatterino/Debugging.cpp
index 2984aa9e9..7a33dac8f 100644
--- a/src/controllers/commands/builtin/chatterino/Debugging.cpp
+++ b/src/controllers/commands/builtin/chatterino/Debugging.cpp
@@ -79,6 +79,7 @@ QString listEnvironmentVariables(const CommandContext &ctx)
QStringList debugMessages{
"recentMessagesApiUrl: " + env.recentMessagesApiUrl,
"linkResolverUrl: " + env.linkResolverUrl,
+ "proxyUrl: " + env.proxyUrl.value_or("N/A"),
"twitchServerHost: " + env.twitchServerHost,
"twitchServerPort: " + QString::number(env.twitchServerPort),
"twitchServerSecure: " + QString::number(env.twitchServerSecure),
diff --git a/src/controllers/completion/sources/UnifiedSource.cpp b/src/controllers/completion/sources/UnifiedSource.cpp
index a0f462ace..6d8dfe15d 100644
--- a/src/controllers/completion/sources/UnifiedSource.cpp
+++ b/src/controllers/completion/sources/UnifiedSource.cpp
@@ -37,7 +37,7 @@ void UnifiedSource::addToListModel(GenericListModel &model,
source->addToListModel(model, maxCount - used);
// Calculate how many items have been added so far
used = model.rowCount() - startingSize;
- if (used >= maxCount)
+ if (used >= static_cast(maxCount))
{
// Used up all of limit
break;
@@ -58,15 +58,15 @@ void UnifiedSource::addToStringList(QStringList &list, size_t maxCount,
}
// Make sure to only add maxCount elements in total.
- int startingSize = list.size();
- int used = 0;
+ auto startingSize = list.size();
+ QStringList::size_type used = 0;
for (const auto &source : this->sources_)
{
source->addToStringList(list, maxCount - used, isFirstWord);
// Calculate how many items have been added so far
used = list.size() - startingSize;
- if (used >= maxCount)
+ if (used >= static_cast(maxCount))
{
// Used up all of limit
break;
diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp
index 7922b16dd..f8e60b6b9 100644
--- a/src/controllers/ignores/IgnoreController.cpp
+++ b/src/controllers/ignores/IgnoreController.cpp
@@ -1,12 +1,134 @@
#include "controllers/ignores/IgnoreController.hpp"
#include "Application.hpp"
+#include "common/Literals.hpp"
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/ignores/IgnorePhrase.hpp"
#include "providers/twitch/TwitchAccount.hpp"
+#include "providers/twitch/TwitchIrc.hpp"
#include "singletons/Settings.hpp"
+namespace {
+
+using namespace chatterino::literals;
+
+/**
+ * Computes (only) the replacement of @a match in @a source.
+ * The parts before and after the match in @a source are ignored.
+ *
+ * Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced
+ * with the string captured by the corresponding capturing group.
+ * This function should only be used if the regex contains capturing groups.
+ *
+ * Since Qt doesn't provide a way of replacing a single match with some replacement
+ * while supporting both capturing groups and lookahead/-behind in the regex,
+ * this is included here. It's essentially the implementation of
+ * QString::replace(const QRegularExpression &, const QString &).
+ * @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703
+ */
+QString makeRegexReplacement(QStringView source,
+ const QRegularExpression ®ex,
+ const QRegularExpressionMatch &match,
+ const QString &replacement)
+{
+ using SizeType = QString::size_type;
+ struct QStringCapture {
+ SizeType pos;
+ SizeType len;
+ int captureNumber;
+ };
+
+ qsizetype numCaptures = regex.captureCount();
+
+ // 1. build the backreferences list, holding where the backreferences
+ // are in the replacement string
+ QVarLengthArray backReferences;
+
+ SizeType replacementLength = replacement.size();
+ for (SizeType i = 0; i < replacementLength - 1; i++)
+ {
+ if (replacement[i] != u'\\')
+ {
+ continue;
+ }
+
+ int no = replacement[i + 1].digitValue();
+ if (no <= 0 || no > numCaptures)
+ {
+ continue;
+ }
+
+ QStringCapture backReference{.pos = i, .len = 2};
+
+ if (i < replacementLength - 2)
+ {
+ int secondDigit = replacement[i + 2].digitValue();
+ if (secondDigit != -1 && ((no * 10) + secondDigit) <= numCaptures)
+ {
+ no = (no * 10) + secondDigit;
+ ++backReference.len;
+ }
+ }
+
+ backReference.captureNumber = no;
+ backReferences.append(backReference);
+ }
+
+ // 2. iterate on the matches.
+ // For every match, copy the replacement string in chunks
+ // with the proper replacements for the backreferences
+
+ // length of the new string, with all the replacements
+ SizeType newLength = 0;
+ QVarLengthArray chunks;
+ QStringView replacementView{replacement};
+
+ // Initially: empty, as we only care about the replacement
+ SizeType len = 0;
+ SizeType lastEnd = 0;
+ for (const QStringCapture &backReference : std::as_const(backReferences))
+ {
+ // part of "replacement" before the backreference
+ len = backReference.pos - lastEnd;
+ if (len > 0)
+ {
+ chunks << replacementView.mid(lastEnd, len);
+ newLength += len;
+ }
+
+ // backreference itself
+ len = match.capturedLength(backReference.captureNumber);
+ if (len > 0)
+ {
+ chunks << source.mid(
+ match.capturedStart(backReference.captureNumber), len);
+ newLength += len;
+ }
+
+ lastEnd = backReference.pos + backReference.len;
+ }
+
+ // add the last part of the replacement string
+ len = replacementView.size() - lastEnd;
+ if (len > 0)
+ {
+ chunks << replacementView.mid(lastEnd, len);
+ newLength += len;
+ }
+
+ // 3. assemble the chunks together
+ QString dst;
+ dst.reserve(newLength);
+ for (const QStringView &chunk : std::as_const(chunks))
+ {
+ dst += chunk;
+ }
+ return dst;
+}
+
+} // namespace
+
namespace chatterino {
bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
@@ -65,4 +187,187 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
return false;
}
+void processIgnorePhrases(const std::vector &phrases,
+ QString &content,
+ std::vector &twitchEmotes)
+{
+ using SizeType = QString::size_type;
+
+ auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
+ // all emotes outside the range come before `it`
+ // all emotes in the range start at `it`
+ auto it = std::partition(
+ twitchEmotes.begin(), twitchEmotes.end(),
+ [pos, len](const auto &item) {
+ // returns true for emotes outside the range
+ return !((item.start >= pos) && item.start < (pos + len));
+ });
+ std::vector emotesInRange(it,
+ twitchEmotes.end());
+ twitchEmotes.erase(it, twitchEmotes.end());
+ return emotesInRange;
+ };
+
+ auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) {
+ for (auto &item : twitchEmotes)
+ {
+ auto &index = item.start;
+ if (index >= pos)
+ {
+ index += by;
+ item.end += by;
+ }
+ }
+ };
+
+ auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase,
+ const auto &midrepl,
+ SizeType startIndex) {
+ if (!phrase.containsEmote())
+ {
+ return;
+ }
+
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ auto words = midrepl.tokenize(u' ');
+#else
+ auto words = midrepl.split(' ');
+#endif
+ SizeType pos = 0;
+ for (const auto &word : words)
+ {
+ for (const auto &emote : phrase.getEmotes())
+ {
+ if (word == emote.first.string)
+ {
+ if (emote.second == nullptr)
+ {
+ qCDebug(chatterinoTwitch)
+ << "emote null" << emote.first.string;
+ }
+ twitchEmotes.push_back(TwitchEmoteOccurrence{
+ static_cast(startIndex + pos),
+ static_cast(startIndex + pos +
+ emote.first.string.length()),
+ emote.second,
+ emote.first,
+ });
+ }
+ }
+ pos += word.length() + 1;
+ }
+ };
+
+ auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
+ SizeType length, const QString &replacement) {
+ auto removedEmotes = removeEmotesInRange(from, length);
+ content.replace(from, length, replacement);
+ auto wordStart = from;
+ while (wordStart > 0)
+ {
+ if (content[wordStart - 1] == ' ')
+ {
+ break;
+ }
+ --wordStart;
+ }
+ auto wordEnd = from + replacement.length();
+ while (wordEnd < content.length())
+ {
+ if (content[wordEnd] == ' ')
+ {
+ break;
+ }
+ ++wordEnd;
+ }
+
+ shiftIndicesAfter(static_cast(from + length),
+ static_cast(replacement.length() - length));
+
+ auto midExtendedRef =
+ QStringView{content}.mid(wordStart, wordEnd - wordStart);
+
+ for (auto &emote : removedEmotes)
+ {
+ if (emote.ptr == nullptr)
+ {
+ qCDebug(chatterinoTwitch)
+ << "Invalid emote occurrence" << emote.name.string;
+ continue;
+ }
+ QRegularExpression emoteregex(
+ "\\b" + emote.name.string + "\\b",
+ QRegularExpression::UseUnicodePropertiesOption);
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ auto match = emoteregex.matchView(midExtendedRef);
+#else
+ auto match = emoteregex.match(midExtendedRef);
+#endif
+ if (match.hasMatch())
+ {
+ emote.start = static_cast(from + match.capturedStart());
+ emote.end = static_cast(from + match.capturedEnd());
+ twitchEmotes.push_back(std::move(emote));
+ }
+ }
+
+ addReplEmotes(phrase, midExtendedRef, wordStart);
+ };
+
+ for (const auto &phrase : phrases)
+ {
+ if (phrase.isBlock())
+ {
+ continue;
+ }
+ const auto &pattern = phrase.getPattern();
+ if (pattern.isEmpty())
+ {
+ continue;
+ }
+ if (phrase.isRegex())
+ {
+ const auto ®ex = phrase.getRegex();
+ if (!regex.isValid())
+ {
+ continue;
+ }
+
+ QRegularExpressionMatch match;
+ size_t iterations = 0;
+ SizeType from = 0;
+ while ((from = content.indexOf(regex, from, &match)) != -1)
+ {
+ auto replacement = phrase.getReplace();
+ if (regex.captureCount() > 0)
+ {
+ replacement = makeRegexReplacement(content, regex, match,
+ replacement);
+ }
+
+ replaceMessageAt(phrase, from, match.capturedLength(),
+ replacement);
+ from += phrase.getReplace().length();
+ iterations++;
+ if (iterations >= 128)
+ {
+ content = u"Too many replacements - check your ignores!"_s;
+ return;
+ }
+ }
+
+ continue;
+ }
+
+ SizeType from = 0;
+ while ((from = content.indexOf(pattern, from,
+ phrase.caseSensitivity())) != -1)
+ {
+ replaceMessageAt(phrase, from, pattern.length(),
+ phrase.getReplace());
+ from += phrase.getReplace().length();
+ }
+ }
+}
+
} // namespace chatterino
diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp
index 4c2048621..955531537 100644
--- a/src/controllers/ignores/IgnoreController.hpp
+++ b/src/controllers/ignores/IgnoreController.hpp
@@ -2,8 +2,13 @@
#include
+#include
+
namespace chatterino {
+class IgnorePhrase;
+struct TwitchEmoteOccurrence;
+
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
struct IgnoredMessageParameters {
@@ -16,4 +21,17 @@ struct IgnoredMessageParameters {
bool isIgnoredMessage(IgnoredMessageParameters &¶ms);
+/// @brief Processes replacement ignore-phrases for a message
+///
+/// @param phrases A list of IgnorePhrases to process. Block phrases as well as
+/// invalid phrases are ignored.
+/// @param content The message text. This gets altered by replacements.
+/// @param twitchEmotes A list of emotes present in the message. Occurrences
+/// that have been removed from the message will also be
+/// removed in this list. Similarly, if new emotes are added
+/// from a replacement, this list gets updated as well.
+void processIgnorePhrases(const std::vector &phrases,
+ QString &content,
+ std::vector &twitchEmotes);
+
} // namespace chatterino
diff --git a/src/controllers/ignores/IgnorePhrase.cpp b/src/controllers/ignores/IgnorePhrase.cpp
index 9f81fbc6e..41a49ec58 100644
--- a/src/controllers/ignores/IgnorePhrase.cpp
+++ b/src/controllers/ignores/IgnorePhrase.cpp
@@ -95,11 +95,11 @@ bool IgnorePhrase::containsEmote() const
{
if (!this->emotesChecked_)
{
- const auto &accvec = getApp()->getAccounts()->twitch.accounts;
- for (const auto &acc : accvec)
+ auto accemotes =
+ getApp()->getAccounts()->twitch.getCurrent()->accessEmotes();
+ if (*accemotes)
{
- const auto &accemotes = *acc->accessEmotes();
- for (const auto &emote : *accemotes)
+ for (const auto &emote : **accemotes)
{
if (this->replace_.contains(emote.first.string,
Qt::CaseSensitive))
diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp
index 03d11750a..4f73c51ee 100644
--- a/src/controllers/plugins/LuaAPI.cpp
+++ b/src/controllers/plugins/LuaAPI.cpp
@@ -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
# include
# include
-}
# include
+# include
# include
# include
# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+
+# include
+# include
+# include
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("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
diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp
index 904c6daa4..bd83dee5a 100644
--- a/src/controllers/plugins/LuaAPI.hpp
+++ b/src/controllers/plugins/LuaAPI.hpp
@@ -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
-}
-# include "controllers/plugins/LuaUtilities.hpp"
-
+# include
# include
+# include
# include
# include
-# include
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 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
diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp
index 64af18c01..58f648f0a 100644
--- a/src/controllers/plugins/LuaUtilities.cpp
+++ b/src/controllers/plugins/LuaUtilities.cpp
@@ -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
# include
-}
# include
# include
@@ -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{};
diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp
index 5443a751f..0f7bdc53f 100644
--- a/src/controllers/plugins/LuaUtilities.hpp
+++ b/src/controllers/plugins/LuaUtilities.hpp
@@ -2,37 +2,20 @@
#ifdef CHATTERINO_HAVE_PLUGINS
-# include "common/QLogging.hpp"
-
-extern "C" {
# include
# include
-}
# include
# include
+# include
# include
-# include
# include
# include
# include
-# include
-# include
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
-StackIdx push(lua_State *L, std::optional val)
-{
- if (val.has_value())
- {
- return lua::push(L, *val);
- }
- lua_pushnil(L);
- return lua_gettop(L);
-}
-
-template
-bool peek(lua_State *L, std::optional *out, StackIdx idx = -1)
-{
- if (lua_isnil(L, idx))
- {
- *out = std::nullopt;
- return true;
- }
-
- *out = T();
- return peek(L, out->operator->(), idx);
-}
-
-template
-bool peek(lua_State *L, std::vector *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 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 , 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 opt = magic_enum::enum_cast(tmp);
- if (opt.has_value())
- {
- *out = opt.value();
- return true;
- }
-
- return false;
-}
-
-/**
- * @brief Converts a vector to Lua and pushes it onto the stack.
- *
- * Needs StackIdx push(lua_State*, T); to work.
- *
- * @return Stack index of newly created table.
- */
-template
-StackIdx push(lua_State *L, std::vector 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 to Lua and pushes it onto the stack.
- *
- * Needs StackIdx push(lua_State*, T); to work.
- *
- * @return Stack index of newly created table.
- */
-template
-StackIdx push(lua_State *L, QList 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 , bool> = true>
-StackIdx push(lua_State *L, T inp)
-{
- std::string_view name = magic_enum::enum_name(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
-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
-StackIdx pushEnumTable(lua_State *L)
+ requires std::is_enum_v
+sol::table createEnumTable(sol::state_view &lua)
{
- // std::array
- auto values = magic_enum::enum_values();
- StackIdx out = lua::pushEmptyTable(L, values.size());
+ constexpr auto values = magic_enum::enum_values();
+ auto out = lua.create_table(0, values.size());
for (const T v : values)
{
std::string_view name = magic_enum::enum_name(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
-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 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
diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp
index dcf7357e8..739fa6372 100644
--- a/src/controllers/plugins/Plugin.cpp
+++ b/src/controllers/plugins/Plugin.cpp
@@ -7,14 +7,13 @@
# include "controllers/plugins/PluginPermission.hpp"
# include "util/QMagicEnum.hpp"
-extern "C" {
# include
-}
# include
# include
# include
# include
# include
+# include
# include
# include
@@ -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)
{
diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp
index f8375247f..a65329468 100644
--- a/src/controllers/plugins/Plugin.hpp
+++ b/src/controllers/plugins/Plugin.hpp
@@ -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
# include
# include
+# include
+# include
+# include
# include
# include
# include
@@ -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;
- std::optional getCompletionCallback()
+ std::optional 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>(
- 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 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> httpRequests;
+
private:
QDir loadDirectory_;
lua_State *state_;
QString error_;
- // maps command name -> function name
- std::unordered_map ownedCommands;
+ // maps command name -> function
+ std::unordered_map ownedCommands;
std::vector activeTimeouts;
int lastTimerId = 0;
friend class PluginController;
+ friend class PluginControllerAccess; // this is for tests
};
} // namespace chatterino
#endif
diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp
index 5ca77ed40..1a2bc3a10 100644
--- a/src/controllers/plugins/PluginController.cpp
+++ b/src/controllers/plugins/PluginController.cpp
@@ -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
# include
# include
-}
# include
+# include
+# include
+# include
+# include
+# include
# include
# include
@@ -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 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(L);
- lua_setfield(L, c2libIdx, "LogLevel");
-
- lua::pushEnumTable(L);
- lua_setfield(L, c2libIdx, "EventType");
-
- lua::pushEnumTable(L);
- lua_setfield(L, c2libIdx, "Platform");
-
- lua::pushEnumTable(L);
- lua_setfield(L, c2libIdx, "ChannelType");
-
- lua::pushEnumTable(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(lua);
+ c2["HTTPMethod"] = lua::createEnumTable(lua);
+ c2["EventType"] = lua::createEnumTable(lua);
+ c2["LogLevel"] = lua::createEnumTable(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>(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 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(errOrList))
+ sol::state_view view(pl->state_);
+ auto errOrList = lua::tryCall(
+ 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(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(errOrList);
+ auto list = lua::api::CompletionList(*errOrList);
if (list.hideOthers)
{
results = QStringList(list.values.begin(), list.values.end());
diff --git a/src/controllers/plugins/PluginController.hpp b/src/controllers/plugins/PluginController.hpp
index 50bc88c7e..ce4fe6170 100644
--- a/src/controllers/plugins/PluginController.hpp
+++ b/src/controllers/plugins/PluginController.hpp
@@ -10,6 +10,7 @@
# include
# include
# include
+# include
# include
# include