[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();
}
if (process_log) {
char time_stamp[80];
EQEmuLogSys::SetCurrentTimeStamp(time_stamp);
if (process_log) {
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);
}
}
@ -633,13 +612,17 @@ EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings()
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,6 +311,14 @@ 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;
@ -306,15 +328,21 @@ public:
// 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
@ -329,17 +357,16 @@ 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;
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

@ -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,6 +33,7 @@
#include "worldserver.h"
#include <list>
#include <signal.h>
#include <thread>
volatile bool RunLoops = true;
@ -42,11 +44,13 @@ const queryservconfig *Config;
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();
@ -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;
}
@ -96,8 +101,9 @@ int main() {
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

@ -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));
}
@ -111,7 +117,7 @@ void WorldServer::HandleMessage(uint16 opcode, const EQ::Net::Packet &p)
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);
if (Members > 0) { Members = Members / sizeof(QSPlayerLogNPCKillsPlayers_Struct); }
database.LogPlayerNPCKill(QS, Members);
break;
}

View File

@ -18,11 +18,11 @@
#ifndef WORLDSERVER_H
#define WORLDSERVER_H
#include <mutex>
#include "../common/eq_packet_structs.h"
#include "../common/net/servertalk_client_connection.h"
class WorldServer
{
class WorldServer {
public:
WorldServer();
~WorldServer();
@ -36,6 +36,7 @@ class WorldServer
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>
@ -39,6 +40,7 @@ extern WorldServer worldserver;
extern Clientlist *g_Clientlist;
extern const ucsconfig *Config;
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 {

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

@ -8426,7 +8426,8 @@ XS(XS__commify) {
}
XS(XS__checknamefilter);
XS(XS__checknamefilter) {
XS(XS__checknamefilter)
{
dXSARGS;
if (items != 1) {
Perl_croak(aTHX_ "Usage: quest::checknamefilter(string name)");
@ -8440,6 +8441,19 @@ XS(XS__checknamefilter) {
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

@ -26,10 +26,12 @@ Copyright (C) 2001-2014 EQEMu Development Team (http://eqemulator.net)
extern WorldServer worldserver;
extern QueryServ *QServ;
QueryServ::QueryServ(){
QueryServ::QueryServ()
{
}
QueryServ::~QueryServ(){
QueryServ::~QueryServ()
{
}
void QueryServ::SendQuery(std::string Query)
@ -45,6 +47,8 @@ void QueryServ::PlayerLogEvent(int Event_Type, int Character_ID, std::string Eve
{
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());
Event_Type,
Character_ID,
EscapeString(Event_Desc).c_str());
SendQuery(query);
}

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

@ -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;
@ -149,7 +151,8 @@ public:
EQ::Random random;
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; }
@ -347,7 +350,8 @@ public:
).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; }
/**