Move HTTPRequest to sol2

This commit is contained in:
Mm2PL 2024-10-04 00:57:30 +02:00
parent 3dde8f7a4c
commit c9656399d1
No known key found for this signature in database
GPG key ID: 94AC9B80EFA15ED9
3 changed files with 97 additions and 398 deletions

View file

@ -169,13 +169,6 @@ void PluginController::openLibrariesFor(Plugin *plugin, const QDir &pluginDir)
lua::pushEnumTable<lua::api::EventType>(L);
lua_setfield(L, c2libIdx, "EventType");
lua::pushEnumTable<NetworkRequestType>(L);
lua_setfield(L, c2libIdx, "HTTPMethod");
// Initialize metatables for objects
lua::api::HTTPRequest::createMetatable(L);
lua_setfield(L, c2libIdx, "HTTPRequest");
lua_setfield(L, gtable, "c2");
// ban functions
@ -299,7 +292,9 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
});
lua::api::ChannelRef::createUserType(c2);
lua::api::HTTPResponse::createUserType(c2);
lua::api::HTTPRequest::createUserType(plugin->state_, c2);
c2["ChannelType"] = lua::createEnumTable<Channel::Type>(lua);
c2["HTTPMethod"] = lua::createEnumTable<NetworkRequestType>(lua);
}
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,

View file

