[Discord Integration] Native Discord Integration (#2140)

* Start of discord integration work

* more testing

* Discord client work

* More discord work

* Cleanup

* Handle retry timer response and max retries

* Update base retry timer

* Move Discord queue handler to UCS, add queuer to own thread

* Post merge

* Send up Zone::SendDiscordMessage

* Start of discord integration work

* more testing

* Discord client work

* More discord work

* Cleanup

* Move Discord queue handler to UCS, add queuer to own thread

* Post merge

* Push up tables

* Quest API stuff.

* Update 2022_05_07_discord_webhooks.sql

* Post merge fixes

* Push up manifest

* Flip logging signs in logic from copy / paste of inverse logic before

* Make sure we add new line to quest api sourced messages

Co-authored-by: Kinglykrab <kinglykrab@gmail.com>
This commit is contained in:
Chris Miles 2022-06-09 17:22:23 -05:00 committed by GitHub
parent 8ef3e87370
commit 4639405fdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1052 additions and 259 deletions

View File

@ -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

View File

@ -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",

View File

@ -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";
}

15
common/discord/discord.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef EQEMU_DISCORD_H
#define EQEMU_DISCORD_H
#include <string>
#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

View File

@ -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();
}

19
common/discord_manager.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef EQEMU_DISCORD_MANAGER_H
#define EQEMU_DISCORD_MANAGER_H
#include <mutex>
#include <map>
#include <vector>
#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<uint32, std::vector<std::string>> webhook_message_queue{};
};
#endif

View File

@ -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 <iostream>
@ -46,6 +48,7 @@ std::ofstream process_log;
#include <unistd.h>
#include <sys/stat.h>
#include <thread>
#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<uint8>(Logs::General);
log_settings[Logs::ChecksumVerification].log_to_gmsay = static_cast<uint8>(Logs::General);
log_settings[Logs::CombatRecord].log_to_gmsay = static_cast<uint8>(Logs::General);
log_settings[Logs::Discord].log_to_console = static_cast<uint8>(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<uint8>(c.log_to_console);
log_settings[c.log_category_id].log_to_file = static_cast<uint8>(c.log_to_file);
log_settings[c.log_category_id].log_to_gmsay = static_cast<uint8>(c.log_to_gmsay);
log_settings[c.log_category_id].log_to_console = static_cast<uint8>(c.log_to_console);
log_settings[c.log_category_id].log_to_file = static_cast<uint8>(c.log_to_file);
log_settings[c.log_category_id].log_to_gmsay = static_cast<uint8>(c.log_to_gmsay);
log_settings[c.log_category_id].log_to_discord = static_cast<uint8>(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;
}

View File

@ -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<void(uint16 log_type, const std::string &)> f) {
EQEmuLogSys *SetGMSayHandler(std::function<void(uint16 log_type, const std::string &)> f)
{
on_log_gmsay_hook = f;
return this;
}
EQEmuLogSys *SetDiscordHandler(std::function<void(uint16 log_category, int webhook_id, const std::string &)> f)
{
on_log_discord_hook = f;
return this;
}
// console
void SetConsoleHandler(
std::function<void(
uint16 debug_level,
uint16 log_type,
const std::string &
)> f
@ -328,18 +356,17 @@ public:
private:
// reference to database
Database *m_database;
std::function<void(uint16 log_category, const std::string &)> on_log_gmsay_hook;
std::function<void(uint16 debug_level, uint16 log_category, const std::string &)> on_log_console_hook;
Database *m_database;
std::function<void(uint16 log_category, const std::string &)> on_log_gmsay_hook;
std::function<void(uint16 log_category, int webhook_id, const std::string &)> on_log_discord_hook;
std::function<void(uint16 log_category, const std::string &)> 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);
};

View File

@ -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__);\

View File

@ -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 <ctime>
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<std::string> Columns()
{
return {
"id",
"webhook_name",
"webhook_url",
"created_at",
"deleted_at",
};
}
static std::vector<std::string> 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<DiscordWebhooks> &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<std::string> 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<std::string> 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<DiscordWebhooks> discord_webhooks_entries
)
{
std::vector<std::string> insert_chunks;
for (auto &discord_webhooks_entry: discord_webhooks_entries) {
std::vector<std::string> 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<std::string> insert_values;
auto results = db.QueryDatabase(
fmt::format(
"{} VALUES {}",
BaseInsert(),
implode(",", insert_chunks)
)
);
return (results.Success() ? results.RowsAffected() : 0);
}
static std::vector<DiscordWebhooks> All(Database& db)
{
std::vector<DiscordWebhooks> 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<DiscordWebhooks> GetWhere(Database& db, std::string where_filter)
{
std::vector<DiscordWebhooks> 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

View File

@ -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);
}

View File

@ -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

View File

@ -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];
};

View File

@ -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

View File

