diff --git a/CHANGELOG.md b/CHANGELOG.md index f2906155..06a8abfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Minor: Added Markdown support to user notes. (#6490) - Minor: Moderators and VIPs in shared chats now show their channel badges. (#6653) - Minor: Added message read/update methods to the `Channel` plugin API. (#6650) +- Minor: Added a display name change event to `Channel` for plugins. (#6594) - Minor: Added action to reset `/watching`. (#6759) - Minor: Removed messaging about running flatpak. This is already apparent in the newer Flatpak runtimes. (#6768) - Minor: Add `/(un)monitor` and `/(un)restrict` commands for moderators. (#6750) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index c1e002e4..30261827 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -46,12 +46,22 @@ declare namespace c2 { game_id: string; } + class ConnectionHandle { + disconnect(): void; + block(): void; + unblock(): void; + is_blocked(): boolean; + is_connected(): boolean; + } + class Channel implements IWeakResource { is_valid(): boolean; get_name(): string; get_type(): ChannelType; get_display_name(): string; + on_display_name_changed(cb: () => void): ConnectionHandle; + send_message(message: string, execute_commands: boolean): void; send_message(message: string): void; diff --git a/docs/lua-meta/globals.lua b/docs/lua-meta/globals.lua index e18866d2..09e340a7 100644 --- a/docs/lua-meta/globals.lua +++ b/docs/lua-meta/globals.lua @@ -244,6 +244,12 @@ function c2.Channel:is_vip() end ---@return string function c2.Channel:__tostring() end +--- Callback when the channel display name changes. +--- +---@param cb fun() +---@return c2.ConnectionHandle hdl +function c2.Channel:on_display_name_changed(cb) end + --- Finds a channel by name. --- Misc channels are marked as Twitch: --- - /whispers @@ -264,6 +270,37 @@ function c2.Channel.by_twitch_id(id) end -- End src/controllers/plugins/api/ChannelRef.hpp +-- Begin src/controllers/plugins/api/ConnectionHandle.hpp + + + +---@class c2.ConnectionHandle +---This type represents a handle to a registration of a callback for an event handler. +---Conceptually, the event has a _connection_ to the callback/handler. +---This handle can be used to modify that connection. +---It does not automatically disconnect the connection when it's destroyed (in `__gc`) - +---`disconnect()` has to be called manually. +c2.ConnectionHandle = {} + +---Disconnect the signal +function c2.ConnectionHandle:disconnect() end + +---Block events on this connection +function c2.ConnectionHandle:block() end + +---Unblock events on this connection +function c2.ConnectionHandle:unblock() end + +---Is this connection currently blocked? +---@return boolean is_blocked +function c2.ConnectionHandle:is_blocked() end + +---Is this connection still connected? +---@return boolean is_connected +function c2.ConnectionHandle:is_connected() end + +-- End src/controllers/plugins/api/ConnectionHandle.hpp + -- Begin src/controllers/plugins/api/HTTPResponse.hpp ---@class c2.HTTPResponse diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index fd859d51..e831f78b 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -352,6 +352,15 @@ saddummys:get_display_name() -- "서새봄냥" +##### `Channel:on_display_name_changed(cb)` + +Callback when the channel display name changes. The callback doesn't get any +arguments, use [`Channel:get_display_name`](#channelget_display_name) to get the +updated name. + +This returns a [`ConnectionHandle`](#connectionhandle) which can be used to +disconnect the handler. + ##### `Channel:send_message(message[, execute_commands])` Sends a message to the channel with the given text. If `execute_commands` is @@ -775,6 +784,34 @@ This table describes links available to plugins. | `JumpToMessage` | ID of the message | Highlight the message with given ID in current split, do nothing if it was not found | n/a | | `InsertText` | Any text, command or emote | Insert text into split input | n/a | +#### `ConnectionHandle` + +This type represents a handle to a registration of a callback for an event handler. +Conceptually, the event has a _connection_ to the callback/handler. +This handle can be used to modify that connection. +It does not automatically disconnect the connection when it's destroyed (in `__gc`) - +[`disconnect()`](#connectionhandledisconnect) has to be called manually. + +##### `ConnectionHandle:disconnect()` + +Disconnect the signal. + +##### `ConnectionHandle:block()` + +Block events on this connection. + +##### `ConnectionHandle:unblock()` + +Unblock events on this connection. + +##### `ConnectionHandle:is_blocked()` + +Is this connection currently blocked? + +##### `ConnectionHandle:is_connected()` + +Is this connection still connected? + ### Input/Output API These functions are wrappers for Lua's I/O library. Functions on file pointer diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b79e9b9e..4f926aa1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -256,6 +256,8 @@ set(SOURCE_FILES controllers/plugins/api/Accounts.hpp controllers/plugins/api/ChannelRef.cpp controllers/plugins/api/ChannelRef.hpp + controllers/plugins/api/ConnectionHandle.cpp + controllers/plugins/api/ConnectionHandle.hpp controllers/plugins/api/DebugLibrary.cpp controllers/plugins/api/DebugLibrary.hpp controllers/plugins/api/EventType.hpp @@ -275,6 +277,8 @@ set(SOURCE_FILES controllers/plugins/api/Message.hpp controllers/plugins/api/WebSocket.cpp controllers/plugins/api/WebSocket.hpp + controllers/plugins/ConnectionManager.cpp + controllers/plugins/ConnectionManager.hpp controllers/plugins/LuaAPI.cpp controllers/plugins/LuaAPI.hpp controllers/plugins/LuaUtilities.cpp @@ -287,6 +291,8 @@ set(SOURCE_FILES controllers/plugins/PluginMeta.hpp controllers/plugins/PluginPermission.cpp controllers/plugins/PluginPermission.hpp + controllers/plugins/PluginRef.cpp + controllers/plugins/PluginRef.hpp controllers/plugins/SolTypes.cpp controllers/plugins/SolTypes.hpp diff --git a/src/controllers/plugins/ConnectionManager.cpp b/src/controllers/plugins/ConnectionManager.cpp new file mode 100644 index 00000000..c0e04bc3 --- /dev/null +++ b/src/controllers/plugins/ConnectionManager.cpp @@ -0,0 +1,36 @@ +#include "controllers/plugins/ConnectionManager.hpp" + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include + +namespace chatterino::lua { + +api::ConnectionHandle ConnectionManager::add( + pajlada::Signals::ScopedConnection &&conn) +{ + this->removeDisconnected(); + return { + .connection = this->connections.emplace_back( + std::make_shared( + std::move(conn))), + }; +} + +void ConnectionManager::clear() +{ + this->connections.clear(); +} + +void ConnectionManager::removeDisconnected() +{ + auto [first, last] = + std::ranges::remove_if(this->connections, [](const auto &conn) { + return !conn || !conn->isConnected(); + }); + this->connections.erase(first, last); +} + +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/ConnectionManager.hpp b/src/controllers/plugins/ConnectionManager.hpp new file mode 100644 index 00000000..c6ae4199 --- /dev/null +++ b/src/controllers/plugins/ConnectionManager.hpp @@ -0,0 +1,46 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include "controllers/plugins/api/ConnectionHandle.hpp" + +# include + +# include + +namespace chatterino::lua { + +class PluginWeakRef; + +/// A manager for pajlada::Signals connections. +/// +/// In contrast to the existing connection managers, such as a regular +/// std::vector, this tries to avoid storing expired connections, because there +/// is only one per plugin. We also store a `std::shared_ptr` +/// to be able to give the plugin a `std::weak_ptr` which it +/// can use to disconnect. +class ConnectionManager +{ +public: + ConnectionManager() = default; + + api::ConnectionHandle add(pajlada::Signals::ScopedConnection &&conn); + + template + api::ConnectionHandle managedConnect(Signal &signal, Callback cb) + { + return this->add(signal.connect(std::forward(cb))); + } + + void clear(); + +private: + void removeDisconnected(); + + std::vector> + connections; +}; + +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index e3342929..1a8702ff 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -88,6 +88,7 @@ sol::table toTable(lua_State *L, const CompletionEvent &ev); * @includefile common/Channel.hpp * @includefile controllers/plugins/api/Accounts.hpp * @includefile controllers/plugins/api/ChannelRef.hpp + * @includefile controllers/plugins/api/ConnectionHandle.hpp * @includefile controllers/plugins/api/HTTPResponse.hpp * @includefile controllers/plugins/api/HTTPRequest.hpp * @includefile controllers/plugins/api/Message.hpp diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp index fcd7c827..cc267e5c 100644 --- a/src/controllers/plugins/Plugin.cpp +++ b/src/controllers/plugins/Plugin.cpp @@ -9,6 +9,7 @@ # include "common/QLogging.hpp" # include "controllers/commands/CommandController.hpp" # include "controllers/plugins/PluginPermission.hpp" +# include "controllers/plugins/SignalCallback.hpp" # include # include @@ -48,6 +49,11 @@ std::unordered_set Plugin::listRegisteredCommands() return out; } +lua::SignalCallback Plugin::createCallback(sol::main_protected_function pfn) +{ + return {this->selfRef_.weak(), std::move(pfn)}; +} + Plugin::~Plugin() { this->onUnloaded(); @@ -57,6 +63,7 @@ Plugin::~Plugin() QObject::disconnect(timer, nullptr, nullptr, nullptr); timer->deleteLater(); } + this->selfRef_.destroy(); this->httpRequests.clear(); qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size() << "timers for plugin" << this->id diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 999f3909..dcd44855 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -7,7 +7,9 @@ #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/api/EventType.hpp" # include "controllers/plugins/api/HTTPRequest.hpp" +# include "controllers/plugins/ConnectionManager.hpp" # include "controllers/plugins/PluginMeta.hpp" +# include "controllers/plugins/PluginRef.hpp" # include # include @@ -29,6 +31,10 @@ namespace chatterino::lua::api { enum class LogLevel; } // namespace chatterino::lua::api +namespace chatterino::lua { +struct SignalCallback; +} // namespace chatterino::lua + namespace chatterino { class Plugin @@ -43,6 +49,7 @@ public: , meta(std::move(meta)) , loadDirectory_(loadDirectory) , state_(state) + , selfRef_(state ? this : nullptr) { } @@ -91,6 +98,8 @@ public: return it->second; } + lua::SignalCallback createCallback(sol::main_protected_function pfn); + /** * If the plugin crashes while evaluating the main file, this function will return the error */ @@ -99,6 +108,8 @@ public: return this->error_; } + lua::PluginWeakRef weakRef() const; + int addTimeout(QTimer *timer); void removeTimeout(QTimer *timer); @@ -119,11 +130,14 @@ public: boost::signals2::signal onUnloaded; boost::signals2::signal onLog; + lua::ConnectionManager connections; private: QDir loadDirectory_; lua_State *state_; + lua::PluginRef selfRef_; + QString error_; // maps command name -> function diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index e631b83d..05589b3e 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -13,6 +13,7 @@ # include "controllers/commands/CommandController.hpp" # include "controllers/plugins/api/Accounts.hpp" # include "controllers/plugins/api/ChannelRef.hpp" +# include "controllers/plugins/api/ConnectionHandle.hpp" # include "controllers/plugins/api/DebugLibrary.hpp" # include "controllers/plugins/api/HTTPRequest.hpp" # include "controllers/plugins/api/HTTPResponse.hpp" @@ -244,6 +245,7 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin) lua::api::HTTPResponse::createUserType(c2); lua::api::HTTPRequest::createUserType(c2); lua::api::WebSocket::createUserType(c2, plugin); + lua::api::ConnectionHandle::createUserType(c2); lua::api::message::createUserType(c2); lua::api::createAccounts(c2); c2["ChannelType"] = lua::createEnumTable(lua); diff --git a/src/controllers/plugins/PluginRef.cpp b/src/controllers/plugins/PluginRef.cpp new file mode 100644 index 00000000..fd2cf421 --- /dev/null +++ b/src/controllers/plugins/PluginRef.cpp @@ -0,0 +1,66 @@ +#include "controllers/plugins/PluginRef.hpp" + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include "Application.hpp" +# include "common/QLogging.hpp" +# include "controllers/plugins/Plugin.hpp" + +# include +# include + +namespace chatterino::lua { + +PluginRef::PluginRef(Plugin *plugin) + : shared(plugin, /*deleter=*/[](void *) {}) +{ + assert(plugin != nullptr); +} + +PluginRef::PluginRef(std::shared_ptr plugin) + : shared(std::move(plugin)) +{ +} + +PluginWeakRef PluginRef::weak() const noexcept +{ + return {this->shared}; +} + +Plugin *PluginRef::plugin() const noexcept +{ + return this->shared.get(); +} + +void PluginRef::destroy() +{ + auto useCount = this->shared.use_count(); + if (useCount > 1) + { + qCWarning(chatterinoLua) + << "Destroying PluginRef with" << useCount + << "strong references. Expected one reference (the plugin itself)."; + assert(false && "Too many strong references."); + } + + this->shared.reset(); +} + +PluginWeakRef::PluginWeakRef(std::weak_ptr weak) + : weak(std::move(weak)) +{ +} + +PluginRef PluginWeakRef::strong() const noexcept +{ + return {this->weak.lock()}; +} + +bool PluginWeakRef::isAlive() const noexcept +{ + return !this->weak.expired(); +} + +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/PluginRef.hpp b/src/controllers/plugins/PluginRef.hpp new file mode 100644 index 00000000..c9e82d04 --- /dev/null +++ b/src/controllers/plugins/PluginRef.hpp @@ -0,0 +1,89 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include + +namespace chatterino { +class Plugin; +} // namespace chatterino + +namespace chatterino::lua { + +class PluginWeakRef; + +/// A strong reference to a plugin. +/// +/// This reference is similar to a shared pointer with the big difference that +/// it doesn't own the plugin. Destroying all `PluginRef`s will _not_ destroy +/// the plugin (-> the destructor of the shared_ptr does nothing). +/// +/// The plugin itself stores one strong reference. This reference is destroyed +/// with destroy() right before the Lua state is closed. At that point, the +/// reference count must be exactly 1 - meaning only the plugin itself has the +/// strong reference. +/// +/// Any object that keeps a reference to the plugin or to Lua values should keep +/// a `PluginWeakRef`. For example, callbacks for signals keep this weak +/// reference and avoid interacting with Lua references if the plugin is dead. +/// While accessing a plugin, objects should upgrade their weak reference to a +/// strong one. This way destroy() can detect potentially dangling references. +class PluginRef +{ +public: + PluginRef() = default; + PluginRef(PluginRef &&) = default; + PluginRef &operator=(const PluginRef &) = default; + PluginRef &operator=(PluginRef &&) = default; + PluginRef(const PluginRef &) = default; + ~PluginRef() = default; + + PluginWeakRef weak() const noexcept; + Plugin *plugin() const noexcept; + + operator bool() const noexcept + { + return this->shared != nullptr; + } + +private: + PluginRef(Plugin *plugin); + PluginRef(std::shared_ptr plugin); + + /// To be called by the plugin before the Lua state is closed. + /// + /// Resets the reference if there are no other shared references. + /// Otherwise, aborts execution. + void destroy(); + + std::shared_ptr shared; + + friend Plugin; + friend PluginWeakRef; +}; + +class PluginWeakRef +{ +public: + PluginWeakRef() = default; + PluginWeakRef(const PluginWeakRef &) = default; + PluginWeakRef(PluginWeakRef &&) = default; + PluginWeakRef &operator=(const PluginWeakRef &) = default; + PluginWeakRef &operator=(PluginWeakRef &&) = default; + ~PluginWeakRef() = default; + + PluginRef strong() const noexcept; + + bool isAlive() const noexcept; + +private: + PluginWeakRef(std::weak_ptr weak); + + std::weak_ptr weak; + + friend PluginRef; +}; + +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/SignalCallback.hpp b/src/controllers/plugins/SignalCallback.hpp new file mode 100644 index 00000000..d260edbe --- /dev/null +++ b/src/controllers/plugins/SignalCallback.hpp @@ -0,0 +1,76 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include "controllers/plugins/PluginRef.hpp" +# include "controllers/plugins/SolTypes.hpp" +# include "debug/AssertInGuiThread.hpp" + +namespace chatterino::lua { + +struct SignalCallback { + SignalCallback(PluginWeakRef pluginRef, sol::main_protected_function pfn) + : pluginRef(std::move(pluginRef)) + , pfn(std::move(pfn)) + { + assert(this->pfn.valid()); + } + + SignalCallback(const SignalCallback &other) = default; + SignalCallback(SignalCallback &&) = default; + + SignalCallback &operator=(const SignalCallback &other) + { + if (this == &other) + { + return *this; + } + + if (!this->pluginRef.isAlive()) + { + // Reset the function reference before we assign a new function to + // it to avoid destroying it. + this->pfn.abandon(); + } + this->pluginRef = other.pluginRef; + this->pfn = other.pfn; + return *this; + } + + SignalCallback &operator=(SignalCallback &&other) noexcept + { + std::swap(this->pfn, other.pfn); + std::swap(this->pluginRef, other.pluginRef); + return *this; + } + + ~SignalCallback() + { + assertInGuiThread(); + if (!this->pluginRef.isAlive()) + { + this->pfn.abandon(); // don't destruct the function in this case + } + } + + void operator()(auto &&...args) const + { + assertInGuiThread(); + auto strong = this->pluginRef.strong(); + if (!strong) + { + assert(false && "faulty signal handling"); + return; + } + loggedVoidCall(this->pfn, u"SignalCallback::operator()", + strong.plugin(), std::forward(args)...); + } + +private: + PluginWeakRef pluginRef; + sol::main_protected_function pfn; +}; + +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/api/ChannelRef.cpp b/src/controllers/plugins/api/ChannelRef.cpp index 5f5995d4..6507a2b2 100644 --- a/src/controllers/plugins/api/ChannelRef.cpp +++ b/src/controllers/plugins/api/ChannelRef.cpp @@ -8,6 +8,8 @@ # include "Application.hpp" # include "common/Channel.hpp" # include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/Plugin.hpp" +# include "controllers/plugins/SignalCallback.hpp" # include "controllers/plugins/SolTypes.hpp" # include "messages/Message.hpp" # include "providers/twitch/TwitchChannel.hpp" @@ -243,6 +245,15 @@ bool ChannelRef::operator==(const ChannelRef &other) const noexcept return weakOwnerEquals(this->weak, other.weak); } +api::ConnectionHandle ChannelRef::on_display_name_changed( + ThisPluginState state, sol::main_protected_function pfn) +{ + auto *plugin = state.plugin(); + return plugin->connections.managedConnect( + this->strong()->displayNameChanged, + plugin->createCallback(std::move(pfn))); +} + std::optional ChannelRef::get_by_name(const QString &name) { auto chan = getApp()->getTwitch()->getChannelOrEmpty(name); @@ -292,6 +303,8 @@ void ChannelRef::createUserType(sol::table &c2) "has_messages", &ChannelRef::has_messages, "count_messages", &ChannelRef::count_messages, + "on_display_name_changed", &ChannelRef::on_display_name_changed, + // TwitchChannel "get_room_modes", &ChannelRef::get_room_modes, "get_stream_status", &ChannelRef::get_stream_status, diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index f6b863c7..0e3492ae 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -9,9 +9,15 @@ # include +namespace chatterino::lua { +class ThisPluginState; +} // namespace chatterino::lua + namespace chatterino::lua::api { // NOLINTBEGIN(readability-identifier-naming) +struct ConnectionHandle; + /** * @includefile providers/twitch/TwitchChannel.hpp */ @@ -241,6 +247,16 @@ public: bool operator==(const ChannelRef &other) const noexcept; + /** + * Callback when the channel display name changes. + * + * @lua@param cb fun() + * @lua@return c2.ConnectionHandle hdl + * @exposed c2.Channel:on_display_name_changed + */ + ConnectionHandle on_display_name_changed(ThisPluginState state, + sol::main_protected_function pfn); + /** * Static functions */ diff --git a/src/controllers/plugins/api/ConnectionHandle.cpp b/src/controllers/plugins/api/ConnectionHandle.cpp new file mode 100644 index 00000000..e572d0fd --- /dev/null +++ b/src/controllers/plugins/api/ConnectionHandle.cpp @@ -0,0 +1,55 @@ +#include "controllers/plugins/api/ConnectionHandle.hpp" + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include + +namespace chatterino::lua::api { + +void ConnectionHandle::createUserType(sol::table &c2) +{ + c2.new_usertype( + "ConnectionHandle", sol::no_constructor, // + "disconnect", + [](ConnectionHandle &hdl) { + auto locked = hdl.connection.lock(); + if (locked) + { + *locked = pajlada::Signals::ScopedConnection{}; + } + }, + "block", + [](ConnectionHandle &hdl) { + if (auto locked = hdl.connection.lock()) + { + locked->block(); + } + }, + "unblock", + [](ConnectionHandle &hdl) { + if (auto locked = hdl.connection.lock()) + { + locked->unblock(); + } + }, + "is_blocked", + [](ConnectionHandle &hdl) { + if (auto locked = hdl.connection.lock()) + { + return locked->isBlocked(); + } + return false; + }, + "is_connected", + [](ConnectionHandle &hdl) { + if (auto locked = hdl.connection.lock()) + { + return locked->isConnected(); + } + return false; + }); +} + +} // namespace chatterino::lua::api + +#endif diff --git a/src/controllers/plugins/api/ConnectionHandle.hpp b/src/controllers/plugins/api/ConnectionHandle.hpp new file mode 100644 index 00000000..96e3cef2 --- /dev/null +++ b/src/controllers/plugins/api/ConnectionHandle.hpp @@ -0,0 +1,48 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include +# include + +# include + +namespace chatterino::lua::api { + +/* @lua-fragment + +---@class c2.ConnectionHandle +---This type represents a handle to a registration of a callback for an event handler. +---Conceptually, the event has a _connection_ to the callback/handler. +---This handle can be used to modify that connection. +---It does not automatically disconnect the connection when it's destroyed (in `__gc`) - +---`disconnect()` has to be called manually. +c2.ConnectionHandle = {} + +---Disconnect the signal +function c2.ConnectionHandle:disconnect() end + +---Block events on this connection +function c2.ConnectionHandle:block() end + +---Unblock events on this connection +function c2.ConnectionHandle:unblock() end + +---Is this connection currently blocked? +---@return boolean is_blocked +function c2.ConnectionHandle:is_blocked() end + +---Is this connection still connected? +---@return boolean is_connected +function c2.ConnectionHandle:is_connected() end + +*/ +struct ConnectionHandle { + std::weak_ptr connection; + + static void createUserType(sol::table &c2); +}; + +} // namespace chatterino::lua::api + +#endif diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index 816809a1..1f1e4052 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -1506,6 +1506,56 @@ TEST_F(PluginTest, LuaVersion) static_assert(LUA_VERSION_NUM >= 504); } +TEST_F(PluginTest, ChannelOnDisplayNameChanged) +{ + this->configure(); + + bool gotEvent = false; + this->lua->set_function("on_test_event", [&] { + gotEvent = true; + }); + + auto chan = std::make_shared("mock"); + this->lua->set("chan", lua::api::ChannelRef(chan)); + sol::protected_function init = this->lua->script(R"lua( + hdl = nil + return function(chan) + hdl = chan:on_display_name_changed(on_test_event) + end + )lua"); + + ASSERT_TRUE(init(lua::api::ChannelRef(chan)).valid()); + + ASSERT_TRUE(this->lua->script("assert(hdl ~= nil)").valid()); + + // regular delivery + chan->displayNameChanged.invoke(); + ASSERT_TRUE(gotEvent); + + // blocked connection + ASSERT_TRUE(this->lua->script("hdl:block()").valid()); + ASSERT_TRUE(this->lua->script("assert(hdl:is_blocked())").valid()); + + gotEvent = false; + chan->displayNameChanged.invoke(); + ASSERT_FALSE(gotEvent); + + // unblocked connection + ASSERT_TRUE(this->lua->script("hdl:unblock()").valid()); + ASSERT_TRUE(this->lua->script("assert(not hdl:is_blocked())").valid()); + + gotEvent = false; + chan->displayNameChanged.invoke(); + ASSERT_TRUE(gotEvent); + + // disconnect + ASSERT_TRUE(this->lua->script("hdl:disconnect()").valid()); + ASSERT_TRUE(this->lua->script("assert(not hdl:is_connected())").valid()); + + gotEvent = false; + ASSERT_FALSE(gotEvent); +} + class PluginMessageConstructionTest : public PluginTest, public ::testing::WithParamInterface