Add basic lua scripting capabilities (#4341)
The scripting capabilities is locked behind a cmake flag, and is not enabled by default. Co-authored-by: nerix <nerixdev@outlook.de> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <QFileInfo>
|
||||
# include <QLoggingCategory>
|
||||
# include <QTextCodec>
|
||||
|
||||
namespace {
|
||||
using namespace chatterino;
|
||||
|
||||
void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc)
|
||||
{
|
||||
stream.noquote();
|
||||
stream << "[" + pl->id + ":" + pl->meta.name + "]";
|
||||
for (int i = 1; i <= argc; i++)
|
||||
{
|
||||
stream << lua::toString(L, i);
|
||||
}
|
||||
lua_pop(L, argc);
|
||||
}
|
||||
|
||||
QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl)
|
||||
{
|
||||
auto base =
|
||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName()));
|
||||
|
||||
using LogLevel = lua::api::LogLevel;
|
||||
|
||||
switch (lvl)
|
||||
{
|
||||
case LogLevel::Debug:
|
||||
return base.debug();
|
||||
case LogLevel::Info:
|
||||
return base.info();
|
||||
case LogLevel::Warning:
|
||||
return base.warning();
|
||||
case LogLevel::Critical:
|
||||
return base.critical();
|
||||
default:
|
||||
assert(false && "if this happens magic_enum must have failed us");
|
||||
return {(QString *)nullptr};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// NOLINTBEGIN(*vararg)
|
||||
// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much
|
||||
namespace chatterino::lua::api {
|
||||
|
||||
int c2_register_command(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString name;
|
||||
if (!lua::peek(L, &name, 1))
|
||||
{
|
||||
luaL_error(L, "cannot get command name (1st arg of register_command, "
|
||||
"expected a string)");
|
||||
return 0;
|
||||
}
|
||||
if (lua_isnoneornil(L, 2))
|
||||
{
|
||||
luaL_error(L, "missing argument for register_command: function "
|
||||
"\"pointer\"");
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto callbackSavedName = QString("c2commandcb-%1").arg(name);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str());
|
||||
auto ok = pl->registerCommand(name, callbackSavedName);
|
||||
|
||||
// delete both name and callback
|
||||
lua_pop(L, 2);
|
||||
|
||||
lua::push(L, ok);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c2_send_msg(lua_State *L)
|
||||
{
|
||||
QString text;
|
||||
QString channel;
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L, "send_msg needs exactly 2 arguments (channel and text)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L, "cannot get text (2nd argument of send_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &channel))
|
||||
{
|
||||
luaL_error(
|
||||
L,
|
||||
"cannot get channel (1st argument of send_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin" << pl->id
|
||||
<< "tried to send a message (using send_msg) to channel" << channel
|
||||
<< "which is not known";
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
QString message = text;
|
||||
message = message.replace('\n', ' ');
|
||||
QString outText = getApp()->commands->execCommand(message, chn, false);
|
||||
chn->sendMessage(outText);
|
||||
lua::push(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c2_system_msg(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L,
|
||||
"system_msg needs exactly 2 arguments (channel and text)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
QString channel;
|
||||
QString text;
|
||||
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L,
|
||||
"cannot get text (2nd argument of system_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &channel))
|
||||
{
|
||||
luaL_error(L, "cannot get channel (1st argument of system_msg, "
|
||||
"expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin" << pl->id
|
||||
<< "tried to show a system message (using system_msg) in channel"
|
||||
<< channel << "which is not known";
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
chn->addMessage(makeSystemMessage(text));
|
||||
lua::push(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c2_log(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "c2_log: internal error: no plugin?");
|
||||
return 0;
|
||||
}
|
||||
auto logc = lua_gettop(L) - 1;
|
||||
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
||||
LogLevel lvl{};
|
||||
if (!lua::pop(L, &lvl, 1))
|
||||
{
|
||||
luaL_error(L, "Invalid log level, use one from c2.LogLevel.");
|
||||
return 0;
|
||||
}
|
||||
QDebug stream = qdebugStreamForLogLevel(lvl);
|
||||
logHelper(L, pl, stream, logc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int g_load(lua_State *L)
|
||||
{
|
||||
# ifdef NDEBUG
|
||||
luaL_error(L, "load() is only usable in debug mode");
|
||||
return 0;
|
||||
# else
|
||||
auto countArgs = lua_gettop(L);
|
||||
QByteArray data;
|
||||
if (lua::peek(L, &data, 1))
|
||||
{
|
||||
auto *utf8 = QTextCodec::codecForName("UTF-8");
|
||||
QTextCodec::ConverterState state;
|
||||
utf8->toUnicode(data.constData(), data.size(), &state);
|
||||
if (state.invalidChars != 0)
|
||||
{
|
||||
luaL_error(L, "invalid utf-8 in load() is not allowed");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
luaL_error(L, "using reader function in load() is not allowed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < countArgs; i++)
|
||||
{
|
||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
||||
}
|
||||
|
||||
// fetch load and call it
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||
|
||||
for (int i = 0; i < countArgs; i++)
|
||||
{
|
||||
lua_geti(L, LUA_REGISTRYINDEX, i);
|
||||
lua_pushnil(L);
|
||||
lua_seti(L, LUA_REGISTRYINDEX, i);
|
||||
}
|
||||
|
||||
lua_call(L, countArgs, LUA_MULTRET);
|
||||
|
||||
return lua_gettop(L);
|
||||
# endif
|
||||
}
|
||||
|
||||
int g_import(lua_State *L)
|
||||
{
|
||||
auto countArgs = lua_gettop(L);
|
||||
// Lua allows dofile() which loads from stdin, but this is very useless in our case
|
||||
if (countArgs == 0)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "it is not allowed to call import() without arguments");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
QString fname;
|
||||
if (!lua::pop(L, &fname))
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: expected a string for a filename");
|
||||
return 1;
|
||||
}
|
||||
auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/");
|
||||
auto file = dir.resolved(fname);
|
||||
|
||||
qCDebug(chatterinoLua) << "plugin" << pl->id << "is trying to load" << file
|
||||
<< "(its dir is" << dir << ")";
|
||||
if (!dir.isParentOf(file))
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: filename must be inside of the "
|
||||
"plugin directory");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto path = file.path(QUrl::FullyDecoded);
|
||||
QFile qf(path);
|
||||
qf.open(QIODevice::ReadOnly);
|
||||
if (qf.size() > 10'000'000)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "chatterino g_import: size limit of 10MB exceeded, what "
|
||||
"the hell are you doing");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// validate utf-8 to block bytecode exploits
|
||||
auto data = qf.readAll();
|
||||
auto *utf8 = QTextCodec::codecForName("UTF-8");
|
||||
QTextCodec::ConverterState state;
|
||||
utf8->toUnicode(data.constData(), data.size(), &state);
|
||||
if (state.invalidChars != 0)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
luaL_error(L, "invalid utf-8 in import() target (%s) is not allowed",
|
||||
fname.toStdString().c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// fetch dofile and call it
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
||||
// maybe data race here if symlink was swapped?
|
||||
lua::push(L, path);
|
||||
lua_call(L, 1, LUA_MULTRET);
|
||||
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
int g_print(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->plugins->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "c2_print: internal error: no plugin?");
|
||||
return 0;
|
||||
}
|
||||
auto argc = lua_gettop(L);
|
||||
// This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop
|
||||
auto stream =
|
||||
(QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE,
|
||||
QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())
|
||||
.debug());
|
||||
logHelper(L, pl, stream, argc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
// NOLINTEND(*vararg)
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
struct lua_State;
|
||||
namespace chatterino::lua::api {
|
||||
// names in this namespace reflect what's visible inside Lua and follow the lua naming scheme
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
// Following functions are exposed in c2 table.
|
||||
int c2_register_command(lua_State *L);
|
||||
int c2_send_msg(lua_State *L);
|
||||
int c2_system_msg(lua_State *L);
|
||||
int c2_log(lua_State *L);
|
||||
|
||||
// These ones are global
|
||||
int g_load(lua_State *L);
|
||||
int g_print(lua_State *L);
|
||||
int g_import(lua_State *L);
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
// Exposed as c2.LogLevel
|
||||
// Represents "calls" to qCDebug, qCInfo ...
|
||||
enum class LogLevel { Debug, Info, Warning, Critical };
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,196 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
|
||||
# include "common/Channel.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
|
||||
# include <climits>
|
||||
# include <cstdlib>
|
||||
|
||||
namespace chatterino::lua {
|
||||
|
||||
void stackDump(lua_State *L, const QString &tag)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "--------------------";
|
||||
auto count = lua_gettop(L);
|
||||
if (!tag.isEmpty())
|
||||
{
|
||||
qCDebug(chatterinoLua) << "Tag: " << tag;
|
||||
}
|
||||
qCDebug(chatterinoLua) << "Count elems: " << count;
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
auto typeint = lua_type(L, i);
|
||||
if (typeint == LUA_TSTRING)
|
||||
{
|
||||
QString str;
|
||||
lua::peek(L, &str, i);
|
||||
qCDebug(chatterinoLua)
|
||||
<< "At" << i << "is a" << lua_typename(L, typeint) << "("
|
||||
<< typeint << "): " << str;
|
||||
}
|
||||
else if (typeint == LUA_TTABLE)
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "At" << i << "is a" << lua_typename(L, typeint) << "("
|
||||
<< typeint << ")"
|
||||
<< "its length is " << lua_rawlen(L, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "At" << i << "is a" << lua_typename(L, typeint) << "("
|
||||
<< typeint << ")";
|
||||
}
|
||||
}
|
||||
qCDebug(chatterinoLua) << "--------------------";
|
||||
}
|
||||
|
||||
QString humanErrorText(lua_State *L, int errCode)
|
||||
{
|
||||
QString errName;
|
||||
switch (errCode)
|
||||
{
|
||||
case LUA_OK:
|
||||
return "ok";
|
||||
case LUA_ERRRUN:
|
||||
errName = "(runtime error)";
|
||||
break;
|
||||
case LUA_ERRMEM:
|
||||
errName = "(memory error)";
|
||||
break;
|
||||
case LUA_ERRERR:
|
||||
errName = "(error while handling another error)";
|
||||
break;
|
||||
case LUA_ERRSYNTAX:
|
||||
errName = "(syntax error)";
|
||||
break;
|
||||
case LUA_YIELD:
|
||||
errName = "(illegal coroutine yield)";
|
||||
break;
|
||||
case LUA_ERRFILE:
|
||||
errName = "(file error)";
|
||||
break;
|
||||
default:
|
||||
errName = "(unknown error type)";
|
||||
}
|
||||
QString errText;
|
||||
if (peek(L, &errText))
|
||||
{
|
||||
errName += " " + errText;
|
||||
}
|
||||
return errName;
|
||||
}
|
||||
|
||||
StackIdx pushEmptyArray(lua_State *L, int countArray)
|
||||
{
|
||||
lua_createtable(L, countArray, 0);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
StackIdx pushEmptyTable(lua_State *L, int countProperties)
|
||||
{
|
||||
lua_createtable(L, 0, countProperties);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const QString &str)
|
||||
{
|
||||
return lua::push(L, str.toStdString());
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const std::string &str)
|
||||
{
|
||||
lua_pushstring(L, str.c_str());
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const CommandContext &ctx)
|
||||
{
|
||||
auto outIdx = pushEmptyTable(L, 2);
|
||||
|
||||
push(L, ctx.words);
|
||||
lua_setfield(L, outIdx, "words");
|
||||
push(L, ctx.channel->getName());
|
||||
lua_setfield(L, outIdx, "channel_name");
|
||||
|
||||
return outIdx;
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const bool &b)
|
||||
{
|
||||
lua_pushboolean(L, int(b));
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, double *out, StackIdx idx)
|
||||
{
|
||||
int ok{0};
|
||||
auto v = lua_tonumberx(L, idx, &ok);
|
||||
if (ok != 0)
|
||||
{
|
||||
*out = v;
|
||||
}
|
||||
return ok != 0;
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, QString *out, StackIdx idx)
|
||||
{
|
||||
size_t len{0};
|
||||
const char *str = lua_tolstring(L, idx, &len);
|
||||
if (str == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (len >= INT_MAX)
|
||||
{
|
||||
assert(false && "string longer than INT_MAX, shit's fucked, yo");
|
||||
}
|
||||
*out = QString::fromUtf8(str, int(len));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx)
|
||||
{
|
||||
size_t len{0};
|
||||
const char *str = lua_tolstring(L, idx, &len);
|
||||
if (str == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (len >= INT_MAX)
|
||||
{
|
||||
assert(false && "string longer than INT_MAX, shit's fucked, yo");
|
||||
}
|
||||
*out = QByteArray(str, int(len));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, std::string *out, StackIdx idx)
|
||||
{
|
||||
size_t len{0};
|
||||
const char *str = lua_tolstring(L, idx, &len);
|
||||
if (str == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (len >= INT_MAX)
|
||||
{
|
||||
assert(false && "string longer than INT_MAX, shit's fucked, yo");
|
||||
}
|
||||
*out = std::string(str, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString toString(lua_State *L, StackIdx idx)
|
||||
{
|
||||
size_t len{};
|
||||
const auto *ptr = luaL_tolstring(L, idx, &len);
|
||||
return QString::fromUtf8(ptr, int(len));
|
||||
}
|
||||
} // namespace chatterino::lua
|
||||
#endif
|
||||
@@ -0,0 +1,191 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <magic_enum.hpp>
|
||||
# include <QList>
|
||||
|
||||
# include <string>
|
||||
# include <string_view>
|
||||
# include <type_traits>
|
||||
# include <vector>
|
||||
struct lua_State;
|
||||
class QJsonObject;
|
||||
namespace chatterino {
|
||||
struct CommandContext;
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::lua {
|
||||
|
||||
/**
|
||||
* @brief Dumps the Lua stack into qCDebug(chatterinoLua)
|
||||
*
|
||||
* @param tag is a string to let you know which dump is which when browsing logs
|
||||
*/
|
||||
void stackDump(lua_State *L, const QString &tag);
|
||||
|
||||
/**
|
||||
* @brief Converts a lua error code and potentially string on top of the stack into a human readable message
|
||||
*/
|
||||
QString humanErrorText(lua_State *L, int errCode);
|
||||
|
||||
/**
|
||||
* Represents an index into Lua's stack
|
||||
*/
|
||||
using StackIdx = int;
|
||||
|
||||
/**
|
||||
* @brief Creates a table with countArray array properties on the Lua stack
|
||||
* @return stack index of the newly created table
|
||||
*/
|
||||
StackIdx pushEmptyArray(lua_State *L, int countArray);
|
||||
|
||||
/**
|
||||
* @brief Creates a table with countProperties named properties on the Lua stack
|
||||
* @return stack index of the newly created table
|
||||
*/
|
||||
StackIdx pushEmptyTable(lua_State *L, int countProperties);
|
||||
|
||||
StackIdx push(lua_State *L, const CommandContext &ctx);
|
||||
StackIdx push(lua_State *L, const QString &str);
|
||||
StackIdx push(lua_State *L, const std::string &str);
|
||||
StackIdx push(lua_State *L, const bool &b);
|
||||
|
||||
// returns OK?
|
||||
bool peek(lua_State *L, double *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, std::string *out, StackIdx idx = -1);
|
||||
|
||||
/**
|
||||
* @brief Converts Lua object at stack index idx to a string.
|
||||
*/
|
||||
QString toString(lua_State *L, StackIdx idx = -1);
|
||||
|
||||
/// TEMPLATES
|
||||
|
||||
/**
|
||||
* @brief Converts object at stack index idx to enum given by template parameter T
|
||||
*/
|
||||
template <typename T,
|
||||
typename std::enable_if<std::is_enum_v<T>, bool>::type = true>
|
||||
bool peek(lua_State *L, T *out, StackIdx idx = -1)
|
||||
{
|
||||
std::string tmp;
|
||||
if (!lua::peek(L, &tmp, idx))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::optional<T> opt = magic_enum::enum_cast<T>(tmp);
|
||||
if (opt.has_value())
|
||||
{
|
||||
*out = opt.value();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts a vector<T> to Lua and pushes it onto the stack.
|
||||
*
|
||||
* Needs StackIdx push(lua_State*, T); to work.
|
||||
*
|
||||
* @return Stack index of newly created table.
|
||||
*/
|
||||
template <typename T>
|
||||
StackIdx push(lua_State *L, std::vector<T> vec)
|
||||
{
|
||||
auto out = pushEmptyArray(L, vec.size());
|
||||
int i = 1;
|
||||
for (const auto &el : vec)
|
||||
{
|
||||
push(L, el);
|
||||
lua_seti(L, out, i);
|
||||
i += 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts a QList<T> to Lua and pushes it onto the stack.
|
||||
*
|
||||
* Needs StackIdx push(lua_State*, T); to work.
|
||||
*
|
||||
* @return Stack index of newly created table.
|
||||
*/
|
||||
template <typename T>
|
||||
StackIdx push(lua_State *L, QList<T> vec)
|
||||
{
|
||||
auto out = pushEmptyArray(L, vec.size());
|
||||
int i = 1;
|
||||
for (const auto &el : vec)
|
||||
{
|
||||
push(L, el);
|
||||
lua_seti(L, out, i);
|
||||
i += 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts an enum given by T to Lua (into a string) and pushes it onto the stack.
|
||||
*
|
||||
* @return Stack index of newly created string.
|
||||
*/
|
||||
template <typename T, std::enable_if<std::is_enum_v<T>>>
|
||||
StackIdx push(lua_State *L, T inp)
|
||||
{
|
||||
std::string_view name = magic_enum::enum_name<T>(inp);
|
||||
return lua::push(L, std::string(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Converts a Lua object into c++ and removes it from the stack.
|
||||
*
|
||||
* Relies on bool peek(lua_State*, T*, StackIdx) existing.
|
||||
*/
|
||||
template <typename T>
|
||||
bool pop(lua_State *L, T *out, StackIdx idx = -1)
|
||||
{
|
||||
auto ok = peek(L, out, idx);
|
||||
if (ok)
|
||||
{
|
||||
if (idx < 0)
|
||||
{
|
||||
idx = lua_gettop(L) + idx + 1;
|
||||
}
|
||||
lua_remove(L, idx);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a table mapping enum names to unique values.
|
||||
*
|
||||
* Values in this table may change.
|
||||
*
|
||||
* @returns stack index of newly created table
|
||||
*/
|
||||
template <typename T>
|
||||
StackIdx pushEnumTable(lua_State *L)
|
||||
{
|
||||
// std::array<T, _>
|
||||
auto values = magic_enum::enum_values<T>();
|
||||
StackIdx out = lua::pushEmptyTable(L, values.size());
|
||||
for (const T v : values)
|
||||
{
|
||||
std::string_view name = magic_enum::enum_name<T>(v);
|
||||
std::string str(name);
|
||||
|
||||
lua::push(L, str);
|
||||
lua_setfield(L, out, str.c_str());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,177 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
|
||||
# include <lua.h>
|
||||
# include <magic_enum.hpp>
|
||||
# include <QJsonArray>
|
||||
# include <QJsonObject>
|
||||
|
||||
# include <unordered_map>
|
||||
# include <unordered_set>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
PluginMeta::PluginMeta(const QJsonObject &obj)
|
||||
{
|
||||
auto homepageObj = obj.value("homepage");
|
||||
if (homepageObj.isString())
|
||||
{
|
||||
this->homepage = homepageObj.toString();
|
||||
}
|
||||
else if (!homepageObj.isUndefined())
|
||||
{
|
||||
QString type = magic_enum::enum_name(homepageObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("homepage is defined but is not a string (its type is %1)")
|
||||
.arg(type));
|
||||
}
|
||||
auto nameObj = obj.value("name");
|
||||
if (nameObj.isString())
|
||||
{
|
||||
this->name = nameObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(nameObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("name is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto descrObj = obj.value("description");
|
||||
if (descrObj.isString())
|
||||
{
|
||||
this->description = descrObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(descrObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("description is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto authorsObj = obj.value("authors");
|
||||
if (authorsObj.isArray())
|
||||
{
|
||||
auto authorsArr = authorsObj.toArray();
|
||||
for (int i = 0; i < authorsArr.size(); i++)
|
||||
{
|
||||
const auto &t = authorsArr.at(i);
|
||||
if (!t.isString())
|
||||
{
|
||||
QString type = magic_enum::enum_name(t.type()).data();
|
||||
this->errors.push_back(
|
||||
QString("authors element #%1 is not a string (it is a %2)")
|
||||
.arg(i)
|
||||
.arg(type));
|
||||
break;
|
||||
}
|
||||
this->authors.push_back(t.toString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(authorsObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("authors is not an array (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto licenseObj = obj.value("license");
|
||||
if (licenseObj.isString())
|
||||
{
|
||||
this->license = licenseObj.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(licenseObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("license is not a string (its type is %1)").arg(type));
|
||||
}
|
||||
|
||||
auto verObj = obj.value("version");
|
||||
if (verObj.isString())
|
||||
{
|
||||
auto v = semver::from_string_noexcept(verObj.toString().toStdString());
|
||||
if (v.has_value())
|
||||
{
|
||||
this->version = v.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->errors.emplace_back("unable to parse version (use semver)");
|
||||
this->version = semver::version(0, 0, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QString type = magic_enum::enum_name(verObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("version is not a string (its type is %1)").arg(type));
|
||||
this->version = semver::version(0, 0, 0);
|
||||
}
|
||||
auto tagsObj = obj.value("tags");
|
||||
if (!tagsObj.isUndefined())
|
||||
{
|
||||
if (!tagsObj.isArray())
|
||||
{
|
||||
QString type = magic_enum::enum_name(tagsObj.type()).data();
|
||||
this->errors.emplace_back(
|
||||
QString("tags is not an array (its type is %1)").arg(type));
|
||||
return;
|
||||
}
|
||||
|
||||
auto tagsArr = tagsObj.toArray();
|
||||
for (int i = 0; i < tagsArr.size(); i++)
|
||||
{
|
||||
const auto &t = tagsArr.at(i);
|
||||
if (!t.isString())
|
||||
{
|
||||
QString type = magic_enum::enum_name(t.type()).data();
|
||||
this->errors.push_back(
|
||||
QString("tags element #%1 is not a string (its type is %2)")
|
||||
.arg(i)
|
||||
.arg(type));
|
||||
return;
|
||||
}
|
||||
this->tags.push_back(t.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Plugin::registerCommand(const QString &name, const QString &functionName)
|
||||
{
|
||||
if (this->ownedCommands.find(name) != this->ownedCommands.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ok = getApp()->commands->registerPluginCommand(name);
|
||||
if (!ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this->ownedCommands.insert({name, functionName});
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unordered_set<QString> Plugin::listRegisteredCommands()
|
||||
{
|
||||
std::unordered_set<QString> out;
|
||||
for (const auto &[name, _] : this->ownedCommands)
|
||||
{
|
||||
out.insert(name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Plugin::~Plugin()
|
||||
{
|
||||
if (this->state_ != nullptr)
|
||||
{
|
||||
lua_close(this->state_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "Application.hpp"
|
||||
|
||||
# include <QDir>
|
||||
# include <QString>
|
||||
# include <semver/semver.hpp>
|
||||
|
||||
# include <unordered_map>
|
||||
# include <unordered_set>
|
||||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct PluginMeta {
|
||||
// for more info on these fields see docs/plugin-info.schema.json
|
||||
|
||||
// display name of the plugin
|
||||
QString name;
|
||||
|
||||
// description shown to the user
|
||||
QString description;
|
||||
|
||||
// plugin authors shown to the user
|
||||
std::vector<QString> authors;
|
||||
|
||||
// license name
|
||||
QString license;
|
||||
|
||||
// version of the plugin
|
||||
semver::version version;
|
||||
|
||||
// optionally a homepage link
|
||||
QString homepage;
|
||||
|
||||
// optionally tags that might help in searching for the plugin
|
||||
std::vector<QString> tags;
|
||||
|
||||
// errors that occurred while parsing info.json
|
||||
std::vector<QString> errors;
|
||||
|
||||
bool isValid() const
|
||||
{
|
||||
return this->errors.empty();
|
||||
}
|
||||
|
||||
explicit PluginMeta(const QJsonObject &obj);
|
||||
};
|
||||
|
||||
class Plugin
|
||||
{
|
||||
public:
|
||||
QString id;
|
||||
PluginMeta meta;
|
||||
|
||||
Plugin(QString id, lua_State *state, PluginMeta meta,
|
||||
const QDir &loadDirectory)
|
||||
: id(std::move(id))
|
||||
, meta(std::move(meta))
|
||||
, loadDirectory_(loadDirectory)
|
||||
, state_(state)
|
||||
{
|
||||
}
|
||||
|
||||
~Plugin();
|
||||
|
||||
/**
|
||||
* @brief Perform all necessary tasks to bind a command name to this plugin
|
||||
* @param name name of the command to create
|
||||
* @param functionName name of the function that should be called when the command is executed
|
||||
* @return true if addition succeeded, false otherwise (for example because the command name is already taken)
|
||||
*/
|
||||
bool registerCommand(const QString &name, const QString &functionName);
|
||||
|
||||
/**
|
||||
* @brief Get names of all commands belonging to this plugin
|
||||
*/
|
||||
std::unordered_set<QString> listRegisteredCommands();
|
||||
|
||||
const QDir &loadDirectory() const
|
||||
{
|
||||
return this->loadDirectory_;
|
||||
}
|
||||
|
||||
private:
|
||||
QDir loadDirectory_;
|
||||
lua_State *state_;
|
||||
|
||||
// maps command name -> function name
|
||||
std::unordered_map<QString, QString> ownedCommands;
|
||||
|
||||
friend class PluginController;
|
||||
};
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
@@ -0,0 +1,302 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
# include "singletons/Settings.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
# include <QJsonDocument>
|
||||
|
||||
# include <memory>
|
||||
# include <utility>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void PluginController::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
(void)paths;
|
||||
|
||||
// actuallyInitialize will be called by this connection
|
||||
settings.pluginsEnabled.connect([this](bool enabled) {
|
||||
if (enabled)
|
||||
{
|
||||
this->loadPlugins();
|
||||
}
|
||||
else
|
||||
{
|
||||
// uninitialize plugins
|
||||
this->plugins_.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void PluginController::loadPlugins()
|
||||
{
|
||||
this->plugins_.clear();
|
||||
auto dir = QDir(getPaths()->pluginsDirectory);
|
||||
qCDebug(chatterinoLua) << "Loading plugins in" << dir.path();
|
||||
for (const auto &info :
|
||||
dir.entryInfoList(QDir::NoFilter | QDir::NoDotAndDotDot))
|
||||
{
|
||||
if (info.isDir())
|
||||
{
|
||||
auto pluginDir = QDir(info.absoluteFilePath());
|
||||
this->tryLoadFromDir(pluginDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||
{
|
||||
// look for init.lua
|
||||
auto index = QFileInfo(pluginDir.filePath("init.lua"));
|
||||
qCDebug(chatterinoLua) << "Looking for init.lua and info.json in"
|
||||
<< pluginDir.path();
|
||||
if (!index.exists())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Missing init.lua in plugin directory:" << pluginDir.path();
|
||||
return false;
|
||||
}
|
||||
qCDebug(chatterinoLua) << "Found init.lua, now looking for info.json!";
|
||||
auto infojson = QFileInfo(pluginDir.filePath("info.json"));
|
||||
if (!infojson.exists())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Missing info.json in plugin directory" << pluginDir.path();
|
||||
return false;
|
||||
}
|
||||
QFile infoFile(infojson.absoluteFilePath());
|
||||
infoFile.open(QIODevice::ReadOnly);
|
||||
auto everything = infoFile.readAll();
|
||||
auto doc = QJsonDocument::fromJson(everything);
|
||||
if (!doc.isObject())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "info.json root is not an object" << pluginDir.path();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto meta = PluginMeta(doc.object());
|
||||
if (!meta.isValid())
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin from" << pluginDir << "is invalid because:";
|
||||
for (const auto &why : meta.errors)
|
||||
{
|
||||
qCWarning(chatterinoLua) << "- " << why;
|
||||
}
|
||||
auto plugin = std::make_unique<Plugin>(pluginDir.dirName(), nullptr,
|
||||
meta, pluginDir);
|
||||
this->plugins_.insert({pluginDir.dirName(), std::move(plugin)});
|
||||
return false;
|
||||
}
|
||||
this->load(index, pluginDir, meta);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PluginController::openLibrariesFor(lua_State *L,
|
||||
const PluginMeta & /*meta*/)
|
||||
{
|
||||
// Stuff to change, remove or hide behind a permission system:
|
||||
static const std::vector<luaL_Reg> loadedlibs = {
|
||||
luaL_Reg{LUA_GNAME, luaopen_base},
|
||||
// - load - don't allow in release mode
|
||||
|
||||
//luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
||||
// - needs special support
|
||||
luaL_Reg{LUA_TABLIBNAME, luaopen_table},
|
||||
// luaL_Reg{LUA_IOLIBNAME, luaopen_io},
|
||||
// - explicit fs access, needs wrapper with permissions, no usage ideas yet
|
||||
// luaL_Reg{LUA_OSLIBNAME, luaopen_os},
|
||||
// - fs access
|
||||
// - environ access
|
||||
// - exit
|
||||
luaL_Reg{LUA_STRLIBNAME, luaopen_string},
|
||||
luaL_Reg{LUA_MATHLIBNAME, luaopen_math},
|
||||
luaL_Reg{LUA_UTF8LIBNAME, luaopen_utf8},
|
||||
};
|
||||
// Warning: Do not add debug library to this, it would make the security of
|
||||
// this a living nightmare due to stuff like registry access
|
||||
// - Mm2PL
|
||||
|
||||
for (const auto ® : loadedlibs)
|
||||
{
|
||||
luaL_requiref(L, reg.name, reg.func, int(true));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg c2Lib[] = {
|
||||
{"system_msg", lua::api::c2_system_msg},
|
||||
{"register_command", lua::api::c2_register_command},
|
||||
{"send_msg", lua::api::c2_send_msg},
|
||||
{"log", lua::api::c2_log},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
lua_pushglobaltable(L);
|
||||
auto global = lua_gettop(L);
|
||||
|
||||
// count of elements in C2LIB + LogLevel
|
||||
auto c2libIdx = lua::pushEmptyTable(L, 5);
|
||||
|
||||
luaL_setfuncs(L, c2Lib, 0);
|
||||
|
||||
lua::pushEnumTable<lua::api::LogLevel>(L);
|
||||
lua_setfield(L, c2libIdx, "LogLevel");
|
||||
|
||||
lua_setfield(L, global, "c2");
|
||||
|
||||
// ban functions
|
||||
// Note: this might not be fully secure? some kind of metatable fuckery might come up?
|
||||
|
||||
lua_pushglobaltable(L);
|
||||
auto gtable = lua_gettop(L);
|
||||
|
||||
// possibly randomize this name at runtime to prevent some attacks?
|
||||
|
||||
# ifndef NDEBUG
|
||||
lua_getfield(L, gtable, "load");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||
# endif
|
||||
|
||||
lua_getfield(L, gtable, "dofile");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_dofile");
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg replacementFuncs[] = {
|
||||
{"load", lua::api::g_load},
|
||||
{"print", lua::api::g_print},
|
||||
|
||||
// This function replaces both `dofile` and `require`, see docs/wip-plugins.md for more info
|
||||
{"import", lua::api::g_import},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
luaL_setfuncs(L, replacementFuncs, 0);
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, gtable, "loadfile");
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, gtable, "dofile");
|
||||
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||
const PluginMeta &meta)
|
||||
{
|
||||
lua_State *l = luaL_newstate();
|
||||
PluginController::openLibrariesFor(l, meta);
|
||||
|
||||
auto pluginName = pluginDir.dirName();
|
||||
auto plugin = std::make_unique<Plugin>(pluginName, l, meta, pluginDir);
|
||||
this->plugins_.insert({pluginName, std::move(plugin)});
|
||||
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||
!getSettings()->pluginsEnabled)
|
||||
{
|
||||
qCDebug(chatterinoLua) << "Skipping loading" << pluginName << "("
|
||||
<< meta.name << ") because it is disabled";
|
||||
return;
|
||||
}
|
||||
qCDebug(chatterinoLua) << "Running lua file:" << index;
|
||||
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
|
||||
if (err != 0)
|
||||
{
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Failed to load" << pluginName << "plugin from" << index << ": "
|
||||
<< lua::humanErrorText(l, err);
|
||||
return;
|
||||
}
|
||||
qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index;
|
||||
}
|
||||
|
||||
bool PluginController::reload(const QString &id)
|
||||
{
|
||||
auto it = this->plugins_.find(id);
|
||||
if (it == this->plugins_.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (it->second->state_ != nullptr)
|
||||
{
|
||||
lua_close(it->second->state_);
|
||||
it->second->state_ = nullptr;
|
||||
}
|
||||
for (const auto &[cmd, _] : it->second->ownedCommands)
|
||||
{
|
||||
getApp()->commands->unregisterPluginCommand(cmd);
|
||||
}
|
||||
it->second->ownedCommands.clear();
|
||||
QDir loadDir = it->second->loadDirectory_;
|
||||
this->plugins_.erase(id);
|
||||
this->tryLoadFromDir(loadDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString PluginController::tryExecPluginCommand(const QString &commandName,
|
||||
const CommandContext &ctx)
|
||||
{
|
||||
for (auto &[name, plugin] : this->plugins_)
|
||||
{
|
||||
if (auto it = plugin->ownedCommands.find(commandName);
|
||||
it != plugin->ownedCommands.end())
|
||||
{
|
||||
const auto &funcName = it->second;
|
||||
|
||||
auto *L = plugin->state_;
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str());
|
||||
lua::push(L, ctx);
|
||||
|
||||
auto res = lua_pcall(L, 1, 0, 0);
|
||||
if (res != LUA_OK)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"Lua error: " + lua::humanErrorText(L, res)));
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
qCCritical(chatterinoLua)
|
||||
<< "Something's seriously up, no plugin owns command" << commandName
|
||||
<< "yet a call to execute it came in";
|
||||
assert(false && "missing plugin command owner");
|
||||
return "";
|
||||
}
|
||||
|
||||
bool PluginController::isPluginEnabled(const QString &id)
|
||||
{
|
||||
auto vec = getSettings()->enabledPlugins.getValue();
|
||||
auto it = std::find(vec.begin(), vec.end(), id);
|
||||
return it != vec.end();
|
||||
}
|
||||
|
||||
Plugin *PluginController::getPluginByStatePtr(lua_State *L)
|
||||
{
|
||||
for (auto &[name, plugin] : this->plugins_)
|
||||
{
|
||||
if (plugin->state_ == L)
|
||||
{
|
||||
return plugin.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::map<QString, std::unique_ptr<Plugin>> &PluginController::plugins()
|
||||
const
|
||||
{
|
||||
return this->plugins_;
|
||||
}
|
||||
|
||||
}; // namespace chatterino
|
||||
#endif
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "common/Singleton.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
|
||||
# include <QDir>
|
||||
# include <QFileInfo>
|
||||
# include <QJsonArray>
|
||||
# include <QJsonObject>
|
||||
# include <QString>
|
||||
|
||||
# include <algorithm>
|
||||
# include <map>
|
||||
# include <memory>
|
||||
# include <utility>
|
||||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Paths;
|
||||
|
||||
class PluginController : public Singleton
|
||||
{
|
||||
public:
|
||||
void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
QString tryExecPluginCommand(const QString &commandName,
|
||||
const CommandContext &ctx);
|
||||
|
||||
// NOTE: this pointer does not own the Plugin, unique_ptr still owns it
|
||||
// This is required to be public because of c functions
|
||||
Plugin *getPluginByStatePtr(lua_State *L);
|
||||
|
||||
const std::map<QString, std::unique_ptr<Plugin>> &plugins() const;
|
||||
|
||||
/**
|
||||
* @brief Reload plugin given by id
|
||||
*
|
||||
* @param id This is the unique identifier of the plugin, the name of the directory it is in
|
||||
*/
|
||||
bool reload(const QString &id);
|
||||
|
||||
/**
|
||||
* @brief Checks settings to tell if a plugin named by id is enabled.
|
||||
*
|
||||
* It is the callers responsibility to check Settings::pluginsEnabled
|
||||
*/
|
||||
static bool isPluginEnabled(const QString &id);
|
||||
|
||||
private:
|
||||
void loadPlugins();
|
||||
void load(const QFileInfo &index, const QDir &pluginDir,
|
||||
const PluginMeta &meta);
|
||||
|
||||
// This function adds lua standard libraries into the state
|
||||
static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/);
|
||||
static void loadChatterinoLib(lua_State *l);
|
||||
bool tryLoadFromDir(const QDir &pluginDir);
|
||||
std::map<QString, std::unique_ptr<Plugin>> plugins_;
|
||||
};
|
||||
|
||||
}; // namespace chatterino
|
||||
#endif
|
||||
Reference in New Issue
Block a user