@ -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})

View File

@ -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 <list>
#include <signal.h>
#include <thread>
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) {

View File

@ -37,10 +37,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include <string.h>
#include <time.h>
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<EQ::Net::ServertalkClient>(Config->WorldIP, Config->WorldTCPPort, false, "QueryServ", Config->SharedKey);
m_connection = std::make_unique<EQ::Net::ServertalkClient>(
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;
}
}
}

View File

@ -18,24 +18,25 @@
#ifndef WORLDSERVER_H
#define WORLDSERVER_H
#include <mutex>
#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<EQ::Net::ServertalkClient> m_connection;
void HandleMessage(uint16 opcode, const EQ::Net::Packet &p);
private:
std::unique_ptr<EQ::Net::ServertalkClient> m_connection;
};
#endif

View File

@ -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

View File

@ -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 <iostream>
#include <string.h>
@ -35,10 +36,11 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include <stdlib.h>
#include <stdarg.h>
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;

View File

@ -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

View File

@ -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;

View File

@ -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<std::string> player_tables = DatabaseSchema::GetPlayerTables();
std::vector<std::string> 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<std::string> server_tables = DatabaseSchema::GetServerTables();
std::vector<std::string> server_tables = DatabaseSchema::GetServerTables();
for (const auto &table: server_tables) {
server_tables_json.append(table);
}
Json::Value login_tables_json;
std::vector<std::string> login_tables = DatabaseSchema::GetLoginTables();
std::vector<std::string> login_tables = DatabaseSchema::GetLoginTables();
for (const auto &table: login_tables) {
login_tables_json.append(table);
}
Json::Value state_tables_json;
std::vector<std::string> state_tables = DatabaseSchema::GetStateTables();
std::vector<std::string> state_tables = DatabaseSchema::GetStateTables();
for (const auto &table: state_tables) {
state_tables_json.append(table);
}

View File

@ -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;

View File

@ -888,9 +888,8 @@ Json::Value ApiSetLoggingLevel(EQ::Net::WebsocketServerConnection *connection, J
void RegisterApiLogEvent(std::unique_ptr<EQ::Net::WebsocketServer> &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);

View File

@ -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);

View File

@ -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

View File

@ -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);
}

View File

@ -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 */
#endif /* QUERYSERV_ZONE_H */

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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<NPC_Emote_Struct *> NPCEmoteList;
@ -187,31 +190,31 @@ public:
LinkedList<ZonePoint *> zone_point_list;
std::vector<ZonePointsRepository::ZonePoints> 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<AltCurrencyDefinition_Struct> AlternateCurrencies;
std::list<InternalVeteranReward> VeteranRewards;
std::map<uint32, LDoNTrapTemplate *> ldon_trap_list;
std::map<uint32, MercTemplate> merc_templates;
std::map<uint32, NPCType *> merctable;
std::map<uint32, NPCType *> npctable;
std::map<uint32, std::list<LDoNTrapTemplate *> > ldon_trap_entry_list;
std::map<uint32, std::list<MerchantList> > merchanttable;
std::map<uint32, std::list<MercSpellEntry> > merc_spells_list;
std::map<uint32, std::list<MercStanceInfo> > merc_stance_list;
std::map<uint32, std::list<TempMerchantList> > tmpmerchanttable;
std::map<uint32, std::string> adventure_entry_list_flavor;
std::map<uint32, ZoneEXPModInfo> level_exp_mod;
std::list<AltCurrencyDefinition_Struct> AlternateCurrencies;
std::list<InternalVeteranReward> VeteranRewards;
std::map<uint32, LDoNTrapTemplate *> ldon_trap_list;
std::map<uint32, MercTemplate> merc_templates;
std::map<uint32, NPCType *> merctable;
std::map<uint32, NPCType *> npctable;
std::map<uint32, std::list<LDoNTrapTemplate *> > ldon_trap_entry_list;
std::map<uint32, std::list<MerchantList> > merchanttable;
std::map<uint32, std::list<MercSpellEntry> > merc_spells_list;
std::map<uint32, std::list<MercStanceInfo> > merc_stance_list;
std::map<uint32, std::list<TempMerchantList> > tmpmerchanttable;
std::map<uint32, std::string> adventure_entry_list_flavor;
std::map<uint32, ZoneEXPModInfo> level_exp_mod;
std::pair<AA::Ability *, AA::Rank *> GetAlternateAdvancementAbilityAndRank(int id, int points_spent);
@ -223,7 +226,7 @@ public:
std::vector<GridEntriesRepository::GridEntry> zone_grid_entries;
std::unordered_map<uint32, std::unique_ptr<DynamicZone>> dynamic_zone_cache;
std::unordered_map<uint32, std::unique_ptr<Expedition>> expedition_cache;
std::unordered_map<uint32, std::unique_ptr<Expedition>> 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; }
/**