feat(plugins): add c2.WebSocket (#6076)
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, \
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user