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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1166 additions and 11 deletions

View file

@ -14,6 +14,7 @@
- Minor: Moderators can now see when users are warned. (#5441)
- Minor: Added support for Brave & google-chrome-stable browsers. (#5452)
- Minor: Added drop indicator line while dragging in tables. (#5256)
- Minor: Introduce HTTP API for plugins. (#5383)
- Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426)
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
- Bugfix: Fixed restricted users usernames not being clickable. (#5405)

32
docs/chatterino.d.ts vendored
View file

@ -32,6 +32,8 @@ declare module c2 {
is_valid(): boolean;
}
interface ISharedResource {}
class RoomModes {
unique_chat: boolean;
subscriber_only: boolean;
@ -69,6 +71,36 @@ declare module c2 {
static by_twitch_id(id: string): null | Channel;
}
enum HTTPMethod {
Get,
Post,
Put,
Delete,
Patch,
}
class HTTPResponse implements ISharedResource {
data(): string;
status(): number | null;
error(): string;
}
type HTTPCallback = (res: HTTPResponse) => void;
class HTTPRequest implements ISharedResource {
on_success(callback: HTTPCallback): void;
on_error(callback: HTTPCallback): void;
finally(callback: () => void): void;
set_timeout(millis: number): void;
set_payload(data: string): void;
set_header(name: string, value: string): void;
execute(): void;
// might error
static create(method: HTTPMethod, url: string): HTTPRequest;
}
function log(level: LogLevel, ...data: any[]): void;
function register_command(
name: String,

View file

@ -5,7 +5,6 @@
-- Add the folder this file is in to "Lua.workspace.library".
c2 = {}
---@alias c2.LogLevel integer
---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel }
c2.LogLevel = {}
@ -159,6 +158,89 @@ function c2.Channel.by_twitch_id(id) end
-- End src/controllers/plugins/api/ChannelRef.hpp
-- Begin src/controllers/plugins/api/HTTPRequest.hpp
---@class HTTPResponse
---@field data string Data received from the server
---@field status integer? HTTP Status code returned by the server
---@field error string A somewhat human readable description of an error if such happened
---@alias HTTPCallback fun(result: HTTPResponse): nil
---@class HTTPRequest
HTTPRequest = {}
--- Sets the success callback
---
---@param callback HTTPCallback Function to call when the HTTP request succeeds
function HTTPRequest:on_success(callback) end
--- Sets the failure callback
---
---@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status
function HTTPRequest:on_error(callback) end
--- Sets the finally callback
---
---@param callback fun(): nil Function to call when the HTTP request finishes
function HTTPRequest:finally(callback) end
--- Sets the timeout
---
---@param timeout integer How long in milliseconds until the times out
function HTTPRequest:set_timeout(timeout) end
--- Sets the request payload
---
---@param data string
function HTTPRequest:set_payload(data) end
--- Sets a header in the request
---
---@param name string
---@param value string
function HTTPRequest:set_header(name, value) end
--- Executes the HTTP request
---
function HTTPRequest:execute() end
--- Creates a new HTTPRequest
---
---@param method HTTPMethod Method to use
---@param url string Where to send the request to
---@return HTTPRequest
function HTTPRequest.create(method, url) end
-- End src/controllers/plugins/api/HTTPRequest.hpp
-- Begin src/controllers/plugins/api/HTTPResponse.hpp
---@class HTTPResponse
HTTPResponse = {}
--- Returns the data. This is not guaranteed to be encoded using any
--- particular encoding scheme. It's just the bytes the server returned.
---
function HTTPResponse:data() end
--- Returns the status code.
---
function HTTPResponse:status() end
--- A somewhat human readable description of an error if such happened
---
function HTTPResponse:error() end
-- End src/controllers/plugins/api/HTTPResponse.hpp
-- Begin src/common/network/NetworkCommon.hpp
---@alias HTTPMethod integer
---@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod }
HTTPMethod = {}
-- End src/common/network/NetworkCommon.hpp
--- Registers a new command called `name` which when executed will call `handler`.
---
---@param name string The name of the command.

View file

@ -85,6 +85,24 @@ Example:
}
```
### Network
Allows the plugin to send HTTP requests.
Example:
```json
{
...,
"permissions": [
{
"type": "Network"
},
...
]
}
```
## Plugins with Typescript
If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io)
@ -370,6 +388,138 @@ Returns `true` if the channel can be moderated by the current user.
Returns `true` if the current user is a VIP in the channel.
#### `HTTPMethod` enum
This table describes HTTP methods available to Lua Plugins. The values behind
the names may change, do not count on them. It has the following keys:
- `Get`
- `Post`
- `Put`
- `Delete`
- `Patch`
#### `HTTPResponse`
An `HTTPResponse` is a table you receive in the callback after a completed `HTTPRequest`.
##### `HTTPResponse.data()`
This function returns the data received from the server as a string. Usually
this will be UTF-8-encoded however that is not guaranteed, this could be any
binary data.
##### `HTTPResponse.error()`
If an error happened this function returns a human readable description of it.
It returns something like: `"ConnectionRefusedError"`, `"401"`.
##### `HTTPResponse.status()`
This function returns the HTTP status code of the request or `nil` if there was
an error before a status code could be received.
```lua
{
data = "This is the data received from the server as a string",
status = 200, -- HTTP status code returned by the server or nil if no response was received because of an error
error = "A somewhat human readable description of an error if such happened"
}
```
#### `HTTPRequest`
Allows you to send an HTTP request to a URL. Do not create requests that you
don't want to call `execute()` on. For the time being that leaks callback
functions and all their upvalues with them.
##### `HTTPRequest.create(method, url)`
Creates a new `HTTPRequest`. The `method` argument is an
[`HTTPMethod`](#HTTPMethod-enum). The `url` argument must be a string
containing a valid URL (ex. `https://example.com/path/to/api`).
```lua
local req = c2.HTTPRequest.create(c2.HTTPMethod.Get, "https://example.com")
req:on_success(function (res)
print(res.data)
end)
req:execute()
```
##### `HTTPRequest:on_success(callback)`
Sets the success callback. It accepts a function that takes a single parameter
of type `HTTPResponse`. The callback will be called on success. This function
returns nothing.
##### `HTTPRequest:on_error(callback)`
Sets the error callback. It accepts a function that takes a single parameter of
type `HTTPResponse`. The callback will be called if the request fails. To see why
it failed check the `error` field of the result. This function returns nothing.
##### `HTTPRequest:finally(callback)`
Sets the finally callback. It accepts a function that takes no parameters and
returns nothing. It will be always called after `success` or `error`. This
function returns nothing.
##### `HTTPRequest:set_timeout(timeout)`
Sets how long the request will take before it times out. The `timeout`
parameter is in milliseconds. This function returns nothing.
##### `HTTPRequest:set_payload(data)`
Sets the data that will be sent with the request. The `data` parameter must be
a string. This function returns nothing.
##### `HTTPRequest:set_header(name, value)`
Adds or overwrites a header in the request. Both `name` and `value` should be
strings. If they are not strings they will be converted to strings. This
function returns nothing.
##### `HTTPRequest:execute()`
Sends the request. This function returns nothing.
```lua
local url = "http://localhost:8080/thing"
local request = c2.HTTPRequest.create("Post", url)
request:set_timeout(1000)
request:set_payload("TEST!")
request:set_header("X-Test", "Testing!")
request:set_header("Content-Type", "text/plain")
request:on_success(function (res)
print('Success!')
-- Data is in res.data
print(res.status)
end)
request:on_error(function (res)
print('Error!')
print(res.status)
print(res.error)
end)
request:finally(function ()
print('Finally')
end)
request:execute()
-- This prints:
-- Success!
-- [content of /thing]
-- 200
-- Finally
-- Or:
-- Error!
-- nil
-- ConnectionRefusedError
```
### Input/Output API
These functions are wrappers for Lua's I/O library. Functions on file pointer

View file

@ -41,7 +41,6 @@ BOILERPLATE = """
-- Add the folder this file is in to "Lua.workspace.library".
c2 = {}
"""
repo_root = Path(__file__).parent.parent

View file

@ -227,6 +227,10 @@ set(SOURCE_FILES
controllers/plugins/api/ChannelRef.hpp
controllers/plugins/api/IOWrapper.cpp
controllers/plugins/api/IOWrapper.hpp
controllers/plugins/api/HTTPRequest.cpp
controllers/plugins/api/HTTPRequest.hpp
controllers/plugins/api/HTTPResponse.cpp
controllers/plugins/api/HTTPResponse.hpp
controllers/plugins/LuaAPI.cpp
controllers/plugins/LuaAPI.hpp
controllers/plugins/PluginPermission.cpp

View file

@ -15,6 +15,9 @@ using NetworkSuccessCallback = std::function<void(NetworkResult)>;
using NetworkErrorCallback = std::function<void(NetworkResult)>;
using NetworkFinallyCallback = std::function<void()>;
/**
* @exposeenum HTTPMethod
*/
enum class NetworkRequestType {
Get,
Post,

View file

@ -98,6 +98,13 @@ NetworkRequest NetworkRequest::header(QNetworkRequest::KnownHeaders header,
return std::move(*this);
}
NetworkRequest NetworkRequest::header(const QByteArray &headerName,
const QByteArray &value) &&
{
this->data->request.setRawHeader(headerName, value);
return std::move(*this);
}
NetworkRequest NetworkRequest::headerList(
const std::vector<std::pair<QByteArray, QByteArray>> &headers) &&
{

View file

@ -57,6 +57,8 @@ public:
NetworkRequest header(const char *headerName, const char *value) &&;
NetworkRequest header(const char *headerName, const QByteArray &value) &&;
NetworkRequest header(const char *headerName, const QString &value) &&;
NetworkRequest header(const QByteArray &headerName,
const QByteArray &value) &&;
NetworkRequest header(QNetworkRequest::KnownHeaders header,
const QVariant &value) &&;
NetworkRequest headerList(

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;
};

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

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_;

View file

@ -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

View file

@ -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";

View file

@ -14,6 +14,7 @@ struct PluginPermission {
enum class Type {
FilesystemRead,
FilesystemWrite,
Network,
};
Type type;
std::vector<QString> errors;

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

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

View file

@ -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

View file

@ -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