Move plugins to Sol (#5622)
Co-authored-by: Nerixyz <nerixdev@outlook.de> Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -1,397 +1,224 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/Channel.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "providers/twitch/TwitchChannel.hpp"
|
||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
}
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
# include <cassert>
|
||||
# include <memory>
|
||||
# include <optional>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(*vararg)
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg CHANNEL_REF_METHODS[] = {
|
||||
{"is_valid", &ChannelRef::is_valid},
|
||||
{"get_name", &ChannelRef::get_name},
|
||||
{"get_type", &ChannelRef::get_type},
|
||||
{"get_display_name", &ChannelRef::get_display_name},
|
||||
{"send_message", &ChannelRef::send_message},
|
||||
{"add_system_message", &ChannelRef::add_system_message},
|
||||
{"is_twitch_channel", &ChannelRef::is_twitch_channel},
|
||||
|
||||
// Twitch
|
||||
{"get_room_modes", &ChannelRef::get_room_modes},
|
||||
{"get_stream_status", &ChannelRef::get_stream_status},
|
||||
{"get_twitch_id", &ChannelRef::get_twitch_id},
|
||||
{"is_broadcaster", &ChannelRef::is_broadcaster},
|
||||
{"is_mod", &ChannelRef::is_mod},
|
||||
{"is_vip", &ChannelRef::is_vip},
|
||||
|
||||
// misc
|
||||
{"__tostring", &ChannelRef::to_string},
|
||||
|
||||
// static
|
||||
{"by_name", &ChannelRef::get_by_name},
|
||||
{"by_twitch_id", &ChannelRef::get_by_twitch_id},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
void ChannelRef::createMetatable(lua_State *L)
|
||||
ChannelRef::ChannelRef(const std::shared_ptr<Channel> &chan)
|
||||
: weak(chan)
|
||||
{
|
||||
lua::StackGuard guard(L, 1);
|
||||
|
||||
luaL_newmetatable(L, "c2.Channel");
|
||||
lua_pushstring(L, "__index");
|
||||
lua_pushvalue(L, -2); // clone metatable
|
||||
lua_settable(L, -3); // metatable.__index = metatable
|
||||
|
||||
// Generic IWeakResource stuff
|
||||
lua_pushstring(L, "__gc");
|
||||
lua_pushcfunction(
|
||||
L, (&WeakPtrUserData<UserData::Type::Channel, ChannelRef>::destroy));
|
||||
lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy
|
||||
|
||||
luaL_setfuncs(L, CHANNEL_REF_METHODS, 0);
|
||||
}
|
||||
|
||||
ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
||||
std::shared_ptr<Channel> ChannelRef::strong()
|
||||
{
|
||||
if (lua_gettop(L) < 1)
|
||||
auto c = this->weak.lock();
|
||||
if (!c)
|
||||
{
|
||||
luaL_error(L, "Called c2.Channel method without a channel object");
|
||||
return nullptr;
|
||||
throw std::runtime_error(
|
||||
"Expired c2.Channel used - use c2.Channel:is_valid() to "
|
||||
"check validity");
|
||||
}
|
||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchChannel> ChannelRef::twitch()
|
||||
{
|
||||
auto c = std::dynamic_pointer_cast<TwitchChannel>(this->weak.lock());
|
||||
if (!c)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Called c2.Channel method with a non-userdata 'self' argument");
|
||||
return nullptr;
|
||||
throw std::runtime_error(
|
||||
"Expired or non-twitch c2.Channel used - use "
|
||||
"c2.Channel:is_valid() and c2.Channe:is_twitch_channel()");
|
||||
}
|
||||
// luaL_checkudata is no-return if check fails
|
||||
auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel");
|
||||
auto *data =
|
||||
WeakPtrUserData<UserData::Type::Channel, Channel>::from(checked);
|
||||
if (data == nullptr)
|
||||
{
|
||||
luaL_error(L,
|
||||
"Called c2.Channel method with an invalid channel pointer");
|
||||
return nullptr;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (data->target.expired())
|
||||
{
|
||||
if (!expiredOk)
|
||||
return c;
|
||||
}
|
||||
|
||||
bool ChannelRef::is_valid()
|
||||
{
|
||||
return !this->weak.expired();
|
||||
}
|
||||
|
||||
QString ChannelRef::get_name()
|
||||
{
|
||||
return this->strong()->getName();
|
||||
}
|
||||
|
||||
Channel::Type ChannelRef::get_type()
|
||||
{
|
||||
return this->strong()->getType();
|
||||
}
|
||||
|
||||
QString ChannelRef::get_display_name()
|
||||
{
|
||||
return this->strong()->getDisplayName();
|
||||
}
|
||||
|
||||
void ChannelRef::send_message(QString text, sol::variadic_args va)
|
||||
{
|
||||
bool execCommands = [&] {
|
||||
if (va.size() >= 1)
|
||||
{
|
||||
luaL_error(L,
|
||||
"Usage of expired c2.Channel object. Underlying "
|
||||
"resource was freed. Use Channel:is_valid() to check");
|
||||
return va.get<bool>();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return data->target.lock();
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchChannel> ChannelRef::getTwitchOrError(lua_State *L)
|
||||
{
|
||||
auto ref = ChannelRef::getOrError(L);
|
||||
auto ptr = dynamic_pointer_cast<TwitchChannel>(ref);
|
||||
if (ptr == nullptr)
|
||||
return false;
|
||||
}();
|
||||
text = text.replace('\n', ' ');
|
||||
auto chan = this->strong();
|
||||
if (execCommands)
|
||||
{
|
||||
luaL_error(L,
|
||||
"c2.Channel Twitch-only operation on non-Twitch channel.");
|
||||
text = getApp()->getCommands()->execCommand(text, chan, false);
|
||||
}
|
||||
return ptr;
|
||||
chan->sendMessage(text);
|
||||
}
|
||||
|
||||
int ChannelRef::is_valid(lua_State *L)
|
||||
void ChannelRef::add_system_message(QString text)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
||||
lua::push(L, that != nullptr);
|
||||
return 1;
|
||||
text = text.replace('\n', ' ');
|
||||
this->strong()->addSystemMessage(text);
|
||||
}
|
||||
|
||||
int ChannelRef::get_name(lua_State *L)
|
||||
bool ChannelRef::is_twitch_channel()
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getName());
|
||||
return 1;
|
||||
return this->strong()->isTwitchChannel();
|
||||
}
|
||||
|
||||
int ChannelRef::get_type(lua_State *L)
|
||||
sol::table ChannelRef::get_room_modes(sol::this_state state)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getType());
|
||||
return 1;
|
||||
return toTable(state.L, *this->twitch()->accessRoomModes());
|
||||
}
|
||||
|
||||
int ChannelRef::get_display_name(lua_State *L)
|
||||
sol::table ChannelRef::get_stream_status(sol::this_state state)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getDisplayName());
|
||||
return 1;
|
||||
return toTable(state.L, *this->twitch()->accessStreamStatus());
|
||||
}
|
||||
|
||||
int ChannelRef::send_message(lua_State *L)
|
||||
QString ChannelRef::get_twitch_id()
|
||||
{
|
||||
if (lua_gettop(L) != 2 && lua_gettop(L) != 3)
|
||||
return this->twitch()->roomId();
|
||||
}
|
||||
|
||||
bool ChannelRef::is_broadcaster()
|
||||
{
|
||||
return this->twitch()->isBroadcaster();
|
||||
}
|
||||
|
||||
bool ChannelRef::is_mod()
|
||||
{
|
||||
return this->twitch()->isMod();
|
||||
}
|
||||
|
||||
bool ChannelRef::is_vip()
|
||||
{
|
||||
return this->twitch()->isVip();
|
||||
}
|
||||
|
||||
QString ChannelRef::to_string()
|
||||
{
|
||||
auto chan = this->weak.lock();
|
||||
if (!chan)
|
||||
{
|
||||
luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message "
|
||||
"text and optionally execute_commands flag)");
|
||||
return 0;
|
||||
return "<c2.Channel expired>";
|
||||
}
|
||||
bool execcmds = false;
|
||||
if (lua_gettop(L) == 3)
|
||||
return QStringView(u"<c2.Channel %1>").arg(chan->getName());
|
||||
}
|
||||
|
||||
std::optional<ChannelRef> ChannelRef::get_by_name(const QString &name)
|
||||
{
|
||||
auto chan = getApp()->getTwitch()->getChannelOrEmpty(name);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
if (!lua::pop(L, &execcmds))
|
||||
return std::nullopt;
|
||||
}
|
||||
return chan;
|
||||
}
|
||||
|
||||
std::optional<ChannelRef> ChannelRef::get_by_twitch_id(const QString &id)
|
||||
{
|
||||
auto chan = getApp()->getTwitch()->getChannelOrEmptyByID(id);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return chan;
|
||||
}
|
||||
|
||||
void ChannelRef::createUserType(sol::table &c2)
|
||||
{
|
||||
// clang-format off
|
||||
c2.new_usertype<ChannelRef>(
|
||||
"Channel", sol::no_constructor,
|
||||
// meta methods
|
||||
sol::meta_method::to_string, &ChannelRef::to_string,
|
||||
|
||||
// Channel
|
||||
"is_valid", &ChannelRef::is_valid,
|
||||
"get_name",&ChannelRef::get_name,
|
||||
"get_type", &ChannelRef::get_type,
|
||||
"get_display_name", &ChannelRef::get_display_name,
|
||||
"send_message", &ChannelRef::send_message,
|
||||
"add_system_message", &ChannelRef::add_system_message,
|
||||
"is_twitch_channel", &ChannelRef::is_twitch_channel,
|
||||
|
||||
// TwitchChannel
|
||||
"get_room_modes", &ChannelRef::get_room_modes,
|
||||
"get_stream_status", &ChannelRef::get_stream_status,
|
||||
"get_twitch_id", &ChannelRef::get_twitch_id,
|
||||
"is_broadcaster", &ChannelRef::is_broadcaster,
|
||||
"is_mod", &ChannelRef::is_mod,
|
||||
"is_vip", &ChannelRef::is_vip,
|
||||
|
||||
// static
|
||||
"by_name", &ChannelRef::get_by_name,
|
||||
"by_twitch_id", &ChannelRef::get_by_twitch_id
|
||||
);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes)
|
||||
{
|
||||
auto maybe = [](int value) {
|
||||
if (value >= 0)
|
||||
{
|
||||
luaL_error(L, "cannot get execute_commands (2nd argument of "
|
||||
"Channel:send_message)");
|
||||
return 0;
|
||||
return std::optional{value};
|
||||
}
|
||||
}
|
||||
|
||||
QString text;
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(L, "cannot get text (1st argument of Channel:send_message)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
|
||||
text = text.replace('\n', ' ');
|
||||
if (execcmds)
|
||||
{
|
||||
text = getApp()->getCommands()->execCommand(text, that, false);
|
||||
}
|
||||
that->sendMessage(text);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ChannelRef::add_system_message(lua_State *L)
|
||||
{
|
||||
// needs to account for the hidden self argument
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Channel:add_system_message needs exactly 1 argument (message "
|
||||
"text)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString text;
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L, "cannot get text (1st argument of Channel:add_system_message)");
|
||||
return 0;
|
||||
}
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
text = text.replace('\n', ' ');
|
||||
that->addSystemMessage(text);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ChannelRef::is_twitch_channel(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->isTwitchChannel());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_room_modes(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
const auto m = tc->accessRoomModes();
|
||||
const auto modes = LuaRoomModes{
|
||||
.unique_chat = m->r9k,
|
||||
.subscriber_only = m->submode,
|
||||
.emotes_only = m->emoteOnly,
|
||||
.follower_only = (m->followerOnly == -1)
|
||||
? std::nullopt
|
||||
: std::optional(m->followerOnly),
|
||||
.slow_mode =
|
||||
(m->slowMode == 0) ? std::nullopt : std::optional(m->slowMode),
|
||||
|
||||
return std::optional<int>{};
|
||||
};
|
||||
lua::push(L, modes);
|
||||
return 1;
|
||||
// clang-format off
|
||||
return sol::table::create_with(L,
|
||||
"subscriber_only", modes.submode,
|
||||
"unique_chat", modes.r9k,
|
||||
"emotes_only", modes.emoteOnly,
|
||||
"follower_only", maybe(modes.followerOnly),
|
||||
"slow_mode", maybe(modes.slowMode)
|
||||
);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
int ChannelRef::get_stream_status(lua_State *L)
|
||||
sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
const auto s = tc->accessStreamStatus();
|
||||
const auto status = LuaStreamStatus{
|
||||
.live = s->live,
|
||||
.viewer_count = static_cast<int>(s->viewerCount),
|
||||
.uptime = s->uptimeSeconds,
|
||||
.title = s->title,
|
||||
.game_name = s->game,
|
||||
.game_id = s->gameId,
|
||||
};
|
||||
lua::push(L, status);
|
||||
return 1;
|
||||
// clang-format off
|
||||
return sol::table::create_with(L,
|
||||
"live", status.live,
|
||||
"viewer_count", status.viewerCount,
|
||||
"title", status.title,
|
||||
"game_name", status.game,
|
||||
"game_id", status.gameId,
|
||||
"uptime", status.uptimeSeconds
|
||||
);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
int ChannelRef::get_twitch_id(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->roomId());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::is_broadcaster(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->isBroadcaster());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::is_mod(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->isMod());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::is_vip(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->isVip());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_by_name(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L, "Channel.by_name needs exactly 2 arguments (channel "
|
||||
"name and platform)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
LPlatform platform{};
|
||||
if (!lua::pop(L, &platform))
|
||||
{
|
||||
luaL_error(L, "cannot get platform (2nd argument of Channel.by_name, "
|
||||
"expected a string)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
QString name;
|
||||
if (!lua::pop(L, &name))
|
||||
{
|
||||
luaL_error(L,
|
||||
"cannot get channel name (1st argument of Channel.by_name, "
|
||||
"expected a string)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
auto chn = getApp()->getTwitch()->getChannelOrEmpty(name);
|
||||
lua::push(L, chn);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_by_twitch_id(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 1)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Channel.by_twitch_id needs exactly 1 arguments (channel owner "
|
||||
"id)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
QString id;
|
||||
if (!lua::pop(L, &id))
|
||||
{
|
||||
luaL_error(L,
|
||||
"cannot get channel name (1st argument of Channel.by_name, "
|
||||
"expected a string)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
auto chn = getApp()->getTwitch()->getChannelOrEmptyByID(id);
|
||||
|
||||
lua::push(L, chn);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::to_string(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
||||
if (that == nullptr)
|
||||
{
|
||||
lua_pushstring(L, "<c2.Channel expired>");
|
||||
return 1;
|
||||
}
|
||||
QString formated = QString("<c2.Channel %1>").arg(that->getName());
|
||||
lua::push(L, formated);
|
||||
return 1;
|
||||
}
|
||||
} // namespace chatterino::lua::api
|
||||
// NOLINTEND(*vararg)
|
||||
//
|
||||
namespace chatterino::lua {
|
||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes)
|
||||
{
|
||||
auto out = lua::pushEmptyTable(L, 6);
|
||||
# define PUSH(field) \
|
||||
lua::push(L, modes.field); \
|
||||
lua_setfield(L, out, #field)
|
||||
PUSH(unique_chat);
|
||||
PUSH(subscriber_only);
|
||||
PUSH(emotes_only);
|
||||
PUSH(follower_only);
|
||||
PUSH(slow_mode);
|
||||
# undef PUSH
|
||||
return out;
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status)
|
||||
{
|
||||
auto out = lua::pushEmptyTable(L, 6);
|
||||
# define PUSH(field) \
|
||||
lua::push(L, status.field); \
|
||||
lua_setfield(L, out, #field)
|
||||
PUSH(live);
|
||||
PUSH(viewer_count);
|
||||
PUSH(uptime);
|
||||
PUSH(title);
|
||||
PUSH(game_name);
|
||||
PUSH(game_id);
|
||||
# undef PUSH
|
||||
return out;
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, ChannelPtr chn)
|
||||
{
|
||||
using namespace chatterino::lua::api;
|
||||
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
lua_pushnil(L);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
WeakPtrUserData<UserData::Type::Channel, Channel>::create(
|
||||
L, chn->weak_from_this());
|
||||
luaL_getmetatable(L, "c2.Channel");
|
||||
lua_setmetatable(L, -2);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua
|
||||
#endif
|
||||
|
||||
@@ -1,48 +1,24 @@
|
||||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/Channel.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
# include <optional>
|
||||
# include <sol/forward.hpp>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
|
||||
/**
|
||||
* This enum describes a platform for the purpose of searching for a channel.
|
||||
* Currently only Twitch is supported because identifying IRC channels is tricky.
|
||||
* @exposeenum c2.Platform
|
||||
* @includefile providers/twitch/TwitchChannel.hpp
|
||||
*/
|
||||
enum class LPlatform {
|
||||
Twitch,
|
||||
//IRC,
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class c2.Channel
|
||||
*/
|
||||
struct ChannelRef {
|
||||
static void createMetatable(lua_State *L);
|
||||
friend class chatterino::PluginController;
|
||||
|
||||
/**
|
||||
* @brief Get the content of the top object on Lua stack, usually first argument to function as a ChannelPtr.
|
||||
* If the object given is not a userdatum or the pointer inside that
|
||||
* userdatum doesn't point to a Channel, a lua error is thrown.
|
||||
*
|
||||
* @param expiredOk Should an expired return nullptr instead of erroring
|
||||
*/
|
||||
static ChannelPtr getOrError(lua_State *L, bool expiredOk = false);
|
||||
|
||||
/**
|
||||
* @brief Casts the result of getOrError to std::shared_ptr<TwitchChannel>
|
||||
* if that fails thows a lua error.
|
||||
*/
|
||||
static std::shared_ptr<TwitchChannel> getTwitchOrError(lua_State *L);
|
||||
|
||||
public:
|
||||
ChannelRef(const std::shared_ptr<Channel> &chan);
|
||||
|
||||
/**
|
||||
* Returns true if the channel this object points to is valid.
|
||||
* If the object expired, returns false
|
||||
@@ -51,7 +27,7 @@ public:
|
||||
* @lua@return boolean success
|
||||
* @exposed c2.Channel:is_valid
|
||||
*/
|
||||
static int is_valid(lua_State *L);
|
||||
bool is_valid();
|
||||
|
||||
/**
|
||||
* Gets the channel's name. This is the lowercase login name.
|
||||
@@ -59,7 +35,7 @@ public:
|
||||
* @lua@return string name
|
||||
* @exposed c2.Channel:get_name
|
||||
*/
|
||||
static int get_name(lua_State *L);
|
||||
QString get_name();
|
||||
|
||||
/**
|
||||
* Gets the channel's type
|
||||
@@ -67,7 +43,7 @@ public:
|
||||
* @lua@return c2.ChannelType
|
||||
* @exposed c2.Channel:get_type
|
||||
*/
|
||||
static int get_type(lua_State *L);
|
||||
Channel::Type get_type();
|
||||
|
||||
/**
|
||||
* Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
||||
@@ -75,17 +51,17 @@ public:
|
||||
* @lua@return string name
|
||||
* @exposed c2.Channel:get_display_name
|
||||
*/
|
||||
static int get_display_name(lua_State *L);
|
||||
QString get_display_name();
|
||||
|
||||
/**
|
||||
* Sends a message to the target channel.
|
||||
* Note that this does not execute client-commands.
|
||||
*
|
||||
* @lua@param message string
|
||||
* @lua@param execute_commands boolean Should commands be run on the text?
|
||||
* @lua@param execute_commands? boolean Should commands be run on the text?
|
||||
* @exposed c2.Channel:send_message
|
||||
*/
|
||||
static int send_message(lua_State *L);
|
||||
void send_message(QString text, sol::variadic_args va);
|
||||
|
||||
/**
|
||||
* Adds a system message client-side
|
||||
@@ -93,7 +69,7 @@ public:
|
||||
* @lua@param message string
|
||||
* @exposed c2.Channel:add_system_message
|
||||
*/
|
||||
static int add_system_message(lua_State *L);
|
||||
void add_system_message(QString text);
|
||||
|
||||
/**
|
||||
* Returns true for twitch channels.
|
||||
@@ -103,7 +79,7 @@ public:
|
||||
* @lua@return boolean
|
||||
* @exposed c2.Channel:is_twitch_channel
|
||||
*/
|
||||
static int is_twitch_channel(lua_State *L);
|
||||
bool is_twitch_channel();
|
||||
|
||||
/**
|
||||
* Twitch Channel specific functions
|
||||
@@ -115,7 +91,7 @@ public:
|
||||
* @lua@return RoomModes
|
||||
* @exposed c2.Channel:get_room_modes
|
||||
*/
|
||||
static int get_room_modes(lua_State *L);
|
||||
sol::table get_room_modes(sol::this_state state);
|
||||
|
||||
/**
|
||||
* Returns a copy of the stream status.
|
||||
@@ -123,7 +99,7 @@ public:
|
||||
* @lua@return StreamStatus
|
||||
* @exposed c2.Channel:get_stream_status
|
||||
*/
|
||||
static int get_stream_status(lua_State *L);
|
||||
sol::table get_stream_status(sol::this_state state);
|
||||
|
||||
/**
|
||||
* Returns the Twitch user ID of the owner of the channel.
|
||||
@@ -131,7 +107,7 @@ public:
|
||||
* @lua@return string
|
||||
* @exposed c2.Channel:get_twitch_id
|
||||
*/
|
||||
static int get_twitch_id(lua_State *L);
|
||||
QString get_twitch_id();
|
||||
|
||||
/**
|
||||
* Returns true if the channel is a Twitch channel and the user owns it
|
||||
@@ -139,7 +115,7 @@ public:
|
||||
* @lua@return boolean
|
||||
* @exposed c2.Channel:is_broadcaster
|
||||
*/
|
||||
static int is_broadcaster(lua_State *L);
|
||||
bool is_broadcaster();
|
||||
|
||||
/**
|
||||
* Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
||||
@@ -148,7 +124,7 @@ public:
|
||||
* @lua@return boolean
|
||||
* @exposed c2.Channel:is_mod
|
||||
*/
|
||||
static int is_mod(lua_State *L);
|
||||
bool is_mod();
|
||||
|
||||
/**
|
||||
* Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
||||
@@ -157,7 +133,7 @@ public:
|
||||
* @lua@return boolean
|
||||
* @exposed c2.Channel:is_vip
|
||||
*/
|
||||
static int is_vip(lua_State *L);
|
||||
bool is_vip();
|
||||
|
||||
/**
|
||||
* Misc
|
||||
@@ -167,7 +143,7 @@ public:
|
||||
* @lua@return string
|
||||
* @exposed c2.Channel:__tostring
|
||||
*/
|
||||
static int to_string(lua_State *L);
|
||||
QString to_string();
|
||||
|
||||
/**
|
||||
* Static functions
|
||||
@@ -184,11 +160,10 @@ public:
|
||||
* - /automod
|
||||
*
|
||||
* @lua@param name string Which channel are you looking for?
|
||||
* @lua@param platform c2.Platform Where to search for the channel?
|
||||
* @lua@return c2.Channel?
|
||||
* @exposed c2.Channel.by_name
|
||||
*/
|
||||
static int get_by_name(lua_State *L);
|
||||
static std::optional<ChannelRef> get_by_name(const QString &name);
|
||||
|
||||
/**
|
||||
* Finds a channel by the Twitch user ID of its owner.
|
||||
@@ -197,79 +172,24 @@ public:
|
||||
* @lua@return c2.Channel?
|
||||
* @exposed c2.Channel.by_twitch_id
|
||||
*/
|
||||
static int get_by_twitch_id(lua_State *L);
|
||||
};
|
||||
static std::optional<ChannelRef> get_by_twitch_id(const QString &id);
|
||||
|
||||
// This is a copy of the TwitchChannel::RoomModes structure, except it uses nicer optionals
|
||||
/**
|
||||
* @lua@class RoomModes
|
||||
*/
|
||||
struct LuaRoomModes {
|
||||
/**
|
||||
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||
*/
|
||||
bool unique_chat = false;
|
||||
static void createUserType(sol::table &c2);
|
||||
|
||||
/**
|
||||
* @lua@field subscriber_only boolean
|
||||
*/
|
||||
bool subscriber_only = false;
|
||||
private:
|
||||
std::weak_ptr<Channel> weak;
|
||||
|
||||
/**
|
||||
* @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
|
||||
*/
|
||||
bool emotes_only = false;
|
||||
/// Locks the weak pointer and throws if the pointer expired
|
||||
std::shared_ptr<Channel> strong();
|
||||
|
||||
/**
|
||||
* @lua@field follower_only number? Time in minutes you need to follow to chat or nil.
|
||||
*/
|
||||
std::optional<int> follower_only;
|
||||
/**
|
||||
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||
*/
|
||||
std::optional<int> slow_mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class StreamStatus
|
||||
*/
|
||||
struct LuaStreamStatus {
|
||||
/**
|
||||
* @lua@field live boolean
|
||||
*/
|
||||
bool live = false;
|
||||
|
||||
/**
|
||||
* @lua@field viewer_count number
|
||||
*/
|
||||
int viewer_count = 0;
|
||||
|
||||
/**
|
||||
* @lua@field uptime number Seconds since the stream started.
|
||||
*/
|
||||
int uptime = 0;
|
||||
|
||||
/**
|
||||
* @lua@field title string Stream title or last stream title
|
||||
*/
|
||||
QString title;
|
||||
|
||||
/**
|
||||
* @lua@field game_name string
|
||||
*/
|
||||
QString game_name;
|
||||
|
||||
/**
|
||||
* @lua@field game_id string
|
||||
*/
|
||||
QString game_id;
|
||||
/// Locks the weak pointer and throws if the pointer is invalid
|
||||
std::shared_ptr<TwitchChannel> twitch();
|
||||
};
|
||||
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
sol::table toTable(lua_State *L, const TwitchChannel::RoomModes &modes);
|
||||
sol::table toTable(lua_State *L, const TwitchChannel::StreamStatus &status);
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
namespace chatterino::lua {
|
||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes);
|
||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status);
|
||||
StackIdx push(lua_State *L, ChannelPtr chn);
|
||||
} // namespace chatterino::lua
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
|
||||
/**
|
||||
* @exposeenum c2.EventType
|
||||
*/
|
||||
enum class EventType {
|
||||
CompletionRequested,
|
||||
};
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
#endif
|
||||
@@ -6,402 +6,173 @@
|
||||
# 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 "controllers/plugins/PluginController.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "util/DebugCount.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
}
|
||||
# include <QChar>
|
||||
# include <QLoggingCategory>
|
||||
# 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>
|
||||
# include <vector>
|
||||
|
||||
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(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, //
|
||||
sol::meta_method::to_string, &HTTPRequest::to_string, //
|
||||
|
||||
"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", &HTTPRequest::create //
|
||||
);
|
||||
}
|
||||
|
||||
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 = std::make_optional(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 = std::make_optional(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 = std::make_optional(func);
|
||||
}
|
||||
|
||||
int HTTPRequest::on_error(lua_State *L)
|
||||
void HTTPRequest::set_payload(QByteArray 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;
|
||||
this->req_ = std::move(this->req_).payload(payload);
|
||||
}
|
||||
|
||||
int HTTPRequest::set_timeout_wrap(lua_State *L)
|
||||
// name and value may be random bytes
|
||||
void HTTPRequest::set_header(QByteArray name, QByteArray value)
|
||||
{
|
||||
lua::StackGuard guard(L, -2);
|
||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
||||
return ptr->set_timeout(L);
|
||||
this->req_ = std::move(this->req_).header(name, value);
|
||||
}
|
||||
|
||||
int HTTPRequest::set_timeout(lua_State *L)
|
||||
std::shared_ptr<HTTPRequest> HTTPRequest::create(sol::this_state L,
|
||||
NetworkRequestType method,
|
||||
QString url)
|
||||
{
|
||||
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)");
|
||||
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 std::make_shared<HTTPRequest>(ConstructorAccessTag{}, std::move(r));
|
||||
}
|
||||
|
||||
int HTTPRequest::execute_wrap(lua_State *L)
|
||||
void HTTPRequest::execute(sol::this_state L)
|
||||
{
|
||||
auto ptr = HTTPRequest::getOrError(L, 1);
|
||||
return ptr->execute(L);
|
||||
}
|
||||
|
||||
int HTTPRequest::execute(lua_State *L)
|
||||
{
|
||||
auto shared = this->shared_from_this();
|
||||
if (this->done)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
"Cannot execute this c2.HTTPRequest, it was executed already!");
|
||||
}
|
||||
this->done = true;
|
||||
|
||||
// this keeps the object alive even if Lua were to forget about it,
|
||||
auto hack = this->weak_from_this();
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
pl->httpRequests.push_back(this->shared_from_this());
|
||||
|
||||
std::move(this->req_)
|
||||
.onSuccess([shared, L](const NetworkResult &res) {
|
||||
.onSuccess([L, hack](const NetworkResult &res) {
|
||||
auto self = hack.lock();
|
||||
if (!self)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!self->cbSuccess.has_value())
|
||||
{
|
||||
return;
|
||||
}
|
||||
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
|
||||
(*self->cbSuccess)(HTTPResponse(res));
|
||||
self->cbSuccess = std::nullopt;
|
||||
})
|
||||
.onError([shared, L](const NetworkResult &res) {
|
||||
.onError([L, hack](const NetworkResult &res) {
|
||||
auto self = hack.lock();
|
||||
if (!self)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!self->cbError.has_value())
|
||||
{
|
||||
return;
|
||||
}
|
||||
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
|
||||
(*self->cbError)(HTTPResponse(res));
|
||||
self->cbError = std::nullopt;
|
||||
})
|
||||
.finally([shared, L]() {
|
||||
.finally([L, hack]() {
|
||||
auto self = hack.lock();
|
||||
if (!self)
|
||||
{
|
||||
// this could happen if the plugin was deleted
|
||||
return;
|
||||
}
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
for (auto it = pl->httpRequests.begin();
|
||||
it < pl->httpRequests.end(); it++)
|
||||
{
|
||||
if (*it == self)
|
||||
{
|
||||
pl->httpRequests.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!self->cbFinally.has_value())
|
||||
{
|
||||
return;
|
||||
}
|
||||
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();
|
||||
(*self->cbFinally)();
|
||||
self->cbFinally = std::nullopt;
|
||||
})
|
||||
.timeout(this->timeout_)
|
||||
.execute();
|
||||
return 0;
|
||||
}
|
||||
|
||||
HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/,
|
||||
@@ -418,34 +189,10 @@ HTTPRequest::~HTTPRequest()
|
||||
// but that's better than accessing a possibly invalid lua_State pointer.
|
||||
}
|
||||
|
||||
StackIdx HTTPRequest::pushPrivate(lua_State *L)
|
||||
QString HTTPRequest::to_string()
|
||||
{
|
||||
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);
|
||||
return "<HTTPRequest>";
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/network/NetworkRequest.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
# include <sol/forward.hpp>
|
||||
# include <sol/types.hpp>
|
||||
|
||||
# include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
class PluginController;
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
|
||||
@@ -33,33 +39,19 @@ public:
|
||||
private:
|
||||
NetworkRequest req_;
|
||||
|
||||
static void createMetatable(lua_State *L);
|
||||
static void createUserType(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;
|
||||
|
||||
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:
|
||||
// - the static wrapper function is called
|
||||
@@ -72,8 +64,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 +72,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 +80,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 +88,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 +96,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(QByteArray payload);
|
||||
|
||||
/**
|
||||
* Sets a header in the request
|
||||
@@ -118,16 +105,19 @@ 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(QByteArray name, QByteArray value);
|
||||
|
||||
/**
|
||||
* Executes the HTTP request
|
||||
*
|
||||
* @exposed HTTPRequest:execute
|
||||
*/
|
||||
static int execute_wrap(lua_State *L);
|
||||
int execute(lua_State *L);
|
||||
void execute(sol::this_state L);
|
||||
/**
|
||||
* @lua@return string
|
||||
* @exposed HTTPRequest:__tostring
|
||||
*/
|
||||
QString to_string();
|
||||
|
||||
/**
|
||||
* Static functions
|
||||
@@ -142,7 +132,9 @@ public:
|
||||
* @lua@return HTTPRequest
|
||||
* @exposed HTTPRequest.create
|
||||
*/
|
||||
static int create(lua_State *L);
|
||||
static std::shared_ptr<HTTPRequest> create(sol::this_state L,
|
||||
NetworkRequestType method,
|
||||
QString url);
|
||||
};
|
||||
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
@@ -2,77 +2,28 @@
|
||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||
|
||||
# include "common/network/NetworkResult.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "util/DebugCount.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
}
|
||||
# include <sol/raii.hpp>
|
||||
# include <sol/types.hpp>
|
||||
|
||||
# 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)
|
||||
void HTTPResponse::createUserType(sol::table &c2)
|
||||
{
|
||||
lua::StackGuard guard(L, 1);
|
||||
c2.new_usertype<HTTPResponse>( //
|
||||
"HTTPResponse", sol::no_constructor,
|
||||
// metamethods
|
||||
sol::meta_method::to_string, &HTTPResponse::to_string, //
|
||||
|
||||
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;
|
||||
"data", &HTTPResponse::data, //
|
||||
"status", &HTTPResponse::status, //
|
||||
"error", &HTTPResponse::error //
|
||||
);
|
||||
}
|
||||
|
||||
HTTPResponse::HTTPResponse(NetworkResult res)
|
||||
@@ -85,60 +36,30 @@ HTTPResponse::~HTTPResponse()
|
||||
DebugCount::decrease("lua::api::HTTPResponse");
|
||||
}
|
||||
|
||||
int HTTPResponse::data_wrap(lua_State *L)
|
||||
QByteArray HTTPResponse::data()
|
||||
{
|
||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
||||
return ptr->data(L);
|
||||
return this->result_.getData();
|
||||
}
|
||||
|
||||
int HTTPResponse::data(lua_State *L)
|
||||
std::optional<int> HTTPResponse::status()
|
||||
{
|
||||
lua::push(L, this->result_.getData().toStdString());
|
||||
return 1;
|
||||
return this->result_.status();
|
||||
}
|
||||
|
||||
int HTTPResponse::status_wrap(lua_State *L)
|
||||
QString HTTPResponse::error()
|
||||
{
|
||||
lua::StackGuard guard(L, 0); // 1 in, 1 out
|
||||
auto ptr = HTTPResponse::getOrError(L, 1);
|
||||
return ptr->status(L);
|
||||
return this->result_.formatError();
|
||||
}
|
||||
|
||||
int HTTPResponse::status(lua_State *L)
|
||||
QString HTTPResponse::to_string()
|
||||
{
|
||||
lua::push(L, this->result_.status());
|
||||
return 1;
|
||||
if (this->status().has_value())
|
||||
{
|
||||
return QStringView(u"<c2.HTTPResponse status %1>")
|
||||
.arg(QString::number(*this->status()));
|
||||
}
|
||||
return "<c2.HTTPResponse no status>";
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/network/NetworkResult.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include <lua.h>
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
# include <memory>
|
||||
extern "C" {
|
||||
# include <lua.h>
|
||||
}
|
||||
|
||||
namespace chatterino {
|
||||
class PluginController;
|
||||
@@ -18,7 +17,7 @@ namespace chatterino::lua::api {
|
||||
/**
|
||||
* @lua@class HTTPResponse
|
||||
*/
|
||||
class HTTPResponse : public std::enable_shared_from_this<HTTPResponse>
|
||||
class HTTPResponse
|
||||
{
|
||||
NetworkResult result_;
|
||||
|
||||
@@ -31,20 +30,9 @@ public:
|
||||
~HTTPResponse();
|
||||
|
||||
private:
|
||||
static void createMetatable(lua_State *L);
|
||||
static void createUserType(sol::table &c2);
|
||||
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
|
||||
@@ -52,29 +40,28 @@ public:
|
||||
*
|
||||
* @exposed HTTPResponse:data
|
||||
*/
|
||||
static int data_wrap(lua_State *L);
|
||||
int data(lua_State *L);
|
||||
QByteArray data();
|
||||
|
||||
/**
|
||||
* Returns the status code.
|
||||
*
|
||||
* @exposed HTTPResponse:status
|
||||
*/
|
||||
static int status_wrap(lua_State *L);
|
||||
int status(lua_State *L);
|
||||
std::optional<int> status();
|
||||
|
||||
/**
|
||||
* A somewhat human readable description of an error if such happened
|
||||
* @exposed HTTPResponse:error
|
||||
*/
|
||||
QString error();
|
||||
|
||||
static int error_wrap(lua_State *L);
|
||||
int error(lua_State *L);
|
||||
/**
|
||||
* @lua@return string
|
||||
* @exposed HTTPResponse:__tostring
|
||||
*/
|
||||
QString to_string();
|
||||
};
|
||||
|
||||
// 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
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
}
|
||||
# include <QString>
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
# include <cerrno>
|
||||
# include <stdexcept>
|
||||
# include <utility>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
|
||||
@@ -91,45 +93,28 @@ struct LuaFileMode {
|
||||
}
|
||||
};
|
||||
|
||||
int ioError(lua_State *L, const QString &value, int errnoequiv)
|
||||
sol::variadic_results ioError(lua_State *L, const QString &value,
|
||||
int errnoequiv)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
lua::push(L, value);
|
||||
lua::push(L, errnoequiv);
|
||||
return 3;
|
||||
sol::variadic_results out;
|
||||
out.push_back(sol::nil);
|
||||
out.push_back(sol::make_object(L, value.toStdString()));
|
||||
out.push_back({L, sol::in_place_type<int>, errnoequiv});
|
||||
return out;
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(*vararg)
|
||||
int io_open(lua_State *L)
|
||||
sol::variadic_results io_open(sol::this_state L, QString filename,
|
||||
QString strmode)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
LuaFileMode mode;
|
||||
if (lua_gettop(L) == 2)
|
||||
LuaFileMode mode(strmode);
|
||||
if (!mode.error.isEmpty())
|
||||
{
|
||||
// we have a mode
|
||||
QString smode;
|
||||
if (!lua::pop(L, &smode))
|
||||
{
|
||||
return luaL_error(
|
||||
L,
|
||||
"io.open mode (2nd argument) must be a string or not present");
|
||||
}
|
||||
mode = LuaFileMode(smode);
|
||||
if (!mode.error.isEmpty())
|
||||
{
|
||||
return luaL_error(L, mode.error.toStdString().c_str());
|
||||
}
|
||||
}
|
||||
QString filename;
|
||||
if (!lua::pop(L, &filename))
|
||||
{
|
||||
return luaL_error(L,
|
||||
"io.open filename (1st argument) must be a string");
|
||||
throw std::runtime_error(mode.error.toStdString());
|
||||
}
|
||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||
auto abs = file.absoluteFilePath();
|
||||
@@ -144,39 +129,35 @@ int io_open(lua_State *L)
|
||||
"Plugin does not have permissions to access given file.",
|
||||
EACCES);
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "open");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua::push(L, abs);
|
||||
lua::push(L, mode.toString());
|
||||
lua_call(L, 2, 3);
|
||||
return 3;
|
||||
|
||||
sol::state_view lua(L);
|
||||
auto open = lua.registry()[REG_REAL_IO_NAME]["open"];
|
||||
sol::protected_function_result res =
|
||||
open(abs.toStdString(), mode.toString().toStdString());
|
||||
return res;
|
||||
}
|
||||
sol::variadic_results io_open_modeless(sol::this_state L, QString filename)
|
||||
{
|
||||
return io_open(L, std::move(filename), "r");
|
||||
}
|
||||
|
||||
int io_lines(lua_State *L)
|
||||
sol::variadic_results io_lines_noargs(sol::this_state L)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"];
|
||||
sol::protected_function_result res = lines();
|
||||
return res;
|
||||
}
|
||||
|
||||
sol::variadic_results io_lines(sol::this_state L, QString filename,
|
||||
sol::variadic_args args)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
if (lua_gettop(L) == 0)
|
||||
{
|
||||
// io.lines() case, just call realio.lines
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "lines");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_call(L, 0, 1);
|
||||
return 1;
|
||||
}
|
||||
QString filename;
|
||||
if (!lua::pop(L, &filename))
|
||||
{
|
||||
return luaL_error(
|
||||
L,
|
||||
"io.lines filename (1st argument) must be a string or not present");
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||
auto abs = file.absoluteFilePath();
|
||||
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||
@@ -185,191 +166,168 @@ int io_lines(lua_State *L)
|
||||
bool ok = pl->hasFSPermissionFor(false, abs);
|
||||
if (!ok)
|
||||
{
|
||||
return ioError(L,
|
||||
"Plugin does not have permissions to access given file.",
|
||||
EACCES);
|
||||
throw std::runtime_error(
|
||||
"Plugin does not have permissions to access given file.");
|
||||
}
|
||||
// Our stack looks like this:
|
||||
// - {...}[1]
|
||||
// - {...}[2]
|
||||
// ...
|
||||
// We want:
|
||||
// - REG[REG_REAL_IO_NAME].lines
|
||||
// - absolute file path
|
||||
// - {...}[1]
|
||||
// - {...}[2]
|
||||
// ...
|
||||
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "lines");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_insert(L, 1); // move function to start of stack
|
||||
lua::push(L, abs);
|
||||
lua_insert(L, 2); // move file name just after the function
|
||||
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
|
||||
return lua_gettop(L);
|
||||
auto lines = lua.registry()[REG_REAL_IO_NAME]["lines"];
|
||||
sol::protected_function_result res = lines(abs.toStdString(), args);
|
||||
return res;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// This is the code for both io.input and io.output
|
||||
int globalFileCommon(lua_State *L, bool output)
|
||||
sol::variadic_results io_input_argless(sol::this_state L)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
// Three signature cases:
|
||||
// io.input()
|
||||
// io.input(file)
|
||||
// io.input(name)
|
||||
if (lua_gettop(L) == 0)
|
||||
{
|
||||
// We have no arguments, call realio.input()
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
if (output)
|
||||
{
|
||||
lua_getfield(L, -1, "output");
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_getfield(L, -1, "input");
|
||||
}
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_call(L, 0, 1);
|
||||
return 1;
|
||||
}
|
||||
if (lua_gettop(L) != 1)
|
||||
{
|
||||
return luaL_error(L, "Too many arguments given to io.input().");
|
||||
}
|
||||
// Now check if we have a file or name
|
||||
auto *p = luaL_testudata(L, 1, LUA_FILEHANDLE);
|
||||
if (p == nullptr)
|
||||
{
|
||||
// this is not a file handle, send it to open
|
||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, REG_C2_IO_NAME);
|
||||
lua_getfield(L, -1, "open");
|
||||
lua_remove(L, -2); // remove io
|
||||
|
||||
lua_pushvalue(L, 1); // dupe arg
|
||||
if (output)
|
||||
{
|
||||
lua_pushstring(L, "w");
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_pushstring(L, "r");
|
||||
}
|
||||
lua_call(L, 2, 1); // call ourio.open(arg1, 'r'|'w')
|
||||
// if this isn't a string ourio.open errors
|
||||
|
||||
// this leaves us with:
|
||||
// 1. arg
|
||||
// 2. new_file
|
||||
lua_remove(L, 1); // remove arg, replacing it with new_file
|
||||
}
|
||||
|
||||
// file handle, pass it off to realio.input
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
if (output)
|
||||
{
|
||||
lua_getfield(L, -1, "output");
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_getfield(L, -1, "input");
|
||||
}
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_pushvalue(L, 1); // duplicate arg
|
||||
lua_call(L, 1, 1);
|
||||
return 1;
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
} // namespace
|
||||
|
||||
int io_input(lua_State *L)
|
||||
{
|
||||
return globalFileCommon(L, false);
|
||||
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||
sol::protected_function_result res = func();
|
||||
return res;
|
||||
}
|
||||
|
||||
int io_output(lua_State *L)
|
||||
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file)
|
||||
{
|
||||
return globalFileCommon(L, true);
|
||||
}
|
||||
|
||||
int io_close(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.close. Expected one or zero.");
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
if (lua_gettop(L) == 0)
|
||||
sol::state_view lua(L);
|
||||
|
||||
auto func = lua.registry()[REG_REAL_IO_NAME]["input"];
|
||||
sol::protected_function_result res = func(file);
|
||||
return res;
|
||||
}
|
||||
sol::variadic_results io_input_name(sol::this_state L, QString filename)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
lua_getfield(L, -1, "close");
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int io_flush(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
sol::state_view lua(L);
|
||||
auto res = io_open(L, std::move(filename), "r");
|
||||
if (res.size() != 1)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.flush. Expected one or zero.");
|
||||
throw std::runtime_error(res.at(1).as<std::string>());
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
lua_getfield(L, -1, "flush");
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int io_read(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
auto obj = res.at(0);
|
||||
if (obj.get_type() != sol::type::userdata)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.read. Expected one or zero.");
|
||||
throw std::runtime_error("a file must be a userdata.");
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
||||
lua_getfield(L, -1, "read");
|
||||
lua_insert(L, 1);
|
||||
lua_insert(L, 2);
|
||||
lua_call(L, lua_gettop(L) - 1, 1);
|
||||
return 1;
|
||||
return io_input_file(L, obj);
|
||||
}
|
||||
|
||||
int io_write(lua_State *L)
|
||||
sol::variadic_results io_output_argless(sol::this_state L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
lua_getfield(L, -1, "write");
|
||||
lua_insert(L, 1);
|
||||
lua_insert(L, 2);
|
||||
// (input)
|
||||
// (input).read
|
||||
// args
|
||||
lua_call(L, lua_gettop(L) - 1, 1);
|
||||
return 1;
|
||||
}
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
int io_popen(lua_State *L)
|
||||
auto func = lua.registry()[REG_REAL_IO_NAME]["output"];
|
||||
sol::protected_function_result res = func();
|
||||
return res;
|
||||
}
|
||||
sol::variadic_results io_output_file(sol::this_state L, sol::userdata file)
|
||||
{
|
||||
return luaL_error(L, "io.popen: This function is a stub!");
|
||||
}
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
|
||||
int io_tmpfile(lua_State *L)
|
||||
auto func = lua.registry()[REG_REAL_IO_NAME]["output"];
|
||||
sol::protected_function_result res = func(file);
|
||||
return res;
|
||||
}
|
||||
sol::variadic_results io_output_name(sol::this_state L, QString filename)
|
||||
{
|
||||
return luaL_error(L, "io.tmpfile: This function is a stub!");
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
throw std::runtime_error("internal error: no plugin");
|
||||
}
|
||||
sol::state_view lua(L);
|
||||
auto res = io_open(L, std::move(filename), "w");
|
||||
if (res.size() != 1)
|
||||
{
|
||||
throw std::runtime_error(res.at(1).as<std::string>());
|
||||
}
|
||||
auto obj = res.at(0);
|
||||
if (obj.get_type() != sol::type::userdata)
|
||||
{
|
||||
throw std::runtime_error("internal error: a file must be a userdata.");
|
||||
}
|
||||
return io_output_file(L, obj);
|
||||
}
|
||||
|
||||
// NOLINTEND(*vararg)
|
||||
bool io_close_argless(sol::this_state L)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
auto out = lua.registry()["_IO_output"];
|
||||
return io_close_file(L, out);
|
||||
}
|
||||
|
||||
bool io_close_file(sol::this_state L, sol::userdata file)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
return file["close"](file);
|
||||
}
|
||||
|
||||
void io_flush_argless(sol::this_state L)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
auto out = lua.registry()["_IO_output"];
|
||||
io_flush_file(L, out);
|
||||
}
|
||||
|
||||
void io_flush_file(sol::this_state L, sol::userdata file)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
file["flush"](file);
|
||||
}
|
||||
|
||||
sol::variadic_results io_read(sol::this_state L, sol::variadic_args args)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
auto inp = lua.registry()["_IO_input"];
|
||||
if (!inp.is<sol::userdata>())
|
||||
{
|
||||
throw std::runtime_error("Input not set to a file");
|
||||
}
|
||||
sol::protected_function read = inp["read"];
|
||||
return read(inp, args);
|
||||
}
|
||||
|
||||
sol::variadic_results io_write(sol::this_state L, sol::variadic_args args)
|
||||
{
|
||||
sol::state_view lua(L);
|
||||
auto out = lua.registry()["_IO_output"];
|
||||
if (!out.is<sol::userdata>())
|
||||
{
|
||||
throw std::runtime_error("Output not set to a file");
|
||||
}
|
||||
sol::protected_function write = out["write"];
|
||||
return write(out, args);
|
||||
}
|
||||
|
||||
void io_popen()
|
||||
{
|
||||
throw std::runtime_error("io.popen: This function is a stub!");
|
||||
}
|
||||
|
||||
void io_tmpfile()
|
||||
{
|
||||
throw std::runtime_error("io.tmpfile: This function is a stub!");
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include <QString>
|
||||
# include <sol/types.hpp>
|
||||
# include <sol/variadic_args.hpp>
|
||||
# include <sol/variadic_results.hpp>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
@@ -8,7 +12,6 @@ namespace chatterino::lua::api {
|
||||
// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality.
|
||||
|
||||
const char *const REG_REAL_IO_NAME = "real_lua_io_lib";
|
||||
const char *const REG_C2_IO_NAME = "c2io";
|
||||
|
||||
/**
|
||||
* Opens a file.
|
||||
@@ -20,7 +23,9 @@ const char *const REG_C2_IO_NAME = "c2io";
|
||||
* @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+"
|
||||
* @exposed io.open
|
||||
*/
|
||||
int io_open(lua_State *L);
|
||||
sol::variadic_results io_open(sol::this_state L, QString filename,
|
||||
QString strmode);
|
||||
sol::variadic_results io_open_modeless(sol::this_state L, QString filename);
|
||||
|
||||
/**
|
||||
* Equivalent to io.input():lines("l") or a specific iterator over given file
|
||||
@@ -32,7 +37,9 @@ int io_open(lua_State *L);
|
||||
* @lua@param ...
|
||||
* @exposed io.lines
|
||||
*/
|
||||
int io_lines(lua_State *L);
|
||||
sol::variadic_results io_lines(sol::this_state L, QString filename,
|
||||
sol::variadic_args args);
|
||||
sol::variadic_results io_lines_noargs(sol::this_state L);
|
||||
|
||||
/**
|
||||
* Opens a file and sets it as default input or if given no arguments returns the default input.
|
||||
@@ -42,7 +49,9 @@ int io_lines(lua_State *L);
|
||||
* @lua@return nil|FILE*
|
||||
* @exposed io.input
|
||||
*/
|
||||
int io_input(lua_State *L);
|
||||
sol::variadic_results io_input_argless(sol::this_state L);
|
||||
sol::variadic_results io_input_file(sol::this_state L, sol::userdata file);
|
||||
sol::variadic_results io_input_name(sol::this_state L, QString filename);
|
||||
|
||||
/**
|
||||
* Opens a file and sets it as default output or if given no arguments returns the default output
|
||||
@@ -52,7 +61,9 @@ int io_input(lua_State *L);
|
||||
* @lua@return nil|FILE*
|
||||
* @exposed io.output
|
||||
*/
|
||||
int io_output(lua_State *L);
|
||||
sol::variadic_results io_output_argless(sol::this_state L);
|
||||
sol::variadic_results io_output_file(sol::this_state L, sol::userdata file);
|
||||
sol::variadic_results io_output_name(sol::this_state L, QString filename);
|
||||
|
||||
/**
|
||||
* Closes given file or io.output() if not given.
|
||||
@@ -61,7 +72,8 @@ int io_output(lua_State *L);
|
||||
* @lua@param nil|FILE*
|
||||
* @exposed io.close
|
||||
*/
|
||||
int io_close(lua_State *L);
|
||||
bool io_close_argless(sol::this_state L);
|
||||
bool io_close_file(sol::this_state L, sol::userdata file);
|
||||
|
||||
/**
|
||||
* Flushes the buffer for given file or io.output() if not given.
|
||||
@@ -70,7 +82,8 @@ int io_close(lua_State *L);
|
||||
* @lua@param nil|FILE*
|
||||
* @exposed io.flush
|
||||
*/
|
||||
int io_flush(lua_State *L);
|
||||
void io_flush_argless(sol::this_state L);
|
||||
void io_flush_file(sol::this_state L, sol::userdata file);
|
||||
|
||||
/**
|
||||
* Reads some data from the default input file
|
||||
@@ -79,7 +92,7 @@ int io_flush(lua_State *L);
|
||||
* @lua@param nil|string
|
||||
* @exposed io.read
|
||||
*/
|
||||
int io_read(lua_State *L);
|
||||
sol::variadic_results io_read(sol::this_state L, sol::variadic_args args);
|
||||
|
||||
/**
|
||||
* Writes some data to the default output file
|
||||
@@ -88,10 +101,10 @@ int io_read(lua_State *L);
|
||||
* @lua@param nil|string
|
||||
* @exposed io.write
|
||||
*/
|
||||
int io_write(lua_State *L);
|
||||
sol::variadic_results io_write(sol::this_state L, sol::variadic_args args);
|
||||
|
||||
int io_popen(lua_State *L);
|
||||
int io_tmpfile(lua_State *L);
|
||||
void io_popen();
|
||||
void io_tmpfile();
|
||||
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
} // namespace chatterino::lua::api
|
||||
|
||||
Reference in New Issue
Block a user