diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 72fefca53..3e5d2a7ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -136,6 +136,8 @@ set(SOURCE_FILES controllers/pings/MutedChannelModel.cpp controllers/pings/MutedChannelModel.hpp + controllers/plugins/ApiChatterino.cpp + controllers/plugins/ApiChatterino.hpp controllers/plugins/Plugin.cpp controllers/plugins/Plugin.hpp controllers/plugins/PluginController.hpp diff --git a/src/controllers/plugins/ApiChatterino.cpp b/src/controllers/plugins/ApiChatterino.cpp new file mode 100644 index 000000000..fd61cff00 --- /dev/null +++ b/src/controllers/plugins/ApiChatterino.cpp @@ -0,0 +1,143 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "ApiChatterino.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" + +// lua stuff +# include "lauxlib.h" +# include "lua.h" +# include "lualib.h" +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"); // NOLINT + return 0; + } + + QString name; + if (!lua::peek(L, &name, 1)) + { + // NOLINTNEXTLINE + luaL_error(L, "cannot get string (1st arg of register_command)"); + return 0; + } + if (lua_isnoneornil(L, 2)) + { + // NOLINTNEXTLINE + 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; + lua::pop(L, &text); + lua::pop(L, &channel); + + const auto chn = getApp()->twitch->getChannelOrEmpty(channel); + if (chn->isEmpty()) + { + qCDebug(chatterinoLua) << "send_msg: no channel" << channel; + 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) + { + qCDebug(chatterinoLua) << "system_msg: need 2 args"; + luaL_error(L, "need exactly 2 arguments"); // NOLINT + lua::push(L, false); + return 1; + } + QString channel; + QString text; + lua::pop(L, &text); + lua::pop(L, &channel); + const auto chn = getApp()->twitch->getChannelOrEmpty(channel); + if (chn->isEmpty()) + { + qCDebug(chatterinoLua) << "system_msg: no channel" << channel; + lua::push(L, false); + return 1; + } + qCDebug(chatterinoLua) << "system_msg: OK!"; + chn->addMessage(makeSystemMessage(text)); + lua::push(L, true); + return 1; +} + +static const QChar REPLACEMENT_CHARACTER = QChar(0xFFFD); +int g_load(lua_State *L) +{ + auto countArgs = lua_gettop(L); + QString str; + if (lua::peek(L, &str, 1)) + { + if (str.contains(REPLACEMENT_CHARACTER)) + { + // NOLINTNEXTLINE + luaL_error(L, "invalid utf-8 in load() is not allowed"); + return 0; + } + } + else + { + // NOLINTNEXTLINE + 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); + qCDebug(chatterinoLua) << "FDM " << lua_gettop(L); + + return lua_gettop(L); +} +} // namespace chatterino::lua::api +#endif diff --git a/src/controllers/plugins/ApiChatterino.hpp b/src/controllers/plugins/ApiChatterino.hpp new file mode 100644 index 000000000..2c6dd7ac4 --- /dev/null +++ b/src/controllers/plugins/ApiChatterino.hpp @@ -0,0 +1,16 @@ +#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 + +int c2_register_command(lua_State *L); // NOLINT(readability-identifier-naming) +int c2_send_msg(lua_State *L); // NOLINT(readability-identifier-naming) +int c2_system_msg(lua_State *L); // NOLINT(readability-identifier-naming) + +int g_load(lua_State *L); // NOLINT(readability-identifier-naming) + +} // namespace chatterino::lua::api + +#endif diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 785a33a41..83920357f 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -1,13 +1,11 @@ #include "PluginController.hpp" -#ifdef CHATTERINO_HAVE_PLUGINS +#ifdef CHATTERINO_HAVE_PLUGINS # include "Application.hpp" # include "common/QLogging.hpp" # include "controllers/commands/CommandContext.hpp" +# include "controllers/plugins/ApiChatterino.hpp" # include "controllers/plugins/LuaUtilities.hpp" -# include "lauxlib.h" -# include "lua.h" -# include "lualib.h" # include "messages/MessageBuilder.hpp" # include "providers/twitch/TwitchIrcServer.hpp" # include "singletons/Paths.hpp" @@ -17,6 +15,11 @@ # include "widgets/splits/Split.hpp" # include "widgets/Window.hpp" +// lua stuff +# include "lauxlib.h" +# include "lua.h" +# include "lualib.h" + # include # include @@ -135,6 +138,41 @@ void PluginController::openLibrariesFor(lua_State *L, luaL_requiref(L, reg.name, reg.func, int(true)); lua_pop(L, 1); } + + // NOLINTNEXTLINE + 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}, + {nullptr, nullptr}, + }; + lua_pushglobaltable(L); + auto global = lua_gettop(L); + + // count of elements in C2LIB - 1 (to account for terminator) + lua::pushEmptyTable(L, 3); + + luaL_setfuncs(L, C2LIB, 0); + 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); + lua_getfield(L, gtable, "load"); + + // possibly randomize this name at runtime to prevent some attacks? + lua_setfield(L, LUA_REGISTRYINDEX, "real_load"); + + // NOLINTNEXTLINE + static const luaL_Reg replacementFuncs[] = { + {"load", lua::api::g_load}, + {nullptr, nullptr}, + }; + luaL_setfuncs(L, replacementFuncs, 0); + + lua_pop(L, 1); } void PluginController::load(const QFileInfo &index, const QDir &pluginDir, @@ -143,7 +181,6 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir, qCDebug(chatterinoLua) << "Running lua file" << index; lua_State *l = luaL_newstate(); PluginController::openLibrariesFor(l, meta); - PluginController::loadChatterinoLib(l); auto pluginName = pluginDir.dirName(); auto plugin = std::make_unique(pluginName, l, meta, pluginDir); @@ -254,111 +291,6 @@ QString PluginController::tryExecPluginCommand(const QString &commandName, return ""; } -extern "C" { - -int luaC2SystemMsg(lua_State *L) -{ - if (lua_gettop(L) != 2) - { - qCDebug(chatterinoLua) << "system_msg: need 2 args"; - luaL_error(L, "need exactly 2 arguments"); // NOLINT - lua::push(L, false); - return 1; - } - QString channel; - QString text; - lua::pop(L, &text); - lua::pop(L, &channel); - const auto chn = getApp()->twitch->getChannelOrEmpty(channel); - if (chn->isEmpty()) - { - qCDebug(chatterinoLua) << "system_msg: no channel" << channel; - lua::push(L, false); - return 1; - } - qCDebug(chatterinoLua) << "system_msg: OK!"; - chn->addMessage(makeSystemMessage(text)); - lua::push(L, true); - return 1; -} - -int luaC2RegisterCommand(lua_State *L) -{ - auto *pl = getApp()->plugins->getPluginByStatePtr(L); - if (pl == nullptr) - { - luaL_error(L, "internal error: no plugin"); // NOLINT - return 0; - } - - QString name; - if (!lua::peek(L, &name, 1)) - { - // NOLINTNEXTLINE - luaL_error(L, "cannot get string (1st arg of register_command)"); - return 0; - } - if (lua_isnoneornil(L, 2)) - { - // NOLINTNEXTLINE - 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 luaC2SendMsg(lua_State *L) -{ - QString text; - QString channel; - lua::pop(L, &text); - lua::pop(L, &channel); - - const auto chn = getApp()->twitch->getChannelOrEmpty(channel); - if (chn->isEmpty()) - { - qCDebug(chatterinoLua) << "send_msg: no channel" << channel; - 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; -} - -// NOLINTNEXTLINE -static const luaL_Reg C2LIB[] = { - {"system_msg", luaC2SystemMsg}, - {"register_command", luaC2RegisterCommand}, - {"send_msg", luaC2SendMsg}, - {nullptr, nullptr}, -}; -} - -void PluginController::loadChatterinoLib(lua_State *L) -{ - lua_pushglobaltable(L); - auto global = lua_gettop(L); - - // count of elements in C2LIB - 1 (to account for terminator) - lua::pushEmptyTable(L, 3); - - luaL_setfuncs(L, C2LIB, 0); - lua_setfield(L, global, "c2"); -} - bool PluginController::isEnabled(const QString &codename) { if (!getSettings()->enableAnyPlugins)