diff --git a/TriviaBot/bot/APIHelper.cpp b/TriviaBot/bot/APIHelper.cpp deleted file mode 100644 index a6633ee..0000000 --- a/TriviaBot/bot/APIHelper.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "APIHelper.hpp" - -#include -#include -#include - -#include "http/HTTPHelper.hpp" -#include "Logger.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) { - if (message == "") { - Logger::write("[send_message] Tried to send empty message", Logger::LogLevel::Warning); - return; - } - - if (message.length() > 4000) { - Logger::write("[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); - std::this_thread::sleep_for(50ms); - send_message(channel_id, second); - return; - } - - 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) { - Logger::write("[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_CTYPE, data.dump(), &response_code); - retries++; - } - - if (response_code != 200) { - Logger::write("[send_message] Giving up on sending message", Logger::LogLevel::Warning); - } -} \ No newline at end of file diff --git a/TriviaBot/bot/DiscordAPI.cpp b/TriviaBot/bot/DiscordAPI.cpp new file mode 100644 index 0000000..3bb582a --- /dev/null +++ b/TriviaBot/bot/DiscordAPI.cpp @@ -0,0 +1,84 @@ +#include "DiscordAPI.hpp" + +#include +#include +#include + +#include "http/HTTPHelper.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) { + 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); + std::this_thread::sleep_for(50ms); + send_message(channel_id, second); + 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); + + 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); + retries++; + } + + if (response_code != 200) { + Logger::write("[API] [send_message] Giving up on sending message", Logger::LogLevel::Warning); + } + } + + json get_gateway() { + std::string response; + long response_code; + response = HTTP::get_request(gateway_url, &response_code); + + 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); + 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/APIHelper.hpp b/TriviaBot/bot/DiscordAPI.hpp similarity index 55% rename from TriviaBot/bot/APIHelper.hpp rename to TriviaBot/bot/DiscordAPI.hpp index 5859463..2c389d7 100644 --- a/TriviaBot/bot/APIHelper.hpp +++ b/TriviaBot/bot/DiscordAPI.hpp @@ -9,18 +9,9 @@ using json = nlohmann::json; class HTTPHelper; -class APIHelper { -public: - APIHelper(); - +namespace DiscordAPI { + json get_gateway(); 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/GatewayHandler.cpp b/TriviaBot/bot/GatewayHandler.cpp index 092f10c..d14792e 100644 --- a/TriviaBot/bot/GatewayHandler.cpp +++ b/TriviaBot/bot/GatewayHandler.cpp @@ -2,7 +2,7 @@ #include -#include "APIHelper.hpp" +#include "DiscordAPI.hpp" #include "Logger.hpp" #include "data_structures/GuildMember.hpp" @@ -11,8 +11,7 @@ extern std::string bot_token; GatewayHandler::GatewayHandler() { last_seq = 0; - ah = std::make_shared(); - command_helper = std::make_unique(); + CommandHelper::init(); } void GatewayHandler::handle_data(std::string data, client &c, websocketpp::connection_hdl &hdl) { @@ -33,9 +32,17 @@ void GatewayHandler::handle_data(std::string data, client &c, websocketpp::conne } } -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 }, @@ -48,14 +55,49 @@ void GatewayHandler::heartbeat(client *c, websocketpp::connection_hdl hdl, int i } } +void GatewayHandler::send_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 } } + } } + }; + + 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"]; 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) { @@ -105,6 +147,9 @@ void GatewayHandler::on_dispatch(json decoded, client &c, websocketpp::connectio else if (event_name == "MESSAGE_CREATE") { on_event_message_create(data); } + else if (event_name == "PRESENCE_UPDATE") { + on_event_presence_update(data); + } } void GatewayHandler::on_event_ready(json data) { @@ -113,13 +158,31 @@ void GatewayHandler::on_event_ready(json data) { 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("Loaded guild " + guild.id + ", now in " + std::to_string(guilds.size()) + " guild(s)", Logger::LogLevel::Info); + 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; + int channels_added = 0, roles_added = 0, members_added = 0, presences_added = 0; for (json channel : data["channels"]) { std::string channel_id = channel["id"]; @@ -147,21 +210,41 @@ void GatewayHandler::on_event_guild_create(json data) { } users[user_id].guilds.push_back(guild.id); - DiscordObjects::GuildMember guild_member(member, &users[user_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]); + guild_member->roles.push_back(&roles[role_id]); } - guilds[guild.id].members[user_id] = guild_member; + 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(guild.id, ah, &guilds, &channels, &users, &roles); + v8_instances[guild.id] = std::make_unique(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 to 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) { @@ -204,37 +287,41 @@ void GatewayHandler::on_event_guild_member_add(json data) { } users[user_id].guilds.push_back(guild_id); - DiscordObjects::GuildMember guild_member(data, &users[user_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]); + guild_member->roles.push_back(&roles[role_id]); } - guilds[guild_id].members[user_id] = guild_member; + guilds[guild_id].members.push_back(guild_member); - Logger::write("Added new member " + guild_member.user->id + " to guild " + guild_id, Logger::LogLevel::Debug); + 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 = guild.members.find(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()) { bool nick_changed = false; size_t roles_change = 0; + DiscordObjects::GuildMember *member = (*it); + std::string nick = data.value("nick", "null"); - if (it->second.nick != nick) { - it->second.nick = nick; + if (member->nick != nick) { + member->nick = nick; nick_changed = true; } - roles_change = it->second.roles.size(); - it->second.roles.clear(); // reset and re-fill, changing the differences is probably more expensive anyway. + 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"]) { - it->second.roles.push_back(&roles[role_id]); + member->roles.push_back(&roles[role_id]); } - roles_change = it->second.roles.size() - roles_change; + 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; @@ -252,14 +339,16 @@ 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 = guild.members.find(user_id); - if (it != guilds[guild.id].members.end()) { + 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); - DiscordObjects::User &user = users[user_id]; - user.guilds.erase(std::remove(user.guilds.begin(), user.guilds.end(), guild.id)); + users[user_id].guilds.erase(std::remove(users[user_id].guilds.begin(), users[user_id].guilds.end(), guild.id)); - if (user.guilds.size() == 0) { + 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); } @@ -327,8 +416,6 @@ void GatewayHandler::on_event_channel_create(json data) { } void GatewayHandler::on_event_channel_update(json data) { - std::cout << "Update: " << data.dump(4) << std::endl; - std::string channel_id = data["id"]; auto it = channels.find(channel_id); @@ -342,8 +429,6 @@ void GatewayHandler::on_event_channel_update(json data) { } void GatewayHandler::on_event_channel_delete(json data) { - std::cout << "Delete: " << data.dump(4) << std::endl; - std::string channel_id = data["id"]; std::string guild_id = data["guild_id"]; @@ -369,19 +454,19 @@ void GatewayHandler::on_event_message_create(json data) { DiscordObjects::Channel &channel = channels[data["channel_id"]]; DiscordObjects::Guild &guild = guilds[channel.guild_id]; - DiscordObjects::GuildMember &sender = guild.members[data["author"]["id"]]; + DiscordObjects::User &sender = users[data["author"]["id"]]; - if (sender.user->bot) return; // ignore bots to prevent looping + if (sender.bot) return; std::vector words; boost::split(words, message, boost::is_any_of(" ")); - Command custom_command; + CommandHelper::Command custom_command; 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!"); + DiscordAPI::send_message(channel.id, ":exclamation: Invalid arguments!"); return; } else if (words.size() > 1) { @@ -391,7 +476,7 @@ void GatewayHandler::on_event_message_create(json data) { help += "\\`trivia **stop**: stops the ongoing game.\n"; help += "\\`trivia **help**: prints this message\n"; - ah->send_message(channel.id, help); + DiscordAPI::send_message(channel.id, help); return; } else if (words[1] == "stop" || words[1] == "s") { @@ -408,13 +493,13 @@ void GatewayHandler::on_event_message_create(json data) { } } catch (std::invalid_argument e) { - ah->send_message(channel.id, ":exclamation: Invalid arguments!"); + DiscordAPI::send_message(channel.id, ":exclamation: Invalid arguments!"); return; } } } - games[channel.id] = std::make_unique(this, ah, channel.id, questions, delay); + games[channel.id] = std::make_unique(this, channel.id, questions, delay); games[channel.id]->start(); } else if (words[0] == "`guilds") { @@ -422,56 +507,59 @@ void GatewayHandler::on_event_message_create(json data) { 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); + DiscordAPI::send_message(channel.id, m); } else if (words[0] == "`info") { - ah->send_message(channel.id, ":information_source: trivia-bot by Jack. "); + DiscordAPI::send_message(channel.id, ":information_source: trivia-bot by Jack. "); } - else if (words[0] == "~js" && message.length() > 4) { + 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, &sender); + it->second->exec_js(js, &channel, member); } } - else if (words[0] == "~createjs" && message.length() > 8) { + else if (words[0] == "~createjs" && words.size() > 1) { 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 = command_helper->insert_command(channel.guild_id, command_name, script); + int result = CommandHelper::insert_command(channel.guild_id, command_name, script); switch (result) { case 0: - ah->send_message(channel.id, ":warning: Error!"); break; + DiscordAPI::send_message(channel.id, ":warning: Error!"); break; case 1: - ah->send_message(channel.id, ":new: Command `" + command_name + "` successfully created."); break; + DiscordAPI::send_message(channel.id, ":new: Command `" + command_name + "` successfully created."); break; case 2: - ah->send_message(channel.id, ":arrow_heading_up: Command `" + command_name + "` successfully updated."); break; + DiscordAPI::send_message(channel.id, ":arrow_heading_up: Command `" + command_name + "` successfully updated."); break; } } } - else if (words[0] == "`shutdown" && sender.user->id == "82232146579689472") { // it me - ah->send_message(channel.id, ":zzz: Goodbye!"); + else if (words[0] == "`shutdown" && sender.id == "82232146579689472") { // it me + DiscordAPI::send_message(channel.id, ":zzz: Goodbye!"); // TODO: without needing c, hdl - c.close(hdl, websocketpp::close::status::going_away, "`shutdown command used."); } else if (words[0] == "`debug") { if (words[1] == "channel" && words.size() == 3) { auto it = channels.find(words[2]); if (it != channels.end()) { - ah->send_message(channel.id, it->second.to_debug_string()); + DiscordAPI::send_message(channel.id, it->second.to_debug_string()); } else { - ah->send_message(channel.id, ":question: Unrecognised channel."); + DiscordAPI::send_message(channel.id, ":question: Unrecognised channel."); } } else if (words[1] == "guild" && words.size() == 3) { auto it = guilds.find(words[2]); if (it != guilds.end()) { - ah->send_message(channel.id, it->second.to_debug_string()); + DiscordAPI::send_message(channel.id, it->second.to_debug_string()); } else { - ah->send_message(channel.id, ":question: Unrecognised guild."); + DiscordAPI::send_message(channel.id, ":question: Unrecognised guild."); } } else if (words[1] == "member" && words.size() == 4) { @@ -479,25 +567,27 @@ void GatewayHandler::on_event_message_create(json data) { if (it != guilds.end()) { std::string user_id = words[3]; - auto it2 = it->second.members.find(user_id); + 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()) { - ah->send_message(channel.id, it2->second.to_debug_string()); + DiscordAPI::send_message(channel.id, (*it2)->to_debug_string()); } else { - ah->send_message(channel.id, ":question: Unrecognised user."); + DiscordAPI::send_message(channel.id, ":question: Unrecognised user."); } } else { - ah->send_message(channel.id, ":question: Unrecognised guild."); + DiscordAPI::send_message(channel.id, ":question: Unrecognised guild."); } } else if (words[1] == "role" && words.size() == 3) { auto it = roles.find(words[2]); if (it != roles.end()) { - ah->send_message(channel.id, it->second.to_debug_string()); + DiscordAPI::send_message(channel.id, it->second.to_debug_string()); } else { - ah->send_message(channel.id, ":question: Unrecognised role."); + DiscordAPI::send_message(channel.id, ":question: Unrecognised role."); } } else if (words[1] == "role" && words.size() == 4) { @@ -511,21 +601,21 @@ void GatewayHandler::on_event_message_create(json data) { auto it2 = std::find_if(it->second.roles.begin(), it->second.roles.end(), check_lambda); if (it2 != it->second.roles.end()) { - ah->send_message(channel.id, (*it2)->to_debug_string()); + DiscordAPI::send_message(channel.id, (*it2)->to_debug_string()); } else { - ah->send_message(channel.id, ":question: Unrecognised role."); + DiscordAPI::send_message(channel.id, ":question: Unrecognised role."); } } else { - ah->send_message(channel.id, ":question: Unrecognised guild."); + DiscordAPI::send_message(channel.id, ":question: Unrecognised guild."); } } else { - ah->send_message(channel.id, ":question: Unknown parameters."); + DiscordAPI::send_message(channel.id, ":question: Unknown parameters."); } } - else if (command_helper->get_command(channel.guild_id, words[0], custom_command)) { + 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); @@ -533,35 +623,17 @@ void GatewayHandler::on_event_message_create(json data) { auto it = v8_instances.find(channel.guild_id); if (it != v8_instances.end() && custom_command.script.length() > 0) { - it->second->exec_js(custom_command.script, &channel, &sender, args); + 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 game - games[channel.id]->handle_answer(message, *sender.user); + games[channel.id]->handle_answer(message, sender); } } -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 } } - } } - }; - - c.send(hdl, identify.dump(), websocketpp::frame::opcode::text); - Logger::write("Sent identify payload", Logger::LogLevel::Debug); -} - void GatewayHandler::delete_game(std::string channel_id) { auto it = games.find(channel_id); diff --git a/TriviaBot/bot/GatewayHandler.hpp b/TriviaBot/bot/GatewayHandler.hpp index 19d579c..f0c8732 100644 --- a/TriviaBot/bot/GatewayHandler.hpp +++ b/TriviaBot/bot/GatewayHandler.hpp @@ -45,21 +45,24 @@ public: 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: int last_seq; int heartbeat_interval; - void on_event_ready(json data); + /* 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 @@ -85,8 +88,6 @@ private: // bot's user obj DiscordObjects::User user_object; - std::unique_ptr command_helper; - /* */ std::map guilds; std::map channels; @@ -99,8 +100,6 @@ private: std::map> v8_instances; std::unique_ptr heartbeat_thread; - - std::shared_ptr ah; }; #endif \ No newline at end of file diff --git a/TriviaBot/bot/TriviaBot.cpp b/TriviaBot/bot/TriviaBot.cpp index 1a22042..f2d970c 100644 --- a/TriviaBot/bot/TriviaBot.cpp +++ b/TriviaBot/bot/TriviaBot.cpp @@ -4,6 +4,7 @@ #include "ClientConnection.hpp" #include "Logger.hpp" +#include "DiscordAPI.hpp" std::string bot_token; @@ -26,21 +27,26 @@ int main(int argc, char *argv[]) { std::cin >> bot_token; } - // todo: get this using API - std::string uri = "wss://gateway.discord.gg/?v=5&encoding=json"; + std::string args = "/?v=5&encoding=json"; + std::string url = DiscordAPI::get_gateway().value("url", "wss://gateway.discord.gg"); - try { - ClientConnection conn; - conn.start(uri); - } - catch (const std::exception &e) { - Logger::write("std exception: " + std::string(e.what()), Logger::LogLevel::Severe); - } - catch (websocketpp::lib::error_code e) { - Logger::write("websocketpp exception: " + e.message(), Logger::LogLevel::Severe); - } - catch (...) { - Logger::write("other exception.", Logger::LogLevel::Severe); + 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(); diff --git a/TriviaBot/bot/TriviaGame.cpp b/TriviaBot/bot/TriviaGame.cpp index 275aa92..7d62884 100644 --- a/TriviaBot/bot/TriviaGame.cpp +++ b/TriviaBot/bot/TriviaGame.cpp @@ -10,13 +10,12 @@ #include #include "GatewayHandler.hpp" -#include "APIHelper.hpp" +#include "DiscordAPI.hpp" #include "data_structures/User.hpp" #include "Logger.hpp" -TriviaGame::TriviaGame(GatewayHandler *gh, std::shared_ptr ah, std::string channel_id, int total_questions, int delay) : interval(delay) { +TriviaGame::TriviaGame(GatewayHandler *gh, std::string channel_id, int total_questions, int delay) : interval(delay) { this->gh = gh; - this->ah = ah; this->channel_id = channel_id; this->total_questions = total_questions; @@ -27,7 +26,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!"); return; } @@ -51,7 +50,7 @@ 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); sqlite3 *db; int rc; std::string sql; @@ -214,7 +213,7 @@ void TriviaGame::question() { 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); question_start = boost::posix_time::microsec_clock::universal_time(); give_hint(0, ""); @@ -281,11 +280,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 + "`**"); } } 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() + "` **"); } void TriviaGame::handle_answer(std::string answer, DiscordObjects::User sender) { @@ -300,7 +299,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)"); increase_score(sender.id); update_average_time(sender.id, diff.total_milliseconds()); diff --git a/TriviaBot/bot/TriviaGame.hpp b/TriviaBot/bot/TriviaGame.hpp index 4031c1d..89b326f 100644 --- a/TriviaBot/bot/TriviaGame.hpp +++ b/TriviaBot/bot/TriviaGame.hpp @@ -11,7 +11,6 @@ #include class GatewayHandler; -class APIHelper; namespace DiscordObjects { class User; } @@ -19,7 +18,7 @@ namespace DiscordObjects { class TriviaGame { public: - TriviaGame(GatewayHandler *gh, std::shared_ptr ah, std::string channel_id, int total_questions, int delay); + TriviaGame(GatewayHandler *gh, std::string channel_id, int total_questions, int delay); ~TriviaGame(); void start(); @@ -38,7 +37,6 @@ private: std::string channel_id; GatewayHandler *gh; - std::shared_ptr ah; const char hide_char = '#'; diff --git a/TriviaBot/bot/data_structures/Channel.hpp b/TriviaBot/bot/data_structures/Channel.hpp index 1529db3..068d0c0 100644 --- a/TriviaBot/bot/data_structures/Channel.hpp +++ b/TriviaBot/bot/data_structures/Channel.hpp @@ -57,7 +57,7 @@ namespace DiscordObjects { type = "text"; } - inline Channel::Channel(json data) { + inline Channel::Channel(json data) : Channel() { load_from_json(data); } diff --git a/TriviaBot/bot/data_structures/Guild.hpp b/TriviaBot/bot/data_structures/Guild.hpp index aa90a44..82b7169 100644 --- a/TriviaBot/bot/data_structures/Guild.hpp +++ b/TriviaBot/bot/data_structures/Guild.hpp @@ -73,7 +73,7 @@ namespace DiscordObjects { bool unavailable; std::vector channels; - std::map members; + std::vector members; std::vector roles; //std::vector> users; }; @@ -83,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"); diff --git a/TriviaBot/bot/data_structures/GuildMember.hpp b/TriviaBot/bot/data_structures/GuildMember.hpp index 0997c21..3c7a06b 100644 --- a/TriviaBot/bot/data_structures/GuildMember.hpp +++ b/TriviaBot/bot/data_structures/GuildMember.hpp @@ -30,13 +30,12 @@ namespace DiscordObjects { inline GuildMember::GuildMember() { user = nullptr; - nick = "null"; - joined_at = "null"; + nick = joined_at = "null"; deaf = false; mute = false; } - inline GuildMember::GuildMember(json data, User *user) { + inline GuildMember::GuildMember(json data, User *user) : GuildMember() { this->user = user; load_from_json(data); } @@ -54,6 +53,8 @@ namespace DiscordObjects { + "\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) diff --git a/TriviaBot/bot/data_structures/Role.hpp b/TriviaBot/bot/data_structures/Role.hpp index 1e88a72..6de6080 100644 --- a/TriviaBot/bot/data_structures/Role.hpp +++ b/TriviaBot/bot/data_structures/Role.hpp @@ -42,7 +42,7 @@ namespace DiscordObjects { mentionable = false; } - inline Role::Role(json data) { + inline Role::Role(json data) : Role() { load_from_json(data); } diff --git a/TriviaBot/bot/data_structures/User.hpp b/TriviaBot/bot/data_structures/User.hpp index 546a1fc..8e39f44 100644 --- a/TriviaBot/bot/data_structures/User.hpp +++ b/TriviaBot/bot/data_structures/User.hpp @@ -39,15 +39,20 @@ namespace DiscordObjects { 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/http/HTTPHelper.cpp b/TriviaBot/bot/http/HTTPHelper.cpp index 5c76703..d1e98ee 100644 --- a/TriviaBot/bot/http/HTTPHelper.cpp +++ b/TriviaBot/bot/http/HTTPHelper.cpp @@ -8,52 +8,97 @@ 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 { - Logger::write("curl error", Logger::LogLevel::Warning); - return ""; - } - - curl_easy_cleanup(curl); - curl_slist_free_all(headers); +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; } - return read_buffer; -} + std::string 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; -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; + 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 ""; + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + } + + return read_buffer; + } + + std::string get_request(std::string url, 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[2]; + header_arr[0] = "Authorization: Bot " + 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/HTTPHelper.hpp b/TriviaBot/bot/http/HTTPHelper.hpp index 3010eff..1acc725 100644 --- a/TriviaBot/bot/http/HTTPHelper.hpp +++ b/TriviaBot/bot/http/HTTPHelper.hpp @@ -5,12 +5,9 @@ #include -class HTTPHelper { -public: +namespace HTTP { 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); -}; + std::string get_request(std::string url, long *response_code); +} #endif \ No newline at end of file diff --git a/TriviaBot/bot/js/CommandHelper.cpp b/TriviaBot/bot/js/CommandHelper.cpp index 4722632..afd8c2e 100644 --- a/TriviaBot/bot/js/CommandHelper.cpp +++ b/TriviaBot/bot/js/CommandHelper.cpp @@ -7,150 +7,158 @@ #include "../Logger.hpp" -CommandHelper::CommandHelper() { - sqlite3 *db; int return_code; - return_code = sqlite3_open("bot/db/trivia.db", &db); +namespace CommandHelper { + std::vector commands; - std::string sql = "SELECT * FROM CustomJS"; + void init() { + sqlite3 *db; int return_code; + return_code = sqlite3_open("bot/db/trivia.db", &db); - sqlite3_stmt *stmt; - return_code = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0); + std::string sql = "SELECT * 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); + 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)); + 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 CommandHelper::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 }; - } -} - -// returns: 0 error, 1 inserted, 2 updated -int CommandHelper::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;"; - std::cout << "Command already exists, updating." << std::endl; - ret_value = 2; - } else { - sql = "INSERT INTO CustomJS(Script, GuildID, CommandName) VALUES (?1, ?2, ?3);"; - std::cout << "Inserting new command." << std::endl; - 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; + commands.push_back({ guild_id, command_name, script }); + } + else if (return_code != SQLITE_DONE) { + sqlite3_finalize(stmt); + std::cerr << "SQLite error." << std::endl; + return; } } - return ret_value; + Logger::write(std::to_string(commands.size()) + " custom command(s) loaded", Logger::LogLevel::Info); + + sqlite3_finalize(stmt); + sqlite3_close(db); } - return 0; -} - -bool CommandHelper::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; -} - -bool CommandHelper::return_code_ok(int return_code) { - // TODO: NotLikeThis - if (return_code != SQLITE_OK) { - Logger::write("SQLite error", Logger::LogLevel::Severe); - return false; + 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; } - return true; } \ No newline at end of file diff --git a/TriviaBot/bot/js/CommandHelper.hpp b/TriviaBot/bot/js/CommandHelper.hpp index 2f95fd9..a34d6c8 100644 --- a/TriviaBot/bot/js/CommandHelper.hpp +++ b/TriviaBot/bot/js/CommandHelper.hpp @@ -3,23 +3,17 @@ #include -struct Command { - std::string guild_id; - std::string command_name; - std::string script; -}; +namespace CommandHelper { + struct Command { + std::string guild_id; + std::string command_name; + std::string script; + }; -class CommandHelper { -public: - CommandHelper(); + 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); - -private: bool command_in_db(std::string guild_id, std::string command_name); - bool return_code_ok(int return_code); - - std::vector commands; -}; +} #endif \ No newline at end of file diff --git a/TriviaBot/bot/js/V8Instance.cpp b/TriviaBot/bot/js/V8Instance.cpp index 04ed7c3..6a6f9b1 100644 --- a/TriviaBot/bot/js/V8Instance.cpp +++ b/TriviaBot/bot/js/V8Instance.cpp @@ -1,15 +1,17 @@ #include #include +#include +#include #include "V8Instance.hpp" -#include "../APIHelper.hpp" +#include "../DiscordAPI.hpp" #include "../Logger.hpp" -V8Instance::V8Instance(std::string guild_id, std::shared_ptr ah, std::map *guilds, std::map *channels, +V8Instance::V8Instance(std::string guild_id, std::map *guilds, std::map *channels, std::map *users, std::map *roles) { + rng = std::mt19937(std::random_device()()); this->guild_id = guild_id; - this->ah = ah; this->guilds = guilds; this->channels = channels; this->users = users; @@ -35,96 +37,645 @@ void V8Instance::create() { // set global context Local context = create_context(); - context->Enter(); + 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 opts_obj = wrap_server(&(*guilds)[guild_id]); + + context->Global()->Set( + context, + String::NewFromUtf8(isolate, "server", NewStringType::kNormal).ToLocalChecked(), + opts_obj + ).FromJust(); + + Logger::write("[v8] Bound server template", Logger::LogLevel::Debug); +} + v8::Local V8Instance::create_context() { Local global = ObjectTemplate::New(isolate); - // bind print() function - Local self = External::New(isolate, (void *) this); + Local self = External::New(isolate, (void *) this); global->Set( - String::NewFromUtf8(isolate, "print", NewStringType::kNormal).ToLocalChecked(), + String::NewFromUtf8(isolate, "print", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, V8Instance::js_print, self) ); - global->SetAccessor( - String::NewFromUtf8(isolate, "server", NewStringType::kNormal).ToLocalChecked(), - V8Instance::js_get_server, - (AccessorSetterCallback) 0, - self + global->Set( + String::NewFromUtf8(isolate, "random", NewStringType::kNormal).ToLocalChecked(), + FunctionTemplate::New(isolate, V8Instance::js_random, self) ); - global->SetAccessor( - String::NewFromUtf8(isolate, "channel", NewStringType::kNormal).ToLocalChecked(), - V8Instance::js_get_channel, - (AccessorSetterCallback) 0, - self - ); - global->SetAccessor( - String::NewFromUtf8(isolate, "user", NewStringType::kNormal).ToLocalChecked(), - V8Instance::js_get_user, - (AccessorSetterCallback) 0, - self - ); - global->SetAccessor( - String::NewFromUtf8(isolate, "input", NewStringType::kNormal).ToLocalChecked(), - V8Instance::js_get_input, - (AccessorSetterCallback) 0, - self + global->Set( + String::NewFromUtf8(isolate, "shuffle", NewStringType::kNormal).ToLocalChecked(), + FunctionTemplate::New(isolate, V8Instance::js_shuffle, self) ); - Logger::write("[v8] Created global obj, linked data and functions", Logger::LogLevel::Debug); + Logger::write("[v8] Created global context, added print function", Logger::LogLevel::Debug); return Context::New(isolate, NULL, global); } -void V8Instance::js_get_server(Local property, const PropertyCallbackInfo &info) { - auto data = info.Data().As(); - V8Instance *self = static_cast(data->Value()); +/* server */ +Local V8Instance::make_server_template() { + EscapableHandleScope handle_scope(isolate); - Local obj = Object::New(info.GetIsolate()); - self->add_to_obj(obj, (*self->guilds)[self->guild_id]); - info.GetReturnValue().Set(obj); + 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); } -void V8Instance::js_get_channel(Local property, const PropertyCallbackInfo &info) { - auto data = info.Data().As(); - V8Instance *self = static_cast(data->Value()); +Local V8Instance::wrap_server(DiscordObjects::Guild *guild) { + EscapableHandleScope handle_scope(isolate); - if (!self->current_channel) { - Logger::write("[v8] current_channel is null pointer", Logger::LogLevel::Severe); - info.GetReturnValue().SetNull(); - return; + if (server_template.IsEmpty()) { + Local raw_template = make_server_template(); + server_template.Reset(isolate, raw_template); } - Local obj = Object::New(info.GetIsolate()); - self->add_to_obj(obj, (*self->current_channel)); - info.GetReturnValue().Set(obj); + 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_user(Local property, const PropertyCallbackInfo &info) { - auto data = info.Data().As(); - V8Instance *self = static_cast(data->Value()); - - if (!self->current_sender) { - Logger::write("[v8] current_sender is null pointer", Logger::LogLevel::Severe); - info.GetReturnValue().SetNull(); +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); - Local obj = Object::New(info.GetIsolate()); - self->add_to_obj(obj, (*self->current_sender)); - info.GetReturnValue().Set(obj); + 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); + } } -void V8Instance::js_get_input(Local property, const PropertyCallbackInfo &info) { - auto data = info.Data().As(); + +/* 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 *self_v = info.Data().As()->Value(); + if (!self_v) { + Logger::write("[v8] [js_get_channel] Class pointer empty", Logger::LogLevel::Warning); + return; + } + V8Instance *self = static_cast(self_v); + + 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 *self_v = info.Data().As()->Value(); + if (!self_v) { + Logger::write("[v8] [js_get_role] Class pointer empty", Logger::LogLevel::Warning); + return; + } + V8Instance *self = static_cast(self_v); + + 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()); - info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), self->current_input.c_str(), NewStringType::kNormal).ToLocalChecked()); + 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::clean_up() { Logger::write("[v8] Cleaning up", Logger::LogLevel::Debug); isolate->Exit(); @@ -138,13 +689,29 @@ void V8Instance::reload() { void V8Instance::exec_js(std::string js, DiscordObjects::Channel *channel, DiscordObjects::GuildMember *sender, std::string args) { HandleScope handle_scope(isolate); - Local context(isolate->GetCurrentContext()); + 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_user(sender); + context->Global()->Set( + String::NewFromUtf8(isolate, "user", NewStringType::kNormal).ToLocalChecked(), + user_obj + ); + // TODO: 'message' object here too, although it's fairly pointless current_sender = sender; current_channel = channel; - current_input = args; - Logger::write("[v8] Preparing JS: " + js, Logger::LogLevel::Debug); + 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(); @@ -154,12 +721,13 @@ void V8Instance::exec_js(std::string js, DiscordObjects::Channel *channel, Disco TryCatch compile_try_catch(isolate); Local