Introduce HTTP API for plugins (#5383)

This commit is contained in:
Mm2PL
2024-06-22 12:04:30 +02:00
committed by GitHub
parent 7dc80bc599
commit c980162656
19 changed files with 1166 additions and 11 deletions
+8 -1
View File
@@ -82,6 +82,9 @@ struct CompletionEvent {
/**
* @includefile common/Channel.hpp
* @includefile controllers/plugins/api/ChannelRef.hpp
* @includefile controllers/plugins/api/HTTPRequest.hpp
* @includefile controllers/plugins/api/HTTPResponse.hpp
* @includefile common/network/NetworkCommon.hpp
*/
/**
@@ -133,7 +136,11 @@ int searcherRelative(lua_State *L);
// This is a fat pointer that allows us to type check values given to functions needing a userdata.
// Ensure ALL userdata given to Lua are a subclass of this! Otherwise we garbage as a pointer!
struct UserData {
enum class Type { Channel };
enum class Type {
Channel,
HTTPRequest,
HTTPResponse,
};
Type type;
bool isWeak;
};
+21 -8
View File
@@ -1,8 +1,10 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/Plugin.hpp"
# include "common/network/NetworkCommon.hpp"
# include "common/QLogging.hpp"
# include "controllers/commands/CommandController.hpp"
# include "controllers/plugins/PluginPermission.hpp"
# include "util/QMagicEnum.hpp"
extern "C" {
@@ -11,6 +13,8 @@ extern "C" {
# include <magic_enum/magic_enum.hpp>
# include <QJsonArray>
# include <QJsonObject>
# include <QLoggingCategory>
# include <QUrl>
# include <algorithm>
# include <unordered_map>
@@ -258,16 +262,25 @@ bool Plugin::hasFSPermissionFor(bool write, const QString &path)
using PType = PluginPermission::Type;
auto typ = write ? PType::FilesystemWrite : PType::FilesystemRead;
// XXX: Older compilers don't have support for std::ranges
// NOLINTNEXTLINE(readability-use-anyofallof)
for (const auto &p : this->meta.permissions)
return std::ranges::any_of(this->meta.permissions, [=](const auto &p) {
return p.type == typ;
});
}
bool Plugin::hasHTTPPermissionFor(const QUrl &url)
{
auto proto = url.scheme();
if (proto != "http" && proto != "https")
{
if (p.type == typ)
{
return true;
}
qCWarning(chatterinoLua).nospace()
<< "Plugin " << this->id << " (" << this->meta.name
<< ") is trying to use a non-http protocol";
return false;
}
return false;
return std::ranges::any_of(this->meta.permissions, [](const auto &p) {
return p.type == PluginPermission::Type::Network;
});
}
} // namespace chatterino
+3
View File
@@ -2,12 +2,14 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "Application.hpp"
# include "common/network/NetworkCommon.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/PluginPermission.hpp"
# include <QDir>
# include <QString>
# include <QUrl>
# include <semver/semver.hpp>
# include <unordered_map>
@@ -139,6 +141,7 @@ public:
void removeTimeout(QTimer *timer);
bool hasFSPermissionFor(bool write, const QString &path);
bool hasHTTPPermissionFor(const QUrl &url);
private:
QDir loadDirectory_;
@@ -3,10 +3,13 @@
# include "Application.hpp"
# include "common/Args.hpp"
# include "common/network/NetworkCommon.hpp"
# include "common/QLogging.hpp"
# include "controllers/commands/CommandContext.hpp"
# include "controllers/commands/CommandController.hpp"
# include "controllers/plugins/api/ChannelRef.hpp"
# include "controllers/plugins/api/HTTPRequest.hpp"
# include "controllers/plugins/api/HTTPResponse.hpp"
# include "controllers/plugins/api/IOWrapper.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
@@ -174,10 +177,19 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
lua::pushEnumTable<Channel::Type>(L);
lua_setfield(L, c2libIdx, "ChannelType");
lua::pushEnumTable<NetworkRequestType>(L);
lua_setfield(L, c2libIdx, "HTTPMethod");
// Initialize metatables for objects
lua::api::ChannelRef::createMetatable(L);
lua_setfield(L, c2libIdx, "Channel");
lua::api::HTTPRequest::createMetatable(L);
lua_setfield(L, c2libIdx, "HTTPRequest");
lua::api::HTTPResponse::createMetatable(L);
lua_setfield(L, c2libIdx, "HTTPResponse");
lua_setfield(L, gtable, "c2");
// ban functions
@@ -37,6 +37,8 @@ QString PluginPermission::toHtml() const
return "Read files in its data directory";
case PluginPermission::Type::FilesystemWrite:
return "Write to or create files in its data directory";
case PluginPermission::Type::Network:
return "Make requests over the internet to third party websites";
default:
assert(false && "invalid PluginPermission type in toHtml()");
return "shut up compiler, this never happens";
@@ -14,6 +14,7 @@ struct PluginPermission {
enum class Type {
FilesystemRead,
FilesystemWrite,
Network,
};
Type type;
std::vector<QString> errors;
+451
View File
@@ -0,0 +1,451 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/api/HTTPRequest.hpp"
# include "Application.hpp"
# include "common/network/NetworkCommon.hpp"
# 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"
extern "C" {
# include <lauxlib.h>
# include <lua.h>
}
# include <QRandomGenerator>
# include <QUrl>
# include <memory>
# 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)
{
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;
}
void HTTPRequest::createMetatable(lua_State *L)
{
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);
}
int HTTPRequest::on_success_wrap(lua_State *L)
{
lua::StackGuard guard(L, -2);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->on_success(L);
}
int HTTPRequest::on_success(lua_State *L)
{
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;
}
int HTTPRequest::on_error_wrap(lua_State *L)
{
lua::StackGuard guard(L, -2);
auto ptr = HTTPRequest::getOrError(L, 1);
return ptr->on_error(L);
}
int HTTPRequest::on_error(lua_State *L)
{
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;
}
int HTTPRequest::set_header_wrap(lua_State *L)
{
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)
{
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)");
}
auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L);
if (!pl->hasHTTPPermissionFor(parsedurl))
{
return luaL_error(
L, "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;
}
int HTTPRequest::execute_wrap(lua_State *L)
{
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);
auto priv = shared->pushPrivate(thread);
lua_getfield(thread, priv, "success");
auto cb = lua_gettop(thread);
if (lua_isfunction(thread, cb))
{
lua::push(thread, std::make_shared<HTTPResponse>(res));
// one arg, no return, no msgh
lua_pcall(thread, 1, 0, 0);
}
else
{
lua_pop(thread, 1); // remove callback
}
lua_closethread(thread, nullptr);
lua_pop(L, 1); // remove thread from L
})
.onError([shared, L](const NetworkResult &res) {
lua::StackGuard guard(L);
auto *thread = lua_newthread(L);
auto priv = shared->pushPrivate(thread);
lua_getfield(thread, priv, "error");
auto cb = lua_gettop(thread);
if (lua_isfunction(thread, cb))
{
lua::push(thread, std::make_shared<HTTPResponse>(res));
// one arg, no return, no msgh
lua_pcall(thread, 1, 0, 0);
}
else
{
lua_pop(thread, 1); // remove callback
}
lua_closethread(thread, nullptr);
lua_pop(L, 1); // remove thread from L
})
.finally([shared, L]() {
lua::StackGuard guard(L);
auto *thread = lua_newthread(L);
auto priv = shared->pushPrivate(thread);
lua_getfield(thread, priv, "finally");
auto cb = lua_gettop(thread);
if (lua_isfunction(thread, cb))
{
// no args, no return, no msgh
lua_pcall(thread, 0, 0, 0);
}
else
{
lua_pop(thread, 1); // remove callback
}
// remove our private data
lua_pushnil(thread);
lua_setfield(thread, LUA_REGISTRYINDEX,
shared->privateKey.toStdString().c_str());
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();
})
.timeout(this->timeout_)
.execute();
return 0;
}
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
NetworkRequest req)
: req_(std::move(req))
{
DebugCount::increase("lua::api::HTTPRequest");
}
HTTPRequest::~HTTPRequest()
{
DebugCount::decrease("lua::api::HTTPRequest");
// We might leak a Lua function or two here if the request isn't executed
// 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
+162
View File
@@ -0,0 +1,162 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include "common/network/NetworkRequest.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/PluginController.hpp"
# include <memory>
namespace chatterino::lua::api {
// NOLINTBEGIN(readability-identifier-naming)
/**
* @lua@class HTTPResponse
* @lua@field data string Data received from the server
* @lua@field status integer? HTTP Status code returned by the server
* @lua@field error string A somewhat human readable description of an error if such happened
*/
/**
* @lua@alias HTTPCallback fun(result: HTTPResponse): nil
*/
/**
* @lua@class HTTPRequest
*/
class HTTPRequest : public std::enable_shared_from_this<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 &&other) = default;
HTTPRequest &operator=(HTTPRequest &&) = default;
HTTPRequest &operator=(HTTPRequest &) = delete;
HTTPRequest(const HTTPRequest &other) = delete;
~HTTPRequest();
private:
NetworkRequest req_;
static void createMetatable(lua_State *L);
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;
public:
// These functions are wrapped so data can be accessed more easily. When a call from Lua comes in:
// - the static wrapper function is called
// - it calls getOrError
// - and then the wrapped method
/**
* Sets the success callback
*
* @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);
/**
* Sets the failure callback
*
* @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);
/**
* Sets the finally callback
*
* @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);
/**
* Sets the timeout
*
* @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);
/**
* Sets the request payload
*
* @lua@param data string
* @exposed HTTPRequest:set_payload
*/
static int set_payload_wrap(lua_State *L);
int set_payload(lua_State *L);
/**
* Sets a header in the request
*
* @lua@param name string
* @lua@param value string
* @exposed HTTPRequest:set_header
*/
static int set_header_wrap(lua_State *L);
int set_header(lua_State *L);
/**
* Executes the HTTP request
*
* @exposed HTTPRequest:execute
*/
static int execute_wrap(lua_State *L);
int execute(lua_State *L);
/**
* Static functions
*/
/**
* Creates a new HTTPRequest
*
* @lua@param method HTTPMethod Method to use
* @lua@param url string Where to send the request to
*
* @lua@return HTTPRequest
* @exposed HTTPRequest.create
*/
static int create(lua_State *L);
};
// NOLINTEND(readability-identifier-naming)
} // namespace chatterino::lua::api
namespace chatterino::lua {
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPRequest> request);
} // namespace chatterino::lua
#endif
@@ -0,0 +1,144 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/api/HTTPResponse.hpp"
# include "common/network/NetworkResult.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "util/DebugCount.hpp"
extern "C" {
# include <lauxlib.h>
}
# include <utility>
namespace chatterino::lua::api {
// NOLINTBEGIN(*vararg)
// NOLINTNEXTLINE(*-avoid-c-arrays)
static const luaL_Reg HTTP_RESPONSE_METHODS[] = {
{"data", &HTTPResponse::data_wrap},
{"status", &HTTPResponse::status_wrap},
{"error", &HTTPResponse::error_wrap},
{nullptr, nullptr},
};
void HTTPResponse::createMetatable(lua_State *L)
{
lua::StackGuard guard(L, 1);
luaL_newmetatable(L, "c2.HTTPResponse");
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::HTTPResponse,
HTTPResponse>::destroy));
lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy
luaL_setfuncs(L, HTTP_RESPONSE_METHODS, 0);
}
std::shared_ptr<HTTPResponse> HTTPResponse::getOrError(lua_State *L,
StackIdx where)
{
if (lua_gettop(L) < 1)
{
// The nullptr is there just to appease the compiler, luaL_error is no return
luaL_error(L, "Called c2.HTTPResponse method without a request object");
return nullptr;
}
if (lua_isuserdata(L, where) == 0)
{
luaL_error(L, "Called c2.HTTPResponse method with a non-userdata "
"'self' argument");
return nullptr;
}
// luaL_checkudata is no-return if check fails
auto *checked = luaL_checkudata(L, where, "c2.HTTPResponse");
auto *data =
SharedPtrUserData<UserData::Type::HTTPResponse, HTTPResponse>::from(
checked);
if (data == nullptr)
{
luaL_error(L, "Called c2.HTTPResponse method with an invalid pointer");
return nullptr;
}
lua_remove(L, where);
if (data->target == nullptr)
{
luaL_error(
L,
"Internal error: SharedPtrUserData<UserData::Type::HTTPResponse, "
"HTTPResponse>::target was null. This is a Chatterino bug!");
return nullptr;
}
return data->target;
}
HTTPResponse::HTTPResponse(NetworkResult res)
: result_(std::move(res))
{
DebugCount::increase("lua::api::HTTPResponse");
}
HTTPResponse::~HTTPResponse()
{
DebugCount::decrease("lua::api::HTTPResponse");
}
int HTTPResponse::data_wrap(lua_State *L)
{
lua::StackGuard guard(L, 0); // 1 in, 1 out
auto ptr = HTTPResponse::getOrError(L, 1);
return ptr->data(L);
}
int HTTPResponse::data(lua_State *L)
{
lua::push(L, this->result_.getData().toStdString());
return 1;
}
int HTTPResponse::status_wrap(lua_State *L)
{
lua::StackGuard guard(L, 0); // 1 in, 1 out
auto ptr = HTTPResponse::getOrError(L, 1);
return ptr->status(L);
}
int HTTPResponse::status(lua_State *L)
{
lua::push(L, this->result_.status());
return 1;
}
int HTTPResponse::error_wrap(lua_State *L)
{
lua::StackGuard guard(L, 0); // 1 in, 1 out
auto ptr = HTTPResponse::getOrError(L, 1);
return ptr->error(L);
}
int HTTPResponse::error(lua_State *L)
{
lua::push(L, this->result_.formatError());
return 1;
}
// NOLINTEND(*vararg)
} // namespace chatterino::lua::api
namespace chatterino::lua {
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPResponse> request)
{
using namespace chatterino::lua::api;
// Prepare table
SharedPtrUserData<UserData::Type::HTTPResponse, HTTPResponse>::create(
L, std::move(request));
luaL_getmetatable(L, "c2.HTTPResponse");
lua_setmetatable(L, -2);
return lua_gettop(L);
}
} // namespace chatterino::lua
#endif
@@ -0,0 +1,80 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include "common/network/NetworkResult.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include <memory>
extern "C" {
# include <lua.h>
}
namespace chatterino {
class PluginController;
} // namespace chatterino
namespace chatterino::lua::api {
// NOLINTBEGIN(readability-identifier-naming)
/**
* @lua@class HTTPResponse
*/
class HTTPResponse : public std::enable_shared_from_this<HTTPResponse>
{
NetworkResult result_;
public:
HTTPResponse(NetworkResult res);
HTTPResponse(HTTPResponse &&other) = default;
HTTPResponse &operator=(HTTPResponse &&) = default;
HTTPResponse &operator=(HTTPResponse &) = delete;
HTTPResponse(const HTTPResponse &other) = delete;
~HTTPResponse();
private:
static void createMetatable(lua_State *L);
friend class chatterino::PluginController;
/**
* @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPResponse
*
* If the object given is not a userdatum or the pointer inside that
* userdatum doesn't point to a HTTPResponse, a lua error is thrown.
*
* This function always returns a non-null pointer.
*/
static std::shared_ptr<HTTPResponse> getOrError(lua_State *L,
StackIdx where = -1);
public:
/**
* Returns the data. This is not guaranteed to be encoded using any
* particular encoding scheme. It's just the bytes the server returned.
*
* @exposed HTTPResponse:data
*/
static int data_wrap(lua_State *L);
int data(lua_State *L);
/**
* Returns the status code.
*
* @exposed HTTPResponse:status
*/
static int status_wrap(lua_State *L);
int status(lua_State *L);
/**
* A somewhat human readable description of an error if such happened
* @exposed HTTPResponse:error
*/
static int error_wrap(lua_State *L);
int error(lua_State *L);
};
// NOLINTEND(readability-identifier-naming)
} // namespace chatterino::lua::api
namespace chatterino::lua {
StackIdx push(lua_State *L, std::shared_ptr<api::HTTPResponse> request);
} // namespace chatterino::lua
#endif