@ -6,402 +6,151 @@
# include "common/network/NetworkRequest.hpp"
# include "common/network/NetworkResult.hpp"
# include "controllers/plugins/api/HTTPResponse.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "util/DebugCount.hpp"
# include <lauxlib.h>
# include <lua.h>
# include <QChar>
# include <QRandomGenerator>
# include <QUrl>
# include <sol/forward.hpp>
# include <sol/raii.hpp>
# include <sol/state_view.hpp>
# include <sol/table.hpp>
# include <sol/types.hpp>
# include <memory>
# include <optional>
# include <stdexcept>
# include <utility>
namespace chatterino::lua::api {
// NOLINTBEGIN(*vararg)
// NOLINTNEXTLINE(*-avoid-c-arrays)
static const luaL_Reg HTTP_REQUEST_METHODS[] = {
{"on_success", &HTTPRequest::on_success_wrap},
{"on_error", &HTTPRequest::on_error_wrap},
{"finally", &HTTPRequest::finally_wrap},
{"execute", &HTTPRequest::execute_wrap},
{"set_timeout", &HTTPRequest::set_timeout_wrap},
{"set_payload", &HTTPRequest::set_payload_wrap},
{"set_header", &HTTPRequest::set_header_wrap},
// static
{"create", &HTTPRequest::create},
{nullptr, nullptr},
};
std::shared_ptr<HTTPRequest> HTTPRequest::getOrError(lua_State *L,
StackIdx where)
void HTTPRequest::createUserType(lua_State *L, sol::table &c2)
{
if (lua_gettop(L) < 1)
{
// The nullptr is there just to appease the compiler, luaL_error is no return
luaL_error(L, "Called c2.HTTPRequest method without a request object");
return nullptr;
}
if (lua_isuserdata(L, where) == 0)
{
luaL_error(
L,
"Called c2.HTTPRequest method with a non-userdata 'self' argument");
return nullptr;
}
// luaL_checkudata is no-return if check fails
auto *checked = luaL_checkudata(L, where, "c2.HTTPRequest");
auto *data =
SharedPtrUserData<UserData::Type::HTTPRequest, HTTPRequest>::from(
checked);
if (data == nullptr)
{
luaL_error(L, "Called c2.HTTPRequest method with an invalid pointer");
return nullptr;
}
lua_remove(L, where);
if (data->target == nullptr)
{
luaL_error(
L, "Internal error: SharedPtrUserData<UserData::Type::HTTPRequest, "
"HTTPRequest>::target was null. This is a Chatterino bug!");
return nullptr;
}
if (data->target->done)
{
luaL_error(L, "This c2.HTTPRequest has already been executed!");
return nullptr;
}
return data->target;
c2.new_usertype<HTTPRequest>( //
"HTTPRequest", sol::no_constructor, //
"on_success", &HTTPRequest::on_success, //
"on_error", &HTTPRequest::on_error, //
"finally", &HTTPRequest::finally, //
"set_timeout", &HTTPRequest::set_timeout, //
"set_payload", &HTTPRequest::set_payload, //
"set_header", &HTTPRequest::set_header, //
"execute", &HTTPRequest::execute, //
"create", [L](NetworkRequestType method, const std::string &url) {
return HTTPRequest::create(L, method, QString::fromStdString(url));
});
}
void HTTPRequest::createMetatable(lua_State *L)
void HTTPRequest::on_success(sol::protected_function func)
{
lua::StackGuard guard(L, 1);
luaL_newmetatable(L, "c2.HTTPRequest");
lua_pushstring(L, "__index");
lua_pushvalue(L, -2); // clone metatable
lua_settable(L, -3); // metatable.__index = metatable
// Generic ISharedResource stuff
lua_pushstring(L, "__gc");
lua_pushcfunction(L, (&SharedPtrUserData<UserData::Type::HTTPRequest,
HTTPRequest>::destroy));
lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy
luaL_setfuncs(L, HTTP_REQUEST_METHODS, 0);
this->cbSuccess = {func};
}
int HTTPRequest::on_success_wrap(lua_State *L)
void HTTPRequest::on_error(sol::protected_function func)
{
lua::StackGuard guard(L, -2);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->on_success(L);
this->cbError = {func};
}
int HTTPRequest::on_success(lua_State *L)
void HTTPRequest::set_timeout(int timeout)
{
auto top = lua_gettop(L);
if (top != 1)
{
return luaL_error(
L, "HTTPRequest:on_success needs 1 argument (a callback "
"that takes an HTTPResult and doesn't return anything)");
}
if (!lua_isfunction(L, top))
{
return luaL_error(
L, "HTTPRequest:on_success needs 1 argument (a callback "
"that takes an HTTPResult and doesn't return anything)");
}
auto shared = this->pushPrivate(L);
lua_pushvalue(L, -2);
lua_setfield(L, shared, "success"); // this deletes the function copy
lua_pop(L, 2); // delete the table and function original
return 0;
this->timeout_ = timeout;
}
int HTTPRequest::on_error_wrap(lua_State *L)
void HTTPRequest::finally(sol::protected_function func)
{
lua::StackGuard guard(L, -2);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->on_error(L);
this->cbFinally = {func};
}
int HTTPRequest::on_error(lua_State *L)
void HTTPRequest::set_payload(const std::string &payload)
{
auto top = lua_gettop(L);
if (top != 1)
{
return luaL_error(
L, "HTTPRequest:on_error needs 1 argument (a callback "
"that takes an HTTPResult and doesn't return anything)");
}
if (!lua_isfunction(L, top))
{
return luaL_error(
L, "HTTPRequest:on_error needs 1 argument (a callback "
"that takes an HTTPResult and doesn't return anything)");
}
auto shared = this->pushPrivate(L);
lua_pushvalue(L, -2);
lua_setfield(L, shared, "error"); // this deletes the function copy
lua_pop(L, 2); // delete the table and function original
return 0;
}
int HTTPRequest::set_timeout_wrap(lua_State *L)
{
lua::StackGuard guard(L, -2);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->set_timeout(L);
}
int HTTPRequest::set_timeout(lua_State *L)
{
auto top = lua_gettop(L);
if (top != 1)
{
return luaL_error(
L, "HTTPRequest:set_timeout needs 1 argument (a number of "
"milliseconds after which the request will time out)");
}
int temporary = -1;
if (!lua::pop(L, &temporary))
{
return luaL_error(
L, "HTTPRequest:set_timeout failed to get timeout, expected a "
"positive integer");
}
if (temporary <= 0)
{
return luaL_error(
L, "HTTPRequest:set_timeout failed to get timeout, expected a "
"positive integer");
}
this->timeout_ = temporary;
return 0;
}
int HTTPRequest::finally_wrap(lua_State *L)
{
lua::StackGuard guard(L, -2);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->finally(L);
}
int HTTPRequest::finally(lua_State *L)
{
auto top = lua_gettop(L);
if (top != 1)
{
return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback "
"that takes nothing and doesn't return anything)");
}
if (!lua_isfunction(L, top))
{
return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback "
"that takes nothing and doesn't return anything)");
}
auto shared = this->pushPrivate(L);
lua_pushvalue(L, -2);
lua_setfield(L, shared, "finally"); // this deletes the function copy
lua_pop(L, 2); // delete the table and function original
return 0;
}
int HTTPRequest::set_payload_wrap(lua_State *L)
{
lua::StackGuard guard(L, -2);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->set_payload(L);
}
int HTTPRequest::set_payload(lua_State *L)
{
auto top = lua_gettop(L);
if (top != 1)
{
return luaL_error(
L, "HTTPRequest:set_payload needs 1 argument (a string payload)");
}
std::string temporary;
if (!lua::pop(L, &temporary))
{
return luaL_error(
L, "HTTPRequest:set_payload failed to get payload, expected a "
"string");
}
this->req_ =
std::move(this->req_).payload(QByteArray::fromStdString(temporary));
return 0;
std::move(this->req_).payload(QByteArray::fromStdString(payload));
}
int HTTPRequest::set_header_wrap(lua_State *L)
// name and value may be random bytes
void HTTPRequest::set_header(std::string name, std::string value)
{
lua::StackGuard guard(L, -3);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->set_header(L);
}
int HTTPRequest::set_header(lua_State *L)
{
auto top = lua_gettop(L);
if (top != 2)
{
return luaL_error(
L, "HTTPRequest:set_header needs 2 arguments (a header name "
"and a value)");
}
std::string value;
if (!lua::pop(L, &value))
{
return luaL_error(
L, "cannot get value (2nd argument of HTTPRequest:set_header)");
}
std::string name;
if (!lua::pop(L, &name))
{
return luaL_error(
L, "cannot get name (1st argument of HTTPRequest:set_header)");
}
this->req_ = std::move(this->req_)
.header(QByteArray::fromStdString(name),
QByteArray::fromStdString(value));
return 0;
}
int HTTPRequest::create(lua_State *L)
HTTPRequest HTTPRequest::create(lua_State *L, NetworkRequestType method,
QString url)
{
lua::StackGuard guard(L, -1);
if (lua_gettop(L) != 2)
{
return luaL_error(
L, "HTTPRequest.create needs exactly 2 arguments (method "
"and url)");
}
QString url;
if (!lua::pop(L, &url))
{
return luaL_error(L,
"cannot get url (2nd argument of HTTPRequest.create, "
"expected a string)");
}
auto parsedurl = QUrl(url);
if (!parsedurl.isValid())
{
return luaL_error(
L, "cannot parse url (2nd argument of HTTPRequest.create, "
"got invalid url in argument)");
}
NetworkRequestType method{};
if (!lua::pop(L, &method))
{
return luaL_error(
L, "cannot get method (1st argument of HTTPRequest.create, "
"expected a string)");
throw std::runtime_error(
"cannot parse url (2nd argument of HTTPRequest.create, "
"got invalid url in argument)");
}
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
if (!pl->hasHTTPPermissionFor(parsedurl))
{
return luaL_error(
L, "Plugin does not have permission to send HTTP requests "
"to this URL");
throw std::runtime_error(
"Plugin does not have permission to send HTTP requests "
"to this URL");
}
NetworkRequest r(parsedurl, method);
lua::push(
L, std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r)));
return 1;
return {ConstructorAccessTag{}, std::move(r), L};
}
int HTTPRequest::execute_wrap(lua_State *L)
void HTTPRequest::execute()
{
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->execute(L);
}
int HTTPRequest::execute(lua_State *L)
{
auto shared = this->shared_from_this();
this->done = true;
std::move(this->req_)
.onSuccess([shared, L](const NetworkResult &res) {
lua::StackGuard guard(L);
auto *thread = lua_newthread(L);
.onSuccess([this](const NetworkResult &res) {
if (!this->cbSuccess.has_value())
{
sol::state_view lua(thread);
auto privid = shared->pushPrivate(thread);
sol::table priv(thread, privid);
auto cb = priv["success"];
if (cb.get_type() == sol::type::function)
{
cb(HTTPResponse(res));
}
priv.pop();
return;
}
lua_closethread(thread, nullptr);
lua_pop(L, 1); // remove thread from L
lua::StackGuard guard(this->state_);
sol::state_view mainState(this->state_);
sol::thread thread = sol::thread::create(mainState);
sol::state_view threadstate = thread.state();
sol::protected_function cb(threadstate, *this->cbSuccess);
cb(HTTPResponse(res));
this->cbSuccess = std::nullopt;
})
.onError([shared, L](const NetworkResult &res) {
lua::StackGuard guard(L);
auto *thread = lua_newthread(L);
.onError([this](const NetworkResult &res) {
if (!this->cbError.has_value())
{
sol::state_view lua(thread);
auto privid = shared->pushPrivate(thread);
sol::table priv(thread, privid);
auto cb = priv["error"];
if (cb.get_type() == sol::type::function)
{
cb(HTTPResponse(res));
}
priv.pop();
return;
}
lua_closethread(thread, nullptr);
lua_pop(L, 1); // remove thread from L
lua::StackGuard guard(this->state_);
sol::state_view mainState(this->state_);
sol::thread thread = sol::thread::create(mainState);
sol::state_view threadstate = thread.state();
sol::protected_function cb(threadstate, *this->cbError);
cb(HTTPResponse(res));
this->cbError = std::nullopt;
})
.finally([shared, L]() {
lua::StackGuard guard(L);
auto *thread = lua_newthread(L);
.finally([this]() {
if (!this->cbFinally.has_value())
{
sol::state_view lua(thread);
auto privid = shared->pushPrivate(thread);
sol::table priv(thread, privid);
auto cb = priv["finally"];
if (cb.get_type() == sol::type::function)
{
cb();
}
priv.pop();
// remove our private data
lua.registry()[shared->privateKey.toStdString()] = sol::nil;
return;
}
lua_closethread(thread, nullptr);
lua_pop(L, 1); // remove thread from L
// we removed our private table, forget the key for it
shared->privateKey = QString();
lua::StackGuard guard(this->state_);
sol::state_view mainState(this->state_);
sol::thread thread = sol::thread::create(mainState);
sol::state_view threadstate = thread.state();
sol::protected_function cb(threadstate, *this->cbFinally);
cb();
this->cbFinally = std::nullopt;
})
.timeout(this->timeout_)
.execute();
return 0;
}
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
NetworkRequest req)
NetworkRequest req, lua_State *state)
: req_(std::move(req))
, state_(state)
{
DebugCount::increase("lua::api::HTTPRequest");
}
@ -413,34 +162,5 @@ HTTPRequest::~HTTPRequest()
// but that's better than accessing a possibly invalid lua_State pointer.
}
StackIdx HTTPRequest::pushPrivate(lua_State *L)
{
if (this->privateKey.isEmpty())
{
this->privateKey = QString("HTTPRequestPrivate%1")
.arg(QRandomGenerator::system()->generate());
pushEmptyTable(L, 4);
lua_setfield(L, LUA_REGISTRYINDEX,
this->privateKey.toStdString().c_str());
}
lua_getfield(L, LUA_REGISTRYINDEX, this->privateKey.toStdString().c_str());
return lua_gettop(L);
}
// NOLINTEND(*vararg)
} // namespace chatterino::lua::api
namespace chatterino::lua {
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPRequest> request)
{
using namespace chatterino::lua::api;
SharedPtrUserData<UserData::Type::HTTPRequest, HTTPRequest>::create(
L, std::move(request));
luaL_getmetatable(L, "c2.HTTPRequest");
lua_setmetatable(L, -2);
return lua_gettop(L);
}
} // namespace chatterino::lua
#endif

View file

@ -4,6 +4,8 @@
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/PluginController.hpp"
# include <sol/forward.hpp>
# include <memory>
namespace chatterino::lua::api {
@ -16,14 +18,15 @@ namespace chatterino::lua::api {
/**
* @lua@class HTTPRequest
*/
class HTTPRequest : public std::enable_shared_from_this<HTTPRequest>
class HTTPRequest
{
// This type is private to prevent the accidental construction of HTTPRequest without a shared pointer
struct ConstructorAccessTag {
};
public:
HTTPRequest(HTTPRequest::ConstructorAccessTag, NetworkRequest req);
HTTPRequest(HTTPRequest::ConstructorAccessTag, NetworkRequest req,
lua_State *state);
HTTPRequest(HTTPRequest &&other) = default;
HTTPRequest &operator=(HTTPRequest &&) = default;
HTTPRequest &operator=(HTTPRequest &) = delete;
@ -33,32 +36,19 @@ public:
private:
NetworkRequest req_;
static void createMetatable(lua_State *L);
static void createUserType(lua_State *L, sol::table &c2);
friend class chatterino::PluginController;
/**
* @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPRequest
*
* If the object given is not a userdatum or the pointer inside that
* userdatum doesn't point to a HTTPRequest, a lua error is thrown.
*
* This function always returns a non-null pointer.
*/
static std::shared_ptr<HTTPRequest> getOrError(lua_State *L,
StackIdx where = -1);
/**
* Pushes the private table onto the lua stack.
*
* This might create it if it doesn't exist.
*/
StackIdx pushPrivate(lua_State *L);
// This is the key in the registry the private table it held at (if it exists)
// This might be a null QString if the request has already been executed or
// the table wasn't created yet.
QString privateKey;
int timeout_ = 10'000;
bool done = false;
lua_State *state_;
std::optional<sol::protected_function> cbSuccess;
std::optional<sol::protected_function> cbError;
std::optional<sol::protected_function> cbFinally;
public:
// These functions are wrapped so data can be accessed more easily. When a call from Lua comes in:
@ -72,8 +62,7 @@ public:
* @lua@param callback HTTPCallback Function to call when the HTTP request succeeds
* @exposed HTTPRequest:on_success
*/
static int on_success_wrap(lua_State *L);
int on_success(lua_State *L);
void on_success(sol::protected_function func);
/**
* Sets the failure callback
@ -81,8 +70,7 @@ public:
* @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
* @exposed HTTPRequest:on_error
*/
static int on_error_wrap(lua_State *L);
int on_error(lua_State *L);
void on_error(sol::protected_function func);
/**
* Sets the finally callback
@ -90,8 +78,7 @@ public:
* @lua@param callback fun(): nil Function to call when the HTTP request finishes
* @exposed HTTPRequest:finally
*/
static int finally_wrap(lua_State *L);
int finally(lua_State *L);
void finally(sol::protected_function func);
/**
* Sets the timeout
@ -99,8 +86,7 @@ public:
* @lua@param timeout integer How long in milliseconds until the times out
* @exposed HTTPRequest:set_timeout
*/
static int set_timeout_wrap(lua_State *L);
int set_timeout(lua_State *L);
void set_timeout(int timeout);
/**
* Sets the request payload
@ -108,8 +94,7 @@ public:
* @lua@param data string
* @exposed HTTPRequest:set_payload
*/
static int set_payload_wrap(lua_State *L);
int set_payload(lua_State *L);
void set_payload(const std::string &payload);
/**
* Sets a header in the request
@ -118,16 +103,14 @@ public:
* @lua@param value string
* @exposed HTTPRequest:set_header
*/
static int set_header_wrap(lua_State *L);
int set_header(lua_State *L);
void set_header(std::string name, std::string value);
/**
* Executes the HTTP request
*
* @exposed HTTPRequest:execute
*/
static int execute_wrap(lua_State *L);
int execute(lua_State *L);
void execute();
/**
* Static functions
@ -142,7 +125,8 @@ public:
* @lua@return HTTPRequest
* @exposed HTTPRequest.create
*/
static int create(lua_State *L);
static HTTPRequest create(lua_State *L, NetworkRequestType method,
QString url);
};
// NOLINTEND(readability-identifier-naming)