feat(plugins): add c2.WebSocket (#6076)

This commit is contained in:
nerix
2025-04-07 19:38:10 +02:00
committed by GitHub
parent d3bab9132f
commit ab66be21b3
31 changed files with 1859 additions and 29 deletions
+1
View File
@@ -85,6 +85,7 @@ sol::table toTable(lua_State *L, const CompletionEvent &ev);
* @includefile controllers/plugins/api/ChannelRef.hpp
* @includefile controllers/plugins/api/HTTPResponse.hpp
* @includefile controllers/plugins/api/HTTPRequest.hpp
* @includefile controllers/plugins/api/WebSocket.hpp
* @includefile common/network/NetworkCommon.hpp
*/
+7
View File
@@ -293,5 +293,12 @@ bool Plugin::hasHTTPPermissionFor(const QUrl &url)
});
}
bool Plugin::hasNetworkPermission() const
{
return std::ranges::any_of(this->meta.permissions, [](const auto &p) {
return p.type == PluginPermission::Type::Network;
});
}
} // namespace chatterino
#endif
+1
View File
@@ -136,6 +136,7 @@ public:
bool hasFSPermissionFor(bool write, const QString &path);
bool hasHTTPPermissionFor(const QUrl &url);
bool hasNetworkPermission() const;
std::map<lua::api::EventType, sol::protected_function> callbacks;
@@ -11,6 +11,7 @@
# include "controllers/plugins/api/HTTPRequest.hpp"
# include "controllers/plugins/api/HTTPResponse.hpp"
# include "controllers/plugins/api/IOWrapper.hpp"
# include "controllers/plugins/api/WebSocket.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/SolTypes.hpp"
@@ -225,6 +226,11 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
c2["EventType"] = lua::createEnumTable<lua::api::EventType>(lua);
c2["LogLevel"] = lua::createEnumTable<lua::api::LogLevel>(lua);
if (plugin->hasNetworkPermission())
{
lua::api::WebSocket::createUserType(c2, plugin);
}
sol::table io = g["io"];
io.set_function(
"open", sol::overload(&lua::api::io_open, &lua::api::io_open_modeless));
@@ -428,5 +434,10 @@ std::pair<bool, QStringList> PluginController::updateCustomCompletions(
return {false, results};
}
WebSocketPool &PluginController::webSocketPool()
{
return this->webSocketPool_;
}
} // namespace chatterino
#endif
@@ -2,6 +2,7 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "common/websockets/WebSocketPool.hpp"
# include "controllers/commands/CommandContext.hpp"
# include "controllers/plugins/Plugin.hpp"
@@ -61,6 +62,8 @@ public:
const QString &query, const QString &fullTextContent,
int cursorPosition, bool isFirstWord) const;
WebSocketPool &webSocketPool();
private:
void loadPlugins();
void load(const QFileInfo &index, const QDir &pluginDir,
@@ -74,6 +77,7 @@ private:
static void loadChatterinoLib(lua_State *l);
bool tryLoadFromDir(const QDir &pluginDir);
std::map<QString, std::unique_ptr<Plugin>> plugins_;
WebSocketPool webSocketPool_;
// This is for tests, pay no attention
friend class PluginControllerAccess;
+10 -1
View File
@@ -1,10 +1,12 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/SolTypes.hpp"
# include "common/QLogging.hpp"
# include "controllers/plugins/PluginController.hpp"
# include <QObject>
# include <sol/thread.hpp>
namespace chatterino::lua {
Plugin *ThisPluginState::plugin()
@@ -22,6 +24,13 @@ Plugin *ThisPluginState::plugin()
return pl;
}
void logError(Plugin *plugin, QStringView context, const QString &msg)
{
qCWarning(chatterinoLua).noquote()
<< "[" + plugin->id + ":" + plugin->meta.name + "]" << context << "-"
<< msg;
}
} // namespace chatterino::lua
// NOLINTBEGIN(readability-named-parameter)
@@ -89,7 +98,7 @@ QByteArray sol_lua_get(sol::types<QByteArray>, lua_State *L, int index,
sol::stack::record &tracking)
{
auto str = sol::stack::get<std::string_view>(L, index, tracking);
return QByteArray::fromRawData(str.data(), str.length());
return {str.data(), static_cast<qsizetype>(str.length())};
}
int sol_lua_push(sol::types<QByteArray>, lua_State *L, const QByteArray &value)
+27 -2
View File
@@ -67,8 +67,12 @@ private:
/// `std::optional<T>` means nil|LuaEquiv<T> (or zero returns)
/// A return type that doesn't match returns an error
template <typename T, typename... Args>
inline nonstd::expected_lite::expected<T, QString> tryCall(
const sol::protected_function &function, Args &&...args)
inline nonstd::expected_lite::expected<T, QString> tryCall(const auto &function,
Args &&...args)
requires(std::same_as<std::remove_cvref_t<decltype(function)>,
sol::protected_function> ||
std::same_as<std::remove_cvref_t<decltype(function)>,
sol::main_protected_function>)
{
sol::protected_function_result result =
function(std::forward<Args>(args)...);
@@ -148,6 +152,27 @@ inline nonstd::expected_lite::expected<T, QString> tryCall(
}
}
void logError(Plugin *plugin, QStringView context, const QString &msg);
template <typename T>
bool hasValueOrLog(const nonstd::expected<T, QString> &res, QStringView context,
Plugin *plugin)
{
if (!res.has_value())
{
logError(plugin, context, res.error());
return false;
}
return true;
}
void loggedVoidCall(const auto &fn, QStringView context, Plugin *plugin,
auto &&...args)
{
auto res = tryCall<void>(fn, std::forward<decltype(args)>(args)...);
hasValueOrLog(res, context, plugin);
}
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
# define SOL_STACK_FUNCTIONS(TYPE) \
bool sol_lua_check(sol::types<TYPE>, lua_State *L, int index, \
+175
View File
@@ -0,0 +1,175 @@
#include "controllers/plugins/api/WebSocket.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS
# include "Application.hpp"
# include "controllers/plugins/PluginController.hpp"
# include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep
# include "util/PostToThread.hpp"
# include <utility>
namespace chatterino::lua::api {
/// A WebSocket listener that dispatches events on the GUI thread to the user type.
class WebSocketListenerProxy final : public WebSocketListener
{
public:
WebSocketListenerProxy(std::weak_ptr<WebSocket> target);
void onClose(std::unique_ptr<WebSocketListener> self) override;
void onBinaryMessage(QByteArray data) override;
void onTextMessage(QByteArray data) override;
private:
std::weak_ptr<WebSocket> target;
};
WebSocket::WebSocket() = default;
void WebSocket::createUserType(sol::table &c2, Plugin *plugin)
{
c2.new_usertype<WebSocket>(
"WebSocket",
sol::factories([plugin](const QString &spec, sol::variadic_args args) {
QUrl url(spec);
if (url.scheme() != "wss" && url.scheme() != "ws")
{
throw std::runtime_error("Scheme must be wss:// or ws://");
}
auto self = std::make_shared<WebSocket>();
self->plugin = plugin;
WebSocketOptions opts{.url = url, .headers = {}};
if (args.size() >= 1)
{
sol::table luaOpts = args[0];
sol::optional<sol::table> headers = luaOpts["headers"];
if (headers)
{
for (const auto &[k, v] : *headers)
{
opts.headers.emplace_back(k.as<std::string>(),
v.as<std::string>());
}
}
sol::optional<sol::main_function> onText = luaOpts["on_text"];
sol::optional<sol::main_function> onBinary =
luaOpts["on_binary"];
sol::optional<sol::main_function> onClose = luaOpts["on_close"];
if (onText)
{
self->onText = std::move(*onText);
}
if (onBinary)
{
self->onBinary = std::move(*onBinary);
}
if (onClose)
{
self->onClose = std::move(*onClose);
}
}
auto handle = getApp()->getPlugins()->webSocketPool().createSocket(
opts, std::make_unique<WebSocketListenerProxy>(self));
self->handle = std::move(handle);
return self;
}),
// Note: These properties could be pointers to members, but Clang 18
// specifically can't compile these - see https://github.com/ThePhD/sol2/issues/1581
"on_close",
sol::property(
[](WebSocket &ws) {
return ws.onClose;
},
[](WebSocket &ws, sol::main_function fn) {
ws.onClose = std::move(fn);
}),
"on_text",
sol::property(
[](WebSocket &ws) {
return ws.onText;
},
[](WebSocket &ws, sol::main_function fn) {
ws.onText = std::move(fn);
}),
"on_binary",
sol::property(
[](WebSocket &ws) {
return ws.onBinary;
},
[](WebSocket &ws, sol::main_function fn) {
ws.onBinary = std::move(fn);
}),
"close", &WebSocket::close, //
"send_text", &WebSocket::sendText, //
"send_binary", &WebSocket::sendBinary //
);
}
void WebSocket::close()
{
this->handle.close();
}
void WebSocket::sendText(const QByteArray &data)
{
this->handle.sendText(data);
}
void WebSocket::sendBinary(const QByteArray &data)
{
this->handle.sendBinary(data);
}
WebSocketListenerProxy::WebSocketListenerProxy(std::weak_ptr<WebSocket> target)
: target(std::move(target))
{
}
void WebSocketListenerProxy::onClose(std::unique_ptr<WebSocketListener> self)
{
runInGuiThread([this, lifetime{std::move(self)}] {
auto strong = this->target.lock();
if (strong)
{
// clear our object, so we can get GC'd
auto cb = std::move(strong->onClose);
strong->onText.reset();
strong->onBinary.reset();
loggedVoidCall(cb, u"WebSocket.on_close", strong->plugin);
}
});
}
void WebSocketListenerProxy::onTextMessage(QByteArray data)
{
auto target = this->target;
runInGuiThread([target, data{std::move(data)}] {
auto strong = target.lock();
if (strong)
{
loggedVoidCall(strong->onText, u"WebSocket.on_text", strong->plugin,
data);
}
});
}
void WebSocketListenerProxy::onBinaryMessage(QByteArray data)
{
auto target = this->target;
runInGuiThread([target, data{std::move(data)}] {
auto strong = target.lock();
if (strong)
{
loggedVoidCall(strong->onBinary, u"WebSocket.on_binary",
strong->plugin, data);
}
});
}
} // namespace chatterino::lua::api
#endif
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include "common/websockets/WebSocketPool.hpp"
# include <sol/protected_function.hpp>
# include <sol/types.hpp>
namespace chatterino {
class Plugin;
} // namespace chatterino
namespace chatterino::lua::api {
/**
* @lua@class c2.WebSocket
*/
class WebSocket
{
public:
/**
* Creates and connects to a WebSocket server. Upon calling this, a
* connection is made immediately.
*
* @lua@param url string The URL to connect to. Must start with `wss://` or `ws://`.
* @lua@param options? { headers?: table<string, string>, on_close?: fun(), on_text?: fun(data: string), on_binary?: fun(data: string) } Additional options for the connection.
* @lua@return c2.WebSocket
* @lua@nodiscard
* @exposed c2.WebSocket.new
*/
WebSocket();
static void createUserType(sol::table &c2, Plugin *plugin);
/**
* Closes the socket.
*
* @exposed c2.WebSocket:close
*/
void close();
/**
* Sends a text message on the socket.
*
* @lua@param data string The text to send.
* @exposed c2.WebSocket:send_text
*/
void sendText(const QByteArray &data);
/**
* Sends a binary message on the socket.
*
* @lua@param data string The binary data to send.
* @exposed c2.WebSocket:send_binary
*/
void sendBinary(const QByteArray &data);
private:
/**
* @lua@field on_close fun()|nil Handler called when the socket is closed.
*/
sol::main_function onClose;
/**
* @lua@field on_text fun(data: string)|nil Handler called when the socket receives a text message.
*/
sol::main_function onText;
/**
* @lua@field on_binary fun(data: string)|nil Handler called when the socket receives a binary message.
*/
sol::main_function onBinary;
WebSocketHandle handle;
// Note: this class lives inside the plugin -> this pointer will be valid.
Plugin *plugin = nullptr;
friend class WebSocketListenerProxy;
};
} // namespace chatterino::lua::api
#endif