#include "commands/lua.hpp" #include #include #include #include #include #include #include #include #include "api/twitch/schemas/user.hpp" #include "bundle.hpp" #include "commands/request.hpp" #include "commands/response.hpp" #include "commands/response_error.hpp" #include "schemas/user.hpp" #include "utils/chrono.hpp" #include "utils/string.hpp" namespace bot::command::lua { namespace library { void add_bot_library(std::shared_ptr state) { state->set_function("bot_get_compiler_version", []() { std::string info; #ifdef __cplusplus info.append("C++" + std::to_string(__cplusplus).substr(2, 2)); #endif #ifdef __VERSION__ info.append(" (gcc " + bot::utils::string::split_text(__VERSION__, ' ')[0] + ")"); #endif return info; }); state->set_function("bot_get_uptime", []() { auto now = std::chrono::steady_clock::now(); auto duration = now - START_TIME; auto seconds = std::chrono::duration_cast(duration).count(); return static_cast(seconds); }); state->set_function("bot_get_memory_usage", []() { struct rusage usage; getrusage(RUSAGE_SELF, &usage); return usage.ru_maxrss; }); state->set_function("bot_get_compile_time", []() { return BOT_COMPILED_TIMESTAMP; }); state->set_function("bot_get_version", []() { return BOT_VERSION; }); } void add_time_library(std::shared_ptr state) { state->set_function("time_current", []() { return static_cast( std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()) .count()); }); state->set_function("time_humanize", [](const int ×tamp) { return utils::chrono::format_timestamp(timestamp); }); } sol::object parse_json_object(std::shared_ptr state, nlohmann::json j) { switch (j.type()) { case nlohmann::json::value_t::null: return sol::make_object(*state, sol::lua_nil); case nlohmann::json::value_t::string: return sol::make_object(*state, j.get()); case nlohmann::json::value_t::number_integer: return sol::make_object(*state, j.get()); case nlohmann::json::value_t::number_unsigned: return sol::make_object(*state, j.get()); case nlohmann::json::value_t::number_float: return sol::make_object(*state, j.get()); case nlohmann::json::value_t::array: { sol::table a = state->create_table(); for (int i = 0; i < j.size(); ++i) { a[i] = parse_json_object(state, j[i]); } return sol::make_object(*state, a); } case nlohmann::json::value_t::object: { sol::table o = state->create_table(); for (const auto &[k, v] : j.items()) { o[k] = parse_json_object(state, v); } return sol::make_object(*state, o); } default: throw std::runtime_error("Unsupported Lua type: " + std::string(j.type_name())); } } nlohmann::json lua_to_json(sol::object o) { switch (o.get_type()) { case sol::type::lua_nil: return nullptr; case sol::type::string: return o.as(); case sol::type::boolean: return o.as(); case sol::type::number: { double num = o.as(); if (std::floor(num) == num) { return static_cast(num); } return num; } case sol::type::table: { sol::table t = o; bool is_array = true; int count = 0; for (auto &kv : t) { sol::object key = kv.first; if (key.get_type() != sol::type::number) { is_array = false; break; } ++count; } if (is_array) { nlohmann::json a = nlohmann::json::array(); for (size_t i = 1; i <= count; ++i) { a.push_back(lua_to_json(t[i])); } return a; } else { nlohmann::json ob = nlohmann::json::object(); for (auto &kv : t) { std::string key = kv.first.as(); ob[key] = lua_to_json(kv.second); } return ob; } } default: throw std::runtime_error( "Unsupported Lua object for JSON conversion"); } } void add_json_library(std::shared_ptr state) { state->set_function("json_parse", [state](const std::string &s) { nlohmann::json j = nlohmann::json::parse(s); return parse_json_object(state, j); }); state->set_function("json_stringify", [](const sol::object &o) { return lua_to_json(o).dump(); }); } void add_base_libraries(std::shared_ptr state) { add_bot_library(state); add_time_library(state); add_json_library(state); } void add_twitch_library(std::shared_ptr state, const Request &request, const InstanceBundle &bundle) { // TODO: ratelimits state->set_function("twitch_get_chatters", [state, &request, &bundle]() { auto chatters = bundle.helix_client.get_chatters( request.channel.get_alias_id(), bundle.irc_client.get_bot_id()); sol::table o = state->create_table(); std::for_each(chatters.begin(), chatters.end(), [state, &o](const api::twitch::schemas::User &x) { sol::table u = state->create_table(); u["id"] = x.id; u["login"] = x.login; o.add(u); }); return o; }); } } std::string parse_lua_response(const sol::table &r, sol::object &res) { if (res.get_type() == sol::type::function) { sol::function f = res.as(); sol::object o = f(r); return parse_lua_response(r, o); } else if (res.get_type() == sol::type::string) { return {"🌑 " + res.as()}; } else if (res.get_type() == sol::type::number) { return {"🌑 " + std::to_string(res.as())}; } else if (res.get_type() == sol::type::boolean) { return {"🌑 " + std::to_string(res.as())}; } else { // should it be ResponseException? return "Empty or unsupported response"; } } command::Response run_safe_lua_script(const Request &request, const InstanceBundle &bundle, const std::string &script) { // shared_ptr is unnecessary here, but my library needs it. std::shared_ptr state = std::make_shared(); state->open_libraries(sol::lib::base, sol::lib::table, sol::lib::string); library::add_base_libraries(state); sol::load_result s = state->load("return " + script); if (!s.valid()) { s = state->load(script); } if (!s.valid()) { sol::error err = s; throw ResponseException( request, bundle.localization, std::string(err.what())); } sol::protected_function_result res = s(); if (!res.valid()) { sol::error err = s; throw ResponseException( request, bundle.localization, std::string(err.what())); } sol::object o = res; return parse_lua_response(request.as_lua_table(state), o); } LuaCommand::LuaCommand(std::shared_ptr luaState, const std::string &script) { this->luaState = luaState; sol::table data = luaState->script(script); this->name = data["name"]; this->delay = data["delay_sec"]; sol::table subcommands = data["subcommands"]; for (auto &k : subcommands) { sol::object value = k.second; if (value.is()) { this->subcommands.push_back(value.as()); } } std::string rights_text = data["minimal_rights"]; if (rights_text == "suspended") { this->level = schemas::PermissionLevel::SUSPENDED; } else if (rights_text == "user") { this->level = schemas::PermissionLevel::USER; } else if (rights_text == "vip") { this->level = schemas::PermissionLevel::VIP; } else if (rights_text == "moderator") { this->level = schemas::PermissionLevel::MODERATOR; } else if (rights_text == "broadcaster") { this->level = schemas::PermissionLevel::BROADCASTER; } else { this->level = schemas::PermissionLevel::USER; } this->handle = data["handle"]; } Response LuaCommand::run(const InstanceBundle &bundle, const Request &request) const { sol::object response = this->handle(request.as_lua_table(this->luaState)); if (response.is()) { return {response.as()}; } else if (response.is()) { sol::table tbl = response.as(); std::vector items; for (auto &kv : tbl) { sol::object value = kv.second; if (value.is()) { items.push_back(value.as()); } } return items; } return {}; } }