diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index e9de82316..1ca2beb9d 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -7,6 +7,7 @@ SET(common_sources compression.cpp condition.cpp content/world_content_service.cpp + discord/discord.cpp crash.cpp crc16.cpp crc32.cpp @@ -34,6 +35,7 @@ SET(common_sources event_sub.cpp expedition_lockout_timer.cpp extprofile.cpp + discord_manager.cpp faction.cpp file_util.cpp guild_base.cpp @@ -491,6 +493,8 @@ SET(common_headers database_schema.h dbcore.h deity.h + discord/discord.h + discord_manager.h dynamic_zone_base.h emu_constants.h emu_limits.h diff --git a/common/dbcore.cpp b/common/dbcore.cpp index 7f1f52363..e3202947f 100644 --- a/common/dbcore.cpp +++ b/common/dbcore.cpp @@ -167,7 +167,7 @@ MySQLRequestResult DBcore::QueryDatabase(const char *query, uint32 querylen, boo if (LogSys.log_settings[Logs::MySQLQuery].is_category_enabled == 1) { if ((strncasecmp(query, "select", 6) == 0)) { LogMySQLQuery( - "{0} ({1} row{2} returned) ({3}s)", + "{0}; -- ({1} row{2} returned) ({3}s)", query, requestResult.RowCount(), requestResult.RowCount() == 1 ? "" : "s", @@ -176,7 +176,7 @@ MySQLRequestResult DBcore::QueryDatabase(const char *query, uint32 querylen, boo } else { LogMySQLQuery( - "{0} ({1} row{2} affected) ({3}s)", + "{0}; -- ({1} row{2} affected) ({3}s)", query, requestResult.RowsAffected(), requestResult.RowsAffected() == 1 ? "" : "s", diff --git a/common/discord/discord.cpp b/common/discord/discord.cpp new file mode 100644 index 000000000..5d17f4d35 --- /dev/null +++ b/common/discord/discord.cpp @@ -0,0 +1,91 @@ +#include "discord.h" +#include "../http/httplib.h" +#include "../json/json.h" +#include "../string_util.h" +#include "../eqemu_logsys.h" + +constexpr int MAX_RETRIES = 10; + +void Discord::SendWebhookMessage(const std::string &message, const std::string &webhook_url) +{ + // validate + if (webhook_url.empty()) { + LogDiscord("[webhook_url] is empty"); + return; + } + + // validate + if (webhook_url.find("http://") == std::string::npos && webhook_url.find("https://") == std::string::npos) { + LogDiscord("[webhook_url] [{}] does not contain a valid http/s prefix.", webhook_url); + return; + } + + // split + auto s = SplitString(webhook_url, '/'); + + // url + std::string base_url = fmt::format("{}//{}", s[0], s[2]); + std::string endpoint = replace_string(webhook_url, base_url, ""); + + // client + httplib::Client cli(base_url.c_str()); + cli.set_connection_timeout(0, 15000000); // 15 sec + cli.set_read_timeout(15, 0); // 15 seconds + cli.set_write_timeout(15, 0); // 15 seconds + httplib::Headers headers = { + {"Content-Type", "application/json"} + }; + + // payload + Json::Value p; + p["content"] = message; + std::stringstream payload; + payload << p; + + bool retry = true; + int retries = 0; + int retry_timer = 1000; + while (retry) { + if (auto res = cli.Post(endpoint.c_str(), payload.str(), "application/json")) { + if (res->status != 200 && res->status != 204) { + LogError("[Discord Client] Code [{}] Error [{}]", res->status, res->body); + } + if (res->status == 429) { + if (!res->body.empty()) { + std::stringstream ss(res->body); + Json::Value response; + + try { + ss >> response; + } + catch (std::exception const &ex) { + LogDiscord("JSON serialization failure [{}] via [{}]", ex.what(), res->body); + } + + retry_timer = std::stoi(response["retry_after"].asString()) + 500; + } + + LogDiscord("Rate limited... retrying message in [{}ms]", retry_timer); + std::this_thread::sleep_for(std::chrono::milliseconds(retry_timer + 500)); + } + if (res->status == 204) { + retry = false; + } + if (retries > MAX_RETRIES) { + LogDiscord("Retries exceeded for message [{}]", message); + retry = false; + } + + retries++; + } + } +} + +std::string Discord::FormatDiscordMessage(uint16 category_id, const std::string &message) +{ + if (category_id == Logs::LogCategory::MySQLQuery) { + return fmt::format("```sql\n{}\n```", message); + } + + return message + "\n"; +} diff --git a/common/discord/discord.h b/common/discord/discord.h new file mode 100644 index 000000000..d4ebc73f1 --- /dev/null +++ b/common/discord/discord.h @@ -0,0 +1,15 @@ +#ifndef EQEMU_DISCORD_H +#define EQEMU_DISCORD_H + + +#include +#include "../types.h" + +class Discord { +public: + static void SendWebhookMessage(const std::string& message, const std::string& webhook_url); + static std::string FormatDiscordMessage(uint16 category_id, const std::string& message); +}; + + +#endif //EQEMU_DISCORD_H diff --git a/common/discord_manager.cpp b/common/discord_manager.cpp new file mode 100644 index 000000000..b2d8a9590 --- /dev/null +++ b/common/discord_manager.cpp @@ -0,0 +1,65 @@ +#include "discord_manager.h" +#include "../common/discord/discord.h" +#include "../common/eqemu_logsys.h" +#include "../common/string_util.h" + +void DiscordManager::QueueWebhookMessage(uint32 webhook_id, const std::string &message) +{ + webhook_queue_lock.lock(); + webhook_message_queue[webhook_id].emplace_back(message); + webhook_queue_lock.unlock(); +} + +constexpr int MAX_MESSAGE_LENGTH = 1900; + +void DiscordManager::ProcessMessageQueue() +{ + if (webhook_message_queue.empty()) { + return; + } + + webhook_queue_lock.lock(); + for (auto &q: webhook_message_queue) { + LogDiscord("Processing [{}] messages in queue for webhook ID [{}]...", q.second.size(), q.first); + + auto webhook = LogSys.discord_webhooks[q.first]; + std::string message; + + for (auto &m: q.second) { + // next message would become too large + bool next_message_too_large = ((int) m.length() + (int) message.length()) > MAX_MESSAGE_LENGTH; + if (next_message_too_large) { + Discord::SendWebhookMessage( + message, + webhook.webhook_url + ); + message = ""; + } + + message += m; + + // one single message was too large + // this should rarely happen but the message will need to be split + if ((int) message.length() > MAX_MESSAGE_LENGTH) { + for (unsigned mi = 0; mi < message.length(); mi += MAX_MESSAGE_LENGTH) { + Discord::SendWebhookMessage( + message.substr(mi, MAX_MESSAGE_LENGTH), + webhook.webhook_url + ); + } + message = ""; + } + } + + // final flush + if (!message.empty()) { + Discord::SendWebhookMessage( + message, + webhook.webhook_url + ); + } + + webhook_message_queue.erase(q.first); + } + webhook_queue_lock.unlock(); +} diff --git a/common/discord_manager.h b/common/discord_manager.h new file mode 100644 index 000000000..cc3573630 --- /dev/null +++ b/common/discord_manager.h @@ -0,0 +1,19 @@ +#ifndef EQEMU_DISCORD_MANAGER_H +#define EQEMU_DISCORD_MANAGER_H + +#include +#include +#include +#include "../common/types.h" + +class DiscordManager { +public: + void QueueWebhookMessage(uint32 webhook_id, const std::string& message); + void ProcessMessageQueue(); +private: + std::mutex webhook_queue_lock{}; + std::map> webhook_message_queue{}; +}; + + +#endif diff --git a/common/eqemu_logsys.cpp b/common/eqemu_logsys.cpp index 8365e6989..5bf508ec5 100644 --- a/common/eqemu_logsys.cpp +++ b/common/eqemu_logsys.cpp @@ -23,6 +23,8 @@ #include "platform.h" #include "string_util.h" #include "misc.h" +#include "discord/discord.h" +#include "repositories/discord_webhooks_repository.h" #include "repositories/logsys_categories_repository.h" #include @@ -46,6 +48,7 @@ std::ofstream process_log; #include #include +#include #endif @@ -89,7 +92,7 @@ namespace Console { EQEmuLogSys::EQEmuLogSys() { on_log_gmsay_hook = [](uint16 log_type, const std::string &) {}; - on_log_console_hook = [](uint16 debug_level, uint16 log_type, const std::string &) {}; + on_log_console_hook = [](uint16 log_type, const std::string &) {}; } /** @@ -108,6 +111,7 @@ EQEmuLogSys *EQEmuLogSys::LoadLogSettingsDefaults() log_settings[log_category_id].log_to_console = 0; log_settings[log_category_id].log_to_file = 0; log_settings[log_category_id].log_to_gmsay = 0; + log_settings[log_category_id].log_to_discord = 0; log_settings[log_category_id].is_category_enabled = 0; } @@ -135,6 +139,7 @@ EQEmuLogSys *EQEmuLogSys::LoadLogSettingsDefaults() log_settings[Logs::ChecksumVerification].log_to_console = static_cast(Logs::General); log_settings[Logs::ChecksumVerification].log_to_gmsay = static_cast(Logs::General); log_settings[Logs::CombatRecord].log_to_gmsay = static_cast(Logs::General); + log_settings[Logs::Discord].log_to_console = static_cast(Logs::General); /** * RFC 5424 @@ -154,7 +159,8 @@ EQEmuLogSys *EQEmuLogSys::LoadLogSettingsDefaults() const bool log_to_console = log_settings[log_category_id].log_to_console > 0; const bool log_to_file = log_settings[log_category_id].log_to_file > 0; const bool log_to_gmsay = log_settings[log_category_id].log_to_gmsay > 0; - const bool is_category_enabled = log_to_console || log_to_file || log_to_gmsay; + const bool log_to_discord = log_settings[log_category_id].log_to_discord > 0; + const bool is_category_enabled = log_to_console || log_to_file || log_to_gmsay || log_to_discord; if (is_category_enabled) { log_settings[log_category_id].is_category_enabled = 1; } @@ -221,41 +227,12 @@ std::string EQEmuLogSys::FormatOutMessageString( return return_string + "[" + Logs::LogCategoryName[log_category] + "] " + in_message; } -/** - * @param debug_level - * @param log_category - * @param message - */ -void EQEmuLogSys::ProcessGMSay( - uint16 debug_level, - uint16 log_category, - const std::string &message -) -{ - /** - * Enabling Netcode based GMSay output creates a feedback loop that ultimately ends in a crash - */ - if (log_category == Logs::LogCategory::Netcode) { - return; - } - - /** - * Processes that actually support hooks - */ - if (EQEmuLogSys::log_platform == EQEmuExePlatform::ExePlatformZone || - EQEmuLogSys::log_platform == EQEmuExePlatform::ExePlatformWorld - ) { - on_log_gmsay_hook(log_category, message); - } -} - /** * @param debug_level * @param log_category * @param message */ void EQEmuLogSys::ProcessLogWrite( - uint16 debug_level, uint16 log_category, const std::string &message ) @@ -273,10 +250,9 @@ void EQEmuLogSys::ProcessLogWrite( crash_log.close(); } - char time_stamp[80]; - EQEmuLogSys::SetCurrentTimeStamp(time_stamp); - if (process_log) { + char time_stamp[80]; + EQEmuLogSys::SetCurrentTimeStamp(time_stamp); process_log << time_stamp << " " << message << std::endl; } } @@ -372,7 +348,7 @@ uint16 EQEmuLogSys::GetGMSayColorFromCategory(uint16 log_category) * @param log_category * @param message */ -void EQEmuLogSys::ProcessConsoleMessage(uint16 debug_level, uint16 log_category, const std::string &message) +void EQEmuLogSys::ProcessConsoleMessage(uint16 log_category, const std::string &message) { #ifdef _WINDOWS HANDLE console_handle; @@ -390,7 +366,7 @@ void EQEmuLogSys::ProcessConsoleMessage(uint16 debug_level, uint16 log_category, std::cout << EQEmuLogSys::GetLinuxConsoleColorFromCategory(log_category) << message << LC_RESET << std::endl; #endif - on_log_console_hook(debug_level, log_category, message); + on_log_console_hook(log_category, message); } /** @@ -447,28 +423,28 @@ void EQEmuLogSys::Out( ... ) { - bool log_to_console = true; - if (log_settings[log_category].log_to_console < debug_level) { - log_to_console = false; - } + bool log_to_console = log_settings[log_category].log_to_console > 0 && + log_settings[log_category].log_to_console >= debug_level; + bool log_to_file = log_settings[log_category].log_to_file > 0 && + log_settings[log_category].log_to_file >= debug_level; + bool log_to_gmsay = log_settings[log_category].log_to_gmsay > 0 && + log_settings[log_category].log_to_gmsay >= debug_level && + log_category != Logs::LogCategory::Netcode && + (EQEmuLogSys::log_platform == EQEmuExePlatform::ExePlatformZone || + EQEmuLogSys::log_platform == EQEmuExePlatform::ExePlatformWorld); + bool log_to_discord = EQEmuLogSys::log_platform == EQEmuExePlatform::ExePlatformZone && + log_settings[log_category].log_to_discord > 0 && + log_settings[log_category].log_to_discord >= debug_level && + log_settings[log_category].discord_webhook_id > 0 && + log_settings[log_category].discord_webhook_id < MAX_DISCORD_WEBHOOK_ID; - bool log_to_file = true; - if (log_settings[log_category].log_to_file < debug_level) { - log_to_file = false; - } - - bool log_to_gmsay = true; - if (log_settings[log_category].log_to_gmsay < debug_level) { - log_to_gmsay = false; - } - - const bool nothing_to_log = !log_to_console && !log_to_file && !log_to_gmsay; + // bail out if nothing to log + const bool nothing_to_log = !log_to_console && !log_to_file && !log_to_gmsay && !log_to_discord; if (nothing_to_log) { return; } std::string prefix; - if (RuleB(Logging, PrintFileFunctionAndLine)) { prefix = fmt::format("[{0}::{1}:{2}] ", base_file_name(file), func, line); } @@ -481,13 +457,16 @@ void EQEmuLogSys::Out( std::string output_debug_message = EQEmuLogSys::FormatOutMessageString(log_category, prefix + output_message); if (log_to_console) { - EQEmuLogSys::ProcessConsoleMessage(debug_level, log_category, output_debug_message); + EQEmuLogSys::ProcessConsoleMessage(log_category, output_debug_message); } if (log_to_gmsay) { - EQEmuLogSys::ProcessGMSay(debug_level, log_category, output_debug_message); + on_log_gmsay_hook(log_category, message); } if (log_to_file) { - EQEmuLogSys::ProcessLogWrite(debug_level, log_category, output_debug_message); + EQEmuLogSys::ProcessLogWrite(log_category, output_debug_message); + } + if (log_to_discord && on_log_discord_hook) { + on_log_discord_hook(log_category, log_settings[log_category].discord_webhook_id, output_message); } } @@ -630,16 +609,20 @@ EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings() continue; } - log_settings[c.log_category_id].log_to_console = static_cast(c.log_to_console); - log_settings[c.log_category_id].log_to_file = static_cast(c.log_to_file); - log_settings[c.log_category_id].log_to_gmsay = static_cast(c.log_to_gmsay); + log_settings[c.log_category_id].log_to_console = static_cast(c.log_to_console); + log_settings[c.log_category_id].log_to_file = static_cast(c.log_to_file); + log_settings[c.log_category_id].log_to_gmsay = static_cast(c.log_to_gmsay); + log_settings[c.log_category_id].log_to_discord = static_cast(c.log_to_discord); + log_settings[c.log_category_id].discord_webhook_id = c.discord_webhook_id; // Determine if any output method is enabled for the category // and set it to 1 so it can used to check if category is enabled const bool log_to_console = log_settings[c.log_category_id].log_to_console > 0; const bool log_to_file = log_settings[c.log_category_id].log_to_file > 0; const bool log_to_gmsay = log_settings[c.log_category_id].log_to_gmsay > 0; - const bool is_category_enabled = log_to_console || log_to_file || log_to_gmsay; + const bool log_to_discord = log_settings[c.log_category_id].log_to_discord > 0 && + log_settings[c.log_category_id].discord_webhook_id > 0; + const bool is_category_enabled = log_to_console || log_to_file || log_to_gmsay || log_to_discord; if (is_category_enabled) { log_settings[c.log_category_id].is_category_enabled = 1; @@ -669,6 +652,7 @@ EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings() new_category.log_to_console = log_settings[i].log_to_console; new_category.log_to_gmsay = log_settings[i].log_to_gmsay; new_category.log_to_file = log_settings[i].log_to_file; + new_category.log_to_discord = log_settings[i].log_to_discord; LogsysCategoriesRepository::InsertOne(*m_database, new_category); } @@ -676,6 +660,14 @@ EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings() LogInfo("Loaded [{}] log categories", categories.size()); + auto webhooks = DiscordWebhooksRepository::All(*m_database); + if (!webhooks.empty()) { + for (auto &w: webhooks) { + discord_webhooks[w.id] = {w.id, w.webhook_name, w.webhook_url}; + } + LogInfo("Loaded [{}] Discord webhooks", webhooks.size()); + } + return this; } @@ -685,3 +677,4 @@ EQEmuLogSys *EQEmuLogSys::SetDatabase(Database *db) return this; } + diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index 0ec2c0701..098851e23 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -130,6 +130,7 @@ namespace Logs { ChecksumVerification, CombatRecord, Hate, + Discord, MaxCategoryID /* Don't Remove this */ }; @@ -218,6 +219,7 @@ namespace Logs { "ChecksumVerification", "CombatRecord", "Hate", + "Discord", }; } @@ -225,6 +227,8 @@ namespace Logs { class Database; +constexpr uint16 MAX_DISCORD_WEBHOOK_ID = 300; + class EQEmuLogSys { public: EQEmuLogSys(); @@ -287,9 +291,19 @@ public: uint8 log_to_file; uint8 log_to_console; uint8 log_to_gmsay; + uint8 log_to_discord; + int discord_webhook_id; uint8 is_category_enabled; /* When any log output in a category > 0, set this to 1 as (Enabled) */ }; + struct OriginationInfo { + std::string zone_short_name; + std::string zone_long_name; + int instance_id; + }; + + OriginationInfo origination_info{}; + /** * Internally used memory reference for all log settings per category * These are loaded via DB and have defaults loaded in LoadLogSettingsDefaults @@ -297,24 +311,38 @@ public: */ LogSettings log_settings[Logs::LogCategory::MaxCategoryID]{}; + struct DiscordWebhooks { + int id; + std::string webhook_name; + std::string webhook_url; + }; + + DiscordWebhooks discord_webhooks[MAX_DISCORD_WEBHOOK_ID]{}; + bool file_logs_enabled = false; - int log_platform = 0; - std::string platform_file_name; + int log_platform = 0; + std::string platform_file_name; // gmsay uint16 GetGMSayColorFromCategory(uint16 log_category); - EQEmuLogSys * SetGMSayHandler(std::function f) { + EQEmuLogSys *SetGMSayHandler(std::function f) + { on_log_gmsay_hook = f; return this; } + EQEmuLogSys *SetDiscordHandler(std::function f) + { + on_log_discord_hook = f; + return this; + } + // console void SetConsoleHandler( std::function f @@ -328,18 +356,17 @@ public: private: // reference to database - Database *m_database; - - std::function on_log_gmsay_hook; - std::function on_log_console_hook; + Database *m_database; + std::function on_log_gmsay_hook; + std::function on_log_discord_hook; + std::function on_log_console_hook; std::string FormatOutMessageString(uint16 log_category, const std::string &in_message); std::string GetLinuxConsoleColorFromCategory(uint16 log_category); uint16 GetWindowsConsoleColorFromCategory(uint16 log_category); - void ProcessConsoleMessage(uint16 debug_level, uint16 log_category, const std::string &message); - void ProcessGMSay(uint16 debug_level, uint16 log_category, const std::string &message); - void ProcessLogWrite(uint16 debug_level, uint16 log_category, const std::string &message); + void ProcessConsoleMessage(uint16 log_category, const std::string &message); + void ProcessLogWrite(uint16 log_category, const std::string &message); bool IsRfc5424LogCategory(uint16 log_category); }; diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 15a0d7787..88c03c98a 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -726,6 +726,16 @@ OutF(LogSys, Logs::Detail, Logs::Hate, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogDiscord(message, ...) do {\ + if (LogSys.log_settings[Logs::Discord].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::Discord, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogDiscordDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::Discord].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::Discord, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.log_settings[log_category].is_category_enabled == 1)\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ diff --git a/common/repositories/base/base_discord_webhooks_repository.h b/common/repositories/base/base_discord_webhooks_repository.h new file mode 100644 index 000000000..f5d84f525 --- /dev/null +++ b/common/repositories/base/base_discord_webhooks_repository.h @@ -0,0 +1,336 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://eqemu.gitbook.io/server/in-development/developer-area/repositories + */ + +#ifndef EQEMU_BASE_DISCORD_WEBHOOKS_REPOSITORY_H +#define EQEMU_BASE_DISCORD_WEBHOOKS_REPOSITORY_H + +#include "../../database.h" +#include "../../string_util.h" +#include + +class BaseDiscordWebhooksRepository { +public: + struct DiscordWebhooks { + int id; + std::string webhook_name; + std::string webhook_url; + time_t created_at; + time_t deleted_at; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "webhook_name", + "webhook_url", + "created_at", + "deleted_at", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "webhook_name", + "webhook_url", + "UNIX_TIMESTAMP(created_at)", + "UNIX_TIMESTAMP(deleted_at)", + }; + } + + static std::string ColumnsRaw() + { + return std::string(implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("discord_webhooks"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static DiscordWebhooks NewEntity() + { + DiscordWebhooks entry{}; + + entry.id = 0; + entry.webhook_name = ""; + entry.webhook_url = ""; + entry.created_at = 0; + entry.deleted_at = 0; + + return entry; + } + + static DiscordWebhooks GetDiscordWebhooksEntry( + const std::vector &discord_webhookss, + int discord_webhooks_id + ) + { + for (auto &discord_webhooks : discord_webhookss) { + if (discord_webhooks.id == discord_webhooks_id) { + return discord_webhooks; + } + } + + return NewEntity(); + } + + static DiscordWebhooks FindOne( + Database& db, + int discord_webhooks_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE id = {} LIMIT 1", + BaseSelect(), + discord_webhooks_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + DiscordWebhooks entry{}; + + entry.id = atoi(row[0]); + entry.webhook_name = row[1] ? row[1] : ""; + entry.webhook_url = row[2] ? row[2] : ""; + entry.created_at = strtoll(row[3] ? row[3] : "-1", nullptr, 10); + entry.deleted_at = strtoll(row[4] ? row[4] : "-1", nullptr, 10); + + return entry; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int discord_webhooks_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + discord_webhooks_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + DiscordWebhooks discord_webhooks_entry + ) + { + std::vector update_values; + + auto columns = Columns(); + + update_values.push_back(columns[1] + " = '" + EscapeString(discord_webhooks_entry.webhook_name) + "'"); + update_values.push_back(columns[2] + " = '" + EscapeString(discord_webhooks_entry.webhook_url) + "'"); + update_values.push_back(columns[3] + " = FROM_UNIXTIME(" + (discord_webhooks_entry.created_at > 0 ? std::to_string(discord_webhooks_entry.created_at) : "null") + ")"); + update_values.push_back(columns[4] + " = FROM_UNIXTIME(" + (discord_webhooks_entry.deleted_at > 0 ? std::to_string(discord_webhooks_entry.deleted_at) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + implode(", ", update_values), + PrimaryKey(), + discord_webhooks_entry.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static DiscordWebhooks InsertOne( + Database& db, + DiscordWebhooks discord_webhooks_entry + ) + { + std::vector insert_values; + + insert_values.push_back(std::to_string(discord_webhooks_entry.id)); + insert_values.push_back("'" + EscapeString(discord_webhooks_entry.webhook_name) + "'"); + insert_values.push_back("'" + EscapeString(discord_webhooks_entry.webhook_url) + "'"); + insert_values.push_back("FROM_UNIXTIME(" + (discord_webhooks_entry.created_at > 0 ? std::to_string(discord_webhooks_entry.created_at) : "null") + ")"); + insert_values.push_back("FROM_UNIXTIME(" + (discord_webhooks_entry.deleted_at > 0 ? std::to_string(discord_webhooks_entry.deleted_at) : "null") + ")"); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + implode(",", insert_values) + ) + ); + + if (results.Success()) { + discord_webhooks_entry.id = results.LastInsertedID(); + return discord_webhooks_entry; + } + + discord_webhooks_entry = NewEntity(); + + return discord_webhooks_entry; + } + + static int InsertMany( + Database& db, + std::vector discord_webhooks_entries + ) + { + std::vector insert_chunks; + + for (auto &discord_webhooks_entry: discord_webhooks_entries) { + std::vector insert_values; + + insert_values.push_back(std::to_string(discord_webhooks_entry.id)); + insert_values.push_back("'" + EscapeString(discord_webhooks_entry.webhook_name) + "'"); + insert_values.push_back("'" + EscapeString(discord_webhooks_entry.webhook_url) + "'"); + insert_values.push_back("FROM_UNIXTIME(" + (discord_webhooks_entry.created_at > 0 ? std::to_string(discord_webhooks_entry.created_at) : "null") + ")"); + insert_values.push_back("FROM_UNIXTIME(" + (discord_webhooks_entry.deleted_at > 0 ? std::to_string(discord_webhooks_entry.deleted_at) : "null") + ")"); + + insert_chunks.push_back("(" + implode(",", insert_values) + ")"); + } + + std::vector insert_values; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + DiscordWebhooks entry{}; + + entry.id = atoi(row[0]); + entry.webhook_name = row[1] ? row[1] : ""; + entry.webhook_url = row[2] ? row[2] : ""; + entry.created_at = strtoll(row[3] ? row[3] : "-1", nullptr, 10); + entry.deleted_at = strtoll(row[4] ? row[4] : "-1", nullptr, 10); + + all_entries.push_back(entry); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, std::string where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + DiscordWebhooks entry{}; + + entry.id = atoi(row[0]); + entry.webhook_name = row[1] ? row[1] : ""; + entry.webhook_url = row[2] ? row[2] : ""; + entry.created_at = strtoll(row[3] ? row[3] : "-1", nullptr, 10); + entry.deleted_at = strtoll(row[4] ? row[4] : "-1", nullptr, 10); + + all_entries.push_back(entry); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, std::string where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + +}; + +#endif //EQEMU_BASE_DISCORD_WEBHOOKS_REPOSITORY_H diff --git a/common/repositories/base/base_logsys_categories_repository.h b/common/repositories/base/base_logsys_categories_repository.h index f7ffb63bb..03b60c0e8 100644 --- a/common/repositories/base/base_logsys_categories_repository.h +++ b/common/repositories/base/base_logsys_categories_repository.h @@ -24,6 +24,8 @@ public: int log_to_console; int log_to_file; int log_to_gmsay; + int log_to_discord; + int discord_webhook_id; }; static std::string PrimaryKey() @@ -39,6 +41,8 @@ public: "log_to_console", "log_to_file", "log_to_gmsay", + "log_to_discord", + "discord_webhook_id", }; } @@ -50,6 +54,8 @@ public: "log_to_console", "log_to_file", "log_to_gmsay", + "log_to_discord", + "discord_webhook_id", }; } @@ -95,6 +101,8 @@ public: entry.log_to_console = 0; entry.log_to_file = 0; entry.log_to_gmsay = 0; + entry.log_to_discord = 0; + entry.discord_webhook_id = 0; return entry; } @@ -135,6 +143,8 @@ public: entry.log_to_console = atoi(row[2]); entry.log_to_file = atoi(row[3]); entry.log_to_gmsay = atoi(row[4]); + entry.log_to_discord = atoi(row[5]); + entry.discord_webhook_id = atoi(row[6]); return entry; } @@ -173,6 +183,8 @@ public: update_values.push_back(columns[2] + " = " + std::to_string(logsys_categories_entry.log_to_console)); update_values.push_back(columns[3] + " = " + std::to_string(logsys_categories_entry.log_to_file)); update_values.push_back(columns[4] + " = " + std::to_string(logsys_categories_entry.log_to_gmsay)); + update_values.push_back(columns[5] + " = " + std::to_string(logsys_categories_entry.log_to_discord)); + update_values.push_back(columns[6] + " = " + std::to_string(logsys_categories_entry.discord_webhook_id)); auto results = db.QueryDatabase( fmt::format( @@ -199,6 +211,8 @@ public: insert_values.push_back(std::to_string(logsys_categories_entry.log_to_console)); insert_values.push_back(std::to_string(logsys_categories_entry.log_to_file)); insert_values.push_back(std::to_string(logsys_categories_entry.log_to_gmsay)); + insert_values.push_back(std::to_string(logsys_categories_entry.log_to_discord)); + insert_values.push_back(std::to_string(logsys_categories_entry.discord_webhook_id)); auto results = db.QueryDatabase( fmt::format( @@ -233,6 +247,8 @@ public: insert_values.push_back(std::to_string(logsys_categories_entry.log_to_console)); insert_values.push_back(std::to_string(logsys_categories_entry.log_to_file)); insert_values.push_back(std::to_string(logsys_categories_entry.log_to_gmsay)); + insert_values.push_back(std::to_string(logsys_categories_entry.log_to_discord)); + insert_values.push_back(std::to_string(logsys_categories_entry.discord_webhook_id)); insert_chunks.push_back("(" + implode(",", insert_values) + ")"); } @@ -271,6 +287,8 @@ public: entry.log_to_console = atoi(row[2]); entry.log_to_file = atoi(row[3]); entry.log_to_gmsay = atoi(row[4]); + entry.log_to_discord = atoi(row[5]); + entry.discord_webhook_id = atoi(row[6]); all_entries.push_back(entry); } @@ -300,6 +318,8 @@ public: entry.log_to_console = atoi(row[2]); entry.log_to_file = atoi(row[3]); entry.log_to_gmsay = atoi(row[4]); + entry.log_to_discord = atoi(row[5]); + entry.discord_webhook_id = atoi(row[6]); all_entries.push_back(entry); } diff --git a/common/repositories/discord_webhooks_repository.h b/common/repositories/discord_webhooks_repository.h new file mode 100644 index 000000000..be934945f --- /dev/null +++ b/common/repositories/discord_webhooks_repository.h @@ -0,0 +1,70 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EQEMU_DISCORD_WEBHOOKS_REPOSITORY_H +#define EQEMU_DISCORD_WEBHOOKS_REPOSITORY_H + +#include "../database.h" +#include "../string_util.h" +#include "base/base_discord_webhooks_repository.h" + +class DiscordWebhooksRepository: public BaseDiscordWebhooksRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * DiscordWebhooksRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * DiscordWebhooksRepository::GetWhereNeverExpires() + * DiscordWebhooksRepository::GetWhereXAndY() + * DiscordWebhooksRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_DISCORD_WEBHOOKS_REPOSITORY_H diff --git a/common/servertalk.h b/common/servertalk.h index 092220e6b..287f9901b 100644 --- a/common/servertalk.h +++ b/common/servertalk.h @@ -219,6 +219,7 @@ #define ServerOP_UCSServerStatusReply 0x4005 #define ServerOP_UCSServerStatusRequest 0x4006 #define ServerOP_UpdateSchedulerEvents 0x4007 +#define ServerOP_DiscordWebhookMessage 0x4008 #define ServerOP_ReloadAAData 0x4100 #define ServerOP_ReloadAlternateCurrencies 0x4101 @@ -1449,6 +1450,11 @@ struct QSMerchantLogTransaction_Struct { QSTransactionItems_Struct items[0]; }; +struct DiscordWebhookMessage_Struct { + uint32 webhook_id; + char message[2000]; +}; + struct QSGeneralQuery_Struct { char QueryString[0]; }; diff --git a/common/version.h b/common/version.h index 7f186361a..d3209402b 100644 --- a/common/version.h +++ b/common/version.h @@ -34,7 +34,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9184 +#define CURRENT_BINARY_DATABASE_VERSION 9185 #ifdef BOTS #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9028 diff --git a/queryserv/CMakeLists.txt b/queryserv/CMakeLists.txt index 15c900e53..0b8eeba05 100644 --- a/queryserv/CMakeLists.txt +++ b/queryserv/CMakeLists.txt @@ -1,18 +1,18 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.2) SET(qserv_sources - database.cpp - lfguild.cpp - queryserv.cpp - queryservconfig.cpp - worldserver.cpp + database.cpp + lfguild.cpp + queryserv.cpp + queryservconfig.cpp + worldserver.cpp ) SET(qserv_headers - database.h - lfguild.h - queryservconfig.h - worldserver.h + database.h + lfguild.h + queryservconfig.h + worldserver.h ) ADD_EXECUTABLE(queryserv ${qserv_sources} ${qserv_headers}) diff --git a/queryserv/queryserv.cpp b/queryserv/queryserv.cpp index 005d54c6a..bfffb5c73 100644 --- a/queryserv/queryserv.cpp +++ b/queryserv/queryserv.cpp @@ -24,6 +24,7 @@ #include "../common/servertalk.h" #include "../common/platform.h" #include "../common/crash.h" +#include "../common/string_util.h" #include "../common/event/event_loop.h" #include "../common/timer.h" #include "database.h" @@ -32,21 +33,24 @@ #include "worldserver.h" #include #include +#include volatile bool RunLoops = true; -Database database; -LFGuildManager lfguildmanager; -std::string WorldShortName; +Database database; +LFGuildManager lfguildmanager; +std::string WorldShortName; const queryservconfig *Config; -WorldServer *worldserver = 0; -EQEmuLogSys LogSys; +WorldServer *worldserver = 0; +EQEmuLogSys LogSys; -void CatchSignal(int sig_num) { +void CatchSignal(int sig_num) +{ RunLoops = false; } -int main() { +int main() +{ RegisterExecutablePlatform(ExePlatformQueryServ); LogSys.LoadLogSettingsDefaults(); set_exception_handler(); @@ -58,7 +62,7 @@ int main() { return 1; } - Config = queryservconfig::get(); + Config = queryservconfig::get(); WorldShortName = Config->ShortName; LogInfo("Connecting to MySQL"); @@ -69,7 +73,8 @@ int main() { Config->QSDatabaseUsername.c_str(), Config->QSDatabasePassword.c_str(), Config->QSDatabaseDB.c_str(), - Config->QSDatabasePort)) { + Config->QSDatabasePort + )) { LogInfo("Cannot continue without a database connection"); return 1; } @@ -78,11 +83,11 @@ int main() { ->LoadLogDatabaseSettings() ->StartFileLogs(); - if (signal(SIGINT, CatchSignal) == SIG_ERR) { + if (signal(SIGINT, CatchSignal) == SIG_ERR) { LogInfo("Could not set signal handler"); return 1; } - if (signal(SIGTERM, CatchSignal) == SIG_ERR) { + if (signal(SIGTERM, CatchSignal) == SIG_ERR) { LogInfo("Could not set signal handler"); return 1; } @@ -94,10 +99,11 @@ int main() { /* Load Looking For Guild Manager */ lfguildmanager.LoadDatabase(); - while(RunLoops) { + while (RunLoops) { Timer::SetCurrentTime(); - if(LFGuildExpireTimer.Check()) + if (LFGuildExpireTimer.Check()) { lfguildmanager.ExpireEntries(); + } EQ::EventLoop::Get().Process(); Sleep(5); @@ -105,7 +111,8 @@ int main() { LogSys.CloseFileLogs(); } -void UpdateWindowTitle(char* iNewTitle) { +void UpdateWindowTitle(char *iNewTitle) +{ #ifdef _WINDOWS char tmp[500]; if (iNewTitle) { diff --git a/queryserv/worldserver.cpp b/queryserv/worldserver.cpp index 1c65b92fd..75b86791a 100644 --- a/queryserv/worldserver.cpp +++ b/queryserv/worldserver.cpp @@ -37,10 +37,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include -extern WorldServer worldserver; +extern WorldServer worldserver; extern const queryservconfig *Config; -extern Database database; -extern LFGuildManager lfguildmanager; +extern Database database; +extern LFGuildManager lfguildmanager; WorldServer::WorldServer() { @@ -52,7 +52,13 @@ WorldServer::~WorldServer() void WorldServer::Connect() { - m_connection = std::make_unique(Config->WorldIP, Config->WorldTCPPort, false, "QueryServ", Config->SharedKey); + m_connection = std::make_unique( + Config->WorldIP, + Config->WorldTCPPort, + false, + "QueryServ", + Config->SharedKey + ); m_connection->OnMessage(std::bind(&WorldServer::HandleMessage, this, std::placeholders::_1, std::placeholders::_2)); } @@ -80,109 +86,109 @@ bool WorldServer::Connected() const void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { switch (opcode) { - case 0: { - break; - } - case ServerOP_KeepAlive: { - break; - } - case ServerOP_Speech: { - Server_Speech_Struct *SSS = (Server_Speech_Struct*)p.Data(); - std::string tmp1 = SSS->from; - std::string tmp2 = SSS->to; - database.AddSpeech(tmp1.c_str(), tmp2.c_str(), SSS->message, SSS->minstatus, SSS->guilddbid, SSS->type); - break; - } - case ServerOP_QSPlayerLogTrades: { - QSPlayerLogTrade_Struct *QS = (QSPlayerLogTrade_Struct*)p.Data(); - database.LogPlayerTrade(QS, QS->_detail_count); - break; - } - case ServerOP_QSPlayerDropItem: { - QSPlayerDropItem_Struct *QS = (QSPlayerDropItem_Struct *) p.Data(); - database.LogPlayerDropItem(QS); - break; - } - case ServerOP_QSPlayerLogHandins: { - QSPlayerLogHandin_Struct *QS = (QSPlayerLogHandin_Struct*)p.Data(); - database.LogPlayerHandin(QS, QS->_detail_count); - break; - } - case ServerOP_QSPlayerLogNPCKills: { - QSPlayerLogNPCKill_Struct *QS = (QSPlayerLogNPCKill_Struct*)p.Data(); - uint32 Members = (uint32)(p.Length() - sizeof(QSPlayerLogNPCKill_Struct)); - if (Members > 0) Members = Members / sizeof(QSPlayerLogNPCKillsPlayers_Struct); - database.LogPlayerNPCKill(QS, Members); - break; - } - case ServerOP_QSPlayerLogDeletes: { - QSPlayerLogDelete_Struct *QS = (QSPlayerLogDelete_Struct*)p.Data(); - uint32 Items = QS->char_count; - database.LogPlayerDelete(QS, Items); - break; - } - case ServerOP_QSPlayerLogMoves: { - QSPlayerLogMove_Struct *QS = (QSPlayerLogMove_Struct*)p.Data(); - uint32 Items = QS->char_count; - database.LogPlayerMove(QS, Items); - break; - } - case ServerOP_QSPlayerLogMerchantTransactions: { - QSMerchantLogTransaction_Struct *QS = (QSMerchantLogTransaction_Struct*)p.Data(); - uint32 Items = QS->char_count + QS->merchant_count; - database.LogMerchantTransaction(QS, Items); - break; - } - case ServerOP_QueryServGeneric: { - /* - The purpose of ServerOP_QueryServerGeneric is so that we don't have to add code to world just to relay packets - each time we add functionality to queryserv. + case 0: { + break; + } + case ServerOP_KeepAlive: { + break; + } + case ServerOP_Speech: { + Server_Speech_Struct *SSS = (Server_Speech_Struct *) p.Data(); + std::string tmp1 = SSS->from; + std::string tmp2 = SSS->to; + database.AddSpeech(tmp1.c_str(), tmp2.c_str(), SSS->message, SSS->minstatus, SSS->guilddbid, SSS->type); + break; + } + case ServerOP_QSPlayerLogTrades: { + QSPlayerLogTrade_Struct *QS = (QSPlayerLogTrade_Struct *) p.Data(); + database.LogPlayerTrade(QS, QS->_detail_count); + break; + } + case ServerOP_QSPlayerDropItem: { + QSPlayerDropItem_Struct *QS = (QSPlayerDropItem_Struct *) p.Data(); + database.LogPlayerDropItem(QS); + break; + } + case ServerOP_QSPlayerLogHandins: { + QSPlayerLogHandin_Struct *QS = (QSPlayerLogHandin_Struct *) p.Data(); + database.LogPlayerHandin(QS, QS->_detail_count); + break; + } + case ServerOP_QSPlayerLogNPCKills: { + QSPlayerLogNPCKill_Struct *QS = (QSPlayerLogNPCKill_Struct *) p.Data(); + uint32 Members = (uint32) (p.Length() - sizeof(QSPlayerLogNPCKill_Struct)); + if (Members > 0) { Members = Members / sizeof(QSPlayerLogNPCKillsPlayers_Struct); } + database.LogPlayerNPCKill(QS, Members); + break; + } + case ServerOP_QSPlayerLogDeletes: { + QSPlayerLogDelete_Struct *QS = (QSPlayerLogDelete_Struct *) p.Data(); + uint32 Items = QS->char_count; + database.LogPlayerDelete(QS, Items); + break; + } + case ServerOP_QSPlayerLogMoves: { + QSPlayerLogMove_Struct *QS = (QSPlayerLogMove_Struct *) p.Data(); + uint32 Items = QS->char_count; + database.LogPlayerMove(QS, Items); + break; + } + case ServerOP_QSPlayerLogMerchantTransactions: { + QSMerchantLogTransaction_Struct *QS = (QSMerchantLogTransaction_Struct *) p.Data(); + uint32 Items = QS->char_count + QS->merchant_count; + database.LogMerchantTransaction(QS, Items); + break; + } + case ServerOP_QueryServGeneric: { + /* + The purpose of ServerOP_QueryServerGeneric is so that we don't have to add code to world just to relay packets + each time we add functionality to queryserv. - A ServerOP_QueryServGeneric packet has the following format: + A ServerOP_QueryServGeneric packet has the following format: - uint32 SourceZoneID - uint32 SourceInstanceID - char OriginatingCharacterName[0] - - Null terminated name of the character this packet came from. This could be just - - an empty string if it has no meaning in the context of a particular packet. - uint32 Type + uint32 SourceZoneID + uint32 SourceInstanceID + char OriginatingCharacterName[0] + - Null terminated name of the character this packet came from. This could be just + - an empty string if it has no meaning in the context of a particular packet. + uint32 Type - The 'Type' field is a 'sub-opcode'. A value of 0 is used for the LFGuild packets. The next feature to be added - to queryserv would use 1, etc. + The 'Type' field is a 'sub-opcode'. A value of 0 is used for the LFGuild packets. The next feature to be added + to queryserv would use 1, etc. - Obviously, any fields in the packet following the 'Type' will be unique to the particular type of packet. The - 'Generic' in the name of this ServerOP code relates to the four header fields. - */ + Obviously, any fields in the packet following the 'Type' will be unique to the particular type of packet. The + 'Generic' in the name of this ServerOP code relates to the four header fields. + */ - auto from = p.GetCString(8); - uint32 Type = p.GetUInt32(8 + from.length() + 1); + auto from = p.GetCString(8); + uint32 Type = p.GetUInt32(8 + from.length() + 1); - switch (Type) { - case QSG_LFGuild: { + switch (Type) { + case QSG_LFGuild: { + ServerPacket pack; + pack.pBuffer = (uchar *) p.Data(); + pack.opcode = opcode; + pack.size = (uint32) p.Length(); + lfguildmanager.HandlePacket(&pack); + pack.pBuffer = nullptr; + break; + } + default: + LogInfo("Received unhandled ServerOP_QueryServGeneric", Type); + break; + } + break; + } + case ServerOP_QSSendQuery: { + /* Process all packets here */ ServerPacket pack; - pack.pBuffer = (uchar*)p.Data(); - pack.opcode = opcode; - pack.size = (uint32)p.Length(); - lfguildmanager.HandlePacket(&pack); + pack.pBuffer = (uchar *) p.Data(); + pack.opcode = opcode; + pack.size = (uint32) p.Length(); + + database.GeneralQueryReceive(&pack); pack.pBuffer = nullptr; break; } - default: - LogInfo("Received unhandled ServerOP_QueryServGeneric", Type); - break; - } - break; - } - case ServerOP_QSSendQuery: { - /* Process all packets here */ - ServerPacket pack; - pack.pBuffer = (uchar*)p.Data(); - pack.opcode = opcode; - pack.size = (uint32)p.Length(); - - database.GeneralQueryReceive(&pack); - pack.pBuffer = nullptr; - break; - } } } diff --git a/queryserv/worldserver.h b/queryserv/worldserver.h index 7a3d21e6f..5774f5ca8 100644 --- a/queryserv/worldserver.h +++ b/queryserv/worldserver.h @@ -18,24 +18,25 @@ #ifndef WORLDSERVER_H #define WORLDSERVER_H +#include #include "../common/eq_packet_structs.h" #include "../common/net/servertalk_client_connection.h" -class WorldServer -{ - public: - WorldServer(); - ~WorldServer(); +class WorldServer { +public: + WorldServer(); + ~WorldServer(); - void Connect(); - bool SendPacket(ServerPacket* pack); - std::string GetIP() const; - uint16 GetPort() const; - bool Connected() const; + void Connect(); + bool SendPacket(ServerPacket *pack); + std::string GetIP() const; + uint16 GetPort() const; + bool Connected() const; + + void HandleMessage(uint16 opcode, const EQ::Net::Packet &p); +private: + std::unique_ptr m_connection; - void HandleMessage(uint16 opcode, const EQ::Net::Packet &p); - private: - std::unique_ptr m_connection; }; #endif diff --git a/ucs/ucs.cpp b/ucs/ucs.cpp index 1e365f41e..986a2df7c 100644 --- a/ucs/ucs.cpp +++ b/ucs/ucs.cpp @@ -37,12 +37,14 @@ #include "../common/net/tcp_server.h" #include "../common/net/servertalk_client_connection.h" +#include "../common/discord_manager.h" ChatChannelList *ChannelList; Clientlist *g_Clientlist; EQEmuLogSys LogSys; Database database; WorldServer *worldserver = nullptr; +DiscordManager discord_manager; const ucsconfig *Config; @@ -87,6 +89,12 @@ void CatchSignal(int sig_num) { } } +void DiscordQueueListener() { + while (caught_loop == 0) { + discord_manager.ProcessMessageQueue(); + Sleep(100); + } +} int main() { RegisterExecutablePlatform(ExePlatformUCS); @@ -165,6 +173,8 @@ int main() { std::signal(SIGKILL, CatchSignal); std::signal(SIGSEGV, CatchSignal); + std::thread(DiscordQueueListener).detach(); + worldserver = new WorldServer; // uncomment to simulate timed crash for catching SIGSEV diff --git a/ucs/worldserver.cpp b/ucs/worldserver.cpp index 64bdfc2a7..f1b3f5bff 100644 --- a/ucs/worldserver.cpp +++ b/ucs/worldserver.cpp @@ -26,6 +26,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "clientlist.h" #include "ucsconfig.h" #include "database.h" +#include "../common/discord_manager.h" #include #include @@ -35,10 +36,11 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include -extern WorldServer worldserver; -extern Clientlist *g_Clientlist; +extern WorldServer worldserver; +extern Clientlist *g_Clientlist; extern const ucsconfig *Config; -extern Database database; +extern Database database; +extern DiscordManager discord_manager; void ProcessMailTo(Client *c, std::string from, std::string subject, std::string message); @@ -72,6 +74,16 @@ void WorldServer::ProcessMessage(uint16 opcode, EQ::Net::Packet &p) { break; } + case ServerOP_DiscordWebhookMessage: { + auto *q = (DiscordWebhookMessage_Struct *) p.Data(); + + discord_manager.QueueWebhookMessage( + q->webhook_id, + q->message + ); + + break; + } case ServerOP_UCSMessage: { char *Buffer = (char *)pack->pBuffer; diff --git a/utils/sql/db_update_manifest.txt b/utils/sql/db_update_manifest.txt index 0b4410c81..6f7a45487 100644 --- a/utils/sql/db_update_manifest.txt +++ b/utils/sql/db_update_manifest.txt @@ -438,6 +438,7 @@ 9182|2022_05_02_npc_types_int64.sql|SHOW COLUMNS FROM `npc_types` LIKE 'hp'|missing|bigint 9183|2022_05_07_merchant_data_buckets.sql|SHOW COLUMNS FROM `merchantlist` LIKE 'bucket_comparison'|empty 9184|2022_05_21_schema_consistency.sql|SELECT * FROM db_version WHERE version >= 9184|empty| +9185|2022_05_07_discord_webhooks.sql|SHOW TABLES LIKE 'discord_webhooks'|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/required/2022_05_07_discord_webhooks.sql b/utils/sql/git/required/2022_05_07_discord_webhooks.sql new file mode 100644 index 000000000..9f368c13e --- /dev/null +++ b/utils/sql/git/required/2022_05_07_discord_webhooks.sql @@ -0,0 +1,15 @@ +CREATE TABLE discord_webhooks +( + id INT auto_increment primary key NULL, + webhook_name varchar(100) NULL, + webhook_url varchar(255) NULL, + created_at DATETIME NULL, + deleted_at DATETIME NULL +) ENGINE=InnoDB +DEFAULT CHARSET=utf8mb4 +COLLATE=utf8mb4_general_ci; + +ALTER TABLE logsys_categories + ADD log_to_discord smallint(11) default 0 AFTER log_to_gmsay; +ALTER TABLE logsys_categories + ADD discord_webhook_id int(11) default 0 AFTER log_to_discord; diff --git a/world/world_server_command_handler.cpp b/world/world_server_command_handler.cpp index 5f12a155e..d02023e1d 100644 --- a/world/world_server_command_handler.cpp +++ b/world/world_server_command_handler.cpp @@ -20,6 +20,7 @@ #include "world_server_command_handler.h" #include "../common/eqemu_logsys.h" +#include "../common/discord/discord.h" #include "../common/json/json.h" #include "../common/version.h" #include "worlddb.h" @@ -30,6 +31,7 @@ #include "../common/rulesys.h" #include "../common/repositories/instance_list_repository.h" #include "../common/repositories/zone_repository.h" +#include "../zone/queryserv.h" namespace WorldserverCommandHandler { @@ -157,14 +159,14 @@ namespace WorldserverCommandHandler { */ void DatabaseGetSchema(int argc, char **argv, argh::parser &cmd, std::string &description) { - description = "Displays server database schema"; + description = "Displays server database schema"; if (cmd[{"-h", "--help"}]) { return; } Json::Value player_tables_json; - std::vector player_tables = DatabaseSchema::GetPlayerTables(); + std::vector player_tables = DatabaseSchema::GetPlayerTables(); for (const auto &table: player_tables) { player_tables_json.append(table); } @@ -176,19 +178,19 @@ namespace WorldserverCommandHandler { } Json::Value server_tables_json; - std::vector server_tables = DatabaseSchema::GetServerTables(); + std::vector server_tables = DatabaseSchema::GetServerTables(); for (const auto &table: server_tables) { server_tables_json.append(table); } Json::Value login_tables_json; - std::vector login_tables = DatabaseSchema::GetLoginTables(); + std::vector login_tables = DatabaseSchema::GetLoginTables(); for (const auto &table: login_tables) { login_tables_json.append(table); } Json::Value state_tables_json; - std::vector state_tables = DatabaseSchema::GetStateTables(); + std::vector state_tables = DatabaseSchema::GetStateTables(); for (const auto &table: state_tables) { state_tables_json.append(table); } diff --git a/world/zoneserver.cpp b/world/zoneserver.cpp index 83a00572f..583023a68 100644 --- a/world/zoneserver.cpp +++ b/world/zoneserver.cpp @@ -1245,6 +1245,7 @@ void ZoneServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) { loginserverlist.SendAccountUpdate(pack); break; } + case ServerOP_DiscordWebhookMessage: case ServerOP_UCSMailMessage: { UCSLink.SendPacket(pack); break; diff --git a/zone/api_service.cpp b/zone/api_service.cpp index 109ef3de8..43fe02ea0 100644 --- a/zone/api_service.cpp +++ b/zone/api_service.cpp @@ -888,9 +888,8 @@ Json::Value ApiSetLoggingLevel(EQ::Net::WebsocketServerConnection *connection, J void RegisterApiLogEvent(std::unique_ptr &server) { LogSys.SetConsoleHandler( - [&](uint16 debug_level, uint16 log_category, const std::string &msg) { + [&](uint16 log_category, const std::string &msg) { Json::Value data; - data["debug_level"] = debug_level; data["log_category"] = log_category; data["msg"] = msg; server->DispatchEvent(EQ::Net::SubscriptionEventLog, data, 50); diff --git a/zone/embparser_api.cpp b/zone/embparser_api.cpp index 1549bdbd4..e4f2ee15f 100644 --- a/zone/embparser_api.cpp +++ b/zone/embparser_api.cpp @@ -2465,7 +2465,7 @@ XS(XS__istaskenabled) { } else { Perl_croak(aTHX_ "Usage: quest::istaskenabled(int task_id)"); } - + ST(0) = boolSV(RETVAL); sv_2mortal(ST(0)); XSRETURN(1); @@ -2483,7 +2483,7 @@ XS(XS__istaskactive) { } else { Perl_croak(aTHX_ "Usage: quest::istaskactive(int task_id)"); } - + ST(0) = boolSV(RETVAL); sv_2mortal(ST(0)); XSRETURN(1); @@ -2502,7 +2502,7 @@ XS(XS__istaskactivityactive) { } else { Perl_croak(aTHX_ "Usage: quest::istaskactivityactive(int task_id, int activity_id)"); } - + ST(0) = boolSV(RETVAL); sv_2mortal(ST(0)); XSRETURN(1); @@ -2818,7 +2818,7 @@ XS(XS__istaskappropriate) { } else { Perl_croak(aTHX_ "Usage: quest::istaskaappropriate(int task_id)"); } - + ST(0) = boolSV(RETVAL); sv_2mortal(ST(0)); XSRETURN(1); @@ -3386,7 +3386,7 @@ XS(XS__CheckInstanceByCharID) { uint16 instance_id = (int) SvUV(ST(0)); uint32 char_id = (int) SvUV(ST(1)); - RETVAL = quest_manager.CheckInstanceByCharID(instance_id, char_id); + RETVAL = quest_manager.CheckInstanceByCharID(instance_id, char_id); ST(0) = boolSV(RETVAL); sv_2mortal(ST(0)); XSRETURN(1); @@ -3660,7 +3660,7 @@ XS(XS__IsRunning) { bool RETVAL; dXSTARG; - RETVAL = quest_manager.IsRunning(); + RETVAL = quest_manager.IsRunning(); ST(0) = boolSV(RETVAL); sv_2mortal(ST(0)); XSRETURN(1); @@ -8426,20 +8426,34 @@ XS(XS__commify) { } XS(XS__checknamefilter); -XS(XS__checknamefilter) { +XS(XS__checknamefilter) +{ dXSARGS; if (items != 1) { Perl_croak(aTHX_ "Usage: quest::checknamefilter(string name)"); } dXSTARG; - std::string name = (std::string) SvPV_nolen(ST(0)); - bool passes = database.CheckNameFilter(name); - ST(0) = boolSV(passes); + std::string name = (std::string) SvPV_nolen(ST(0)); + bool passes = database.CheckNameFilter(name); + ST(0) = boolSV(passes); sv_2mortal(ST(0)); XSRETURN(1); } +XS(XS__discordsend); +XS(XS__discordsend) { + dXSARGS; + if (items != 2) + Perl_croak(aTHX_ "Usage: quest::discordsend(string webhook_name, string message)"); + { + std::string webhook_name = (std::string) SvPV_nolen(ST(0)); + std::string message = (std::string) SvPV_nolen(ST(1)); + zone->SendDiscordMessage(webhook_name, message); + } + XSRETURN_EMPTY; +} + /* This is the callback perl will look for to setup the quest package's XSUBs @@ -8704,6 +8718,7 @@ EXTERN_C XS(boot_quest) { newXS(strcpy(buf, "disable_spawn2"), XS__disable_spawn2, file); newXS(strcpy(buf, "disablerecipe"), XS__disablerecipe, file); newXS(strcpy(buf, "disabletask"), XS__disabletask, file); + newXS(strcpy(buf, "discordsend"), XS__discordsend, file); newXS(strcpy(buf, "doanim"), XS__doanim, file); newXS(strcpy(buf, "echo"), XS__echo, file); newXS(strcpy(buf, "emote"), XS__emote, file); diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index 5913a4e30..a13ed4c77 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -3392,10 +3392,16 @@ std::string lua_commify(std::string number) { return commify(number); } -bool lua_check_name_filter(std::string name) { +bool lua_check_name_filter(std::string name) +{ return database.CheckNameFilter(name); } +void lua_discord_send(std::string webhook_name, std::string message) +{ + zone->SendDiscordMessage(webhook_name, message); +} + #define LuaCreateNPCParse(name, c_type, default_value) do { \ cur = table[#name]; \ if(luabind::type(cur) != LUA_TNIL) { \ @@ -3850,6 +3856,7 @@ luabind::scope lua_register_general() { luabind::def("get_environmental_damage_name", &lua_get_environmental_damage_name), luabind::def("commify", &lua_commify), luabind::def("check_name_filter", &lua_check_name_filter), + luabind::def("discord_send", &lua_discord_send), /* Cross Zone diff --git a/zone/queryserv.cpp b/zone/queryserv.cpp index 6e8b57423..e80be7944 100644 --- a/zone/queryserv.cpp +++ b/zone/queryserv.cpp @@ -24,12 +24,14 @@ Copyright (C) 2001-2014 EQEMu Development Team (http://eqemulator.net) extern WorldServer worldserver; -extern QueryServ* QServ; +extern QueryServ *QServ; -QueryServ::QueryServ(){ +QueryServ::QueryServ() +{ } -QueryServ::~QueryServ(){ +QueryServ::~QueryServ() +{ } void QueryServ::SendQuery(std::string Query) @@ -44,7 +46,9 @@ void QueryServ::SendQuery(std::string Query) void QueryServ::PlayerLogEvent(int Event_Type, int Character_ID, std::string Event_Desc) { std::string query = StringFormat( - "INSERT INTO `qs_player_events` (event, char_id, event_desc, time) VALUES (%i, %i, '%s', UNIX_TIMESTAMP(now()))", - Event_Type, Character_ID, EscapeString(Event_Desc).c_str()); + "INSERT INTO `qs_player_events` (event, char_id, event_desc, time) VALUES (%i, %i, '%s', UNIX_TIMESTAMP(now()))", + Event_Type, + Character_ID, + EscapeString(Event_Desc).c_str()); SendQuery(query); } diff --git a/zone/queryserv.h b/zone/queryserv.h index 8aafcafda..5cb12332e 100644 --- a/zone/queryserv.h +++ b/zone/queryserv.h @@ -21,7 +21,7 @@ enum PlayerGenericLogEventTypes { Player_Log_Issued_Commands, Player_Log_Money_Transactions, Player_Log_Alternate_Currency_Transactions, -}; +}; class QueryServ{ @@ -32,4 +32,4 @@ class QueryServ{ void PlayerLogEvent(int Event_Type, int Character_ID, std::string Event_Desc); }; -#endif /* QUERYSERV_ZONE_H */ \ No newline at end of file +#endif /* QUERYSERV_ZONE_H */ diff --git a/zone/worldserver.cpp b/zone/worldserver.cpp index dfd4166ae..57d07e11b 100644 --- a/zone/worldserver.cpp +++ b/zone/worldserver.cpp @@ -57,6 +57,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/shared_tasks.h" #include "shared_task_zone_messaging.h" #include "dialogue_window.h" +#include "queryserv.h" extern EntityList entity_list; extern Zone* zone; @@ -208,6 +209,8 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p) else { LogInfo("World assigned Port: [{}] for this zone", sci->port); ZoneConfig::SetZonePort(sci->port); + + LogSys.SetDiscordHandler(&Zone::DiscordWebhookMessageHandler); } break; } diff --git a/zone/zone.cpp b/zone/zone.cpp index 4c66c6c79..35376caf6 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -655,7 +655,7 @@ void Zone::LoadNewMerchantData(uint32 merchantid) { void Zone::GetMerchantDataForZoneLoad() { LogInfo("Loading Merchant Lists"); - + auto query = fmt::format( SQL ( SELECT @@ -1209,6 +1209,11 @@ bool Zone::Init(bool is_static) { //MODDING HOOK FOR ZONE INIT mod_init(); + // logging origination information + LogSys.origination_info.zone_short_name = zone->short_name; + LogSys.origination_info.zone_long_name = zone->long_name; + LogSys.origination_info.instance_id = zone->instanceid; + return true; } @@ -2776,3 +2781,32 @@ void Zone::SendReloadMessage(std::string reload_type) ).c_str() ); } + +void Zone::SendDiscordMessage(int webhook_id, const std::string& message) +{ + if (worldserver.Connected()) { + auto pack = new ServerPacket(ServerOP_DiscordWebhookMessage, sizeof(DiscordWebhookMessage_Struct) + 1); + auto *q = (DiscordWebhookMessage_Struct *) pack->pBuffer; + + strn0cpy(q->message, message.c_str(), 2000); + q->webhook_id = webhook_id; + + worldserver.SendPacket(pack); + safe_delete(pack); + } +} + +void Zone::SendDiscordMessage(const std::string& webhook_name, const std::string &message) +{ + bool not_found = true; + for (auto & w : LogSys.discord_webhooks) { + if (w.webhook_name == webhook_name) { + SendDiscordMessage(w.id, message + "\n"); + not_found = false; + } + } + + if (not_found) { + LogDiscord("[SendDiscordMessage] Did not find valid webhook by webhook name [{}]", webhook_name); + } +} diff --git a/zone/zone.h b/zone/zone.h index 8450b0f18..915fa8c43 100755 --- a/zone/zone.h +++ b/zone/zone.h @@ -35,6 +35,8 @@ #include "aa_ability.h" #include "pathfinder_interface.h" #include "global_loot_manager.h" +#include "queryserv.h" +#include "../common/discord/discord.h" class DynamicZone; @@ -147,9 +149,10 @@ public: const char *GetSpellBlockedMessage(uint32 spell_id, const glm::vec3 &location); EQ::Random random; - EQTime zone_time; + EQTime zone_time; - ZonePoint *GetClosestZonePoint(const glm::vec3 &location, const char *to_name, Client *client, float max_distance = 40000.0f); + ZonePoint * + GetClosestZonePoint(const glm::vec3 &location, const char *to_name, Client *client, float max_distance = 40000.0f); inline bool BuffTimersSuspended() const { return newzone_data.SuspendBuffs != 0; }; inline bool HasMap() { return zonemap != nullptr; } @@ -179,7 +182,7 @@ public: void DumpMerchantList(uint32 npcid); int SaveTempItem(uint32 merchantid, uint32 npcid, uint32 item, int32 charges, bool sold = false); int32 MobsAggroCount() { return aggroedmobs; } - DynamicZone* GetDynamicZone(); + DynamicZone *GetDynamicZone(); IPathfinder *pathing; LinkedList NPCEmoteList; @@ -187,31 +190,31 @@ public: LinkedList zone_point_list; std::vector virtual_zone_point_list; - Map *zonemap; + Map *zonemap; MercTemplate *GetMercTemplate(uint32 template_id); - NewZone_Struct newzone_data; + NewZone_Struct newzone_data; QGlobalCache *CreateQGlobals() { qGlobals = new QGlobalCache(); return qGlobals; } QGlobalCache *GetQGlobals() { return qGlobals; } - SpawnConditionManager spawn_conditions; - SpawnGroupList spawn_group_list; + SpawnConditionManager spawn_conditions; + SpawnGroupList spawn_group_list; - std::list AlternateCurrencies; - std::list VeteranRewards; - std::map ldon_trap_list; - std::map merc_templates; - std::map merctable; - std::map npctable; - std::map > ldon_trap_entry_list; - std::map > merchanttable; - std::map > merc_spells_list; - std::map > merc_stance_list; - std::map > tmpmerchanttable; - std::map adventure_entry_list_flavor; - std::map level_exp_mod; + std::list AlternateCurrencies; + std::list VeteranRewards; + std::map ldon_trap_list; + std::map merc_templates; + std::map merctable; + std::map npctable; + std::map > ldon_trap_entry_list; + std::map > merchanttable; + std::map > merc_spells_list; + std::map > merc_stance_list; + std::map > tmpmerchanttable; + std::map adventure_entry_list_flavor; + std::map level_exp_mod; std::pair GetAlternateAdvancementAbilityAndRank(int id, int points_spent); @@ -223,7 +226,7 @@ public: std::vector zone_grid_entries; std::unordered_map> dynamic_zone_cache; - std::unordered_map> expedition_cache; + std::unordered_map> expedition_cache; time_t weather_timer; Timer spawn2_timer; @@ -344,10 +347,11 @@ public: fmt::format( "--- {}", message_split[iter] - ).c_str() + ).c_str() ); } - } else { + } + else { entity_list.MessageStatus( 0, AccountStatus::QuestTroupe, @@ -357,6 +361,22 @@ public: } } + static void SendDiscordMessage(int webhook_id, const std::string& message); + static void SendDiscordMessage(const std::string& webhook_name, const std::string& message); + static void DiscordWebhookMessageHandler(uint16 log_category, int webhook_id, const std::string &message) + { + std::string message_prefix; + if (!LogSys.origination_info.zone_short_name.empty()) { + message_prefix = fmt::format( + "[**{}**] **Zone** [**{}**] ", + Logs::LogCategoryName[log_category], + LogSys.origination_info.zone_short_name + ); + } + + SendDiscordMessage(webhook_id, message_prefix + Discord::FormatDiscordMessage(log_category, message)); + }; + double GetMaxMovementUpdateRange() const { return max_movement_update_range; } /**