feat(plugins): add display name change event to Channel (#6594)

Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
This commit is contained in:
Nerixyz
2026-01-29 18:25:39 +01:00
committed by GitHub
parent 60ecf1a8d7
commit a869cd4bd1
19 changed files with 610 additions and 0 deletions
+6
View File
@@ -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
@@ -0,0 +1,36 @@
#include "controllers/plugins/ConnectionManager.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS
# include <algorithm>
namespace chatterino::lua {
api::ConnectionHandle ConnectionManager::add(
pajlada::Signals::ScopedConnection &&conn)
{
this->removeDisconnected();
return {
.connection = this->connections.emplace_back(
std::make_shared<pajlada::Signals::ScopedConnection>(
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
@@ -0,0 +1,46 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/api/ConnectionHandle.hpp"
# include <pajlada/signals/scoped-connection.hpp>
# include <vector>
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<ScopedConnection>`
/// to be able to give the plugin a `std::weak_ptr<ScopedConnection>` which it
/// can use to disconnect.
class ConnectionManager
{
public:
ConnectionManager() = default;
api::ConnectionHandle add(pajlada::Signals::ScopedConnection &&conn);
template <typename Signal, typename Callback>
api::ConnectionHandle managedConnect(Signal &signal, Callback cb)
{
return this->add(signal.connect(std::forward<Callback>(cb)));
}
void clear();
private:
void removeDisconnected();
std::vector<std::shared_ptr<pajlada::Signals::ScopedConnection>>
connections;
};
} // namespace chatterino::lua
#endif
+1
View File
@@ -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
+7
View File
@@ -9,6 +9,7 @@
# include "common/QLogging.hpp"
# include "controllers/commands/CommandController.hpp"
# include "controllers/plugins/PluginPermission.hpp"
# include "controllers/plugins/SignalCallback.hpp"
# include <lua.h>
# include <QLoggingCategory>
@@ -48,6 +49,11 @@ std::unordered_set<QString> 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
+14
View File
@@ -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 <boost/signals2/signal.hpp>
# include <QDir>
@@ -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<void()> onUnloaded;
boost::signals2::signal<void(lua::api::LogLevel, const QString &)> onLog;
lua::ConnectionManager connections;
private:
QDir loadDirectory_;
lua_State *state_;
lua::PluginRef selfRef_;
QString error_;
// maps command name -> function
@@ -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<Channel::Type>(lua);
+66
View File
@@ -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 <QMessageBox>
# include <QStringBuilder>
namespace chatterino::lua {
PluginRef::PluginRef(Plugin *plugin)
: shared(plugin, /*deleter=*/[](void *) {})
{
assert(plugin != nullptr);
}
PluginRef::PluginRef(std::shared_ptr<Plugin> 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<Plugin> 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
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include <memory>
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> 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<Plugin> 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<Plugin> weak);
std::weak_ptr<Plugin> weak;
friend PluginRef;
};
} // namespace chatterino::lua
#endif
@@ -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<decltype(args)>(args)...);
}
private:
PluginWeakRef pluginRef;
sol::main_protected_function pfn;
};
} // namespace chatterino::lua
#endif
@@ -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> 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,
@@ -9,9 +9,15 @@
# include <sol/forward.hpp>
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
*/
@@ -0,0 +1,55 @@
#include "controllers/plugins/api/ConnectionHandle.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS
# include <sol/sol.hpp>
namespace chatterino::lua::api {
void ConnectionHandle::createUserType(sol::table &c2)
{
c2.new_usertype<ConnectionHandle>(
"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
@@ -0,0 +1,48 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include <pajlada/signals/scoped-connection.hpp>
# include <sol/forward.hpp>
# include <memory>
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<pajlada::Signals::ScopedConnection> connection;
static void createUserType(sol::table &c2);
};
} // namespace chatterino::lua::api
#endif