summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorilotterytea <iltsu@alright.party>2025-04-06 17:28:47 +0400
committerilotterytea <iltsu@alright.party>2025-04-06 17:28:47 +0400
commit2a49844a95593ac98e919c18651320e62f276fa7 (patch)
tree01b7e2ebb1dc7a9ac92e7c3105edfd098271f29a
parenta1a36cf4d4999b5ce89dce95364c9fd839b54b5d (diff)
feat: implementing lua coding
-rw-r--r--.gitmodules4
-rw-r--r--CMakeLists.txt2
-rw-r--r--bot/CMakeLists.txt21
-rw-r--r--bot/src/commands/command.cpp30
-rw-r--r--bot/src/commands/command.hpp5
-rw-r--r--bot/src/commands/lua.cpp68
-rw-r--r--bot/src/commands/lua.hpp45
-rw-r--r--bot/src/commands/request.cpp28
-rw-r--r--bot/src/commands/request.hpp5
-rw-r--r--bot/src/main.cpp6
-rw-r--r--bot/src/schemas/channel.cpp60
-rw-r--r--bot/src/schemas/channel.hpp7
-rw-r--r--bot/src/schemas/user.cpp42
-rw-r--r--bot/src/schemas/user.hpp5
-rw-r--r--lib/lua/CMakeLists.txt48
-rw-r--r--luamods/hello.lua10
16 files changed, 385 insertions, 1 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..dd97534
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "lib/lua/src"]
+ path = lib/lua/src
+ url = https://github.com/lua/lua
+ branch = v5.4.0 \ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 268e50d..8e88ce4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -25,6 +25,8 @@ option(BUILD_BOT "Build the bot" ON)
option(BUILD_WEB "Build the web application" ON)
if (BUILD_BOT)
+ set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src")
+ add_subdirectory(lib/lua)
add_subdirectory(bot)
endif()
diff --git a/bot/CMakeLists.txt b/bot/CMakeLists.txt
index 1806c03..a01cc7f 100644
--- a/bot/CMakeLists.txt
+++ b/bot/CMakeLists.txt
@@ -4,6 +4,7 @@ include(FetchContent)
# Creating symbolic links
create_symlink_if_exists("${CMAKE_SOURCE_DIR}/localization" "${CMAKE_CURRENT_BINARY_DIR}/localization")
create_symlink_if_exists("${CMAKE_SOURCE_DIR}/luamods" "${CMAKE_CURRENT_BINARY_DIR}/luamods")
+
if (EXISTS "${CMAKE_SOURCE_DIR}/.env")
create_symlink_if_exists("${CMAKE_SOURCE_DIR}/.env" "${CMAKE_CURRENT_BINARY_DIR}/.env")
endif()
@@ -53,6 +54,7 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(pqxx)
+# websockets
FetchContent_Declare(
ixwebsocket
GIT_REPOSITORY https://github.com/machinezone/IXWebSocket
@@ -60,5 +62,22 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(ixwebsocket)
-target_link_libraries(Bot PRIVATE ixwebsocket::ixwebsocket pqxx nlohmann_json::nlohmann_json cpr::cpr)
+set(SOL2_LUA_VERSION "5.4.8" CACHE STRING "The version of Lua used")
+set(SOL2_BUILD_LUA FALSE CACHE BOOL "Always build Lua, do not search for it in the system")
+
+FetchContent_Declare(
+ sol
+ GIT_REPOSITORY https://github.com/ThePhD/sol2.git
+ GIT_TAG v3.5.0
+)
+FetchContent_MakeAvailable(sol)
+
+target_link_libraries(Bot PRIVATE
+ ixwebsocket::ixwebsocket
+ pqxx
+ nlohmann_json::nlohmann_json
+ cpr::cpr
+ lua
+ sol2::sol2
+)
diff --git a/bot/src/commands/command.cpp b/bot/src/commands/command.cpp
index bd33c68..97dd88b 100644
--- a/bot/src/commands/command.cpp
+++ b/bot/src/commands/command.cpp
@@ -3,9 +3,13 @@
#include <algorithm>
#include <chrono>
#include <ctime>
+#include <fstream>
#include <memory>
#include <optional>
#include <pqxx/pqxx>
+#include <sol/state.hpp>
+#include <sol/types.hpp>
+#include <stdexcept>
#include <string>
#include "../bundle.hpp"
@@ -23,6 +27,8 @@
#include "../modules/timer.hpp"
#include "../modules/user.hpp"
#include "../utils/chrono.hpp"
+#include "commands/lua.hpp"
+#include "logger.hpp"
#include "request.hpp"
#include "response.hpp"
@@ -42,6 +48,30 @@ namespace bot {
this->add_command(std::make_unique<mod::Settings>());
this->add_command(std::make_unique<mod::User>());
this->add_command(std::make_unique<mod::MinecraftServerCheck>());
+
+ this->luaState = std::make_shared<sol::state>();
+ this->luaState->open_libraries(sol::lib::base, sol::lib::string);
+ }
+
+ void CommandLoader::load_lua_directory(const std::string &folder_path) {
+ for (const auto &entry :
+ std::filesystem::directory_iterator(folder_path)) {
+ std::ifstream ifs(entry.path());
+ if (!ifs.is_open()) {
+ throw new std::runtime_error("Failed to open the Lua file at " +
+ entry.path().string());
+ }
+ std::string content, line;
+
+ while (std::getline(ifs, line)) {
+ content += line + '\n';
+ }
+
+ ifs.close();
+
+ this->add_command(
+ std::make_unique<lua::LuaCommand>(this->luaState, content));
+ }
}
void CommandLoader::add_command(std::unique_ptr<Command> command) {
diff --git a/bot/src/commands/command.hpp b/bot/src/commands/command.hpp
index 793446e..c2398ec 100644
--- a/bot/src/commands/command.hpp
+++ b/bot/src/commands/command.hpp
@@ -2,6 +2,8 @@
#include <memory>
#include <optional>
+#include <sol/sol.hpp>
+#include <sol/state.hpp>
#include <string>
#include <vector>
@@ -41,6 +43,7 @@ namespace bot {
~CommandLoader() = default;
void add_command(std::unique_ptr<Command> cmd);
+ void load_lua_directory(const std::string &folder_path);
std::optional<Response> run(const InstanceBundle &bundle,
const Request &msg) const;
@@ -50,6 +53,8 @@ namespace bot {
private:
std::vector<std::unique_ptr<Command>> commands;
+
+ std::shared_ptr<sol::state> luaState;
};
}
}
diff --git a/bot/src/commands/lua.cpp b/bot/src/commands/lua.cpp
new file mode 100644
index 0000000..cd7bc84
--- /dev/null
+++ b/bot/src/commands/lua.cpp
@@ -0,0 +1,68 @@
+#include "commands/lua.hpp"
+
+#include <sol/table.hpp>
+#include <string>
+
+#include "bundle.hpp"
+#include "commands/request.hpp"
+#include "commands/response.hpp"
+#include "schemas/user.hpp"
+
+namespace bot::command::lua {
+ LuaCommand::LuaCommand(std::shared_ptr<sol::state> 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<std::string>()) {
+ this->subcommands.push_back(value.as<std::string>());
+ }
+ }
+
+ 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["run"];
+ }
+
+ Response LuaCommand::run(const InstanceBundle &bundle,
+ const Request &request) const {
+ sol::object response = this->handle(request.as_lua_table(this->luaState));
+
+ if (response.is<std::string>()) {
+ return {response.as<std::string>()};
+ } else if (response.is<sol::table>()) {
+ sol::table tbl = response.as<sol::table>();
+ std::vector<std::string> items;
+
+ for (auto &kv : tbl) {
+ sol::object value = kv.second;
+ if (value.is<std::string>()) {
+ items.push_back(value.as<std::string>());
+ }
+ }
+
+ return items;
+ }
+
+ return {};
+ }
+} \ No newline at end of file
diff --git a/bot/src/commands/lua.hpp b/bot/src/commands/lua.hpp
new file mode 100644
index 0000000..11e98d3
--- /dev/null
+++ b/bot/src/commands/lua.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <sol/sol.hpp>
+#include <sol/state.hpp>
+#include <sol/table.hpp>
+#include <sol/types.hpp>
+#include <string>
+#include <vector>
+
+#include "commands/command.hpp"
+#include "commands/response.hpp"
+#include "schemas/user.hpp"
+
+void print_lua_object_type(const sol::object &obj);
+
+namespace bot::command::lua {
+ class LuaCommand : public Command {
+ public:
+ LuaCommand(std::shared_ptr<sol::state> luaState,
+ const std::string &content);
+ ~LuaCommand() = default;
+
+ Response run(const InstanceBundle &bundle,
+ const Request &request) const override;
+
+ std::string get_name() const override { return this->name; }
+ int get_delay_seconds() const override { return this->delay; }
+ schemas::PermissionLevel get_permission_level() const override {
+ return this->level;
+ }
+ std::vector<std::string> get_subcommand_ids() const override {
+ return this->subcommands;
+ }
+
+ private:
+ std::string name;
+ int delay;
+ schemas::PermissionLevel level;
+ std::vector<std::string> subcommands;
+
+ sol::function handle;
+
+ std::shared_ptr<sol::state> luaState;
+ };
+} \ No newline at end of file
diff --git a/bot/src/commands/request.cpp b/bot/src/commands/request.cpp
new file mode 100644
index 0000000..7acc107
--- /dev/null
+++ b/bot/src/commands/request.cpp
@@ -0,0 +1,28 @@
+#include "commands/request.hpp"
+
+#include <sol/types.hpp>
+
+namespace bot::command {
+ sol::table Request::as_lua_table(std::shared_ptr<sol::state> luaState) const {
+ sol::table o = luaState->create_table();
+
+ o["command_id"] = this->command_id;
+ if (this->subcommand_id.has_value()) {
+ o["subcommand_id"] = this->subcommand_id.value();
+ } else {
+ o["subcommand_id"] = sol::lua_nil;
+ }
+ if (this->message.has_value()) {
+ o["message"] = this->message.value();
+ } else {
+ o["message"] = sol::lua_nil;
+ }
+
+ o["sender"] = this->user.as_lua_table(luaState);
+ o["channel"] = this->channel.as_lua_table(luaState);
+ o["channel_preference"] = this->channel_preferences.as_lua_table(luaState);
+ o["rights"] = this->user_rights.as_lua_table(luaState);
+
+ return o;
+ }
+} \ No newline at end of file
diff --git a/bot/src/commands/request.hpp b/bot/src/commands/request.hpp
index e2685f1..b6ed534 100644
--- a/bot/src/commands/request.hpp
+++ b/bot/src/commands/request.hpp
@@ -1,7 +1,10 @@
#pragma once
+#include <memory>
#include <optional>
#include <pqxx/pqxx>
+#include <sol/state.hpp>
+#include <sol/table.hpp>
#include <string>
#include "../irc/message.hpp"
@@ -21,5 +24,7 @@ namespace bot::command {
schemas::UserRights user_rights;
pqxx::connection &conn;
+
+ sol::table as_lua_table(std::shared_ptr<sol::state> luaState) const;
};
}
diff --git a/bot/src/main.cpp b/bot/src/main.cpp
index a95e4a8..212d190 100644
--- a/bot/src/main.cpp
+++ b/bot/src/main.cpp
@@ -1,5 +1,7 @@
+#include <memory>
#include <optional>
#include <pqxx/pqxx>
+#include <sol/state.hpp>
#include <string>
#include <thread>
#include <vector>
@@ -7,6 +9,8 @@
#include "api/twitch/helix_client.hpp"
#include "bundle.hpp"
#include "commands/command.hpp"
+#include "commands/lua.hpp"
+#include "commands/response.hpp"
#include "config.hpp"
#include "github.hpp"
#include "handlers.hpp"
@@ -49,6 +53,8 @@ int main(int argc, char *argv[]) {
bot::irc::Client client(cfg.twitch_credentials.client_id,
cfg.twitch_credentials.token);
bot::command::CommandLoader command_loader;
+ command_loader.load_lua_directory("luamods");
+
bot::loc::Localization localization("localization");
bot::api::twitch::HelixClient helix_client(cfg.twitch_credentials.token,
cfg.twitch_credentials.client_id);
diff --git a/bot/src/schemas/channel.cpp b/bot/src/schemas/channel.cpp
index ae4ea53..540b7c0 100644
--- a/bot/src/schemas/channel.cpp
+++ b/bot/src/schemas/channel.cpp
@@ -1,5 +1,7 @@
#include "channel.hpp"
+#include <optional>
+
namespace bot::schemas {
std::optional<ChannelFeature> string_to_channel_feature(
const std::string &value) {
@@ -11,4 +13,62 @@ namespace bot::schemas {
return std::nullopt;
}
}
+
+ std::optional<std::string> channelfeature_to_string(
+ const ChannelFeature &value) {
+ switch (value) {
+ case MARKOV_RESPONSES:
+ return "markov_responses";
+ case RANDOM_MARKOV_RESPONSES:
+ return "random_markov_responses";
+ default:
+ std::nullopt;
+ }
+ }
+
+ sol::table Channel::as_lua_table(std::shared_ptr<sol::state> luaState) const {
+ sol::table o = luaState->create_table();
+
+ o["id"] = this->id;
+ o["alias_id"] = this->alias_id;
+ o["alias_name"] = this->alias_name;
+
+ o["joined_at"] =
+ static_cast<long>(std::chrono::duration_cast<std::chrono::seconds>(
+ this->joined_at.time_since_epoch())
+ .count());
+ if (this->opted_out_at.has_value()) {
+ o["opted_out_at"] =
+ static_cast<long>(std::chrono::duration_cast<std::chrono::seconds>(
+ this->opted_out_at->time_since_epoch())
+ .count());
+ } else {
+ o["opted_out_at"] = sol::lua_nil;
+ }
+
+ return o;
+ }
+
+ sol::table ChannelPreferences::as_lua_table(
+ std::shared_ptr<sol::state> luaState) const {
+ sol::table o = luaState->create_table();
+
+ o["id"] = this->channel_id; // TODO: remove it later too.
+ o["channel_id"] = this->channel_id;
+ o["prefix"] = this->prefix;
+ o["language"] = this->locale;
+
+ sol::table f = luaState->create_table();
+
+ for (const ChannelFeature &feature : this->features) {
+ std::optional<std::string> ff = channelfeature_to_string(feature);
+ if (ff.has_value()) {
+ f.add(ff.value());
+ }
+ }
+
+ o["features"] = f;
+
+ return o;
+ }
} \ No newline at end of file
diff --git a/bot/src/schemas/channel.hpp b/bot/src/schemas/channel.hpp
index 08ed8ad..d2f13eb 100644
--- a/bot/src/schemas/channel.hpp
+++ b/bot/src/schemas/channel.hpp
@@ -3,6 +3,7 @@
#include <chrono>
#include <optional>
#include <pqxx/pqxx>
+#include <sol/sol.hpp>
#include <string>
#include <vector>
@@ -40,6 +41,8 @@ namespace bot::schemas {
return this->opted_out_at;
}
+ sol::table as_lua_table(std::shared_ptr<sol::state> luaState) const;
+
private:
int id, alias_id;
std::string alias_name;
@@ -50,6 +53,8 @@ namespace bot::schemas {
enum ChannelFeature { MARKOV_RESPONSES, RANDOM_MARKOV_RESPONSES };
std::optional<ChannelFeature> string_to_channel_feature(
const std::string &value);
+ std::optional<std::string> channelfeature_to_string(
+ const ChannelFeature &value);
class ChannelPreferences {
public:
@@ -90,6 +95,8 @@ namespace bot::schemas {
return this->features;
}
+ sol::table as_lua_table(std::shared_ptr<sol::state> luaState) const;
+
private:
int channel_id;
std::string prefix, locale;
diff --git a/bot/src/schemas/user.cpp b/bot/src/schemas/user.cpp
new file mode 100644
index 0000000..bbf5711
--- /dev/null
+++ b/bot/src/schemas/user.cpp
@@ -0,0 +1,42 @@
+#include "schemas/user.hpp"
+
+#include <chrono>
+#include <sol/types.hpp>
+
+namespace bot::schemas {
+ sol::table User::as_lua_table(std::shared_ptr<sol::state> luaState) const {
+ sol::table o = luaState->create_table();
+
+ o["id"] = this->id;
+ o["alias_id"] = this->alias_id;
+ o["alias_name"] = this->alias_name;
+
+ o["joined_at"] =
+ static_cast<long>(std::chrono::duration_cast<std::chrono::seconds>(
+ this->joined_at.time_since_epoch())
+ .count());
+ if (this->opted_out_at.has_value()) {
+ o["opted_out_at"] =
+ static_cast<long>(std::chrono::duration_cast<std::chrono::seconds>(
+ this->opted_out_at->time_since_epoch())
+ .count());
+ } else {
+ o["opted_out_at"] = sol::lua_nil;
+ }
+
+ return o;
+ }
+
+ sol::table UserRights::as_lua_table(
+ std::shared_ptr<sol::state> luaState) const {
+ sol::table o = luaState->create_table();
+
+ o["id"] = this->id;
+ o["user_id"] = this->user_id;
+ o["channel_id"] = this->channel_id;
+ o["level"] = this->level;
+ o["is_fixed"] = false; // TODO: remove it later
+
+ return o;
+ }
+} \ No newline at end of file
diff --git a/bot/src/schemas/user.hpp b/bot/src/schemas/user.hpp
index 0bd1368..e9d7e0f 100644
--- a/bot/src/schemas/user.hpp
+++ b/bot/src/schemas/user.hpp
@@ -3,6 +3,7 @@
#include <chrono>
#include <optional>
#include <pqxx/pqxx>
+#include <sol/sol.hpp>
#include <string>
#include "../utils/chrono.hpp"
@@ -40,6 +41,8 @@ namespace bot::schemas {
return this->opted_out_at;
}
+ sol::table as_lua_table(std::shared_ptr<sol::state> luaState) const;
+
private:
int id, alias_id;
std::string alias_name;
@@ -66,6 +69,8 @@ namespace bot::schemas {
const PermissionLevel &get_level() const { return this->level; }
void set_level(PermissionLevel level) { this->level = level; }
+ sol::table as_lua_table(std::shared_ptr<sol::state> luaState) const;
+
private:
int id, user_id, channel_id;
PermissionLevel level;
diff --git a/lib/lua/CMakeLists.txt b/lib/lua/CMakeLists.txt
new file mode 100644
index 0000000..6f90b2b
--- /dev/null
+++ b/lib/lua/CMakeLists.txt
@@ -0,0 +1,48 @@
+### lua
+add_library(lua
+ src/lapi.c
+ src/lauxlib.c
+ src/lbaselib.c
+ src/lcode.c
+ src/lcorolib.c
+ src/lctype.c
+ src/ldblib.c
+ src/ldebug.c
+ src/ldo.c
+ src/ldump.c
+ src/lfunc.c
+ src/lgc.c
+ src/linit.c
+ src/liolib.c
+ src/llex.c
+ src/lmathlib.c
+ src/lmem.c
+ src/loadlib.c
+ src/lobject.c
+ src/lopcodes.c
+ src/loslib.c
+ src/lparser.c
+ src/lstate.c
+ src/lstring.c
+ src/lstrlib.c
+ src/ltable.c
+ src/ltablib.c
+ src/ltests.c
+ src/ltm.c
+ src/lua.c
+ src/lundump.c
+ src/lutf8lib.c
+ src/lvm.c
+ src/lzio.c
+)
+add_library(lua::lua ALIAS lua)
+
+if (WIN32)
+ target_compile_definitions(lua PRIVATE LUA_USE_WINDOWS)
+endif()
+
+target_include_directories(lua PUBLIC src)
+if (UNIX)
+ target_compile_definitions(lua PRIVATE LUA_USE_LINUX)
+ target_link_libraries(lua PRIVATE dl)
+endif() \ No newline at end of file
diff --git a/luamods/hello.lua b/luamods/hello.lua
new file mode 100644
index 0000000..5facbf5
--- /dev/null
+++ b/luamods/hello.lua
@@ -0,0 +1,10 @@
+return {
+ name = "hello",
+ delay_sec = 5,
+ options = {},
+ subcommands = {},
+ minimal_rights = "user",
+ handle = function(request)
+ return "hello, " .. request.sender.alias_name .. "!"
+ end
+}