diff --git a/.gitignore b/.gitignore index fc45dfb..e9b86b7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,16 @@ # Data files /TriviaBot/data_management/questions -/TriviaBot/bot/db/trivia.db \ No newline at end of file +/TriviaBot/bot/db/trivia.db + +# Config file +config.json + +# Compiled sqlite file +sqlite3.obj + +# V8 lib +lib/v8 + +# Built files +build/ diff --git a/README.md b/README.md index 7502a69..d96aab7 100644 --- a/README.md +++ b/README.md @@ -32,22 +32,23 @@ LoadDB.cpp takes some time to execute. ### Commands `` `trivia`` is the base command. -| Argument | Description | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `questions` `interval` | Where `questions` and `interval` are integers. Makes the game last `questions` number of questions, optionally sets the time interval between hints to `interval` seconds. | -| stop | Stops the trivia game currently in the channel the message is sent from, if there is one. | -| help | Prints a help list, similar to this table. | +| Argument | Description | +| --- | --- | +| `questions` `interval` | Where `questions` and `interval` are integers. Makes the game last `questions` number of questions, optionally sets the time interval between hints to `interval` seconds. | +| stop | Stops the trivia game currently in the channel the message is sent from, if there is one. | +| help | Prints a help list, similar to this table. | ### Compiling #### Dependencies -| Name | Website | Notes | -| ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -| boost | [boost.org](http://www.boost.org/) | | -| websocketpp | [zaphoyd/websocketpp](https://github.com/zaphoyd/websocketpp) | Included as submodule. | -| cURL | [curl.haxx.se](https://curl.haxx.se/) | | -| sqlite3 | [sqlite.org](https://www.sqlite.org/) | Included as submodule. Uses a [different repo](https://github.com/azadkuh/sqlite-amalgamation/). | -| nlohmann/json | [nlohmann/json](https://github.com/nlohmann/json) | (Slightly modified) source file included in repo. | +| Name | Website | Notes | +| --- | --- | --- | +| boost | [boost.org](http://www.boost.org/) | | +| websocketpp | [zaphoyd/websocketpp](https://github.com/zaphoyd/websocketpp) | Included as submodule. | +| cURL | [curl.haxx.se](https://curl.haxx.se/) | | +| sqlite3 | [sqlite.org](https://www.sqlite.org/) | Included as submodule. Uses a [different repo](https://github.com/azadkuh/sqlite-amalgamation/). | +| nlohmann/json | [nlohmann/json](https://github.com/nlohmann/json) | (Slightly modified) source file included in repo. | +| V8 | [Google V8](https://developers.google.com/v8/) | Debian/Ubuntu `libv8` packages are too outdated. | #### Linux (debian) c++14 support is required. gcc 5 and above recommended. diff --git a/TriviaBot/CMakeLists.txt b/TriviaBot/CMakeLists.txt index 001e4ac..00b718f 100644 --- a/TriviaBot/CMakeLists.txt +++ b/TriviaBot/CMakeLists.txt @@ -7,6 +7,8 @@ project(TriviaBot) file(GLOB_RECURSE sources bot/*.cpp bot/*.hpp ../lib/sqlite3/sqlite3.c) +link_directories(../lib/v8/lib) + ############################################################################### ## target definitions ######################################################### ############################################################################### @@ -29,8 +31,14 @@ target_link_libraries(TriviaBot PUBLIC ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} ${CURL_LIBRARIES} - pthread + v8 + v8_libplatform + v8_libbase + icui18n + icuuc + rt dl + pthread ) include_directories( @@ -39,7 +47,10 @@ include_directories( ${CURL_INCLUDE_DIR} ../lib/websocketpp ../lib/sqlite3 + ../lib/v8 ) # don't know if necessary, too scared to remove -add_definitions(-D_WEBSOCKETPP_CPP11_STL_) \ No newline at end of file +add_definitions(-D_WEBSOCKETPP_CPP11_STL_) + +set(CMAKE_BUILD_TYPE Release) diff --git a/TriviaBot/bot/APIHelper.cpp b/TriviaBot/bot/APIHelper.cpp deleted file mode 100644 index f4b0703..0000000 --- a/TriviaBot/bot/APIHelper.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "http/HTTPHelper.hpp" - -#include -#include -#include - -#include "APIHelper.hpp" - -using namespace std::chrono_literals; - -APIHelper::APIHelper() : BASE_URL("https://discordapp.com/api"), CHANNELS_URL(BASE_URL + "/channels"), - JSON_CTYPE("application/json") { - http = new HTTPHelper(); -} - -void APIHelper::send_message(std::string channel_id, std::string message) { - const std::string url = CHANNELS_URL + "/" + channel_id + "/messages"; - json data = { - { "content", message } - }; - - std::string response; - long response_code = -1; - response = http->post_request(url, JSON_CTYPE, data.dump(), &response_code); - - int retries = 0; - while (response_code != 200 && retries < 2) { - std::this_thread::sleep_for(100ms); - // try 3 times. usually enough to prevent 502 bad gateway issues - response = http->post_request(url, JSON_CTYPE, data.dump(), &response_code); - retries++; - } -} \ No newline at end of file diff --git a/TriviaBot/bot/APIHelper.hpp b/TriviaBot/bot/APIHelper.hpp deleted file mode 100644 index 5859463..0000000 --- a/TriviaBot/bot/APIHelper.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef BOT_APIHELPER -#define BOT_APIHELPER - -#include - -#include "json/json.hpp" - -using json = nlohmann::json; - -class HTTPHelper; - -class APIHelper { -public: - APIHelper(); - - void send_message(std::string channel_id, std::string message); - -private: - const std::string BASE_URL; - const std::string CHANNELS_URL; - const std::string JSON_CTYPE; - - HTTPHelper *http; -}; - -#endif \ No newline at end of file diff --git a/TriviaBot/bot/BotConfig.cpp b/TriviaBot/bot/BotConfig.cpp new file mode 100644 index 0000000..ed55ad5 --- /dev/null +++ b/TriviaBot/bot/BotConfig.cpp @@ -0,0 +1,60 @@ +#include "BotConfig.hpp" + +#include +#include +#include + +#include "json/json.hpp" + +#include "Logger.hpp" + +using json = nlohmann::json; + +BotConfig::BotConfig() { + is_new_config = false; + std::stringstream ss; + + std::ifstream config_file("config.json"); + if(!config_file) { + config_file.close(); + create_new_file(); + return; + } + + ss << config_file.rdbuf(); + config_file.close(); + std::string config = ss.str(); + load_from_json(config); +} + +void BotConfig::load_from_json(std::string data) { + json parsed = json::parse(data); + + token = parsed.value("bot_token", ""); + owner_id = parsed.value("owner_id", ""); + cert_location = parsed.value("api_cert_file", "bot/http/DiscordCA.crt"); + + createjs_roles = parsed["v8"].value("createjs_allowed_roles", std::unordered_set { "Admin", "Coder" }); + + Logger::write("config.json file loaded", Logger::LogLevel::Info); +} + +void BotConfig::create_new_file() { + std::string config = json { + { "bot_token", "" }, + { "owner_id", "" }, + { "api_cert_file", "bot/http/DiscordCA.crt" }, + { "v8", { + { "createjs_allowed_roles", { + "Admin", "Coder", "Bot Commander" + } } + } } + }.dump(4); + + std::ofstream config_file("config.json"); + config_file << config; + config_file.close(); + + Logger::write("Created new config.json file", Logger::LogLevel::Info); + is_new_config = true; +} \ No newline at end of file diff --git a/TriviaBot/bot/BotConfig.hpp b/TriviaBot/bot/BotConfig.hpp new file mode 100644 index 0000000..23dc842 --- /dev/null +++ b/TriviaBot/bot/BotConfig.hpp @@ -0,0 +1,23 @@ +#ifndef BOT_BOTCONFIG +#define BOT_BOTCONFIG + +#include +#include + +class BotConfig { +public: + BotConfig(); + + bool is_new_config; + + std::string token; + std::string owner_id; + std::string cert_location; + std::unordered_set createjs_roles; + +private: + void load_from_json(std::string data); + void create_new_file(); +}; + +#endif diff --git a/TriviaBot/bot/ClientConnection.cpp b/TriviaBot/bot/ClientConnection.cpp index a0a3143..b148d4b 100644 --- a/TriviaBot/bot/ClientConnection.cpp +++ b/TriviaBot/bot/ClientConnection.cpp @@ -3,73 +3,74 @@ #include #include -#include "GatewayHandler.hpp" +#include "Logger.hpp" +#include "BotConfig.hpp" -ClientConnection::ClientConnection() { +ClientConnection::ClientConnection(BotConfig &c) : config(c), gh(config) { // Reset the log channels - c.clear_access_channels(websocketpp::log::alevel::all); + cli.clear_access_channels(websocketpp::log::alevel::all); // Only want application logging, logging from the initial connection stages or any error logging - c.set_access_channels(websocketpp::log::alevel::app | websocketpp::log::alevel::connect); - c.set_error_channels(websocketpp::log::elevel::all); + cli.set_access_channels(websocketpp::log::alevel::app | websocketpp::log::alevel::connect); + cli.set_error_channels(websocketpp::log::elevel::all); // Initialize ASIO - c.init_asio(); + cli.init_asio(); // Bind handlers - c.set_socket_init_handler(bind( + cli.set_socket_init_handler(bind( &ClientConnection::on_socket_init, this, websocketpp::lib::placeholders::_1 )); - c.set_tls_init_handler(bind( + cli.set_tls_init_handler(bind( &ClientConnection::on_tls_init, this, websocketpp::lib::placeholders::_1 )); - c.set_message_handler(bind( + cli.set_message_handler(bind( &ClientConnection::on_message, this, websocketpp::lib::placeholders::_1, websocketpp::lib::placeholders::_2 )); - c.set_open_handler(bind( + cli.set_open_handler(bind( &ClientConnection::on_open, this, websocketpp::lib::placeholders::_1 )); - c.set_close_handler(bind( + cli.set_close_handler(bind( &ClientConnection::on_close, this, websocketpp::lib::placeholders::_1 )); - c.set_fail_handler(bind( + cli.set_fail_handler(bind( &ClientConnection::on_fail, this, websocketpp::lib::placeholders::_1 )); - - gh = std::make_unique(); } // Open a connection to the URI provided void ClientConnection::start(std::string uri) { websocketpp::lib::error_code ec; - client::connection_ptr con = c.get_connection(uri, ec); + client::connection_ptr con = cli.get_connection(uri, ec); if (ec) { // failed to create connection - c.get_alog().write(websocketpp::log::alevel::app, ec.message()); + Logger::write("Failed to create connection: " + ec.message(), Logger::LogLevel::Severe); return; } // Open the connection - c.connect(con); - c.run(); + cli.connect(con); + cli.run(); + + Logger::write("Finished running", Logger::LogLevel::Debug); } // Event handlers void ClientConnection::on_socket_init(websocketpp::connection_hdl) { - c.get_alog().write(websocketpp::log::alevel::app, "Socket intialised."); + Logger::write("Socket initialised", Logger::LogLevel::Debug); } context_ptr ClientConnection::on_tls_init(websocketpp::connection_hdl) { @@ -81,41 +82,43 @@ context_ptr ClientConnection::on_tls_init(websocketpp::connection_hdl) { boost::asio::ssl::context::no_sslv3 | boost::asio::ssl::context::single_dh_use); } - catch (std::exception& e) { - std::cout << "Error in context pointer: " << e.what() << std::endl; + catch (std::exception &e) { + Logger::write("[tls] Error in context pointer: " + std::string(e.what()), Logger::LogLevel::Severe); } return ctx; } void ClientConnection::on_fail(websocketpp::connection_hdl hdl) { - client::connection_ptr con = c.get_con_from_hdl(hdl); + client::connection_ptr con = cli.get_con_from_hdl(hdl); // Print as much information as possible - c.get_elog().write(websocketpp::log::elevel::warn, - "Fail handler: \n" + + Logger::write("Fail handler: \n" + std::to_string(con->get_state()) + "\n" + std::to_string(con->get_local_close_code()) + "\n" + con->get_local_close_reason() + "\n" + std::to_string(con->get_remote_close_code()) + "\n" + con->get_remote_close_reason() + "\n" + - std::to_string(con->get_ec().value()) + " - " + con->get_ec().message() + "\n"); + std::to_string(con->get_ec().value()) + " - " + con->get_ec().message() + "\n", + Logger::LogLevel::Severe); } void ClientConnection::on_open(websocketpp::connection_hdl hdl) { + Logger::write("Connection opened", Logger::LogLevel::Debug); } void ClientConnection::on_message(websocketpp::connection_hdl hdl, message_ptr message) { if (message->get_opcode() != websocketpp::frame::opcode::text) { // If the message is not text, just print as hex - std::cout << "<< " << websocketpp::utility::to_hex(message->get_payload()) << std::endl; + Logger::write("Non-text message received: " + websocketpp::utility::to_hex(message->get_payload()), Logger::LogLevel::Warning); return; } // Pass the message to the gateway handler - gh->handle_data(message->get_payload(), c, hdl); + gh.handle_data(message->get_payload(), cli, hdl); } void ClientConnection::on_close(websocketpp::connection_hdl) { - std::cout << "Closed." << std::endl; + Logger::write("Connection closed", Logger::LogLevel::Info); + cli.stop(); } \ No newline at end of file diff --git a/TriviaBot/bot/ClientConnection.hpp b/TriviaBot/bot/ClientConnection.hpp index 546ae7f..67faa80 100644 --- a/TriviaBot/bot/ClientConnection.hpp +++ b/TriviaBot/bot/ClientConnection.hpp @@ -5,6 +5,8 @@ #include #include "json/json.hpp" +#include "GatewayHandler.hpp" + typedef websocketpp::client client; using websocketpp::lib::bind; @@ -14,15 +16,20 @@ typedef websocketpp::config::asio_tls_client::message_type::ptr message_ptr; typedef websocketpp::lib::shared_ptr context_ptr; typedef client::connection_ptr connection_ptr; -#include "GatewayHandler.hpp" +class BotConfig; class ClientConnection { public: - ClientConnection(); + ClientConnection(BotConfig &c); // Open a connection to the URI provided void start(std::string uri); +private: + client cli; + BotConfig &config; + GatewayHandler gh; + // Event handlers void on_socket_init(websocketpp::connection_hdl); context_ptr on_tls_init(websocketpp::connection_hdl); @@ -30,10 +37,6 @@ public: void on_open(websocketpp::connection_hdl hdl); void on_message(websocketpp::connection_hdl hdl, message_ptr message); void on_close(websocketpp::connection_hdl); - -private: - client c; - std::unique_ptr gh; }; #endif \ No newline at end of file diff --git a/TriviaBot/bot/DiscordAPI.cpp b/TriviaBot/bot/DiscordAPI.cpp new file mode 100644 index 0000000..6bbbfc7 --- /dev/null +++ b/TriviaBot/bot/DiscordAPI.cpp @@ -0,0 +1,84 @@ +#include "DiscordAPI.hpp" + +#include +#include +#include + +#include "http/HTTP.hpp" +#include "Logger.hpp" + +using namespace std::chrono_literals; + +namespace DiscordAPI { + const std::string base_url = "https://discordapp.com/api"; + const std::string channels_url = base_url + "/channels"; + const std::string gateway_url = base_url + "/gateway"; + + const std::string json_mime_type = "application/json"; + + void send_message(std::string channel_id, std::string message, std::string token, std::string ca_location) { + if (message == "") { + Logger::write("[API] [send_message] Tried to send empty message", Logger::LogLevel::Warning); + return; + } + + if (message.length() > 4000) { + Logger::write("[API] [send_message] Tried to send a message over 4000 characters", Logger::LogLevel::Warning); + return; + } + else if (message.length() > 2000) { + std::cout << message.length() << std::endl; + + std::string first = message.substr(0, 2000); + std::string second = message.substr(2000); + send_message(channel_id, first, token, ca_location); + std::this_thread::sleep_for(50ms); + send_message(channel_id, second, token, ca_location); + return; + } + + const std::string url = channels_url + "/" + channel_id + "/messages"; + json data = { + { "content", message } + }; + + std::string response; + long response_code = 0; + response = HTTP::post_request(url, json_mime_type, data.dump(), &response_code, token, ca_location); + + int retries = 0; + while (response_code != 200 && retries < 2) { + Logger::write("[API] [send_message] Got non-200 response code, retrying", Logger::LogLevel::Warning); + std::this_thread::sleep_for(100ms); + // try 3 times. usually enough to prevent 502 bad gateway issues + response = HTTP::post_request(url, json_mime_type, data.dump(), &response_code, token, ca_location); + retries++; + } + + if (response_code != 200) { + Logger::write("[API] [send_message] Giving up on sending message", Logger::LogLevel::Warning); + } + } + + json get_gateway(std::string ca_location) { + std::string response; + long response_code; + response = HTTP::get_request(gateway_url, &response_code, "", ca_location); + + int retries = 0; + while (response_code != 200 && retries < 4) { + Logger::write("[API] [get_gateway] Got non-200 response code, retrying", Logger::LogLevel::Warning); + std::this_thread::sleep_for(100ms); + // try 3 times. usually enough to prevent 502 bad gateway issues + response = HTTP::get_request(gateway_url, &response_code, "", ca_location); + retries++; + } + + if (response_code != 200) { + Logger::write("[API] [get_gateway] Giving up on getting gateway url", Logger::LogLevel::Warning); + return json {}; + } + + return json::parse(response); + } +} \ No newline at end of file diff --git a/TriviaBot/bot/DiscordAPI.hpp b/TriviaBot/bot/DiscordAPI.hpp new file mode 100644 index 0000000..a5005fe --- /dev/null +++ b/TriviaBot/bot/DiscordAPI.hpp @@ -0,0 +1,17 @@ +#ifndef BOT_APIHELPER +#define BOT_APIHELPER + +#include + +#include "json/json.hpp" + +using json = nlohmann::json; + +class BotConfig; + +namespace DiscordAPI { + json get_gateway(std::string ca_location); + void send_message(std::string channel_id, std::string message, std::string token, std::string ca_location); +} + +#endif \ No newline at end of file diff --git a/TriviaBot/bot/GatewayHandler.cpp b/TriviaBot/bot/GatewayHandler.cpp index f15ab29..9a1d0e4 100644 --- a/TriviaBot/bot/GatewayHandler.cpp +++ b/TriviaBot/bot/GatewayHandler.cpp @@ -2,15 +2,15 @@ #include -#include "APIHelper.hpp" -#include "data_structures/User.hpp" +#include "DiscordAPI.hpp" +#include "Logger.hpp" +#include "data_structures/GuildMember.hpp" +#include "BotConfig.hpp" -extern std::string bot_token; - -GatewayHandler::GatewayHandler() { +GatewayHandler::GatewayHandler(BotConfig &c) : config(c) { last_seq = 0; - ah = new APIHelper(); + CommandHelper::init(); } void GatewayHandler::handle_data(std::string data, client &c, websocketpp::connection_hdl &hdl) { @@ -26,14 +26,22 @@ void GatewayHandler::handle_data(std::string data, client &c, websocketpp::conne on_hello(decoded, c, hdl); break; case 11: - c.get_alog().write(websocketpp::log::alevel::app, "Heartbeat acknowledged."); + Logger::write("Heartbeat acknowledged", Logger::LogLevel::Debug); break; } } -void GatewayHandler::heartbeat(client *c, websocketpp::connection_hdl hdl, int interval) { +void GatewayHandler::send_heartbeat(client *c, websocketpp::connection_hdl hdl, int interval) { while (true) { boost::this_thread::sleep(boost::posix_time::milliseconds(interval)); + if (!c) { + Logger::write("[send_heartbeat] Client pointer is null", Logger::LogLevel::Severe); + break; + } + else if (c->stopped()) { + break; + } + json heartbeat = { { "op", 1 }, @@ -42,18 +50,53 @@ void GatewayHandler::heartbeat(client *c, websocketpp::connection_hdl hdl, int i c->send(hdl, heartbeat.dump(), websocketpp::frame::opcode::text); - c->get_alog().write(websocketpp::log::alevel::app, "Sent heartbeat. (seq: " + std::to_string(last_seq) + ")"); + Logger::write("Sent heartbeat (seq: " + std::to_string(last_seq) + ")", Logger::LogLevel::Debug); } } +void GatewayHandler::send_identify(client &c, websocketpp::connection_hdl &hdl) { + json identify = { + { "op", 2 }, + { "d", { + { "token", config.token }, + { "properties",{ + { "$browser", "Microsoft Windows 10" }, + { "$device", "TriviaBot-0.0" }, + { "$referrer", "" }, + { "$referring_domain", "" } + } }, + { "compress", false }, + { "large_threshold", 250 }, + { "shard",{ 0, 1 } } + } } + }; + + c.send(hdl, identify.dump(), websocketpp::frame::opcode::text); + Logger::write("Sent identify payload", Logger::LogLevel::Debug); +} + +void GatewayHandler::send_request_guild_members(client &c, websocketpp::connection_hdl &hdl, std::string guild_id) { + json request_guild_members = { + { "op", 8 }, + { "d", { + { "guild_id", guild_id }, + { "query", "" }, + { "limit", 0 } + } } + }; + + c.send(hdl, request_guild_members.dump(), websocketpp::frame::opcode::text); + Logger::write("Requested guild members for " + guild_id, Logger::LogLevel::Debug); +} + void GatewayHandler::on_hello(json decoded, client &c, websocketpp::connection_hdl &hdl) { heartbeat_interval = decoded["d"]["heartbeat_interval"]; - c.get_alog().write(websocketpp::log::alevel::app, "Heartbeat interval: " + std::to_string(heartbeat_interval / 1000.0f) + " seconds"); + Logger::write("Heartbeat interval: " + std::to_string(heartbeat_interval / 1000.0f) + " seconds", Logger::LogLevel::Debug); - heartbeat_thread = std::make_unique(boost::bind(&GatewayHandler::heartbeat, this, &c, hdl, heartbeat_interval)); + heartbeat_thread = std::make_unique(boost::bind(&GatewayHandler::send_heartbeat, this, &c, hdl, heartbeat_interval)); - identify(c, hdl); + send_identify(c, hdl); } void GatewayHandler::on_dispatch(json decoded, client &c, websocketpp::connection_hdl &hdl) { @@ -62,125 +105,554 @@ void GatewayHandler::on_dispatch(json decoded, client &c, websocketpp::connectio json data = decoded["d"]; if (event_name == "READY") { - user_object.load_from_json(data["user"]); - - c.get_alog().write(websocketpp::log::alevel::app, "Sign-on confirmed. (@" + user_object.username + "#" + user_object.discriminator + ")"); + on_event_ready(data); } else if (event_name == "GUILD_CREATE") { - std::string guild_id = data["id"]; - try { - guilds[guild_id] = std::make_unique(data); - } - catch (std::domain_error err) { - // this doesn't even work - c.get_alog().write(websocketpp::log::elevel::rerror, "Domain error"); - } - - - c.get_alog().write(websocketpp::log::alevel::app, "Loaded guild: " + guilds[guild_id]->name); - - for (json channel : data["channels"]) { - std::string channel_id = channel["id"]; - channel["guild_id"] = guild_id; - // create channel obj, add to overall channel list - channels[channel_id] = std::make_shared(channel); - // add ptr to said channel list to guild's channel list - guilds[guild_id]->channels.push_back(std::shared_ptr(channels[channel_id])); - } + on_event_guild_create(data); + } + else if (event_name == "GUILD_UPDATE") { + on_event_guild_update(data); + } + else if (event_name == "GUILD_DELETE") { + on_event_guild_delete(data); + } + else if (event_name == "GUILD_MEMBER_ADD") { + on_event_guild_member_add(data); + } + else if (event_name == "GUILD_MEMBER_UPDATE") { + on_event_guild_member_update(data); + } + else if (event_name == "GUILD_MEMBER_REMOVE") { + on_event_guild_member_remove(data); + } + else if (event_name == "GUILD_ROLE_CREATE") { + on_event_guild_role_create(data); + } + else if (event_name == "GUILD_ROLE_UPDATE") { + on_event_guild_role_update(data); + } + else if (event_name == "GUILD_ROLE_DELETE") { + on_event_guild_role_delete(data); + } + else if (event_name == "CHANNEL_CREATE") { + on_event_channel_create(data); + } + else if (event_name == "CHANNEL_UPDATE") { + on_event_channel_update(data); + } + else if (event_name == "CHANNEL_DELETE") { + on_event_channel_delete(data); } - else if (event_name == "TYPING_START") {} else if (event_name == "MESSAGE_CREATE") { - std::string message = data["content"]; - auto channel = channels[data["channel_id"]]; - - DiscordObjects::User sender(data["author"]); - - std::vector words; - boost::split(words, message, boost::is_any_of(" ")); - if (words[0] == "`trivia" || words[0] == "`t") { - int questions = 10; - int delay = 8; - - if (words.size() > 3) { - ah->send_message(channel->id, ":exclamation: Invalid arguments!"); - return; - } - else if(words.size() > 1) { - if (words[1] == "help" || words[1] == "h") { - std::string help = "**Base command \\`t[rivia]**. Arguments:\n"; - help += "\\`trivia **{x}** **{y}**: Makes the game last **x** number of questions, optionally sets the time interval between hints to **y** seconds\n"; - help += "\\`trivia **stop**: stops the ongoing game.\n"; - help += "\\`trivia **help**: prints this message\n"; - - ah->send_message(channel->id, help); - return; - } - else if (words[1] == "stop" || words[1] == "s") { - if (games.find(channel->id) != games.end()) { - delete_game(channel->id); - return; - } - } - else { - try { - questions = std::stoi(words[1]); - if (words.size() == 3) { - delay = std::stoi(words[2]); - } - } - catch (std::invalid_argument e) { - ah->send_message(channel->id, ":exclamation: Invalid arguments!"); - return; - } - } - } - - games[channel->id] = std::make_unique(this, ah, channel->id, questions, delay); - games[channel->id]->start(); - } - else if (words[0] == "`channels") { - std::string m = "Channel List:\n"; - for (auto ch : channels) { - m += "> " + ch.second->name + " (" + ch.second->id + ") [" + ch.second->type + "] Guild: " - + guilds[ch.second->guild_id]->name + " (" + ch.second->guild_id + ")\n"; - } - ah->send_message(channel->id, m); - } - else if (words[0] == "`guilds") { - std::string m = "Guild List:\n"; - for (auto &gu : guilds) { - m += "> " + gu.second->name + " (" + gu.second->id + ") Channels: " + std::to_string(gu.second->channels.size()) + "\n"; - } - ah->send_message(channel->id, m); - } - else if (words[0] == "`info") { - ah->send_message(channel->id, ":information_source: trivia-bot by Jack. "); - } - else if (games.find(channel->id) != games.end()) { // message received in channel with ongoing game - games[channel->id]->handle_answer(message, sender); - } + on_event_message_create(data, c, hdl); + } + else if (event_name == "PRESENCE_UPDATE") { + on_event_presence_update(data); } } -void GatewayHandler::identify(client &c, websocketpp::connection_hdl &hdl) { - json identify = { - { "op", 2 }, - { "d", { - { "token", bot_token }, - { "properties", { - { "$browser", "Microsoft Windows 10" }, - { "$device", "TriviaBot-0.0" }, - { "$referrer", "" }, - { "$referring_domain", "" } - } }, - { "compress", false }, - { "large_threshold", 250 }, - { "shard", { 0, 1 } } - } } - }; +void GatewayHandler::on_event_ready(json data) { + user_object.load_from_json(data["user"]); - c.send(hdl, identify.dump(), websocketpp::frame::opcode::text); - c.get_alog().write(websocketpp::log::alevel::app, "Sent identify payload."); + Logger::write("Sign-on confirmed. (@" + user_object.username + "#" + user_object.discriminator + ")", Logger::LogLevel::Info); +} + +void GatewayHandler::on_event_presence_update(json data) { + std::string user_id = data["user"]["id"]; + + auto it = users.find(user_id); + if (it != users.end()) { + it->second.status = data.value("status", "offline"); + if (data["game"] == nullptr) { + it->second.game = "null"; + } + else { + it->second.game = data["game"].value("name", "null"); + } + } + else { + Logger::write("Tried to add presence for user " + user_id + " who doesn't exist", Logger::LogLevel::Warning); + } +} + +void GatewayHandler::on_event_guild_create(json data) { + guilds[data["id"]] = DiscordObjects::Guild(data); + DiscordObjects::Guild &guild = guilds[data["id"]]; + + Logger::write("Received info for guild " + guild.id + ", now in " + std::to_string(guilds.size()) + " guild(s)", Logger::LogLevel::Info); + + int channels_added = 0, roles_added = 0, members_added = 0, presences_added = 0; + + for (json channel : data["channels"]) { + std::string channel_id = channel["id"]; + channel["guild_id"] = guild.id; + + channels[channel_id] = DiscordObjects::Channel(channel); + guilds[guild.id].channels.push_back(&channels[channel_id]); + + channels_added++; + } + for (json role : data["roles"]) { + std::string role_id = role["id"]; + + roles[role_id] = DiscordObjects::Role(role); + guilds[guild.id].roles.push_back(&roles[role_id]); + + roles_added++; + } + for (json member : data["members"]) { + std::string user_id = member["user"]["id"]; + + auto it = users.find(user_id); + if (it == users.end()) { // new user + users[user_id] = DiscordObjects::User(member["user"]); + } + users[user_id].guilds.push_back(guild.id); + + DiscordObjects::GuildMember *guild_member = new DiscordObjects::GuildMember(member, &users[user_id]); + for (std::string role_id : member["roles"]) { + guild_member->roles.push_back(&roles[role_id]); + } + + guilds[guild.id].members.push_back(guild_member); + + members_added++; + } + for (json presence : data["presences"]) { + std::string user_id = presence["user"]["id"]; + + auto it = users.find(user_id); + if (it != users.end()) { + it->second.status = presence.value("status", "offline"); + if (presence["game"] == nullptr) { + it->second.game = "null"; + } else { + it->second.game = presence["game"].value("name", "null"); + } + + presences_added++; + } + else { + Logger::write("Tried to add presence for user " + user_id + " who doesn't exist", Logger::LogLevel::Warning); + } + } + + if (v8_instances.count(guild.id) == 0) { + v8_instances[guild.id] = std::make_unique(config, guild.id, &guilds, &channels, &users, &roles); + Logger::write("Created v8 instance for guild " + guild.id, Logger::LogLevel::Debug); + } + + Logger::write("Loaded " + std::to_string(channels_added) + " channels, " + std::to_string(roles_added) + " roles and " + + std::to_string(members_added) + " members (with " + std::to_string(presences_added) + " presences) to guild " + guild.id, Logger::LogLevel::Debug); +} + +void GatewayHandler::on_event_guild_update(json data) { + std::string guild_id = data["id"]; + + guilds[guild_id].load_from_json(data); + Logger::write("Updated guild " + guild_id, Logger::LogLevel::Debug); +} + +void GatewayHandler::on_event_guild_delete(json data) { + std::string guild_id = data["id"]; + bool unavailable = data.value("unavailable", false); + + if (unavailable) { + Logger::write("Guild " + guild_id + " has become unavailable", Logger::LogLevel::Info); + guilds[guild_id].unavailable = true; + } else { + int channels_removed = 0; + for (auto it = channels.cbegin(); it != channels.cend();) { + if (it->second.guild_id == guild_id) { + channels.erase(it++); + channels_removed++; + } else { + ++it; + } + } + + guilds.erase(guilds.find(guild_id)); + Logger::write("Guild " + guild_id + " and " + std::to_string(channels_removed) + " channels removed", Logger::LogLevel::Info); + } +} + +void GatewayHandler::on_event_guild_member_add(json data) { + std::string guild_id = data["guild_id"]; + std::string user_id = data["user"]["id"]; + + auto it = users.find(user_id); + if (it == users.end()) { // new user + users[user_id] = DiscordObjects::User(data["user"]); + } + users[user_id].guilds.push_back(guild_id); + + DiscordObjects::GuildMember *guild_member = new DiscordObjects::GuildMember(data, &users[user_id]); + for (std::string role_id : data["roles"]) { + guild_member->roles.push_back(&roles[role_id]); + } + + guilds[guild_id].members.push_back(guild_member); + + Logger::write("Added new member " + guild_member->user->id + " to guild " + guild_id, Logger::LogLevel::Debug); +} + +void GatewayHandler::on_event_guild_member_update(json data) { + std::string user_id = data["user"]["id"]; + DiscordObjects::Guild &guild = guilds[data["guild_id"]]; + + auto it = std::find_if(guild.members.begin(), guild.members.end(), [user_id](DiscordObjects::GuildMember *member) { + return user_id == member->user->id; + }); + if (it != guild.members.end()) { + bool nick_changed = false; + int roles_change = 0; + + DiscordObjects::GuildMember *member = (*it); + + std::string nick = data.value("nick", "null"); + if (member->nick != nick) { + member->nick = nick; + nick_changed = true; + } + + roles_change = member->roles.size(); + member->roles.clear(); // reset and re-fill, changing the differences is probably more expensive anyway. + for (std::string role_id : data["roles"]) { + member->roles.push_back(&roles[role_id]); + } + roles_change = member->roles.size() - roles_change; + + std::string debug_string = "Updated member " + user_id + " of guild " + guild.id; + if (nick_changed) debug_string += ". Nick changed to " + nick; + if (roles_change != 0) debug_string += ". No. of roles changed by " + std::to_string(roles_change); + debug_string += "."; + + Logger::write(debug_string, Logger::LogLevel::Debug); + } + else { + Logger::write("Tried to update member " + user_id + " (of guild " + guild.id + ") who does not exist.", Logger::LogLevel::Warning); + } +} + +void GatewayHandler::on_event_guild_member_remove(json data) { + DiscordObjects::Guild &guild = guilds[data["guild_id"]]; + std::string user_id = data["user"]["id"]; + + auto it = std::find_if(guild.members.begin(), guild.members.end(), [user_id](DiscordObjects::GuildMember *member) { + return user_id == member->user->id; + }); + if (it != guild.members.end()) { + delete (*it); + guild.members.erase(it); + + users[user_id].guilds.erase(std::remove(users[user_id].guilds.begin(), users[user_id].guilds.end(), guild.id)); + + if (users[user_id].guilds.size() == 0) { + users.erase(users.find(user_id)); + Logger::write("User " + user_id + " removed from guild " + guild.id + " and no longer visible, deleted.", Logger::LogLevel::Debug); + } + else { + Logger::write("User " + user_id + " removed from guild " + guild.id, Logger::LogLevel::Debug); + } + } + else { + Logger::write("Tried to remove guild member " + user_id + " who doesn't exist", Logger::LogLevel::Warning); + } +} + +void GatewayHandler::on_event_guild_role_create(json data) { + std::string role_id = data["role"]["id"]; + std::string guild_id = data["guild_id"]; + roles[role_id] = DiscordObjects::Role(data["role"]); + + guilds[guild_id].roles.push_back(&roles[role_id]); + + Logger::write("Created role " + role_id + " on guild " + guild_id, Logger::LogLevel::Debug); +} + +void GatewayHandler::on_event_guild_role_update(json data) { + std::string role_id = data["role"]["id"]; + + roles[role_id].load_from_json(data["role"]); +} + +void GatewayHandler::on_event_guild_role_delete(json data) { + std::string role_id = data["role_id"]; + auto it = roles.find(role_id); + + if (it != roles.end()) { + DiscordObjects::Guild &guild = guilds[data["guild_id"]]; + + auto check_lambda = [role_id](const DiscordObjects::Role *r) { + return r->id == role_id; + }; + + auto it2 = std::find_if(guild.roles.begin(), guild.roles.end(), check_lambda); + if (it2 != guild.roles.end()) { + guild.roles.erase(it2); + } + else { + Logger::write("Tried to delete role " + role_id + " from guild " + guild.id + " but it doesn't exist there", Logger::LogLevel::Warning); + } + + roles.erase(it); + Logger::write("Deleted role " + role_id + " (guild " + guild.id + ").", Logger::LogLevel::Debug); + } + else { + Logger::write("Tried to delete role " + role_id + " but it doesn't exist.", Logger::LogLevel::Warning); + } +} + +void GatewayHandler::on_event_channel_create(json data) { + std::string channel_id = data["id"]; + std::string guild_id = data["guild_id"]; + + channels[channel_id] = DiscordObjects::Channel(data); + Logger::write("Added channel " + channel_id + " to channel list. Now " + std::to_string(channels.size()) + " channels stored", Logger::LogLevel::Debug); + guilds[guild_id].channels.push_back(&channels[channel_id]); + Logger::write("Added channel " + channel_id + " to guild " + guild_id + "'s list. Now " + std::to_string(guilds[guild_id].channels.size()) + " channels stored", Logger::LogLevel::Debug); +} + +void GatewayHandler::on_event_channel_update(json data) { + std::string channel_id = data["id"]; + + auto it = channels.find(channel_id); + if (it == channels.end()) { + Logger::write("Got channel update for channel " + channel_id + " that doesn't exist. Creating channel instead.", Logger::LogLevel::Warning); + on_event_channel_create(data); + } else { + channels[channel_id].load_from_json(data); + Logger::write("Updated channel " + channel_id, Logger::LogLevel::Debug); + } +} + +void GatewayHandler::on_event_channel_delete(json data) { + std::string channel_id = data["id"]; + std::string guild_id = data["guild_id"]; + + auto it = channels.find(channel_id); + if (it == channels.end()) { + Logger::write("Tried to delete channel " + channel_id + " which doesn't exist", Logger::LogLevel::Warning); + } + else { + auto it2 = std::find_if(guilds[guild_id].channels.begin(), guilds[guild_id].channels.begin(), [channel_id](const DiscordObjects::Channel *c) { + return c->id == channel_id; + }); + guilds[guild_id].channels.erase(it2); + Logger::write("Removed channel " + channel_id + " from guild " + guild_id + "'s list. Now " + + std::to_string(guilds[guild_id].channels.size()) + " channels stored", Logger::LogLevel::Debug); + + channels.erase(it); + Logger::write("Removed channel " + channel_id + " from channel list. Now " + std::to_string(channels.size()) + " channels stored.", Logger::LogLevel::Debug); + } +} + +void GatewayHandler::on_event_message_create(json data, client &c, websocketpp::connection_hdl &hdl) { + std::string message = data["content"]; + + DiscordObjects::Channel &channel = channels[data["channel_id"]]; + DiscordObjects::Guild &guild = guilds[channel.guild_id]; + DiscordObjects::User &sender = users[data["author"]["id"]]; + + if (sender.bot) return; + + std::vector words; + boost::split(words, message, boost::is_any_of(" ")); + CommandHelper::Command custom_command; + if (words[0] == "`trivia" || words[0] == "`t") { + int questions = 10; + int delay = 8; + + if (words.size() > 3) { + DiscordAPI::send_message(channel.id, ":exclamation: Invalid arguments!", config.token, config.cert_location); + return; + } + else if (words.size() > 1) { + if (words[1] == "help" || words[1] == "h") { + std::string help = "**Base command \\`t[rivia]**. Arguments:\n"; + help += "\\`trivia **{x}** **{y}**: Makes the game last **x** number of questions, optionally sets the time interval between hints to **y** seconds\n"; + help += "\\`trivia **stop**: stops the ongoing game.\n"; + help += "\\`trivia **help**: prints this message\n"; + + DiscordAPI::send_message(channel.id, help, config.token, config.cert_location); + return; + } + else if (words[1] == "stop" || words[1] == "s") { + if (games.find(channel.id) != games.end()) { + delete_game(channel.id); + } + else { + DiscordAPI::send_message(channel.id, ":warning: Couldn't find an ongoing trivia game for this channel.", config.token, config.cert_location); + } + return; + } + else { + try { + questions = std::stoi(words[1]); + if (words.size() == 3) { + delay = std::stoi(words[2]); + } + } + catch (std::invalid_argument e) { + DiscordAPI::send_message(channel.id, ":exclamation: Invalid arguments!", config.token, config.cert_location); + return; + } + } + } + + games[channel.id] = std::make_unique(config, this, channel.id, questions, delay); + games[channel.id]->start(); + } + else if (words[0] == "`guilds") { + std::string m = "**Guild List:**\n"; + for (auto &gu : guilds) { + m += ":small_orange_diamond: " + gu.second.name + " (" + gu.second.id + ") Channels: " + std::to_string(gu.second.channels.size()) + "\n"; + } + DiscordAPI::send_message(channel.id, m, config.token, config.cert_location); + } + else if (words[0] == "`info") { + DiscordAPI::send_message(channel.id, ":information_source: **trivia-bot** by Jack. ", config.token, config.cert_location); + } + else if (words[0] == "~js" && words.size() > 1) { + DiscordObjects::GuildMember *member = *std::find_if(guild.members.begin(), guild.members.end(), [sender](DiscordObjects::GuildMember *m) { + return sender.id == m->user->id; + }); + std::string js = message.substr(4); + auto it = v8_instances.find(channel.guild_id); + if (it != v8_instances.end() && js.length() > 0) { + it->second->exec_js(js, &channel, member); + } + } + else if (words[0] == "~createjs" && words.size() > 1) { + auto &member = *std::find_if(guild.members.begin(), guild.members.end(), [sender](DiscordObjects::GuildMember *m) { return sender.id == m->user->id; }); + BotConfig &conf = config; + bool disallowed = std::find_if(member->roles.begin(), member->roles.end(), [conf](DiscordObjects::Role *r) -> bool { + return conf.createjs_roles.count(r->name); + }) == member->roles.end(); // checks if the user has the required roles + + if (disallowed) { + DiscordAPI::send_message(channel.id, ":warning: You do not have permission to use this command.", config.token, config.cert_location); + return; + } + + std::string args = message.substr(10); + size_t seperator_loc = args.find("|"); + if (seperator_loc != std::string::npos) { + std::string command_name = args.substr(0, seperator_loc); + std::string script = args.substr(seperator_loc + 1); + int result = CommandHelper::insert_command(channel.guild_id, command_name, script); + switch (result) { + case 0: + DiscordAPI::send_message(channel.id, ":warning: Error!", config.token, config.cert_location); break; + case 1: + DiscordAPI::send_message(channel.id, ":new: Command `" + command_name + "` successfully created.", config.token, config.cert_location); break; + case 2: + DiscordAPI::send_message(channel.id, ":arrow_heading_up: Command `" + command_name + "` successfully updated.", config.token, config.cert_location); break; + } + } + } + else if (words[0] == "`shutdown" && sender.id == "82232146579689472") { // it me + DiscordAPI::send_message(channel.id, ":zzz: Goodbye!", config.token, config.cert_location); + for (auto &game : games) { + delete_game(game.first); + } + v8_instances.clear(); + c.close(hdl, websocketpp::close::status::going_away, ""); + } + else if (words[0] == "`debug") { + if (words[1] == "channel" && words.size() == 3) { + auto it = channels.find(words[2]); + if (it == channels.end()) { + DiscordAPI::send_message(channel.id, ":question: Unrecognised channel.", config.token, config.cert_location); + return; + } + + DiscordAPI::send_message(channel.id, it->second.to_debug_string(), config.token, config.cert_location); + } + else if (words[1] == "guild" && words.size() == 3) { + auto it = guilds.find(words[2]); + if (it == guilds.end()) { + DiscordAPI::send_message(channel.id, ":question: Unrecognised guild.", config.token, config.cert_location); + return; + } + + DiscordAPI::send_message(channel.id, it->second.to_debug_string(), config.token, config.cert_location); + } + else if (words[1] == "member" && words.size() == 4) { + auto it = guilds.find(words[2]); + if (it == guilds.end()) { + DiscordAPI::send_message(channel.id, ":question: Unrecognised guild.", config.token, config.cert_location); + return; + } + + std::string user_id = words[3]; + auto it2 = std::find_if(it->second.members.begin(), it->second.members.end(), [user_id](DiscordObjects::GuildMember *member) { + return user_id == member->user->id; + }); + if (it2 == it->second.members.end()) { + DiscordAPI::send_message(channel.id, ":question: Unrecognised user.", config.token, config.cert_location); + return; + } + + DiscordAPI::send_message(channel.id, (*it2)->to_debug_string(), config.token, config.cert_location); + } + else if (words[1] == "role" && words.size() == 3) { + auto it = roles.find(words[2]); + if (it == roles.end()) { + DiscordAPI::send_message(channel.id, ":question: Unrecognised role.", config.token, config.cert_location); + return; + } + + DiscordAPI::send_message(channel.id, it->second.to_debug_string(), config.token, config.cert_location); + } + else if (words[1] == "role" && words.size() == 4) { + std::string role_name = words[3]; + + auto it = guilds.find(words[2]); + if (it == guilds.end()) { + DiscordAPI::send_message(channel.id, ":question: Unrecognised guild.", config.token, config.cert_location); + return; + } + + auto it2 = std::find_if(it->second.roles.begin(), it->second.roles.end(), [role_name](DiscordObjects::Role *r) { + return role_name == r->name; + }); + if (it2 == it->second.roles.end()) { + DiscordAPI::send_message(channel.id, ":question: Unrecognised role.", config.token, config.cert_location); + return; + } + + DiscordAPI::send_message(channel.id, (*it2)->to_debug_string(), config.token, config.cert_location); + } + else { + DiscordAPI::send_message(channel.id, ":question: Unknown parameters.", config.token, config.cert_location); + } + } + else if (CommandHelper::get_command(channel.guild_id, words[0], custom_command)) { + std::string args = ""; + if (message.length() > (words[0].length() + 1)) { + args = message.substr(words[0].length() + 1); + } + + if (custom_command.script.length() == 0) { + DiscordAPI::send_message(channel.id, ":warning: Script has 0 length.", config.token, config.cert_location); + return; + } + + auto it = v8_instances.find(channel.guild_id); + if (it == v8_instances.end()) { + DiscordAPI::send_message(channel.id, ":warning: No V8 instance exists for this server - it's our fault not yours!", config.token, config.cert_location); + return; + } + + DiscordObjects::GuildMember *member = *std::find_if(guild.members.begin(), guild.members.end(), [sender](DiscordObjects::GuildMember *m) { + return sender.id == m->user->id; + }); + it->second->exec_js(custom_command.script, &channel, member, args); + } + else if (games.find(channel.id) != games.end()) { // message received in channel with ongoing trivia game + games[channel.id]->handle_answer(message, sender); + } } void GatewayHandler::delete_game(std::string channel_id) { @@ -191,6 +663,6 @@ void GatewayHandler::delete_game(std::string channel_id) { // remove from map games.erase(it); } else { - std::cerr << "Tried to delete a game that didn't exist."; + Logger::write("Tried to delete a game that didn't exist (channel_id: " + channel_id + ")", Logger::LogLevel::Warning); } } \ No newline at end of file diff --git a/TriviaBot/bot/GatewayHandler.hpp b/TriviaBot/bot/GatewayHandler.hpp index 4b60448..3be7ca3 100644 --- a/TriviaBot/bot/GatewayHandler.hpp +++ b/TriviaBot/bot/GatewayHandler.hpp @@ -9,9 +9,12 @@ #include "json/json.hpp" #include "TriviaGame.hpp" +#include "js/CommandHelper.hpp" +#include "js/V8Instance.hpp" #include "data_structures/User.hpp" #include "data_structures/Guild.hpp" #include "data_structures/Channel.hpp" +#include "data_structures/Role.hpp" typedef websocketpp::client client; using json = nlohmann::json; @@ -34,44 +37,71 @@ using json = nlohmann::json; *****************************************************************************************************************************/ class TriviaGame; -class APIHelper; +class BotConfig; class GatewayHandler { public: - GatewayHandler(); + GatewayHandler(BotConfig &c); void handle_data(std::string data, client &c, websocketpp::connection_hdl &hdl); - void heartbeat(client *c, websocketpp::connection_hdl hdl, int interval); - - void on_hello(json decoded, client &c, websocketpp::connection_hdl &hdl); - - void on_dispatch(json decoded, client &c, websocketpp::connection_hdl &hdl); - - void identify(client &c, websocketpp::connection_hdl &hdl); - void delete_game(std::string channel_id); private: + BotConfig &config; + int last_seq; int heartbeat_interval; + /* payload dispatchers */ + void send_heartbeat(client *c, websocketpp::connection_hdl hdl, int interval); + void send_identify(client &c, websocketpp::connection_hdl &hdl); + void send_request_guild_members(client &c, websocketpp::connection_hdl &hdl, std::string guild_id); // not sure if required atm + + /* payload handlers */ + void on_hello(json decoded, client &c, websocketpp::connection_hdl &hdl); + void on_dispatch(json decoded, client &c, websocketpp::connection_hdl &hdl); + + /* misc events */ + void on_event_ready(json data); // https://discordapp.com/developers/docs/topics/gateway#ready + void on_event_presence_update(json data); // https://discordapp.com/developers/docs/topics/gateway#presence-update + + /* guild events */ + void on_event_guild_create(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-create + void on_event_guild_update(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-update + void on_event_guild_delete(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-delete + void on_event_guild_member_add(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-member-add + void on_event_guild_member_update(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-member-update + void on_event_guild_member_remove(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-member-remove + void on_event_guild_role_create(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-role-create + void on_event_guild_role_update(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-role-update + void on_event_guild_role_delete(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-role-delete + + /* channel events */ + void on_event_channel_create(json data); // https://discordapp.com/developers/docs/topics/gateway#channel-create + void on_event_channel_update(json data); // https://discordapp.com/developers/docs/topics/gateway#channel-update + void on_event_channel_delete(json data); // https://discordapp.com/developers/docs/topics/gateway#channel-delete + + /* message events */ + void on_event_message_create(json data, client &c, websocketpp::connection_hdl &hdl); // https://discordapp.com/developers/docs/topics/gateway#message-create + const int protocol_version = 5; // bot's user obj DiscordObjects::User user_object; - // - std::map> guilds; - // channels pointers are shared pointers, held here but also in guild objects - std::map> channels; + /* */ + std::map guilds; + std::map channels; + std::map users; + std::map roles; // std::map> games; + // + std::map> v8_instances; std::unique_ptr heartbeat_thread; - - APIHelper *ah; }; #endif \ No newline at end of file diff --git a/TriviaBot/bot/Logger.cpp b/TriviaBot/bot/Logger.cpp new file mode 100644 index 0000000..525b6bd --- /dev/null +++ b/TriviaBot/bot/Logger.cpp @@ -0,0 +1,47 @@ +#include "Logger.hpp" + +#include +#include + +namespace Logger { + std::ostream &operator<<(std::ostream &out, const LogLevel log_level) { + switch (log_level) { + case LogLevel::Debug: + return out << "debug"; + case LogLevel::Info: + return out << "info"; + case LogLevel::Warning: + return out << "warning"; + case LogLevel::Severe: + return out << "severe"; + } + return out << ""; + } + + std::ostream &get_ostream(LogLevel log_level) { + switch (log_level) { + case LogLevel::Debug: + case LogLevel::Info: + return std::clog; + case LogLevel::Severe: + case LogLevel::Warning: + return std::cerr; + } + + return std::cerr; + } + + void write(std::string text, LogLevel log_level) { + time_t rawtime; + struct tm *timeinfo; + char buffer[80]; + + time(&rawtime); + timeinfo = localtime(&rawtime); + + strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", timeinfo); + std::string time_str(buffer); + + get_ostream(log_level) << "[" << time_str << "] [" << log_level << "] " << text << std::endl; + } +} diff --git a/TriviaBot/bot/Logger.hpp b/TriviaBot/bot/Logger.hpp new file mode 100644 index 0000000..b587b81 --- /dev/null +++ b/TriviaBot/bot/Logger.hpp @@ -0,0 +1,14 @@ +#ifndef BOT_LOGGER +#define BOT_LOGGER + +#include + +namespace Logger { + enum class LogLevel { + Debug, Info, Warning, Severe + }; + + void write(std::string text, LogLevel log_level); +} + +#endif diff --git a/TriviaBot/bot/TriviaBot.cpp b/TriviaBot/bot/TriviaBot.cpp index 548ad33..719735c 100644 --- a/TriviaBot/bot/TriviaBot.cpp +++ b/TriviaBot/bot/TriviaBot.cpp @@ -1,40 +1,66 @@ +#include +#include + #include +#include +#include #include "ClientConnection.hpp" - -std::string bot_token; +#include "Logger.hpp" +#include "DiscordAPI.hpp" +#include "BotConfig.hpp" int main(int argc, char *argv[]) { + BotConfig config; + if (config.is_new_config) { + Logger::write("Since the config.json file is newly generated, the program will exit now to allow you to edit it.", Logger::LogLevel::Info); + return 0; + } + curl_global_init(CURL_GLOBAL_DEFAULT); - if (argc == 2) { - bot_token = argv[1]; - } - else { - std::cout << "Please enter your bot token: " << std::endl; - std::cin >> bot_token; + v8::V8::InitializeICUDefaultLocation(argv[0]); + v8::V8::InitializeExternalStartupData(argv[0]); + v8::Platform* platform = v8::platform::CreateDefaultPlatform(); + v8::V8::InitializePlatform(platform); + v8::V8::Initialize(); + + Logger::write("Initialised V8 and curl", Logger::LogLevel::Debug); + + std::string args = "/?v=5&encoding=json"; + std::string url = DiscordAPI::get_gateway(config.cert_location).value("url", "wss://gateway.discord.gg"); + + bool retry = true; + int exit_code = 0; + while (retry) { + retry = false; + + try { + ClientConnection conn(config); + conn.start(url + args); + } + catch (const std::exception &e) { + Logger::write("std exception: " + std::string(e.what()), Logger::LogLevel::Severe); + exit_code = 1; + } + catch (websocketpp::lib::error_code e) { + Logger::write("websocketpp exception: " + e.message(), Logger::LogLevel::Severe); + std::this_thread::sleep_for(std::chrono::seconds(10)); + retry = true; // should just be an occasional connection issue + } + catch (...) { + Logger::write("other exception.", Logger::LogLevel::Severe); + exit_code = 2; + } } - // todo: get this using API - std::string uri = "wss://gateway.discord.gg/?v=5&encoding=json"; - - try { - ClientConnection conn; - conn.start(uri); - } - catch (const std::exception & e) { - std::cerr << e.what() << std::endl; - } - catch (websocketpp::lib::error_code e) { - std::cerr << e.message() << std::endl; - } - catch (...) { - std::cerr << "other exception" << std::endl; - } - - std::getchar(); + v8::V8::Dispose(); + v8::V8::ShutdownPlatform(); + delete platform; curl_global_cleanup(); - return 0; -} \ No newline at end of file + Logger::write("Cleaned up", Logger::LogLevel::Info); + + return exit_code; +} diff --git a/TriviaBot/bot/TriviaBot.cpp.save b/TriviaBot/bot/TriviaBot.cpp.save new file mode 100644 index 0000000..5ebbefd --- /dev/null +++ b/TriviaBot/bot/TriviaBot.cpp.save @@ -0,0 +1,64 @@ +#include +#include +#include + +#include "ClientConnection.hpp" +#include "Logger.hpp" +#include "DiscordAPI.hpp" + +std::string bot_token; + +int main(int argc, char *argv[]) { + curl_global_init(CURL_GLOBAL_DEFAULT); + + v8::V8::InitializeICUDefaultLocation(argv[0]); + v8::V8::InitializeExternalStartupData(argv[0]); + v8::Platform* platform = v8::platform::CreateDefaultPlatform(); + v8::V8::InitializePlatform(platform); + v8::V8::Initialize(); + + Logger::write("Initialised V8 and curl", Logger::LogLevel::Debug); + + if (argc == 2) { + bot_token = argv[1]; + } + else { + std::cout << "Please enter your bot token: " << std::endl; + std::cin >> bot_token; + } + + std::string args = "/?v=5&encoding=json"; + std::string url = DiscordAPI::get_gateway().value("url", "wss://gateway.discord.gg"); + + bool retry = true; + while (retry) { + try { + ClientConnection conn; + conn.start(url + args); + } + catch (const std::exception &e) { + Logger::write("std exception: " + std::string(e.what()), Logger::LogLevel::Severe); + retry = false; + } + catch (websocketpp::lib::error_code e) { + Logger::write("websocketpp exception: " + e.message(), Logger::LogLevel::Severe); + } + catch (...) { + Logger::write("other exception.", Logger::LogLevel::Severe); + retry = false; + } + } + + v8::V8::Dispose(); + v8::V8::ShutdownPlatform(); + delete platform; + + curl_global_cleanup(); + + Logger::write("Cleaned up", Logger::LogLevel::Info); + + std::cout << "Press enter to exit" << std::endl; + std::getchar(); + + return 0; +} diff --git a/TriviaBot/bot/TriviaGame.cpp b/TriviaBot/bot/TriviaGame.cpp index a2385f1..b2310e4 100644 --- a/TriviaBot/bot/TriviaGame.cpp +++ b/TriviaBot/bot/TriviaGame.cpp @@ -10,12 +10,13 @@ #include #include "GatewayHandler.hpp" -#include "APIHelper.hpp" +#include "DiscordAPI.hpp" #include "data_structures/User.hpp" +#include "Logger.hpp" +#include "BotConfig.hpp" -TriviaGame::TriviaGame(GatewayHandler *gh, APIHelper *ah, std::string channel_id, int total_questions, int delay) : interval(delay) { +TriviaGame::TriviaGame(BotConfig &c, GatewayHandler *gh, std::string channel_id, int total_questions, int delay) : config(c), interval(delay) { this->gh = gh; - this->ah = ah; this->channel_id = channel_id; this->total_questions = total_questions; @@ -26,7 +27,7 @@ TriviaGame::~TriviaGame() { current_thread.reset(); if (scores.size() == 0) { - ah->send_message(channel_id, ":red_circle: Game cancelled!"); + DiscordAPI::send_message(channel_id, ":red_circle: Game cancelled!", config.token, config.cert_location); return; } @@ -50,13 +51,13 @@ TriviaGame::~TriviaGame() { average_time.pop_back(); average_time.pop_back(); average_time.pop_back(); message += ":small_blue_diamond: <@!" + p.first + ">: " + std::to_string(p.second) + " (Avg: " + average_time + " seconds)\n"; } - ah->send_message(channel_id, message); + DiscordAPI::send_message(channel_id, message, config.token, config.cert_location); sqlite3 *db; int rc; std::string sql; rc = sqlite3_open("bot/db/trivia.db", &db); if (rc) { - std::cerr << "Cant't open database: " << sqlite3_errmsg(db) << std::endl; + Logger::write("Can't open database: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); } std::string sql_in_list; @@ -71,7 +72,7 @@ TriviaGame::~TriviaGame() { rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0); if (rc != SQLITE_OK) { - std::cerr << "SQL error." << std::endl; + Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); } // insert arguments @@ -79,7 +80,7 @@ TriviaGame::~TriviaGame() { rc = sqlite3_bind_text(stmt, i + 1, pairs[i].first.c_str(), -1, (sqlite3_destructor_type) -1); if (rc != SQLITE_OK) { - std::cerr << "SQL error." << std::endl; + Logger::write("Error binding prepared statement argument: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); break; } } @@ -97,7 +98,7 @@ TriviaGame::~TriviaGame() { data[id] = std::pair(total_score, average_time); } else if (rc != SQLITE_DONE) { sqlite3_finalize(stmt); - std::cerr << "SQLite error." << std::endl; + Logger::write("Error fetching results: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); break; } } @@ -105,7 +106,7 @@ TriviaGame::~TriviaGame() { std::string update_sql; if (data.size() < scores.size()) { // some users dont have entries yet - std::string sql = "INSERT INTO TotalScores (User, TotalScore, AverageTime) VALUES "; + sql = "INSERT INTO TotalScores (User, TotalScore, AverageTime) VALUES "; for (auto &i : scores) { if (data.find(i.first) == data.end()) { sql += "(?, ?, ?),"; @@ -116,7 +117,7 @@ TriviaGame::~TriviaGame() { rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0); if (rc != SQLITE_OK) { - std::cerr << "SQL error." << std::endl; + Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); } int count = 1; @@ -140,7 +141,7 @@ TriviaGame::~TriviaGame() { if (update_sql != "") { rc = sqlite3_prepare_v2(db, update_sql.c_str(), -1, &stmt, 0); if (rc != SQLITE_OK) { - std::cerr << "SQL error." << std::endl; + Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); } int index = 1; @@ -180,16 +181,15 @@ void TriviaGame::question() { /// open db rc = sqlite3_open("bot/db/trivia.db", &db); if (rc) { - std::cerr << "Cant't open database: " << sqlite3_errmsg(db) << std::endl; + Logger::write("Error opening database: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); } // prepare statement sqlite3_stmt *stmt; sql = "SELECT * FROM Questions ORDER BY RANDOM() LIMIT 1;"; rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0); - if (rc != SQLITE_OK) { - std::cerr << "SQL error." << std::endl; + Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); } rc = sqlite3_step(stmt); @@ -205,16 +205,17 @@ void TriviaGame::question() { boost::split(current_answers, answer, boost::is_any_of("*")); } - else if (rc != SQLITE_DONE) { + else { sqlite3_finalize(stmt); - std::cerr << "SQLite error." << std::endl; + Logger::write("Error fetching question: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe); } sqlite3_finalize(stmt); sqlite3_close(db); questions_asked++; - ah->send_message(channel_id, ":question: **(" + std::to_string(questions_asked) + "/" + std::to_string(total_questions) + ")** " + current_question); + DiscordAPI::send_message(channel_id, ":question: **(" + std::to_string(questions_asked) + "/" + std::to_string(total_questions) + ")** " + current_question, + config.token, config.cert_location); question_start = boost::posix_time::microsec_clock::universal_time(); give_hint(0, ""); @@ -253,8 +254,8 @@ void TriviaGame::give_hint(int hints_given, std::string hint) { // count number of *s int length = 0; - for (unsigned int i = 0; i < word.length(); i++) { - if (word[i] == hide_char) { + for (unsigned int j = 0; j < word.length(); j++) { + if (word[j] == hide_char) { length++; } } @@ -281,11 +282,11 @@ void TriviaGame::give_hint(int hints_given, std::string hint) { hints_given++; // now equal to the amount of [hide_char]s that need to be present in each word if (print) { - ah->send_message(channel_id, ":small_orange_diamond: Hint: **`" + hint + "`**"); + DiscordAPI::send_message(channel_id, ":small_orange_diamond: Hint: **`" + hint + "`**", config.token, config.cert_location); } } boost::this_thread::sleep(interval); - ah->send_message(channel_id, ":exclamation: Question failed. Answer: ** `" + *current_answers.begin() + "` **"); + DiscordAPI::send_message(channel_id, ":exclamation: Question failed. Answer: ** `" + *current_answers.begin() + "` **", config.token, config.cert_location); } void TriviaGame::handle_answer(std::string answer, DiscordObjects::User sender) { @@ -300,7 +301,7 @@ void TriviaGame::handle_answer(std::string answer, DiscordObjects::User sender) // remove the last three 0s time_taken.pop_back(); time_taken.pop_back(); time_taken.pop_back(); - ah->send_message(channel_id, ":heavy_check_mark: <@!" + sender.id + "> You got it! (" + time_taken + " seconds)"); + DiscordAPI::send_message(channel_id, ":heavy_check_mark: <@!" + sender.id + "> You got it! (" + time_taken + " seconds)", config.token, config.cert_location); increase_score(sender.id); update_average_time(sender.id, diff.total_milliseconds()); @@ -338,4 +339,4 @@ void TriviaGame::update_average_time(std::string user_id, int time) { // yeah it probably loses accuracy here, doesn't really matter average_times[user_id] = (int) (total / questions_answered); } -} \ No newline at end of file +} diff --git a/TriviaBot/bot/TriviaGame.hpp b/TriviaBot/bot/TriviaGame.hpp index fb393db..477c7f1 100644 --- a/TriviaBot/bot/TriviaGame.hpp +++ b/TriviaBot/bot/TriviaGame.hpp @@ -11,15 +11,14 @@ #include class GatewayHandler; -class APIHelper; +class BotConfig; namespace DiscordObjects { class User; } - class TriviaGame { public: - TriviaGame(GatewayHandler *gh, APIHelper *ah, std::string channel_id, int total_questions, int delay); + TriviaGame(BotConfig &c, GatewayHandler *gh, std::string channel_id, int total_questions, int delay); ~TriviaGame(); void start(); @@ -27,6 +26,8 @@ public: void handle_answer(std::string answer, DiscordObjects::User sender); private: + BotConfig &config; + int questions_asked; int total_questions; boost::posix_time::seconds interval; @@ -38,7 +39,6 @@ private: std::string channel_id; GatewayHandler *gh; - APIHelper *ah; const char hide_char = '#'; diff --git a/TriviaBot/bot/data_structures/Channel.hpp b/TriviaBot/bot/data_structures/Channel.hpp index 9aef03d..068d0c0 100644 --- a/TriviaBot/bot/data_structures/Channel.hpp +++ b/TriviaBot/bot/data_structures/Channel.hpp @@ -32,6 +32,7 @@ namespace DiscordObjects { Channel(json data); void load_from_json(json data); + std::string to_debug_string(); bool operator==(Channel rhs); @@ -56,7 +57,7 @@ namespace DiscordObjects { type = "text"; } - inline Channel::Channel(json data) { + inline Channel::Channel(json data) : Channel() { load_from_json(data); } @@ -73,6 +74,19 @@ namespace DiscordObjects { user_limit = data.value("user_limit", -1); } + inline std::string Channel::to_debug_string() { + return "**__Channel " + id + "__**" + + "\n**guild_id:** " + guild_id + + "\n**name:** " + name + + "\n**type:** " + type + + "\n**position:** " + std::to_string(position) + + "\n**is_private:** " + std::to_string(is_private) + + "\n**topic:** " + (topic == "" ? "[empty]" : topic) + + "\n**last_message_id:** " + last_message_id + + "\n**bitrate:** " + std::to_string(bitrate) + + "\n**user_limit:** " + std::to_string(user_limit); + } + inline bool Channel::operator==(Channel rhs) { return id == rhs.id && id != "null"; } diff --git a/TriviaBot/bot/data_structures/Guild.hpp b/TriviaBot/bot/data_structures/Guild.hpp index 933d082..82b7169 100644 --- a/TriviaBot/bot/data_structures/Guild.hpp +++ b/TriviaBot/bot/data_structures/Guild.hpp @@ -1,13 +1,16 @@ -#ifndef BOT_DATA__STRUCTURES_Guild -#define BOT_DATA__STRUCTURES_Guild +#ifndef BOT_DATA__STRUCTURES_GUILD +#define BOT_DATA__STRUCTURES_GUILD #include -#include +#include +#include #include "../json/json.hpp" #include "Channel.hpp" #include "User.hpp" +#include "Role.hpp" +#include "GuildMember.hpp" using json = nlohmann::json; @@ -48,6 +51,7 @@ namespace DiscordObjects { Guild(json data); void load_from_json(json data); + std::string to_debug_string(); bool operator==(Guild rhs); @@ -62,13 +66,15 @@ namespace DiscordObjects { // bool embed_enabled; // std::string embed_channel_id; int verification_level; - // TODO: Implement all guil fields + // TODO: Implement all guild fields // std::vector voice_states - // std::vector roles // std::vector emojis // std::vector features + bool unavailable; - std::vector> channels; + std::vector channels; + std::vector members; + std::vector roles; //std::vector> users; }; @@ -77,13 +83,11 @@ namespace DiscordObjects { afk_timeout = verification_level = -1; } - inline Guild::Guild(json data) { + inline Guild::Guild(json data) : Guild() { load_from_json(data); } inline void Guild::load_from_json(json data) { - Guild(); - id = data.value("id", "null"); name = data.value("name", "null"); icon = data.value("icon", "null"); @@ -93,6 +97,23 @@ namespace DiscordObjects { afk_channel_id = data.value("afk_channel_id", "null"); afk_timeout = data.value("afk_timeout", -1); verification_level = data.value("verification_level", -1); + unavailable = data.value("unavailable", false); + } + + inline std::string Guild::to_debug_string() { + return "**__Guild " + id + "__**" + + "\n**name:** " + name + + "\n**icon:** " + icon + + "\n**splash:** " + splash + + "\n**owner_id:** " + owner_id + + "\n**region:** " + region + + "\n**afk_channel_id:** " + afk_channel_id + + "\n**afk_timeout:** " + std::to_string(afk_timeout) + + "\n**verification_level:** " + std::to_string(verification_level) + + "\n**unavailable:** " + std::to_string(unavailable) + + "\n**channels:** " + std::to_string(channels.size()) + + "\n**roles:** " + std::to_string(roles.size()) + + "\n**members:** " + std::to_string(members.size()); } inline bool Guild::operator==(Guild rhs) { diff --git a/TriviaBot/bot/data_structures/GuildMember.hpp b/TriviaBot/bot/data_structures/GuildMember.hpp new file mode 100644 index 0000000..3c7a06b --- /dev/null +++ b/TriviaBot/bot/data_structures/GuildMember.hpp @@ -0,0 +1,69 @@ +#ifndef BOT_DATA__STRUCTURES_GUILDMEMBER +#define BOT_DATA__STRUCTURES_GUILDMEMBER + +#include +#include + +#include "../json/json.hpp" + +#include "User.hpp" +#include "Role.hpp" + +namespace DiscordObjects { + class GuildMember { + public: + GuildMember(); + GuildMember(json data, User *user); + + void load_from_json(json data); + std::string to_debug_string(); + + bool operator==(GuildMember rhs); + + User *user; + std::string nick; + std::vector roles; + std::string joined_at; // TODO: better type + bool deaf; + bool mute; + }; + + inline GuildMember::GuildMember() { + user = nullptr; + nick = joined_at = "null"; + deaf = false; + mute = false; + } + + inline GuildMember::GuildMember(json data, User *user) : GuildMember() { + this->user = user; + load_from_json(data); + } + + inline void GuildMember::load_from_json(json data) { + nick = data.value("nick", "null"); + joined_at = data.value("joined_at", "null"); + deaf = data.value("deaf", false); + mute = data.value("mute", false); + } + + inline std::string GuildMember::to_debug_string() { + return "**__GuildMember " + user->id + "__**" + + "\n**mention:** <@" + user->id + "> / " + user->username + "#" + user->discriminator + + "\n**bot:** " + std::to_string(user->bot) + + "\n**mfa_enabled:** " + std::to_string(user->mfa_enabled) + + "\n**avatar:** " + user->avatar + + "\n**status:** " + user->status + + "\n**game name:** " + user->game + + "\n**nick:** " + nick + + "\n**joined_at:** " + joined_at + + "\n**deaf:** " + std::to_string(deaf) + + "\n**mute:** " + std::to_string(mute); + } + + inline bool GuildMember::operator==(GuildMember rhs) { + return user->id == rhs.user->id; + } +} + +#endif \ No newline at end of file diff --git a/TriviaBot/bot/data_structures/Role.hpp b/TriviaBot/bot/data_structures/Role.hpp new file mode 100644 index 0000000..6de6080 --- /dev/null +++ b/TriviaBot/bot/data_structures/Role.hpp @@ -0,0 +1,114 @@ +#ifndef BOT_DATA__STRUCTURES_ROLE +#define BOT_DATA__STRUCTURES_ROLE + +#include +#include +#include + +#include "../json/json.hpp" + +using json = nlohmann::json; + +namespace DiscordObjects { + + class Role { + public: + Role(); + Role(json data); + + void load_from_json(json data); + std::string to_debug_string(); + + bool operator==(Role rhs); + + std::string id; + std::string name; + int colour; + bool hoist; + int position; + int permissions; + bool managed; + bool mentionable; + }; + + inline Role::Role() { + id = "null"; + name = "null"; + colour = -1; + hoist = false; + position = -1; + permissions = 0; + managed = false; + mentionable = false; + } + + inline Role::Role(json data) : Role() { + load_from_json(data); + } + + inline void Role::load_from_json(json data) { + id = data.value("id", "null"); + name = data.value("name", "null"); + colour = data.value("color", -1); + hoist = data.value("hoist", false); + position = data.value("position", -1); + permissions = data.value("permissions", 0); + managed = data.value("managed", false); + mentionable = data.value("mentionable", false); + } + + inline std::string Role::to_debug_string() { + return "**__Role " + id + "__**" + + "\n**name:** " + name + + "\n**colour:** " + std::to_string(colour) + + "\n**hoist:** " + std::to_string(hoist) + + "\n**position:** " + std::to_string(position) + + "\n**permissions:** " + std::to_string(permissions) + + "\n**managed:** " + std::to_string(managed) + + "\n**mentionable:** " + std::to_string(mentionable); + } + + inline bool Role::operator==(Role rhs) { + return id == rhs.id; + } + + + /* permission values */ + enum class Permission { + CreateInstantInvite = 0x00000001, // Allows creation of instant invites + KickMembers = 0x00000002, // Allows kicking members + BanMembers = 0x00000004, // Allows banning members + Administrator = 0x00000008, // Allows all permissions and bypasses channel permission overwrites + ManageChannels = 0x00000010, // Allows management and editing of channels + ManageGuild = 0x00000020, // Allows management and editing of the guild + ReadMessages = 0x00000400, // Allows reading messages in a channel.The channel will not appear for users without this permission + SendMessages = 0x00000800, // Allows for sending messages in a channel. + SendTTSMessages = 0x00001000, // Allows for sending of / tts messages + ManageMessages = 0x00002000, // Allows for deletion of other users messages + EmbedLinks = 0x00004000, // Links sent by this user will be auto - embedded + AttachFiles = 0x00008000, // Allows for uploading images and files + ReadMessageHistory = 0x00010000, // Allows for reading of message history + MentionEveryone = 0x00020000, // Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel + Connect = 0x00100000, // Allows for joining of a voice channel + Speak = 0x00200000, // Allows for speaking in a voice channel + MuteMembers = 0x00400000, // Allows for muting members in a voice channel + DeafenMembers = 0x00800000, // Allows for deafening of members in a voice channel + MoveMembers = 0x01000000, // Allows for moving of members between voice channels + UseVAD = 0x02000000, // Allows for using voice - activity - detection in a voice channel + ChangeNickname = 0x04000000, // Allows for modification of own nickname + ManageNicknames = 0x08000000, // Allows for modification of other users nicknames + ManageRoles = 0x10000000 // Allows management and editing of roles + }; + + /* implement bitwise operators */ + inline Permission operator|(Permission lhs, Permission rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); + } + + inline Permission operator|=(Permission &lhs, Permission rhs) { + lhs = static_cast(static_cast(lhs) | static_cast(rhs)); + return lhs; + } +} + +#endif \ No newline at end of file diff --git a/TriviaBot/bot/data_structures/Text.txt b/TriviaBot/bot/data_structures/Text.txt deleted file mode 100644 index c1e31ab..0000000 --- a/TriviaBot/bot/data_structures/Text.txt +++ /dev/null @@ -1,125 +0,0 @@ -Example data: - -!!!!!!!!!!!!!!!!! GUILD_CREATE Event -{ - "afk_channel_id": null, - "afk_timeout": 300, - "channels": [ - { - "id": "200398901767962624", - "is_private": false, - "last_message_id": "201355522635595776", - "name": "general", - "permission_overwrites": [], - "position": 0, - "topic": "", - "type": "text" - }, - { - "bitrate": 64000, - "id": "200398901767962625", - "is_private": false, - "name": "General", - "permission_overwrites": [], - "position": 0, - "type": "voice", - "user_limit": 0 - } - ], - "default_message_notifications": 0, - "emojis": [], - "features": [], - "icon": null, - "id": "200398901767962624", - "joined_at": "2016-07-06T23:54:20.824000+00:00", - "large": false, - "member_count": 2, - "members": [ - { - "deaf": false, - "joined_at": "2016-07-06T23:53:41.425000+00:00", - "mute": false, - "roles": [ - "200399346498273280" - ], - "user": { - "avatar": "1dc076d2d273615dd23546c86dbdfd9c", - "discriminator": "8212", - "id": "82232146579689472", - "username": "Jack" - } - }, - { - "deaf": false, - "joined_at": "2016-07-06T23:54:20.824000+00:00", - "mute": false, - "roles": [ - "200399601507893248" - ], - "user": { - "avatar": "e871ceecaa362718af6d3174bc941977", - "bot": true, - "discriminator": "8194", - "id": "199657095258177539", - "username": "trivia-bot" - } - } - ], - "mfa_level": 0, - "name": "EleGiggle", - "owner_id": "82232146579689472", - "presences": [ - { - "game": null, - "status": "online", - "user": { - "id": "82232146579689472" - } - }, - { - "game": null, - "status": "online", - "user": { - "id": "199657095258177539" - } - } - ], - "region": "london", - "roles": [ - { - "color": 0, - "hoist": false, - "id": "200398901767962624", - "managed": false, - "mentionable": false, - "name": "@everyone", - "permissions": 36953089, - "position": 0 - }, - { - "color": 3066993, - "hoist": true, - "id": "200399346498273280", - "managed": false, - "mentionable": false, - "name": "All Perms", - "permissions": 506715199, - "position": 1 - }, - { - "color": 15844367, - "hoist": true, - "id": "200399601507893248", - "managed": false, - "mentionable": false, - "name": "Robot", - "permissions": 536083519, - "position": 1 - } - ], - "splash": null, - "unavailable": false, - "verification_level": 0, - "voice_states": [] -} - diff --git a/TriviaBot/bot/data_structures/User.hpp b/TriviaBot/bot/data_structures/User.hpp index 1bc36e8..8e39f44 100644 --- a/TriviaBot/bot/data_structures/User.hpp +++ b/TriviaBot/bot/data_structures/User.hpp @@ -2,6 +2,7 @@ #define BOT_DATA__STRUCTURES_USER #include +#include #include "../json/json.hpp" @@ -37,14 +38,21 @@ namespace DiscordObjects { std::string avatar; bool bot; bool mfa_enabled; + + // presence + std::string game; + std::string status; + + std::vector guilds; }; inline User::User() { - id = username = discriminator = avatar = "null"; + id = username = discriminator = avatar = game = "null"; + status = "offline"; bot = mfa_enabled = false; } - inline User::User(json data) { + inline User::User(json data) : User() { load_from_json(data); } diff --git a/TriviaBot/bot/db/schema.sqlite b/TriviaBot/bot/db/schema.sqlite index 462cf6a..7cae0cd 100644 --- a/TriviaBot/bot/db/schema.sqlite +++ b/TriviaBot/bot/db/schema.sqlite @@ -1,14 +1,20 @@ BEGIN TRANSACTION; CREATE TABLE "TotalScores" ( - `User` TEXT UNIQUE, - `TotalScore` INTEGER, - `AverageTime` INTEGER, + `User` TEXT NOT NULL, + `TotalScore` INTEGER NOT NULL, + `AverageTime` INTEGER NOT NULL, PRIMARY KEY(User) ); CREATE TABLE "Questions" ( - `ID` INTEGER PRIMARY KEY AUTOINCREMENT, - `Category` TEXT, - `Question` TEXT, - `Answer` TEXT + `ID` INTEGER PRIMARY KEY AUTOINCREMENT, + `Category` TEXT NOT NULL, + `Question` TEXT NOT NULL, + `Answer` TEXT NOT NULL ); -COMMIT; \ No newline at end of file +CREATE TABLE `CustomJS` ( + `ID` INTEGER PRIMARY KEY AUTOINCREMENT, + `GuildID` TEXT NOT NULL, + `CommandName` TEXT NOT NULL, + `Script` TEXT NOT NULL +); +COMMIT; diff --git a/TriviaBot/bot/http/HTTP.cpp b/TriviaBot/bot/http/HTTP.cpp new file mode 100644 index 0000000..fa28ec4 --- /dev/null +++ b/TriviaBot/bot/http/HTTP.cpp @@ -0,0 +1,102 @@ +#include "HTTP.hpp" + +#include "../Logger.hpp" +#include "../BotConfig.hpp" + +/* +* Warning: (Awful) C Code +*/ +namespace HTTP { + size_t write_callback(void *contents, size_t size, size_t nmemb, void *read_buffer) { + static_cast(read_buffer)->append(static_cast(contents), size * nmemb); + return size * nmemb; + } + + std::string post_request(std::string url, std::string content_type, std::string data, long *response_code, std::string token, std::string ca_location) { + CURL *curl; + CURLcode res; + std::string read_buffer; + struct curl_slist *headers = nullptr; + + curl = curl_easy_init(); + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + + // Now with real HTTPS! + curl_easy_setopt(curl, CURLOPT_CAINFO, ca_location.c_str()); + + std::string header_arr[3]; + header_arr[0] = "Content-Type: " + content_type; + header_arr[1] = "Authorization: Bot " + token; + header_arr[2] = "User-Agent: DiscordBot(http://github.com/jackb-p/triviadiscord, 1.0)"; + + for (std::string h : header_arr) { + headers = curl_slist_append(headers, h.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer); + + res = curl_easy_perform(curl); + + if (res == CURLE_OK) { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, response_code); + } + else { + return ""; + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + } + + return read_buffer; + } + + std::string get_request(std::string url, long *response_code, std::string token, std::string ca_location) { + CURL *curl; + CURLcode res; + std::string read_buffer; + struct curl_slist *headers = nullptr; + + curl = curl_easy_init(); + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + + // Now with real HTTPS! + curl_easy_setopt(curl, CURLOPT_CAINFO, ca_location.c_str()); + + std::string header_arr[2]; + header_arr[0] = "Authorization: Bot " + token; + header_arr[1] = "User-Agent: DiscordBot (http://github.com/jackb-p/triviadiscord, 1.0)"; + + for (std::string h : header_arr) { + headers = curl_slist_append(headers, h.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &HTTP::write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer); + + res = curl_easy_perform(curl); + + if (res == CURLE_OK) { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, response_code); + } + else { + Logger::write("curl error: " + std::string(curl_easy_strerror(res)), Logger::LogLevel::Warning); + return read_buffer; + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + } + + return read_buffer; + } +} \ No newline at end of file diff --git a/TriviaBot/bot/http/HTTP.hpp b/TriviaBot/bot/http/HTTP.hpp new file mode 100644 index 0000000..1f84f7d --- /dev/null +++ b/TriviaBot/bot/http/HTTP.hpp @@ -0,0 +1,15 @@ +#ifndef BOT_HTTP_HTTP +#define BOT_HTTP_HTTP + +#include + +#include + +class BotConfig; + +namespace HTTP { + std::string post_request(std::string url, std::string content_type, std::string data, long *response_code, std::string token, std::string ca_location); + std::string get_request(std::string url, long *response_code, std::string token, std::string ca_location); +} + +#endif \ No newline at end of file diff --git a/TriviaBot/bot/http/HTTPHelper.cpp b/TriviaBot/bot/http/HTTPHelper.cpp deleted file mode 100644 index 33b6c0a..0000000 --- a/TriviaBot/bot/http/HTTPHelper.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "HTTPHelper.hpp" - -extern std::string bot_token; - -/* -* Warning: (Awful) C Code -*/ - -std::string HTTPHelper::post_request(std::string url, std::string content_type, std::string data, long *response_code) { - CURL *curl; - CURLcode res; - std::string read_buffer; - struct curl_slist *headers = nullptr; - - curl = curl_easy_init(); - if (curl) { - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - - // Now with real HTTPS! - curl_easy_setopt(curl, CURLOPT_CAINFO, "bot/http/DiscordCA.crt"); - - std::string header_arr[3]; - header_arr[0] = "Content-Type: " + content_type; - header_arr[1] = "Authorization: Bot " + bot_token; - header_arr[2] = "User-Agent: DiscordBot(http://github.com/jackb-p/triviadiscord, 1.0)"; - - for (std::string h : header_arr) { - headers = curl_slist_append(headers, h.c_str()); - } - - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer); - - res = curl_easy_perform(curl); - - if (res == CURLE_OK) { - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, response_code); - } else { - return ""; - } - - /* always cleanup */ - curl_easy_cleanup(curl); - curl_slist_free_all(headers); - } - - return read_buffer; -} - -size_t HTTPHelper::write_callback(void *contents, size_t size, size_t nmemb, void *userp) { - ((std::string *) userp)->append((char *) contents, size * nmemb); - return size * nmemb; -} \ No newline at end of file diff --git a/TriviaBot/bot/http/HTTPHelper.hpp b/TriviaBot/bot/http/HTTPHelper.hpp deleted file mode 100644 index 3010eff..0000000 --- a/TriviaBot/bot/http/HTTPHelper.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef BOT_HTTP_HTTPHELPER -#define BOT_HTTP_HTTPHELPER - -#include - -#include - -class HTTPHelper { -public: - std::string post_request(std::string url, std::string content_type, std::string data, long *response_code); - -private: - static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp); -}; - -#endif \ No newline at end of file diff --git a/TriviaBot/bot/js/CommandHelper.cpp b/TriviaBot/bot/js/CommandHelper.cpp new file mode 100644 index 0000000..31f4bde --- /dev/null +++ b/TriviaBot/bot/js/CommandHelper.cpp @@ -0,0 +1,164 @@ +#include "CommandHelper.hpp" + +#include +#include + +#include + +#include "../Logger.hpp" + +namespace CommandHelper { + std::vector commands; + + void init() { + sqlite3 *db; int return_code; + return_code = sqlite3_open("bot/db/trivia.db", &db); + + std::string sql = "SELECT GuildID, CommandName, Script FROM CustomJS"; + + sqlite3_stmt *stmt; + return_code = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0); + + + while (return_code != SQLITE_DONE) { + return_code = sqlite3_step(stmt); + + if (return_code == SQLITE_ROW) { + std::string guild_id = reinterpret_cast(sqlite3_column_text(stmt, 0)); + std::string command_name = reinterpret_cast(sqlite3_column_text(stmt, 1)); + std::string script = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + commands.push_back({ guild_id, command_name, script }); + } + else if (return_code != SQLITE_DONE) { + sqlite3_finalize(stmt); + std::cerr << "SQLite error." << std::endl; + return; + } + } + + Logger::write(std::to_string(commands.size()) + " custom command(s) loaded", Logger::LogLevel::Info); + + sqlite3_finalize(stmt); + sqlite3_close(db); + } + + bool return_code_ok(int return_code) { + // TODO: NotLikeThis + if (return_code != SQLITE_OK) { + Logger::write("SQLite error", Logger::LogLevel::Severe); + return false; + } + return true; + } + + bool get_command(std::string guild_id, std::string command_name, Command &command) { + auto check_lambda = [guild_id, command_name](const Command &c) { + return guild_id == c.guild_id && command_name == c.command_name; + }; + + auto it = std::find_if(commands.begin(), commands.end(), check_lambda); + if (it == commands.end()) { + command = {}; + return false; + } + else { + command = { it->guild_id, it->command_name, it->script }; + return true; + } + } + + // returns: 0 error, 1 inserted, 2 updated + int insert_command(std::string guild_id, std::string command_name, std::string script) { + // TODO: if script empty, delete command + + Command command{ guild_id, command_name, script }; + + int ret_value; + std::string sql; + if (command_in_db(guild_id, command_name)) { + sql = "UPDATE CustomJS SET Script=?1 WHERE GuildID=?2 AND CommandName=?3;"; + Logger::write("Command already exists, updating.", Logger::LogLevel::Debug); + ret_value = 2; + } + else { + sql = "INSERT INTO CustomJS(Script, GuildID, CommandName) VALUES (?1, ?2, ?3);"; + Logger::write("Inserting new command.", Logger::LogLevel::Debug); + ret_value = 1; + } + + sqlite3 *db; int return_code; + return_code = sqlite3_open("bot/db/trivia.db", &db); + if (!return_code_ok(return_code)) return 0; + + sqlite3_stmt *stmt; + return_code = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0); + if (!return_code_ok(return_code)) return 0; + + return_code = sqlite3_bind_text(stmt, 1, script.c_str(), -1, (sqlite3_destructor_type)-1); + if (!return_code_ok(return_code)) return 0; + + return_code = sqlite3_bind_text(stmt, 2, guild_id.c_str(), -1, (sqlite3_destructor_type)-1); + if (!return_code_ok(return_code)) return 0; + + return_code = sqlite3_bind_text(stmt, 3, command_name.c_str(), -1, (sqlite3_destructor_type)-1); + if (!return_code_ok(return_code)) return 0; + + return_code = sqlite3_step(stmt); + bool success = return_code == SQLITE_DONE; + + sqlite3_finalize(stmt); + sqlite3_close(db); + + if (success) { + if (ret_value == 1) { + commands.push_back({ guild_id, command_name, script }); + } + if (ret_value == 2) { + // update command, don't add + auto check_lambda = [guild_id, command_name](const Command &c) { + return guild_id == c.guild_id && command_name == c.command_name; + }; + + auto it = std::find_if(commands.begin(), commands.end(), check_lambda); + if (it == commands.end()) { + return 0; + } + else { + it->script = script; + } + } + + return ret_value; + } + + return 0; + } + + bool command_in_db(std::string guild_id, std::string command_name) { + sqlite3 *db; int return_code; + return_code = sqlite3_open("bot/db/trivia.db", &db); + if (!return_code_ok(return_code)) return false; + + std::string sql = "SELECT EXISTS(SELECT 1 FROM CustomJS WHERE GuildID=?1 AND CommandName=?2);"; + + sqlite3_stmt *stmt; + return_code = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0); + if (!return_code_ok(return_code)) return false; + + return_code = sqlite3_bind_text(stmt, 1, guild_id.c_str(), -1, (sqlite3_destructor_type)-1); + if (!return_code_ok(return_code)) return false; + + return_code = sqlite3_bind_text(stmt, 2, command_name.c_str(), -1, (sqlite3_destructor_type)-1); + if (!return_code_ok(return_code)) return false; + + sqlite3_step(stmt); + + bool exists = sqlite3_column_int(stmt, 0) == 1; // returns 1 (true) if exists + + sqlite3_finalize(stmt); + sqlite3_close(db); + + return exists; + } +} diff --git a/TriviaBot/bot/js/CommandHelper.hpp b/TriviaBot/bot/js/CommandHelper.hpp new file mode 100644 index 0000000..e10aed8 --- /dev/null +++ b/TriviaBot/bot/js/CommandHelper.hpp @@ -0,0 +1,20 @@ +#ifndef BOT_JS_COMMANDHELPER +#define BOT_JS_COMMANDHELPER + +#include +#include + +namespace CommandHelper { + struct Command { + std::string guild_id; + std::string command_name; + std::string script; + }; + + void init(); + int insert_command(std::string guild_id, std::string command_name, std::string script); + bool get_command(std::string guild_id, std::string name, Command &command); + bool command_in_db(std::string guild_id, std::string command_name); +} + +#endif diff --git a/TriviaBot/bot/js/V8Instance.cpp b/TriviaBot/bot/js/V8Instance.cpp new file mode 100644 index 0000000..6769c31 --- /dev/null +++ b/TriviaBot/bot/js/V8Instance.cpp @@ -0,0 +1,729 @@ +#include +#include +#include +#include + +#include "V8Instance.hpp" +#include "../DiscordAPI.hpp" +#include "../Logger.hpp" +#include "../BotConfig.hpp" + +using namespace v8; + +V8Instance::V8Instance(BotConfig &c, std::string guild_id, std::map *guilds, std::map *channels, + std::map *users, std::map *roles) : config(c) { + + rng = std::mt19937(std::random_device()()); + this->guild_id = guild_id; + this->guilds = guilds; + this->channels = channels; + this->users = users; + this->roles = roles; + + create(); +} + +void V8Instance::create() { + Isolate::CreateParams create_params; + create_params.array_buffer_allocator = ArrayBuffer::Allocator::NewDefaultAllocator(); + + isolate = Isolate::New(create_params); + isolate->Enter(); + Logger::write("[v8] Created isolate", Logger::LogLevel::Debug); + + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + + // set global context + Local context = create_context(); + context_.Reset(isolate, context); + Context::Scope context_scope(context); + + initialise(context); + + Logger::write("[v8] Created context and context scope", Logger::LogLevel::Debug); +} + +void V8Instance::initialise(Local context) { + HandleScope handle_scope(isolate); + + Local server_obj = wrap_server(&(*guilds)[guild_id]); + + context->Global()->Set( + context, + String::NewFromUtf8(isolate, "server", NewStringType::kNormal).ToLocalChecked(), + server_obj + ).FromJust(); + + Logger::write("[v8] Bound server template", Logger::LogLevel::Debug); +} + +v8::Local V8Instance::create_context() { + Local global = ObjectTemplate::New(isolate); + + Local self = External::New(isolate, (void *) this); + global->Set( + String::NewFromUtf8(isolate, "print", NewStringType::kNormal).ToLocalChecked(), + FunctionTemplate::New(isolate, V8Instance::js_print, self) + ); + global->Set( + String::NewFromUtf8(isolate, "random", NewStringType::kNormal).ToLocalChecked(), + FunctionTemplate::New(isolate, V8Instance::js_random, self) + ); + global->Set( + String::NewFromUtf8(isolate, "shuffle", NewStringType::kNormal).ToLocalChecked(), + FunctionTemplate::New(isolate, V8Instance::js_shuffle, self) + ); + + Logger::write("[v8] Created global context, added print function", Logger::LogLevel::Debug); + + return Context::New(isolate, NULL, global); +} + +/* server */ +Local V8Instance::make_server_template() { + EscapableHandleScope handle_scope(isolate); + + Local templ = ObjectTemplate::New(isolate); + templ->SetInternalFieldCount(1); + templ->SetHandler( + NamedPropertyHandlerConfiguration( + V8Instance::js_get_server, + (GenericNamedPropertySetterCallback) 0, + (GenericNamedPropertyQueryCallback) 0, + (GenericNamedPropertyDeleterCallback) 0, + (GenericNamedPropertyEnumeratorCallback) 0, + External::New(isolate, (void *) this) + ) + ); + + return handle_scope.Escape(templ); +} + +Local V8Instance::wrap_server(DiscordObjects::Guild *guild) { + EscapableHandleScope handle_scope(isolate); + + if (server_template.IsEmpty()) { + Local raw_template = make_server_template(); + server_template.Reset(isolate, raw_template); + } + + Local templ = Local::New(isolate, server_template); + Local result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); + + Local guild_ptr = External::New(isolate, guild); + result->SetInternalField(0, guild_ptr); + + return handle_scope.Escape(result); +} + +void V8Instance::js_get_server(Local property, const PropertyCallbackInfo &info) { + void *self_v = info.Data().As()->Value(); + if (!self_v) { + Logger::write("[v8] [js_get_server] Class pointer empty", Logger::LogLevel::Warning); + return; + } + V8Instance *self = static_cast(self_v); + + void *guild_v = info.Holder()->GetInternalField(0).As()->Value(); + if (!guild_v) { + Logger::write("[v8] [js_get_server] Guild pointer empty", Logger::LogLevel::Warning); + return; + } + DiscordObjects::Guild *guild = static_cast(guild_v); + + if (!property->IsString()) { + return; + } + std::string property_s = *String::Utf8Value(property); + + if (property_s == "Id") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), guild->id.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Name") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), guild->name.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "IconUrl") { + std::string icon_url = "https://discordapp.com/api/guilds/" + guild->id + "/icons/" + guild->icon + ".jpg"; + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), icon_url.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Owner") { + std::string owner_id = guild->owner_id; + DiscordObjects::GuildMember *owner = *std::find_if(guild->members.begin(), guild->members.end(), [owner_id](DiscordObjects::GuildMember *m) { + return owner_id == m->user->id; + }); + Local owner_obj = self->wrap_user(owner); + info.GetReturnValue().Set(owner_obj); + } + else if (property_s == "Roles") { + Local roles_obj = self->wrap_role_list(&guild->roles); + info.GetReturnValue().Set(roles_obj); + } + else if (property_s == "Channels") { + Local channels_obj = self->wrap_channel_list(&guild->channels); + info.GetReturnValue().Set(channels_obj); + } + else if (property_s == "Users") { + Local users_obj = self->wrap_user_list(&guild->members); + info.GetReturnValue().Set(users_obj); + } +} + + +/* channel */ +Local V8Instance::make_channel_template() { + EscapableHandleScope handle_scope(isolate); + + Local templ = ObjectTemplate::New(isolate); + templ->SetInternalFieldCount(1); + templ->SetHandler( + NamedPropertyHandlerConfiguration( + V8Instance::js_get_channel, + (GenericNamedPropertySetterCallback) 0, + (GenericNamedPropertyQueryCallback) 0, + (GenericNamedPropertyDeleterCallback) 0, + (GenericNamedPropertyEnumeratorCallback) 0, + External::New(isolate, (void *) this) + ) + ); + + return handle_scope.Escape(templ); +} + +Local V8Instance::wrap_channel(DiscordObjects::Channel *channel) { + EscapableHandleScope handle_scope(isolate); + + if (role_template.IsEmpty()) { + Local raw_template = make_channel_template(); + channel_template.Reset(isolate, raw_template); + } + + Local templ = Local::New(isolate, channel_template); + Local result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); + + Local channel_ptr = External::New(isolate, channel); + result->SetInternalField(0, channel_ptr); + + return handle_scope.Escape(result); +} + +void V8Instance::js_get_channel(Local property, const PropertyCallbackInfo &info) { + void *channel_v = info.Holder()->GetInternalField(0).As()->Value(); + if (!channel_v) { + Logger::write("[v8] [js_get_channel] Channel pointer empty", Logger::LogLevel::Warning); + return; + } + DiscordObjects::Channel *channel = static_cast(channel_v); + + if (!property->IsString()) { + return; + } + std::string property_s = *String::Utf8Value(property); + + if (property_s == "Id") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), channel->id.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Name") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), channel->name.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Topic") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), channel->topic.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "IsVoice") { + info.GetReturnValue().Set(Boolean::New(info.GetIsolate(), channel->type == "voice")); + } + else if (property_s == "Users") { + info.GetIsolate()->ThrowException(String::NewFromUtf8(info.GetIsolate(), "Channel.Users not implemented.", NewStringType::kNormal).ToLocalChecked()); + } +} + +/* channel list */ +Local V8Instance::make_channel_list_template() { + EscapableHandleScope handle_scope(isolate); + + Local templ = ObjectTemplate::New(isolate); + templ->SetInternalFieldCount(1); + templ->SetHandler( + IndexedPropertyHandlerConfiguration( + V8Instance::js_get_channel_list, + (IndexedPropertySetterCallback) 0, + (IndexedPropertyQueryCallback) 0, + (IndexedPropertyDeleterCallback) 0, + (IndexedPropertyEnumeratorCallback) 0, + External::New(isolate, (void *) this) + ) + ); + + return handle_scope.Escape(templ); +} + +Local V8Instance::wrap_channel_list(std::vector *channel_list) { + EscapableHandleScope handle_scope(isolate); + + if (channel_list_template.IsEmpty()) { + Local raw_template = make_channel_list_template(); + channel_list_template.Reset(isolate, raw_template); + } + + Local templ = Local::New(isolate, channel_list_template); + Local result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); + + // imitate an array + result->Set(String::NewFromUtf8(isolate, "length", NewStringType::kNormal).ToLocalChecked(), Integer::New(isolate, (*channel_list).size())); + result->SetPrototype(Array::New(isolate)->GetPrototype()); + + Local channel_list_ptr = External::New(isolate, channel_list); + result->SetInternalField(0, channel_list_ptr); + + return handle_scope.Escape(result); +} + +void V8Instance::js_get_channel_list(uint32_t index, const PropertyCallbackInfo &info) { + void *self_v = info.Data().As()->Value(); + if (!self_v) { + Logger::write("[v8] [js_get_channel_list] Class pointer empty", Logger::LogLevel::Warning); + return; + } + V8Instance *self = static_cast(self_v); + + void *channel_list_v = info.Holder()->GetInternalField(0).As()->Value(); + if (!channel_list_v) { + Logger::write("[v8] [js_get_channel_list] Channel List pointer empty", Logger::LogLevel::Warning); + return; + } + std::vector *channel_list = static_cast *>(channel_list_v); + + + if (index < (*channel_list).size()) { + Local channel_obj = self->wrap_channel((*channel_list)[index]); + info.GetReturnValue().Set(channel_obj); + } + else { + info.GetReturnValue().SetUndefined(); + } +} + + +/* user */ +Local V8Instance::make_user_template() { + EscapableHandleScope handle_scope(isolate); + + Local templ = ObjectTemplate::New(isolate); + templ->SetInternalFieldCount(1); + templ->SetHandler( + NamedPropertyHandlerConfiguration( + V8Instance::js_get_user, + (GenericNamedPropertySetterCallback)0, + (GenericNamedPropertyQueryCallback)0, + (GenericNamedPropertyDeleterCallback)0, + (GenericNamedPropertyEnumeratorCallback)0, + External::New(isolate, (void *) this) + ) + ); + + return handle_scope.Escape(templ); +} + +Local V8Instance::wrap_user(DiscordObjects::GuildMember *member) { + EscapableHandleScope handle_scope(isolate); + + if (user_template.IsEmpty()) { + Local raw_template = make_user_template(); + user_template.Reset(isolate, raw_template); + } + + Local templ = Local::New(isolate, user_template); + Local result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); + + Local member_ptr = External::New(isolate, member); + result->SetInternalField(0, member_ptr); + + return handle_scope.Escape(result); +} + +void V8Instance::js_get_user(Local property, const PropertyCallbackInfo &info) { + void *self_v = info.Data().As()->Value(); + if (!self_v) { + Logger::write("[v8] [js_get_user] Class pointer empty", Logger::LogLevel::Warning); + return; + } + V8Instance *self = static_cast(self_v); + + void *member_v = info.Holder()->GetInternalField(0).As()->Value(); + if (!member_v) { + Logger::write("[v8] [js_get_user] GuildMember pointer empty", Logger::LogLevel::Warning); + return; + } + DiscordObjects::GuildMember *member = static_cast(member_v); + + if (!property->IsString()) { + return; + } + std::string property_s = *String::Utf8Value(property); + + if (property_s == "Id") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), member->user->id.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Name") { + std::string name = member->nick == "null" ? member->user->username : member->nick; + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), name.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Mention") { + std::string mention = "<@" + member->user->id + ">"; + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), mention.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "AvatarUrl") { + std::string avatar_url = "https://discordapp.com/api/users/" + member->user->id + "/avatars/" + member->user->avatar + ".jpg"; + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), avatar_url.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Roles") { + Local roles_obj = self->wrap_role_list(&member->roles); + info.GetReturnValue().Set(roles_obj); + } + else if (property_s == "State") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), member->user->status.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "CurrentGame") { + if (member->user->game == "null") { + info.GetReturnValue().SetNull(); + } else { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), member->user->game.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + } +} + +/* user list */ +Local V8Instance::make_user_list_template() { + EscapableHandleScope handle_scope(isolate); + + Local templ = ObjectTemplate::New(isolate); + templ->SetInternalFieldCount(1); + templ->SetHandler( + IndexedPropertyHandlerConfiguration( + V8Instance::js_get_user_list, + (IndexedPropertySetterCallback) 0, + (IndexedPropertyQueryCallback) 0, + (IndexedPropertyDeleterCallback) 0, + (IndexedPropertyEnumeratorCallback) 0, + External::New(isolate, (void *) this) + ) + ); + + return handle_scope.Escape(templ); +} + +Local V8Instance::wrap_user_list(std::vector *user_list) { + EscapableHandleScope handle_scope(isolate); + + if (user_list_template.IsEmpty()) { + Local raw_template = make_user_list_template(); + user_list_template.Reset(isolate, raw_template); + } + + Local templ = Local::New(isolate, user_list_template); + Local result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); + + // imitate an array + result->Set(String::NewFromUtf8(isolate, "length", NewStringType::kNormal).ToLocalChecked(), Integer::New(isolate, (*user_list).size())); + result->SetPrototype(Array::New(isolate)->GetPrototype()); + + Local user_list_ptr = External::New(isolate, user_list); + result->SetInternalField(0, user_list_ptr); + + return handle_scope.Escape(result); +} + +void V8Instance::js_get_user_list(uint32_t index, const PropertyCallbackInfo &info) { + void *self_v = info.Data().As()->Value(); + if (!self_v) { + Logger::write("[v8] [js_get_user_list] Class pointer empty", Logger::LogLevel::Warning); + return; + } + V8Instance *self = static_cast(self_v); + + void *user_list_v = info.Holder()->GetInternalField(0).As()->Value(); + if (!user_list_v) { + Logger::write("[v8] [js_get_user_list] GuildMember List pointer empty", Logger::LogLevel::Warning); + return; + } + std::vector *user_list = static_cast *>(user_list_v); + + if (index < (*user_list).size()) { + Local role_obj = self->wrap_user((*user_list)[index]); + info.GetReturnValue().Set(role_obj); + } + else { + info.GetReturnValue().SetUndefined(); + } +} + + +/* role */ +Local V8Instance::make_role_template() { + EscapableHandleScope handle_scope(isolate); + + Local templ = ObjectTemplate::New(isolate); + templ->SetInternalFieldCount(1); + templ->SetHandler( + NamedPropertyHandlerConfiguration( + V8Instance::js_get_role, + (GenericNamedPropertySetterCallback)0, + (GenericNamedPropertyQueryCallback)0, + (GenericNamedPropertyDeleterCallback)0, + (GenericNamedPropertyEnumeratorCallback)0, + External::New(isolate, (void *) this) + ) + ); + + return handle_scope.Escape(templ); +} + +Local V8Instance::wrap_role(DiscordObjects::Role *role) { + EscapableHandleScope handle_scope(isolate); + + if (role_template.IsEmpty()) { + Local raw_template = make_role_template(); + role_template.Reset(isolate, raw_template); + } + + Local templ = Local::New(isolate, role_template); + Local result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); + + Local role_ptr = External::New(isolate, role); + result->SetInternalField(0, role_ptr); + + return handle_scope.Escape(result); +} + +void V8Instance::js_get_role(Local property, const PropertyCallbackInfo &info) { + void *role_v = info.Holder()->GetInternalField(0).As()->Value(); + if (!role_v) { + Logger::write("[v8] [js_get_role] Role pointer empty", Logger::LogLevel::Warning); + return; + } + DiscordObjects::Role *role = static_cast(role_v); + + if (!property->IsString()) { + return; + } + std::string property_s = *String::Utf8Value(property); + + if (property_s == "Id") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), role->id.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Name") { + info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), role->name.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else if (property_s == "Position") { + info.GetReturnValue().Set(Integer::New(info.GetIsolate(), role->position)); + } + else if (property_s == "Red" || property_s == "Green" || property_s == "Blue") { + info.GetIsolate()->ThrowException(String::NewFromUtf8(info.GetIsolate(), "Role.[Colour] not implemented.", NewStringType::kNormal).ToLocalChecked()); + } +} + +/* role list */ +Local V8Instance::make_role_list_template() { + EscapableHandleScope handle_scope(isolate); + + Local templ = ObjectTemplate::New(isolate); + templ->SetInternalFieldCount(1); + templ->SetHandler( + IndexedPropertyHandlerConfiguration( + V8Instance::js_get_role_list, + (IndexedPropertySetterCallback) 0, + (IndexedPropertyQueryCallback) 0, + (IndexedPropertyDeleterCallback) 0, + (IndexedPropertyEnumeratorCallback) 0, + External::New(isolate, (void *) this) + ) + ); + + return handle_scope.Escape(templ); +} + +Local V8Instance::wrap_role_list(std::vector *role_list) { + EscapableHandleScope handle_scope(isolate); + + if (role_list_template.IsEmpty()) { + Local raw_template = make_role_list_template(); + role_list_template.Reset(isolate, raw_template); + } + + Local templ = Local::New(isolate, role_list_template); + Local result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked(); + + // imitate an array + result->Set(String::NewFromUtf8(isolate, "length", NewStringType::kNormal).ToLocalChecked(), Integer::New(isolate, (*role_list).size())); + result->SetPrototype(Array::New(isolate)->GetPrototype()); + + Local role_list_ptr = External::New(isolate, role_list); + result->SetInternalField(0, role_list_ptr); + + return handle_scope.Escape(result); +} + +void V8Instance::js_get_role_list(uint32_t index, const PropertyCallbackInfo &info) { + void *self_v = info.Data().As()->Value(); + if (!self_v) { + Logger::write("[v8] [js_get_role_list] Class pointer empty", Logger::LogLevel::Warning); + return; + } + V8Instance *self = static_cast(self_v); + + void *role_list_v = info.Holder()->GetInternalField(0).As()->Value(); + if (!role_list_v) { + Logger::write("[v8] [js_get_role_list] Role List pointer empty", Logger::LogLevel::Warning); + return; + } + std::vector *role_list = static_cast *>(role_list_v); + + + if (index < (*role_list).size()) { + Local role_obj = self->wrap_role((*role_list)[index]); + info.GetReturnValue().Set(role_obj); + } + else { + info.GetReturnValue().SetUndefined(); + } +} + + +/* global functions */ +void V8Instance::js_print(const v8::FunctionCallbackInfo &args) { + auto data = args.Data().As(); + V8Instance *self = static_cast(data->Value()); + + std::string output = ""; + for (int i = 0; i < args.Length(); i++) { + v8::String::Utf8Value str(args[i]); + self->print_text += *str; + } + self->print_text += "\n"; +} + +void V8Instance::js_random(const v8::FunctionCallbackInfo &args) { + auto data = args.Data().As(); + V8Instance *self = static_cast(data->Value()); + + int number_args = args.Length(); + + if (number_args == 0) { + std::uniform_real_distribution dist(0, 1); + double random_val = dist(self->rng); + args.GetReturnValue().Set(Number::New(args.GetIsolate(), random_val)); + } + else if (number_args == 1) { + int64_t max = args[0]->IntegerValue(); + std::uniform_int_distribution dist(0, max); + int random_val = dist(self->rng); + args.GetReturnValue().Set(Integer::New(args.GetIsolate(), random_val)); + } + else if (number_args == 2) { + int64_t min = args[0]->IntegerValue(); + int64_t max = args[1]->IntegerValue(); + std::uniform_int_distribution dist(min, max); + int random_val = dist(self->rng); + args.GetReturnValue().Set(Integer::New(args.GetIsolate(), random_val)); + } + else { + std::string err_msg = "random() requires 0-2 arguments. You gave: " + std::to_string(number_args); + args.GetIsolate()->ThrowException(String::NewFromUtf8(args.GetIsolate(), err_msg.c_str(), NewStringType::kNormal).ToLocalChecked()); + } +} + +void V8Instance::js_shuffle(const v8::FunctionCallbackInfo &args) { + auto data = args.Data().As(); + V8Instance *self = static_cast(data->Value()); + + if (!args[0]->IsArray()) { + std::string err_msg = "shuffle() requires an array as it's argument. You gave: " + std::string(*String::Utf8Value(args[0]->TypeOf(args.GetIsolate()))); + args.GetIsolate()->ThrowException(String::NewFromUtf8(args.GetIsolate(), err_msg.c_str(), NewStringType::kNormal).ToLocalChecked()); + } + else { + Local given_arr = Local::Cast(args[0]); + const int length = given_arr->Length(); + Local return_arr = Array::New(args.GetIsolate(), length); + + std::vector> cpp_arr; + for (uint32_t i = 0; i < given_arr->Length(); i++) { + cpp_arr.push_back(given_arr->Get(i)); + } + + std::shuffle(cpp_arr.begin(), cpp_arr.end(), self->rng); + + for (uint32_t i = 0; i < given_arr->Length(); i++) { + return_arr->Set(i, cpp_arr[i]); + } + + args.GetReturnValue().Set(return_arr); + } +} + +void V8Instance::exec_js(std::string js, DiscordObjects::Channel *channel, DiscordObjects::GuildMember *sender, std::string args) { + HandleScope handle_scope(isolate); + Local context = Local::New(isolate, context_); + Context::Scope context_scope(context); + + context->Global()->Set( + String::NewFromUtf8(isolate, "input", NewStringType::kNormal).ToLocalChecked(), + String::NewFromUtf8(isolate, args.c_str(), NewStringType::kNormal).ToLocalChecked() + ); + Local user_obj = wrap_user(sender); + context->Global()->Set( + String::NewFromUtf8(isolate, "user", NewStringType::kNormal).ToLocalChecked(), + user_obj + ); + Local channel_obj = wrap_channel(channel); + context->Global()->Set( + String::NewFromUtf8(isolate, "channel", NewStringType::kNormal).ToLocalChecked(), + channel_obj + ); + // TODO: 'message' object here too, although it's fairly pointless + + current_sender = sender; + current_channel = channel; + + Logger::write("[v8] Preparing JS (guild " + (*guilds)[guild_id].id + ", channel " + channel->id + ")", Logger::LogLevel::Debug); + + Local source = String::NewFromUtf8(isolate, js.c_str(), NewStringType::kNormal).ToLocalChecked(); + + // compile + Logger::write("[v8] Isolate nullptr? " + std::to_string(isolate == nullptr) + " Context empty? " + std::to_string(context.IsEmpty()), Logger::LogLevel::Debug); + + TryCatch compile_try_catch(isolate); + Local