From 032db2d067dcc4d88132e1c2f45186d085541ac8 Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:19:07 +0100 Subject: [PATCH 1/8] Absolute clusterfuck --- CMakeLists.txt | 3 + library/CMakeLists.txt | 31 +++ library/include/akashi_addon_global.h | 9 + library/include/discordhook.h | 123 +++++++++ library/include/service.h | 40 +++ library/include/serviceregistry.h | 66 +++++ library/include/servicetypes.h | 0 library/include/servicewrapper.h | 34 +++ library/src/discordhook.cpp | 346 ++++++++++++++++++++++++++ library/src/service.cpp | 1 + library/src/serviceregistry.cpp | 34 +++ library/src/servicewrapper.cpp | 1 + src/server.cpp | 20 ++ src/server.h | 3 + 14 files changed, 711 insertions(+) create mode 100644 library/CMakeLists.txt create mode 100644 library/include/akashi_addon_global.h create mode 100644 library/include/discordhook.h create mode 100644 library/include/service.h create mode 100644 library/include/serviceregistry.h create mode 100644 library/include/servicetypes.h create mode 100644 library/include/servicewrapper.h create mode 100644 library/src/discordhook.cpp create mode 100644 library/src/service.cpp create mode 100644 library/src/serviceregistry.cpp create mode 100644 library/src/servicewrapper.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bf69976..2d64c74b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,8 @@ set(CMAKE_AUTOUIC ON) find_package(Qt6 6.4 REQUIRED COMPONENTS Core Network WebSockets Sql) find_package(Qt6 REQUIRED COMPONENTS Core Network WebSockets Sql) +add_subdirectory(library) + qt_standard_project_setup() qt_add_executable(akashi src/commands/area.cpp @@ -118,6 +120,7 @@ target_link_libraries(akashi PRIVATE Qt6::Sql Qt6::Network Qt6::WebSockets + akashi_addon ) target_include_directories(akashi PRIVATE src src/logger src/network src/packet) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt new file mode 100644 index 00000000..6b76cc8f --- /dev/null +++ b/library/CMakeLists.txt @@ -0,0 +1,31 @@ +add_library(akashi_addon SHARED + include/akashi_addon_global.h + include/service.h src/service.cpp + include/serviceregistry.h src/serviceregistry.cpp + include/servicewrapper.h src/servicewrapper.cpp + include/discordhook.h src/discordhook.cpp +) +add_library(akashi::Addon ALIAS akashi_addon) + +set_target_properties(akashi_addon PROPERTIES + LIBRARY_OUTPUT_DIRECTORY $<1:${CMAKE_SOURCE_DIR}/bin> + RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_SOURCE_DIR}/bin> +) + +target_compile_definitions(akashi_addon PRIVATE + AKASHI_ADDON_LIBRARY +) + +target_include_directories(akashi_addon + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +target_link_libraries(akashi_addon + PUBLIC + Qt6::Core + Qt6::Network +) diff --git a/library/include/akashi_addon_global.h b/library/include/akashi_addon_global.h new file mode 100644 index 00000000..8f149c19 --- /dev/null +++ b/library/include/akashi_addon_global.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#if defined(AKASHI_ADDON_LIBRARY) +#define AKASHI_ADDON_EXPORT Q_DECL_EXPORT +#else +#define AKASHI_ADDON_EXPORT Q_DECL_IMPORT +#endif diff --git a/library/include/discordhook.h b/library/include/discordhook.h new file mode 100644 index 00000000..6d2e5073 --- /dev/null +++ b/library/include/discordhook.h @@ -0,0 +1,123 @@ +#pragma once + +#include "akashi_addon_global.h" +#include "service.h" + +#include +#include +#include +#include +#include +#include + +class QNetworkAccessManager; +class QNetworkReply; + +class DiscordMessageCommon +{ + public: + const QString &requestUrl() const { return m_request_url; } + + protected: + QString m_request_url; +}; + +class AKASHI_ADDON_EXPORT DiscordMessage : public DiscordMessageCommon +{ + public: + DiscordMessage() = default; + ~DiscordMessage() = default; + + DiscordMessage &setRequestUrl(const QString &url); + DiscordMessage &setContent(const QString &content); + DiscordMessage &setUsername(const QString &username); + DiscordMessage &setAvatarUrl(const QString &avatar_url); + DiscordMessage &setTts(bool tts); + + DiscordMessage &beginEmbed(); + DiscordMessage &setEmbedTitle(const QString &title); + DiscordMessage &setEmbedDescription(const QString &description); + DiscordMessage &setEmbedUrl(const QString &url); + DiscordMessage &setEmbedColor(int color); + DiscordMessage &setEmbedTimestamp(const QString ×tamp); + DiscordMessage &setEmbedFooter(const QString &text, const QString &icon_url = ""); + DiscordMessage &setEmbedImage(const QString &url); + DiscordMessage &setEmbedThumbnail(const QString &url); + DiscordMessage &setEmbedAuthor(const QString &name, const QString &url = "", const QString &icon_url = ""); + DiscordMessage &addEmbedField(const QString &name, const QString &value, bool inline_field = false); + DiscordMessage &endEmbed(); + + QJsonObject toJson() const; + + private: + QMap m_fields; + QVector m_embeds; + QVariantMap m_current_embed; + bool m_building_embed = false; +}; + +struct DiscordMultipart +{ + QByteArray data; + QString name; + QString filename; + QString mime_type; + QString charset; + + template + requires std::convertible_to + DiscordMultipart(T data, QString name, QString filename = "", + QString mime_type = "", QString charset = "") : data(std::move(data)), + name(std::move(name)), + filename(std::move(filename)), + mime_type(std::move(mime_type)), + charset(std::move(charset)) + {} +}; + +class AKASHI_ADDON_EXPORT DiscordMultipartMessage : public DiscordMessageCommon +{ + public: + DiscordMultipartMessage() = default; + ~DiscordMultipartMessage() = default; + + template + DiscordMultipartMessage &addPart(T data, QString name, QString filename = "") + { + m_parts.append(DiscordMultipart(std::move(data), std::move(name), std::move(filename))); + return *this; + } + + DiscordMultipartMessage &setRequestUrl(const QString &url); + DiscordMultipartMessage &setPayloadJson(const QJsonObject &payload); + + int size() const { return m_parts.size(); } + const DiscordMultipart &partAt(int index) const { return m_parts.at(index); } + const QVector &parts() const { return m_parts; } + const QJsonObject &payloadJson() const { return m_payload_json; } + + private: + QVector m_parts; + QJsonObject m_payload_json; +}; + +class AKASHI_ADDON_EXPORT DiscordHook : public Service +{ + Q_OBJECT + + public: + DiscordHook(QObject *parent = nullptr); + ~DiscordHook() = default; + + void setServiceRegistry(ServiceRegistry *f_registry = nullptr) override; + + void + post(const DiscordMessage &message); + void post(const DiscordMultipartMessage &message); + + private slots: + void onDiscordResponse(QNetworkReply *reply); + + private: + QNetworkAccessManager *m_network_manager = nullptr; +}; diff --git a/library/include/service.h b/library/include/service.h new file mode 100644 index 00000000..81c9869d --- /dev/null +++ b/library/include/service.h @@ -0,0 +1,40 @@ +#pragma once + +#include "akashi_addon_global.h" + +#include +#include +#include +#include + +class ServiceRegistry; + +class AKASHI_ADDON_EXPORT Service : public QObject +{ + Q_OBJECT + + public: + enum State + { + PENDING, + OK, + FAILED + }; + Q_ENUM(State); + + Service(QObject *parent = nullptr) : QObject{parent} {}; + ~Service() = default; + + virtual void setServiceRegistry(ServiceRegistry *f_registry = nullptr) {}; + Service::State getState() { return m_state; }; + + QString getServiceProperty(QString f_key) const + { + return m_service_properties.value(f_key, {}); + } + + protected: + Service::State m_state = State::PENDING; + QMap m_service_properties; + ServiceRegistry *m_registry; +}; diff --git a/library/include/serviceregistry.h b/library/include/serviceregistry.h new file mode 100644 index 00000000..8246fddd --- /dev/null +++ b/library/include/serviceregistry.h @@ -0,0 +1,66 @@ +#pragma once + +// Welcome to absolute hell :) + +#include "akashi_addon_global.h" +#include "servicewrapper.h" + +#include +#include + +class Service; + +// Services are commonly owned by the ServiceRegistry. +// Why? Because I say so. - Salanto + +class AKASHI_ADDON_EXPORT ServiceRegistry : public QObject +{ + Q_OBJECT + + public: + ServiceRegistry(QObject *parent = nullptr); + + template + requires std::is_base_of_v + inline void create() + { + T *l_ptr = new T(this); + l_ptr->setServiceRegistry(this); + insertService(l_ptr); + } + + template + requires std::is_base_of_v + inline void createWrapped(const QString &f_identifier, + const QString &f_version, const QString &f_author) + { + T *l_ptr = new T(this); + ServiceWrapper *l_wrapper = new ServiceWrapper(l_ptr, f_identifier, f_version, f_author, this); + insertService(l_wrapper); + } + + template + requires std::is_base_of_v + inline std::optional get(const QString f_identifier) + { + if (!m_services.contains(f_identifier)) { + qCritical() << "Unable to get service with identifier" << f_identifier; + return std::nullopt; + } + + Service *l_service = m_services.value(f_identifier); + + const Service::State l_service_state = l_service->getState(); + if (l_service_state != Service::OK) { + qCritical() << "Unable to get service with identifier" << f_identifier << "due to state:" << l_service_state; + return std::nullopt; + } + + return std::make_optional(static_cast(l_service)); + } + + private: + void insertService(Service *f_ptr); + + QMap m_services; +}; diff --git a/library/include/servicetypes.h b/library/include/servicetypes.h new file mode 100644 index 00000000..e69de29b diff --git a/library/include/servicewrapper.h b/library/include/servicewrapper.h new file mode 100644 index 00000000..0dd5ded8 --- /dev/null +++ b/library/include/servicewrapper.h @@ -0,0 +1,34 @@ +#pragma once + +#include "service.h" +#include +#include +#include + +// This is a non-owning wrapper. Because I say so. - Salanto + +template + requires std::is_base_of_v +class ServiceWrapper : public Service +{ + public: + ServiceWrapper() = delete; + ServiceWrapper(T *f_ptr, const QString &f_identifier, + const QString &f_version, const QString &f_author, + QObject *parent = nullptr) : Service(parent), + m_ptr(f_ptr) + { + m_service_properties["identifier"] = f_identifier; + m_service_properties["version"] = f_version; + m_service_properties["author"] = f_author; + + m_state = Service::OK; + } + + T *get() { return m_ptr.data(); } + T *operator->() { return m_ptr.data(); } + bool isValid() const { return !m_ptr.isNull(); } + + private: + QPointer m_ptr; +}; diff --git a/library/src/discordhook.cpp b/library/src/discordhook.cpp new file mode 100644 index 00000000..2935ab28 --- /dev/null +++ b/library/src/discordhook.cpp @@ -0,0 +1,346 @@ +#include "discordhook.h" +#include "serviceregistry.h" +#include "servicewrapper.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +DiscordMessage &DiscordMessage::setRequestUrl(const QString &url) +{ + m_request_url = url; + return *this; +} + +DiscordMessage &DiscordMessage::setContent(const QString &content) +{ + m_fields["content"] = content; + return *this; +} + +DiscordMessage &DiscordMessage::setUsername(const QString &username) +{ + m_fields["username"] = username; + return *this; +} + +DiscordMessage &DiscordMessage::setAvatarUrl(const QString &avatar_url) +{ + m_fields["avatar_url"] = avatar_url; + return *this; +} + +DiscordMessage &DiscordMessage::setTts(bool tts) +{ + m_fields["tts"] = tts ? "true" : "false"; + return *this; +} + +DiscordMessage &DiscordMessage::beginEmbed() +{ + if (m_building_embed) { + endEmbed(); + } + m_current_embed.clear(); + m_building_embed = true; + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedTitle(const QString &title) +{ + if (m_building_embed) { + m_current_embed["title"] = title; + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedDescription(const QString &description) +{ + if (m_building_embed) { + m_current_embed["description"] = description; + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedUrl(const QString &url) +{ + if (m_building_embed) { + m_current_embed["url"] = url; + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedColor(int color) +{ + if (m_building_embed) { + m_current_embed["color"] = color; + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedTimestamp(const QString ×tamp) +{ + if (m_building_embed) { + m_current_embed["timestamp"] = timestamp; + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedFooter(const QString &text, const QString &icon_url) +{ + if (m_building_embed) { + QMap l_footer; + l_footer["text"] = text; + if (!icon_url.isEmpty()) { + l_footer["icon_url"] = icon_url; + } + m_current_embed["footer"] = QVariant::fromValue(l_footer); + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedImage(const QString &url) +{ + if (m_building_embed) { + QMap l_image; + l_image["url"] = url; + m_current_embed["image"] = QVariant::fromValue(l_image); + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedThumbnail(const QString &url) +{ + if (m_building_embed) { + QMap l_thumbnail; + l_thumbnail["url"] = url; + m_current_embed["thumbnail"] = QVariant::fromValue(l_thumbnail); + } + return *this; +} + +DiscordMessage &DiscordMessage::setEmbedAuthor(const QString &name, const QString &url, const QString &icon_url) +{ + if (m_building_embed) { + QMap l_author; + l_author["name"] = name; + if (!url.isEmpty()) { + l_author["url"] = url; + } + if (!icon_url.isEmpty()) { + l_author["icon_url"] = icon_url; + } + m_current_embed["author"] = QVariant::fromValue(l_author); + } + return *this; +} + +DiscordMessage &DiscordMessage::addEmbedField(const QString &name, const QString &value, bool inline_field) +{ + if (m_building_embed) { + QMap l_field; + l_field["name"] = name; + l_field["value"] = value; + l_field["inline"] = inline_field; + + QVariantList l_fields; + if (m_current_embed.contains("fields")) { + l_fields = m_current_embed["fields"].toList(); + } + l_fields.append(QVariant::fromValue(l_field)); + m_current_embed["fields"] = l_fields; + } + return *this; +} + +DiscordMessage &DiscordMessage::endEmbed() +{ + if (m_building_embed) { + m_embeds.append(m_current_embed); + m_current_embed.clear(); + m_building_embed = false; + } + return *this; +} + +QJsonObject DiscordMessage::toJson() const +{ + QJsonObject json; + + // Add all fields (including content) + for (auto it = m_fields.constBegin(); it != m_fields.constEnd(); ++it) { + if (!it.value().isNull()) { + json[it.key()] = QJsonValue::fromVariant(it.value()); + } + } + + // Handle embeds + if (!m_embeds.isEmpty()) { + QJsonArray embedsArray; + + for (const QVariantMap &embedMap : m_embeds) { + QJsonObject embedObj; + + for (auto it = embedMap.constBegin(); it != embedMap.constEnd(); ++it) { + if (it.value().isNull()) { + continue; + } + + // Handle nested maps (e.g., author, footer, etc.) + if (it.value().canConvert()) { + QJsonObject nestedObj; + QVariantMap nestedMap = it.value().toMap(); + + for (auto nestedIt = nestedMap.constBegin(); + nestedIt != nestedMap.constEnd(); + ++nestedIt) { + if (!nestedIt.value().isNull()) { + nestedObj[nestedIt.key()] = QJsonValue::fromVariant(nestedIt.value()); + } + } + + embedObj[it.key()] = nestedObj; + } + else { + embedObj[it.key()] = QJsonValue::fromVariant(it.value()); + } + } + + if (!embedObj.isEmpty()) { + embedsArray.append(embedObj); + } + } + + json["embeds"] = embedsArray; + } + + return json; +} + +DiscordMultipartMessage &DiscordMultipartMessage::setRequestUrl(const QString &url) +{ + m_request_url = url; + return *this; +} + +DiscordMultipartMessage &DiscordMultipartMessage::setPayloadJson(const QJsonObject &payload) +{ + m_payload_json = payload; + return *this; +} + +DiscordHook::DiscordHook(QObject *parent) : Service{parent} +{ + m_service_properties = {{"author", "Salanto"}, + {"version", "1.0.0"}, + {"identifier", "akashi.network.discordhook"}}; +} + +void DiscordHook::setServiceRegistry(ServiceRegistry *f_registry) +{ + m_registry = f_registry; + + auto l_service = m_registry->get>("qt.network.manager"); + if (!l_service.has_value()) { + m_state = FAILED; + } + + m_state = OK; + m_network_manager = l_service.value()->get(); +} + +void DiscordHook::post(const DiscordMessage &message) +{ + if (!m_network_manager) { + qWarning() << "Cannot post DiscordMessage: QNetworkAccessManager not installed"; + return; + } + + QUrl url(message.requestUrl()); + if (!url.isValid()) { + qWarning() << "Failed to post DiscordMessage: Invalid URL" << message.requestUrl(); + return; + } + + QJsonObject json = message.toJson(); + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QNetworkReply *reply = m_network_manager->post(request, QJsonDocument(json).toJson()); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + onDiscordResponse(reply); + }); +} + +void DiscordHook::post(const DiscordMultipartMessage &message) +{ + if (!m_network_manager) { + qWarning() << "Cannot post DiscordMultipartMessage: QNetworkAccessManager not installed"; + return; + } + + QUrl url(message.requestUrl()); + if (!url.isValid()) { + qWarning() << "Failed to post DiscordMultipartMessage: Invalid URL" << message.requestUrl(); + return; + } + + auto *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + if (!message.payloadJson().isEmpty()) { + QHttpPart json_part; + json_part.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"payload_json\""); + json_part.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + json_part.setBody(QJsonDocument(message.payloadJson()).toJson(QJsonDocument::Compact)); + multipart->append(json_part); + } + + for (int i = 0; i < message.size(); ++i) { + const DiscordMultipart &part_data = message.partAt(i); + + QHttpPart http_part; + + QString disposition = QString("form-data; name=\"%1\"").arg(part_data.name); + if (!part_data.filename.isEmpty()) { + disposition += QString("; filename=\"%1\"").arg(part_data.filename); + } + http_part.setHeader(QNetworkRequest::ContentDispositionHeader, disposition); + + if (!part_data.mime_type.isEmpty()) { + QString content_type = part_data.mime_type; + if (!part_data.charset.isEmpty()) { + content_type += "; charset=" + part_data.charset; + } + http_part.setHeader(QNetworkRequest::ContentTypeHeader, content_type); + } + + http_part.setBody(part_data.data); + multipart->append(http_part); + } + + QNetworkRequest request(url); + + QNetworkReply *reply = m_network_manager->post(request, multipart); + multipart->setParent(reply); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + onDiscordResponse(reply); + }); +} + +void DiscordHook::onDiscordResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Discord webhook failed:" << reply->errorString(); + qWarning() << "Response body:" << reply->readAll(); + return; + } +} diff --git a/library/src/service.cpp b/library/src/service.cpp new file mode 100644 index 00000000..e6ba28da --- /dev/null +++ b/library/src/service.cpp @@ -0,0 +1 @@ +#include "service.h" diff --git a/library/src/serviceregistry.cpp b/library/src/serviceregistry.cpp new file mode 100644 index 00000000..89149e84 --- /dev/null +++ b/library/src/serviceregistry.cpp @@ -0,0 +1,34 @@ +#include "serviceregistry.h" +#include "service.h" +#include "servicewrapper.h" + +#include + +ServiceRegistry::ServiceRegistry(QObject *parent) : QObject{parent} +{ + qInfo() << "Creating new service registry instance at" << this; +} + +void ServiceRegistry::insertService(Service *f_ptr) +{ + std::unique_ptr l_ptr_scoped(f_ptr); + const QString l_identifier = l_ptr_scoped->getServiceProperty("identifier"); + const QString l_version = l_ptr_scoped->getServiceProperty("version"); + const QString l_author = l_ptr_scoped->getServiceProperty("author"); + + if (l_identifier.isEmpty()) { + qCritical() << "Unable to register service: Service identifier is empty."; + return; + } + + if (m_services.contains(l_identifier)) { + qCritical() << "Unable to register service: Service identifier is already taken."; + return; + } + + // We are out of the hot path. We can now safely remove ourselves from this equation. + Service *l_ptr = l_ptr_scoped.release(); + m_services.insert(l_identifier, l_ptr); + + qInfo() << QString("Adding Service: %1:%2:%3").arg(l_identifier, l_version, l_author); +} diff --git a/library/src/servicewrapper.cpp b/library/src/servicewrapper.cpp new file mode 100644 index 00000000..df4d1116 --- /dev/null +++ b/library/src/servicewrapper.cpp @@ -0,0 +1 @@ +#include "servicewrapper.h" diff --git a/src/server.cpp b/src/server.cpp index 2a3260e6..e07b7c3d 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -17,6 +17,7 @@ ////////////////////////////////////////////////////////////////////////////////////// #include "server.h" +#include "../library/include/serviceregistry.h" #include "acl_roles_handler.h" #include "aoclient.h" #include "area_data.h" @@ -24,6 +25,7 @@ #include "config_manager.h" #include "db_manager.h" #include "discord.h" +#include "discordhook.h" #include "logger/u_logger.h" #include "music_manager.h" #include "network/network_socket.h" @@ -54,6 +56,24 @@ Server::Server(int p_ws_port, QObject *parent) : connect(this, &Server::logConnectionAttempt, logger, &ULogger::logConnectionAttempt); AOPacket::registerPackets(); + + service_registry = new ServiceRegistry(this); + service_registry->createWrapped("qt.network.manager", QT_VERSION_STR, "Qt"); + service_registry->create(); + + DiscordMessage l_message; + l_message.setRequestUrl("youwishedItoldyou") + .setContent("This is a sample message.") + .beginEmbed() + .setEmbedDescription("This is an embed description") + .setEmbedTitle("This is an embed title.") + .addEmbedField("Field1", "Field1Data", true) + .addEmbedField("Field2", "Field2Data", true) + .addEmbedField("\u200B", "\u200B") + .setEmbedImage("bunnyurl") + .endEmbed(); + + service_registry->get("akashi.network.discordhook").value()->post(l_message); } void Server::start() diff --git a/src/server.h b/src/server.h index 6f94a38d..d7496e88 100644 --- a/src/server.h +++ b/src/server.h @@ -43,6 +43,7 @@ class DBManager; class Discord; class MusicManager; class ULogger; +class ServiceRegistry; /** * @brief The class that represents the actual server as it is. @@ -439,6 +440,8 @@ class Server : public QObject */ MusicManager *music_manager; + ServiceRegistry *service_registry; + /** * @brief The port through which the server will accept WebSocket connections. */ From a77eb29f397d53ba74b7987898b9e4035fe4d597 Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:46:25 +0100 Subject: [PATCH 2/8] Replace Discord.cpp with DiscordHook --- CMakeLists.txt | 2 - library/include/discordhook.h | 12 ++- library/include/service.h | 13 ++- library/include/serviceregistry.h | 43 ++++++++- library/include/servicewrapper.h | 2 + library/src/discordhook.cpp | 41 ++++----- library/src/service.cpp | 27 ++++++ library/src/serviceregistry.cpp | 29 +----- library/src/servicewrapper.cpp | 2 + src/aoclient.cpp | 3 +- src/aoclient.h | 8 +- src/commands/moderation.cpp | 14 ++- src/discord.cpp | 131 -------------------------- src/discord.h | 148 ------------------------------ src/main.cpp | 19 ++-- src/packet/packet_ma.cpp | 13 ++- src/packet/packet_zz.cpp | 22 ++++- src/server.cpp | 66 ++++--------- src/server.h | 41 ++------- 19 files changed, 193 insertions(+), 443 deletions(-) delete mode 100644 src/discord.cpp delete mode 100644 src/discord.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d64c74b..172852c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,8 +97,6 @@ qt_add_executable(akashi src/data_types.h src/db_manager.cpp src/db_manager.h - src/discord.cpp - src/discord.h src/main.cpp src/medieval_parser.cpp src/medieval_parser.h diff --git a/library/include/discordhook.h b/library/include/discordhook.h index 6d2e5073..58568f9a 100644 --- a/library/include/discordhook.h +++ b/library/include/discordhook.h @@ -38,7 +38,7 @@ class AKASHI_ADDON_EXPORT DiscordMessage : public DiscordMessageCommon DiscordMessage &setEmbedTitle(const QString &title); DiscordMessage &setEmbedDescription(const QString &description); DiscordMessage &setEmbedUrl(const QString &url); - DiscordMessage &setEmbedColor(int color); + DiscordMessage &setEmbedColor(QString color); DiscordMessage &setEmbedTimestamp(const QString ×tamp); DiscordMessage &setEmbedFooter(const QString &text, const QString &icon_url = ""); DiscordMessage &setEmbedImage(const QString &url); @@ -82,9 +82,9 @@ class AKASHI_ADDON_EXPORT DiscordMultipartMessage : public DiscordMessageCommon ~DiscordMultipartMessage() = default; template - DiscordMultipartMessage &addPart(T data, QString name, QString filename = "") + DiscordMultipartMessage &addPart(T data, QString name, QString filename = "", QString mime_type = "", QString charset = "") { - m_parts.append(DiscordMultipart(std::move(data), std::move(name), std::move(filename))); + m_parts.append(DiscordMultipart(std::move(data), std::move(name), std::move(filename), std::move(mime_type), std::move(charset))); return *this; } @@ -101,6 +101,7 @@ class AKASHI_ADDON_EXPORT DiscordMultipartMessage : public DiscordMessageCommon QJsonObject m_payload_json; }; +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiDiscordHook, AKASHI_ADDON_EXPORT) class AKASHI_ADDON_EXPORT DiscordHook : public Service { Q_OBJECT @@ -109,7 +110,10 @@ class AKASHI_ADDON_EXPORT DiscordHook : public Service DiscordHook(QObject *parent = nullptr); ~DiscordHook() = default; - void setServiceRegistry(ServiceRegistry *f_registry = nullptr) override; + inline const static QString SERVICE_ID = "akashi.addon.discordook"; + + void + setServiceRegistry(ServiceRegistry *f_registry = nullptr) override; void post(const DiscordMessage &message); diff --git a/library/include/service.h b/library/include/service.h index 81c9869d..48cf78db 100644 --- a/library/include/service.h +++ b/library/include/service.h @@ -2,13 +2,14 @@ #include "akashi_addon_global.h" +#include #include #include #include -#include class ServiceRegistry; +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiService, AKASHI_ADDON_EXPORT) class AKASHI_ADDON_EXPORT Service : public QObject { Q_OBJECT @@ -25,13 +26,11 @@ class AKASHI_ADDON_EXPORT Service : public QObject Service(QObject *parent = nullptr) : QObject{parent} {}; ~Service() = default; - virtual void setServiceRegistry(ServiceRegistry *f_registry = nullptr) {}; - Service::State getState() { return m_state; }; + virtual void setServiceRegistry(ServiceRegistry *f_registry = nullptr); + void setState(Service::State f_state); + Service::State getState(); - QString getServiceProperty(QString f_key) const - { - return m_service_properties.value(f_key, {}); - } + QString getServiceProperty(QString f_key) const; protected: Service::State m_state = State::PENDING; diff --git a/library/include/serviceregistry.h b/library/include/serviceregistry.h index 8246fddd..3c2fb179 100644 --- a/library/include/serviceregistry.h +++ b/library/include/serviceregistry.h @@ -5,11 +5,14 @@ #include "akashi_addon_global.h" #include "servicewrapper.h" +#include #include #include class Service; +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiServiceRegistry, AKASHI_ADDON_EXPORT) + // Services are commonly owned by the ServiceRegistry. // Why? Because I say so. - Salanto @@ -19,6 +22,7 @@ class AKASHI_ADDON_EXPORT ServiceRegistry : public QObject public: ServiceRegistry(QObject *parent = nullptr); + ~ServiceRegistry(); template requires std::is_base_of_v @@ -26,7 +30,7 @@ class AKASHI_ADDON_EXPORT ServiceRegistry : public QObject { T *l_ptr = new T(this); l_ptr->setServiceRegistry(this); - insertService(l_ptr); + insertService(l_ptr); } template @@ -36,7 +40,7 @@ class AKASHI_ADDON_EXPORT ServiceRegistry : public QObject { T *l_ptr = new T(this); ServiceWrapper *l_wrapper = new ServiceWrapper(l_ptr, f_identifier, f_version, f_author, this); - insertService(l_wrapper); + insertService>(l_wrapper); } template @@ -44,7 +48,7 @@ class AKASHI_ADDON_EXPORT ServiceRegistry : public QObject inline std::optional get(const QString f_identifier) { if (!m_services.contains(f_identifier)) { - qCritical() << "Unable to get service with identifier" << f_identifier; + qCCritical(akashiServiceRegistry) << qUtf8Printable(QString("Unable to get service with identifier %1").arg(f_identifier)); return std::nullopt; } @@ -52,15 +56,44 @@ class AKASHI_ADDON_EXPORT ServiceRegistry : public QObject const Service::State l_service_state = l_service->getState(); if (l_service_state != Service::OK) { - qCritical() << "Unable to get service with identifier" << f_identifier << "due to state:" << l_service_state; + qCCritical(akashiServiceRegistry) << qUtf8Printable(QString("Unable to get service with identifier %1 due to state: %2").arg(f_identifier).arg(l_service_state)); return std::nullopt; } return std::make_optional(static_cast(l_service)); } + inline bool exists(const QString &f_identifier) + { + return m_services.contains(f_identifier); + } + private: - void insertService(Service *f_ptr); + template + requires std::is_base_of_v + void insertService(T *f_ptr) + { + std::unique_ptr l_ptr_scoped(f_ptr); + const QString l_identifier = l_ptr_scoped->getServiceProperty("identifier"); + const QString l_version = l_ptr_scoped->getServiceProperty("version"); + const QString l_author = l_ptr_scoped->getServiceProperty("author"); + + if (l_identifier.isEmpty()) { + qCCritical(akashiServiceRegistry) << "Unable to register service: Service identifier is empty."; + return; + } + + if (m_services.contains(l_identifier)) { + qCCritical(akashiServiceRegistry) << "Unable to register service: Service identifier is already taken."; + return; + } + + // We are out of the hot path. We can now safely remove ourselves from this equation. + T *l_ptr = l_ptr_scoped.release(); + m_services.insert(l_identifier, l_ptr); + + qCInfo(akashiServiceRegistry) << qUtf8Printable(QString("Adding Service: %1:%2:%3").arg(l_identifier, l_version, l_author)); + } QMap m_services; }; diff --git a/library/include/servicewrapper.h b/library/include/servicewrapper.h index 0dd5ded8..05c45563 100644 --- a/library/include/servicewrapper.h +++ b/library/include/servicewrapper.h @@ -7,6 +7,8 @@ // This is a non-owning wrapper. Because I say so. - Salanto +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiServiceWrapper, AKASHI_ADDON_EXPORT) + template requires std::is_base_of_v class ServiceWrapper : public Service diff --git a/library/src/discordhook.cpp b/library/src/discordhook.cpp index 2935ab28..94d61624 100644 --- a/library/src/discordhook.cpp +++ b/library/src/discordhook.cpp @@ -11,6 +11,8 @@ #include #include +Q_LOGGING_CATEGORY(akashiDiscordHook, "akashi.addon.discordhook") + DiscordMessage &DiscordMessage::setRequestUrl(const QString &url) { m_request_url = url; @@ -75,7 +77,7 @@ DiscordMessage &DiscordMessage::setEmbedUrl(const QString &url) return *this; } -DiscordMessage &DiscordMessage::setEmbedColor(int color) +DiscordMessage &DiscordMessage::setEmbedColor(QString color) { if (m_building_embed) { m_current_embed["color"] = color; @@ -238,7 +240,7 @@ DiscordHook::DiscordHook(QObject *parent) : Service{parent} { m_service_properties = {{"author", "Salanto"}, {"version", "1.0.0"}, - {"identifier", "akashi.network.discordhook"}}; + {"identifier", SERVICE_ID}}; } void DiscordHook::setServiceRegistry(ServiceRegistry *f_registry) @@ -247,23 +249,23 @@ void DiscordHook::setServiceRegistry(ServiceRegistry *f_registry) auto l_service = m_registry->get>("qt.network.manager"); if (!l_service.has_value()) { - m_state = FAILED; + setState(State::FAILED); } - m_state = OK; + setState(State::OK); m_network_manager = l_service.value()->get(); } void DiscordHook::post(const DiscordMessage &message) { if (!m_network_manager) { - qWarning() << "Cannot post DiscordMessage: QNetworkAccessManager not installed"; + qCWarning(akashiDiscordHook) << "Cannot post DiscordMessage: QNetworkAccessManager not installed"; return; } QUrl url(message.requestUrl()); if (!url.isValid()) { - qWarning() << "Failed to post DiscordMessage: Invalid URL" << message.requestUrl(); + qCWarning(akashiDiscordHook) << "Failed to post DiscordMessage: Invalid URL" << qUtf8Printable(message.requestUrl()); return; } @@ -280,27 +282,14 @@ void DiscordHook::post(const DiscordMessage &message) void DiscordHook::post(const DiscordMultipartMessage &message) { - if (!m_network_manager) { - qWarning() << "Cannot post DiscordMultipartMessage: QNetworkAccessManager not installed"; - return; - } - QUrl url(message.requestUrl()); if (!url.isValid()) { - qWarning() << "Failed to post DiscordMultipartMessage: Invalid URL" << message.requestUrl(); + qCWarning(akashiDiscordHook) << "Failed to post DiscordMultipartMessage: Invalid URL" << qUtf8Printable(message.requestUrl()); return; } auto *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - if (!message.payloadJson().isEmpty()) { - QHttpPart json_part; - json_part.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"payload_json\""); - json_part.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - json_part.setBody(QJsonDocument(message.payloadJson()).toJson(QJsonDocument::Compact)); - multipart->append(json_part); - } - for (int i = 0; i < message.size(); ++i) { const DiscordMultipart &part_data = message.partAt(i); @@ -324,6 +313,14 @@ void DiscordHook::post(const DiscordMultipartMessage &message) multipart->append(http_part); } + if (!message.payloadJson().isEmpty()) { + QHttpPart json_part; + json_part.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"payload_json\""); + json_part.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + json_part.setBody(QJsonDocument(message.payloadJson()).toJson(QJsonDocument::Compact)); + multipart->append(json_part); + } + QNetworkRequest request(url); QNetworkReply *reply = m_network_manager->post(request, multipart); @@ -339,8 +336,8 @@ void DiscordHook::onDiscordResponse(QNetworkReply *reply) reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { - qWarning() << "Discord webhook failed:" << reply->errorString(); - qWarning() << "Response body:" << reply->readAll(); + qCWarning(akashiDiscordHook) << "Discord webhook failed:" << qUtf8Printable(reply->errorString()); + qCWarning(akashiDiscordHook) << "Response body:" << reply->readAll(); return; } } diff --git a/library/src/service.cpp b/library/src/service.cpp index e6ba28da..65cb8823 100644 --- a/library/src/service.cpp +++ b/library/src/service.cpp @@ -1 +1,28 @@ #include "service.h" + +#include + +Q_LOGGING_CATEGORY(akashiService, "akashi.addon.service") + +void Service::setServiceRegistry(ServiceRegistry *f_registry) +{ + qCDebug(akashiService) << "Called dummy implementation of" << Q_FUNC_INFO; + qCDebug(akashiService) << "This is likely not what is being intended."; +} + +void Service::setState(State f_state) +{ + m_state = f_state; + qCDebug(akashiService) << "ServiceState of" << getServiceProperty("identifier") << "is set to" << m_state; +} + +Service::State Service::getState() +{ + qCDebug(akashiService) << "ServiceState of" << getServiceProperty("identifier") << "is" << m_state; + return m_state; +} + +QString Service::getServiceProperty(QString f_key) const +{ + return m_service_properties.value(f_key, {}); +} diff --git a/library/src/serviceregistry.cpp b/library/src/serviceregistry.cpp index 89149e84..c1985fd7 100644 --- a/library/src/serviceregistry.cpp +++ b/library/src/serviceregistry.cpp @@ -1,34 +1,13 @@ #include "serviceregistry.h" #include "service.h" -#include "servicewrapper.h" #include +Q_LOGGING_CATEGORY(akashiServiceRegistry, "akashi.addon.serviceregistry") + ServiceRegistry::ServiceRegistry(QObject *parent) : QObject{parent} { - qInfo() << "Creating new service registry instance at" << this; + qCDebug(akashiServiceRegistry) << "Created at" << this; } -void ServiceRegistry::insertService(Service *f_ptr) -{ - std::unique_ptr l_ptr_scoped(f_ptr); - const QString l_identifier = l_ptr_scoped->getServiceProperty("identifier"); - const QString l_version = l_ptr_scoped->getServiceProperty("version"); - const QString l_author = l_ptr_scoped->getServiceProperty("author"); - - if (l_identifier.isEmpty()) { - qCritical() << "Unable to register service: Service identifier is empty."; - return; - } - - if (m_services.contains(l_identifier)) { - qCritical() << "Unable to register service: Service identifier is already taken."; - return; - } - - // We are out of the hot path. We can now safely remove ourselves from this equation. - Service *l_ptr = l_ptr_scoped.release(); - m_services.insert(l_identifier, l_ptr); - - qInfo() << QString("Adding Service: %1:%2:%3").arg(l_identifier, l_version, l_author); -} +ServiceRegistry::~ServiceRegistry() {} diff --git a/library/src/servicewrapper.cpp b/library/src/servicewrapper.cpp index df4d1116..5541cfe1 100644 --- a/library/src/servicewrapper.cpp +++ b/library/src/servicewrapper.cpp @@ -1 +1,3 @@ #include "servicewrapper.h" + +Q_LOGGING_CATEGORY(akashiServiceWrapper, "akashi.addon.servicewrapper") diff --git a/src/aoclient.cpp b/src/aoclient.cpp index c9a0fba9..a2d90e23 100644 --- a/src/aoclient.cpp +++ b/src/aoclient.cpp @@ -607,13 +607,14 @@ void AOClient::onAfkTimeout() m_is_afk = true; } -AOClient::AOClient(Server *p_server, NetworkSocket *socket, QObject *parent, int user_id, MusicManager *p_manager) : +AOClient::AOClient(Server *p_server, NetworkSocket *socket, QObject *parent, int user_id, MusicManager *p_manager, ServiceRegistry *f_registry) : QObject(parent), m_remote_ip(socket->peerAddress()), m_password(""), m_joined(false), m_socket(socket), m_music_manager(p_manager), + m_service_registry(f_registry), m_last_wtce_time(0), m_id(user_id), m_current_area(0), diff --git a/src/aoclient.h b/src/aoclient.h index 44417776..7d9af2c6 100644 --- a/src/aoclient.h +++ b/src/aoclient.h @@ -36,6 +36,7 @@ class MusicManager; class Server; class NetworkSocket; class AOPacket; +class ServiceRegistry; /** * @brief Represents a client connected to the server running Attorney Online 2 or one of its derivatives. @@ -84,7 +85,7 @@ class AOClient : public QObject * @param user_id The user ID of the client. * @param parent Qt-based parent, passed along to inherited constructor from QObject. */ - AOClient(Server *p_server, NetworkSocket *socket, QObject *parent = nullptr, int user_id = 0, MusicManager *p_manager = nullptr); + AOClient(Server *p_server, NetworkSocket *socket, QObject *parent = nullptr, int user_id = 0, MusicManager *p_manager = nullptr, ServiceRegistry *f_registry = nullptr); /** * @brief Destructor for the AOClient instance. @@ -527,6 +528,11 @@ class AOClient : public QObject */ MusicManager *m_music_manager; + /** + * @brief Pointer to the servers service registry. + */ + ServiceRegistry *m_service_registry; + /** * @brief The text of the last in-character message that was sent by the client. * diff --git a/src/commands/moderation.cpp b/src/commands/moderation.cpp index d998ecb2..ce68d229 100644 --- a/src/commands/moderation.cpp +++ b/src/commands/moderation.cpp @@ -21,7 +21,9 @@ #include "command_extension.h" #include "config_manager.h" #include "db_manager.h" +#include "discordhook.h" #include "server.h" +#include "serviceregistry.h" // This file is for commands under the moderation category in aoclient.h // Be sure to register the command in the header before adding it here! @@ -85,8 +87,16 @@ void AOClient::cmdBan(int argc, QStringList argv) l_kick_counter++; emit logBan(l_ban.moderator, l_ban.ipid, l_ban_duration, l_ban.reason); - if (ConfigManager::discordBanWebhookEnabled()) - emit server->banWebhookRequest(l_ban.ipid, l_ban.moderator, l_ban_duration, l_ban.reason, l_ban_id); + if (ConfigManager::discordBanWebhookEnabled() && m_service_registry->exists(DiscordHook::SERVICE_ID)) { + DiscordMessage l_message; + l_message.setRequestUrl(ConfigManager::discordBanWebhookUrl()) + .beginEmbed() + .setEmbedColor(ConfigManager::discordWebhookColor()) + .setEmbedTitle("Ban issued by " + l_ban.moderator) + .setEmbedDescription("Client IPID : " + l_ban.ipid + "\nBan ID: " + QString::number(l_ban.id) + "\nBan reason : " + l_ban.reason + "\nBanned until : " + QString::number(l_ban.duration)) + .endEmbed(); + m_service_registry->get(DiscordHook::SERVICE_ID).value()->post(l_message); + } } if (l_kick_counter > 1) diff --git a/src/discord.cpp b/src/discord.cpp deleted file mode 100644 index 30971691..00000000 --- a/src/discord.cpp +++ /dev/null @@ -1,131 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////// -// akashi - a server for Attorney Online 2 // -// Copyright (C) 2020 scatterflower // -// // -// This program is free software: you can redistribute it and/or modify // -// it under the terms of the GNU Affero General Public License as // -// published by the Free Software Foundation, either version 3 of the // -// License, or (at your option) any later version. // -// // -// This program is distributed in the hope that it will be useful, // -// but WITHOUT ANY WARRANTY; without even the implied warranty of // -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // -// GNU Affero General Public License for more details. // -// // -// You should have received a copy of the GNU Affero General Public License // -// along with this program. If not, see . // -////////////////////////////////////////////////////////////////////////////////////// -#include "discord.h" - -#include "config_manager.h" - -Discord::Discord(QObject *parent) : - QObject(parent) -{ - m_nam = new QNetworkAccessManager(); - connect(m_nam, &QNetworkAccessManager::finished, - this, &Discord::onReplyFinished); -} - -void Discord::onModcallWebhookRequested(const QString &f_name, const QString &f_area, const QString &f_reason, const QQueue &f_buffer) -{ - m_request.setUrl(QUrl(ConfigManager::discordModcallWebhookUrl())); - QJsonDocument l_json = constructModcallJson(f_name, f_area, f_reason); - postJsonWebhook(l_json); - - if (ConfigManager::discordModcallWebhookSendFile()) { - QHttpMultiPart *l_multipart = constructLogMultipart(f_buffer); - postMultipartWebhook(*l_multipart); - } -} - -void Discord::onBanWebhookRequested(const QString &f_ipid, const QString &f_moderator, const QString &f_duration, const QString &f_reason, const int &f_banID) -{ - m_request.setUrl(QUrl(ConfigManager::discordBanWebhookUrl())); - QJsonDocument l_json = constructBanJson(f_ipid, f_moderator, f_duration, f_reason, f_banID); - postJsonWebhook(l_json); -} - -QJsonDocument Discord::constructModcallJson(const QString &f_name, const QString &f_area, const QString &f_reason) const -{ - QJsonObject l_json; - QJsonArray l_array; - QJsonObject l_object{ - {"color", ConfigManager::discordWebhookColor()}, - {"title", f_name + " filed a modcall in " + f_area}, - {"description", f_reason}}; - l_array.append(l_object); - - if (!ConfigManager::discordModcallWebhookContent().isEmpty()) - l_json["content"] = ConfigManager::discordModcallWebhookContent(); - l_json["embeds"] = l_array; - - return QJsonDocument(l_json); -} - -QJsonDocument Discord::constructBanJson(const QString &f_ipid, const QString &f_moderator, const QString &f_duration, const QString &f_reason, const int &f_banID) -{ - QJsonObject l_json; - QJsonArray l_array; - QJsonObject l_object{ - {"color", ConfigManager::discordWebhookColor()}, - {"title", "Ban issued by " + f_moderator}, - {"description", "Client IPID : " + f_ipid + "\nBan ID: " + QString::number(f_banID) + "\nBan reason : " + f_reason + "\nBanned until : " + f_duration}}; - l_array.append(l_object); - l_json["embeds"] = l_array; - - return QJsonDocument(l_json); -} - -QHttpMultiPart *Discord::constructLogMultipart(const QQueue &f_buffer) const -{ - QHttpMultiPart *l_multipart = new QHttpMultiPart(); - QHttpPart l_logdata; - l_logdata.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); - l_logdata.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain; charset=utf-8"); - QString l_log; - for (const QString &log_entry : f_buffer) { - l_log.append(log_entry); - } - l_logdata.setBody(l_log.toUtf8()); - l_multipart->append(l_logdata); - return l_multipart; -} - -void Discord::postJsonWebhook(const QJsonDocument &f_json) -{ - if (!QUrl(m_request.url()).isValid()) { - qWarning("Invalid webhook URL!"); - return; - } - m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_nam->post(m_request, f_json.toJson()); -} - -void Discord::postMultipartWebhook(QHttpMultiPart &f_multipart) -{ - if (!QUrl(m_request.url()).isValid()) { - qWarning("Invalid webhook URL!"); - f_multipart.deleteLater(); - return; - } - m_request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=" + f_multipart.boundary()); - QNetworkReply *l_reply = m_nam->post(m_request, &f_multipart); - f_multipart.setParent(l_reply); -} - -void Discord::onReplyFinished(QNetworkReply *f_reply) -{ - auto l_data = f_reply->readAll(); - f_reply->deleteLater(); -#ifdef DISCORD_DEBUG - QDebug() << l_data; -#else - Q_UNUSED(l_data); -#endif -} - -Discord::~Discord() -{ - m_nam->deleteLater(); -} diff --git a/src/discord.h b/src/discord.h deleted file mode 100644 index 1a10f56d..00000000 --- a/src/discord.h +++ /dev/null @@ -1,148 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////// -// akashi - a server for Attorney Online 2 // -// Copyright (C) 2020 scatterflower // -// // -// This program is free software: you can redistribute it and/or modify // -// it under the terms of the GNU Affero General Public License as // -// published by the Free Software Foundation, either version 3 of the // -// License, or (at your option) any later version. // -// // -// This program is distributed in the hope that it will be useful, // -// but WITHOUT ANY WARRANTY; without even the implied warranty of // -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // -// GNU Affero General Public License for more details. // -// // -// You should have received a copy of the GNU Affero General Public License // -// along with this program. If not, see . // -////////////////////////////////////////////////////////////////////////////////////// -#ifndef DISCORD_H -#define DISCORD_H - -#include -#include - -class ConfigManager; - -/** - * @brief A class for handling all Discord webhook requests. - */ -class Discord : public QObject -{ - Q_OBJECT - - public: - /** - * @brief Constructor for the Discord object - * - * @param f_webhook_url The URL to send webhook POST requests to. - * @param f_webhook_content The content to include in the webhook POST request. - * @param f_webhook_sendfile Whether or not to send a file containing area logs with the webhook POST request. - * @param parent Qt-based parent, passed along to inherited constructor from QObject. - */ - Discord(QObject *parent = nullptr); - - /** - * @brief Deconstructor for the Discord class. - * - * @details Marks the network access manager to be deleted later. - */ - ~Discord(); - - /** - * @brief Method to start the Uptime Webhook posting timer. - */ - void startUptimeTimer(); - - /** - * @brief Method to stop the Uptime Webhook posting timer. - */ - void stopUptimeTimer(); - - public slots: - /** - * @brief Handles a modcall webhook request. - * - * @param f_name The name of the modcall sender. - * @param f_area The name of the area the modcall was sent from. - * @param f_reason The reason for the modcall. - * @param f_buffer The area's log buffer. - */ - void onModcallWebhookRequested(const QString &f_name, const QString &f_area, const QString &f_reason, const QQueue &f_buffer); - - /** - * @brief Handles a ban webhook request. - * - * @param f_ipid The IPID of the client. - * @param f_moderator The name of the moderator banning. - * @param f_duration The date the ban expires. - * @param f_reason The reason of the ban. - */ - void onBanWebhookRequested(const QString &f_ipid, const QString &f_moderator, const QString &f_duration, const QString &f_reason, const int &f_banID); - - private: - /** - * @brief The QNetworkAccessManager for webhooks. - */ - QNetworkAccessManager *m_nam; - - /** - * @brief The QNetworkRequest for webhooks. - */ - QNetworkRequest m_request; - - /** - * @brief Constructs a new JSON document for modcalls. - * - * @param f_name The name of the modcall sender. - * @param f_area The name of the area the modcall was sent from. - * @param f_reason The reason for the modcall. - * - * @return A JSON document for the modcall. - */ - QJsonDocument constructModcallJson(const QString &f_name, const QString &f_area, const QString &f_reason) const; - - /** - * @brief Constructs a new QHttpMultiPart document for log files. - * - * @param f_buffer The area's log buffer. - * - * @return A QHttpMultiPart containing the log file. - */ - QHttpMultiPart *constructLogMultipart(const QQueue &f_buffer) const; - - private slots: - /** - * @brief Handles a network reply from a webhook POST request. - * - * @param f_reply Pointer to the QNetworkReply created by the webhook POST request. - */ - void onReplyFinished(QNetworkReply *f_reply); - - /** - * @brief Sends a webhook POST request with the given JSON document. - * - * @param f_json The JSON document to send. - */ - void postJsonWebhook(const QJsonDocument &f_json); - - /** - * @brief Sends a webhook POST request with the given QHttpMultiPart. - * - * @param f_multipart The QHttpMultiPart to send. - */ - void postMultipartWebhook(QHttpMultiPart &f_multipart); - - /** - * @brief Constructs a new JSON document for bans. - * - * @param f_ipid The IPID of the client. - * @param f_moderator The name of the moderator banning. - * @param f_duration The date the ban expires. - * @param f_reason The reason of the ban. - * - * @return A JSON document for the ban. - */ - QJsonDocument constructBanJson(const QString &f_ipid, const QString &f_moderator, const QString &f_duration, const QString &f_reason, const int &f_banID); -}; - -#endif // DISCORD_H diff --git a/src/main.cpp b/src/main.cpp index 888fe4a1..bbeaf67a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,25 +17,20 @@ ////////////////////////////////////////////////////////////////////////////////////// #include "config_manager.h" #include "server.h" +#include "serviceregistry.h" #include #include #include -Server *server; - -void cleanup() -{ - server->deleteLater(); -} - int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName("akashi"); QCoreApplication::setApplicationVersion("jackfruit (1.9)"); - std::atexit(cleanup); + + ServiceRegistry l_registry(&app); // Verify server configuration is sound. if (!ConfigManager::verifyServerConfig()) { @@ -44,10 +39,10 @@ int main(int argc, char *argv[]) exit(EXIT_FAILURE); QCoreApplication::quit(); } - else { - server = new Server(ConfigManager::serverPort(), &app); - server->start(); - } + + Server l_server(ConfigManager::serverPort(), &l_registry, &app); + l_server.initServices(); + l_server.start(); return app.exec(); } diff --git a/src/packet/packet_ma.cpp b/src/packet/packet_ma.cpp index 7b9760eb..f49c05ed 100644 --- a/src/packet/packet_ma.cpp +++ b/src/packet/packet_ma.cpp @@ -2,7 +2,9 @@ #include "config_manager.h" #include "db_manager.h" +#include "discordhook.h" #include "server.h" +#include "serviceregistry.h" PacketMA::PacketMA(QStringList &contents) : AOPacket(contents) @@ -103,8 +105,15 @@ void PacketMA::handlePacket(AreaData *area, AOClient &client) const client.sendServerMessage("Banned " + QString::number(clients.size()) + " client(s) with ipid " + target->m_ipid + " for reason: " + reason); int ban_id = client.getServer()->getDatabaseManager()->getBanID(ban.ip); - if (ConfigManager::discordBanWebhookEnabled()) { - Q_EMIT client.getServer()->banWebhookRequest(ban.ipid, ban.moderator, timestamp, ban.reason, ban_id); + if (ConfigManager::discordBanWebhookEnabled() && client.m_service_registry->exists(DiscordHook::SERVICE_ID)) { + DiscordMessage l_message; + l_message.setRequestUrl(ConfigManager::discordBanWebhookUrl()) + .beginEmbed() + .setEmbedColor(ConfigManager::discordWebhookColor()) + .setEmbedTitle("Ban issued by " + ban.moderator) + .setEmbedDescription("Client IPID : " + ban.ipid + "\nBan ID: " + QString::number(ban_id) + "\nBan reason : " + ban.reason + "\nBanned until : " + QString::number(ban.duration)) + .endEmbed(); + client.m_service_registry->get(DiscordHook::SERVICE_ID).value()->post(l_message); } } } diff --git a/src/packet/packet_zz.cpp b/src/packet/packet_zz.cpp index a308e6b7..9f2bb1e9 100644 --- a/src/packet/packet_zz.cpp +++ b/src/packet/packet_zz.cpp @@ -1,7 +1,9 @@ #include "packet/packet_zz.h" #include "config_manager.h" +#include "discordhook.h" #include "packet/packet_factory.h" #include "server.h" +#include "serviceregistry.h" #include @@ -60,6 +62,24 @@ void PacketZZ::handlePacket(AreaData *area, AOClient &client) const } } - emit client.getServer()->modcallWebhookRequest(l_name, l_areaName, webhook_reason, client.getServer()->getAreaBuffer(l_areaName)); + DiscordMessage l_message; + l_message.setContent(ConfigManager::discordModcallWebhookContent()) + .beginEmbed() + .setEmbedColor(ConfigManager::discordWebhookColor()) + .setEmbedTitle(l_name + " filed a modcall in " + l_areaName) + .setEmbedDescription(webhook_reason) + .endEmbed(); + + const auto l_data = client.getServer()->getAreaBuffer(l_areaName); + QString l_log; + + for (const QString &l_entry : l_data) { + l_log.append(l_entry); + } + DiscordMultipartMessage l_multi_message; + l_multi_message.setRequestUrl(ConfigManager::discordModcallWebhookUrl()); + l_multi_message.addPart(l_log.toUtf8(), "file", "log.txt", "text/plain", "utf-8").setPayloadJson(l_message.toJson()); + + client.m_service_registry->get(DiscordHook::SERVICE_ID).value()->post(l_multi_message); } } diff --git a/src/server.cpp b/src/server.cpp index e07b7c3d..29f24613 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -17,25 +17,27 @@ ////////////////////////////////////////////////////////////////////////////////////// #include "server.h" -#include "../library/include/serviceregistry.h" #include "acl_roles_handler.h" #include "aoclient.h" #include "area_data.h" #include "command_extension.h" #include "config_manager.h" #include "db_manager.h" -#include "discord.h" #include "discordhook.h" #include "logger/u_logger.h" #include "music_manager.h" #include "network/network_socket.h" #include "packet/packet_factory.h" #include "serverpublisher.h" +#include "serviceregistry.h" -Server::Server(int p_ws_port, QObject *parent) : +#include + +Server::Server(int p_ws_port, ServiceRegistry *f_registry, QObject *parent) : QObject(parent), m_port(p_ws_port), - m_player_count(0) + m_player_count(0), + m_service_registry{f_registry} { timer = new QTimer(this); @@ -49,31 +51,10 @@ Server::Server(int p_ws_port, QObject *parent) : command_extension_collection->setCommandNameWhitelist(AOClient::COMMANDS.keys()); command_extension_collection->loadFile("config/command_extensions.ini"); - // We create it, even if its not used later on. - discord = new Discord(this); - logger = new ULogger(this); connect(this, &Server::logConnectionAttempt, logger, &ULogger::logConnectionAttempt); AOPacket::registerPackets(); - - service_registry = new ServiceRegistry(this); - service_registry->createWrapped("qt.network.manager", QT_VERSION_STR, "Qt"); - service_registry->create(); - - DiscordMessage l_message; - l_message.setRequestUrl("youwishedItoldyou") - .setContent("This is a sample message.") - .beginEmbed() - .setEmbedDescription("This is an embed description") - .setEmbedTitle("This is an embed title.") - .addEmbedField("Field1", "Field1Data", true) - .addEmbedField("Field2", "Field2Data", true) - .addEmbedField("\u200B", "\u200B") - .setEmbedImage("bunnyurl") - .endEmbed(); - - service_registry->get("akashi.network.discordhook").value()->post(l_message); } void Server::start() @@ -98,9 +79,6 @@ void Server::start() qInfo() << "Server listening on" << server->serverPort(); } - // Checks if any Discord webhooks are enabled. - handleDiscordIntegration(); - // Construct modern advertiser if enabled in config server_publisher = new ServerPublisher(server->serverPort(), &m_player_count, this); @@ -171,7 +149,7 @@ void Server::clientConnected() } int user_id = m_available_ids.pop(); - AOClient *client = new AOClient(this, l_socket, l_socket, user_id, music_manager); + AOClient *client = new AOClient(this, l_socket, l_socket, user_id, music_manager, m_service_registry); m_clients_ids.insert(user_id, client); m_player_state_observer.registerClient(client); @@ -302,7 +280,6 @@ void Server::reloadSettings() ConfigManager::reloadSettings(); emit reloadRequest(ConfigManager::serverName(), ConfigManager::serverDescription()); emit updateHTTPConfiguration(); - handleDiscordIntegration(); logger->loadLogtext(); m_ipban_list = ConfigManager::iprangeBans(); acl_roles_handler->loadFile("config/acl_roles.ini"); @@ -510,21 +487,6 @@ void Server::allowMessage() m_can_send_ic_messages = true; } -void Server::handleDiscordIntegration() -{ - // Prevent double connecting by preemtively disconnecting them. - disconnect(this, nullptr, discord, nullptr); - - if (ConfigManager::discordWebhookEnabled()) { - if (ConfigManager::discordModcallWebhookEnabled()) - connect(this, &Server::modcallWebhookRequest, discord, &Discord::onModcallWebhookRequested); - - if (ConfigManager::discordBanWebhookEnabled()) - connect(this, &Server::banWebhookRequest, discord, &Discord::onBanWebhookRequested); - } - return; -} - void Server::markIDFree(const int &f_user_id) { m_player_state_observer.unregisterClient(m_clients_ids[f_user_id]); @@ -575,8 +537,20 @@ Server::~Server() l_client->deleteLater(); } server->deleteLater(); - discord->deleteLater(); acl_roles_handler->deleteLater(); delete db_manager; } + +bool Server::initServices() +{ + if (!m_service_registry) { + qCritical() << "Failed to create services. ServiceRegistry does not exist"; + return false; + } + + m_service_registry->createWrapped("qt.network.manager", QT_VERSION_STR, "Qt"); + m_service_registry->create(); + + return true; +} diff --git a/src/server.h b/src/server.h index d7496e88..474cd45e 100644 --- a/src/server.h +++ b/src/server.h @@ -59,7 +59,7 @@ class Server : public QObject * @param p_ws_port The port to listen for connections on. * @param parent Qt-based parent, passed along to inherited constructor from QObject. */ - Server(int p_ws_port, QObject *parent = nullptr); + Server(int p_ws_port, ServiceRegistry *f_registry = nullptr, QObject *parent = nullptr); /** * @brief Destructor for the Server class. @@ -68,6 +68,11 @@ class Server : public QObject */ ~Server(); + /** + * @brief Initialises service instances. + */ + bool initServices(); + /** * @brief Starts the server. * @@ -351,13 +356,6 @@ class Server : public QObject */ void clientConnected(); - /** - * @brief Method to construct and reconstruct Discord Webhook Integration. - * - * @details Constructs or rebuilds Discord Object during server startup and configuration reload. - */ - void handleDiscordIntegration(); - /** * @brief Marks a userID as free and ads it back to the available client id queue. */ @@ -386,26 +384,6 @@ class Server : public QObject */ void updateHTTPConfiguration(); - /** - * @brief Sends a modcall webhook request, emitted by AOClient::pktModcall. - * - * @param f_name The character or OOC name of the client who sent the modcall. - * @param f_area The name of the area the modcall was sent from. - * @param f_reason The reason the client specified for the modcall. - * @param f_buffer The area's log buffer. - */ - void modcallWebhookRequest(const QString &f_name, const QString &f_area, const QString &f_reason, const QQueue &f_buffer); - - /** - * @brief Sends a ban webhook request, emitted by AOClient::cmdBan - * @param f_ipid The IPID of the banned client. - * @param f_moderator The moderator who issued the ban. - * @param f_duration The duration of the ban in a human readable format. - * @param f_reason The reason for the ban. - * @param f_banID The ID of the issued ban. - */ - void banWebhookRequest(const QString &f_ipid, const QString &f_moderator, const QString &f_duration, const QString &f_reason, const int &f_banID); - /** * @brief Signal connected to universal logger. Logs a client connection attempt. * @param f_ip_address The IP Address of the incoming connection. @@ -420,11 +398,6 @@ class Server : public QObject */ QWebSocketServer *server; - /** - * @brief Handles Discord webhooks. - */ - Discord *discord; - /** * @brief Handles HTTP server advertising. */ @@ -440,7 +413,7 @@ class Server : public QObject */ MusicManager *music_manager; - ServiceRegistry *service_registry; + ServiceRegistry *m_service_registry; /** * @brief The port through which the server will accept WebSocket connections. From 985b25798f53a9c685cf6fd5dd3874ebfe12929b Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:49:32 +0100 Subject: [PATCH 3/8] Minor tweak --- src/packet/packet_zz.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packet/packet_zz.cpp b/src/packet/packet_zz.cpp index 9f2bb1e9..beae9c4c 100644 --- a/src/packet/packet_zz.cpp +++ b/src/packet/packet_zz.cpp @@ -47,7 +47,7 @@ void PacketZZ::handlePacket(AreaData *area, AOClient &client) const } emit client.logModcall((client.character() + " " + client.characterName()), client.m_ipid, client.name(), client.getServer()->getAreaById(client.areaId())->name()); - if (ConfigManager::discordModcallWebhookEnabled()) { + if (ConfigManager::discordModcallWebhookEnabled() && client.m_service_registry->exists(DiscordHook::SERVICE_ID)) { QString l_name = client.name(); if (client.name().isEmpty()) l_name = client.character(); From 8c6c859f7bf08eab3553d2450fd5f9c214243d81 Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:14:57 +0100 Subject: [PATCH 4/8] Add helper, fix noisy console output --- CMakeLists.txt | 1 + bin/config_sample/qtlogging.ini | 2 + library/CMakeLists.txt | 1 + library/include/discordhook.h | 96 +-------------------------------- library/include/discordtypes.h | 92 +++++++++++++++++++++++++++++++ library/src/service.cpp | 4 +- src/commands/moderation.cpp | 13 ++--- src/discordmessagehelper.h | 42 +++++++++++++++ src/main.cpp | 1 + src/packet/packet_ma.cpp | 13 ++--- src/packet/packet_zz.cpp | 22 ++------ 11 files changed, 156 insertions(+), 131 deletions(-) create mode 100644 bin/config_sample/qtlogging.ini create mode 100644 library/include/discordtypes.h create mode 100644 src/discordmessagehelper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 172852c2..b581b7f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,7 @@ qt_add_executable(akashi src/serverpublisher.h src/testimony_recorder.cpp src/typedefs.h + src/discordmessagehelper.h ) target_link_libraries(akashi PRIVATE diff --git a/bin/config_sample/qtlogging.ini b/bin/config_sample/qtlogging.ini new file mode 100644 index 00000000..51579ed2 --- /dev/null +++ b/bin/config_sample/qtlogging.ini @@ -0,0 +1,2 @@ +[Rules] +*.debug=false \ No newline at end of file diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 6b76cc8f..1ddc522f 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(akashi_addon SHARED include/serviceregistry.h src/serviceregistry.cpp include/servicewrapper.h src/servicewrapper.cpp include/discordhook.h src/discordhook.cpp + include/discordtypes.h ) add_library(akashi::Addon ALIAS akashi_addon) diff --git a/library/include/discordhook.h b/library/include/discordhook.h index 58568f9a..4b37c4aa 100644 --- a/library/include/discordhook.h +++ b/library/include/discordhook.h @@ -1,106 +1,12 @@ #pragma once #include "akashi_addon_global.h" +#include "discordtypes.h" #include "service.h" -#include -#include -#include -#include -#include -#include - class QNetworkAccessManager; class QNetworkReply; -class DiscordMessageCommon -{ - public: - const QString &requestUrl() const { return m_request_url; } - - protected: - QString m_request_url; -}; - -class AKASHI_ADDON_EXPORT DiscordMessage : public DiscordMessageCommon -{ - public: - DiscordMessage() = default; - ~DiscordMessage() = default; - - DiscordMessage &setRequestUrl(const QString &url); - DiscordMessage &setContent(const QString &content); - DiscordMessage &setUsername(const QString &username); - DiscordMessage &setAvatarUrl(const QString &avatar_url); - DiscordMessage &setTts(bool tts); - - DiscordMessage &beginEmbed(); - DiscordMessage &setEmbedTitle(const QString &title); - DiscordMessage &setEmbedDescription(const QString &description); - DiscordMessage &setEmbedUrl(const QString &url); - DiscordMessage &setEmbedColor(QString color); - DiscordMessage &setEmbedTimestamp(const QString ×tamp); - DiscordMessage &setEmbedFooter(const QString &text, const QString &icon_url = ""); - DiscordMessage &setEmbedImage(const QString &url); - DiscordMessage &setEmbedThumbnail(const QString &url); - DiscordMessage &setEmbedAuthor(const QString &name, const QString &url = "", const QString &icon_url = ""); - DiscordMessage &addEmbedField(const QString &name, const QString &value, bool inline_field = false); - DiscordMessage &endEmbed(); - - QJsonObject toJson() const; - - private: - QMap m_fields; - QVector m_embeds; - QVariantMap m_current_embed; - bool m_building_embed = false; -}; - -struct DiscordMultipart -{ - QByteArray data; - QString name; - QString filename; - QString mime_type; - QString charset; - - template - requires std::convertible_to - DiscordMultipart(T data, QString name, QString filename = "", - QString mime_type = "", QString charset = "") : data(std::move(data)), - name(std::move(name)), - filename(std::move(filename)), - mime_type(std::move(mime_type)), - charset(std::move(charset)) - {} -}; - -class AKASHI_ADDON_EXPORT DiscordMultipartMessage : public DiscordMessageCommon -{ - public: - DiscordMultipartMessage() = default; - ~DiscordMultipartMessage() = default; - - template - DiscordMultipartMessage &addPart(T data, QString name, QString filename = "", QString mime_type = "", QString charset = "") - { - m_parts.append(DiscordMultipart(std::move(data), std::move(name), std::move(filename), std::move(mime_type), std::move(charset))); - return *this; - } - - DiscordMultipartMessage &setRequestUrl(const QString &url); - DiscordMultipartMessage &setPayloadJson(const QJsonObject &payload); - - int size() const { return m_parts.size(); } - const DiscordMultipart &partAt(int index) const { return m_parts.at(index); } - const QVector &parts() const { return m_parts; } - const QJsonObject &payloadJson() const { return m_payload_json; } - - private: - QVector m_parts; - QJsonObject m_payload_json; -}; - Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiDiscordHook, AKASHI_ADDON_EXPORT) class AKASHI_ADDON_EXPORT DiscordHook : public Service { diff --git a/library/include/discordtypes.h b/library/include/discordtypes.h new file mode 100644 index 00000000..30c106c7 --- /dev/null +++ b/library/include/discordtypes.h @@ -0,0 +1,92 @@ +#pragma once + +#include "akashi_addon_global.h" + +#include +#include +#include +#include +#include +#include + +class DiscordMessageCommon +{ + public: + const QString &requestUrl() const { return m_request_url; } + + protected: + QString m_request_url; +}; + +class AKASHI_ADDON_EXPORT DiscordMessage : public DiscordMessageCommon +{ + public: + DiscordMessage &setRequestUrl(const QString &url); + DiscordMessage &setContent(const QString &content); + DiscordMessage &setUsername(const QString &username); + DiscordMessage &setAvatarUrl(const QString &avatar_url); + DiscordMessage &setTts(bool tts); + + DiscordMessage &beginEmbed(); + DiscordMessage &setEmbedTitle(const QString &title); + DiscordMessage &setEmbedDescription(const QString &description); + DiscordMessage &setEmbedUrl(const QString &url); + DiscordMessage &setEmbedColor(QString color); + DiscordMessage &setEmbedTimestamp(const QString ×tamp); + DiscordMessage &setEmbedFooter(const QString &text, const QString &icon_url = ""); + DiscordMessage &setEmbedImage(const QString &url); + DiscordMessage &setEmbedThumbnail(const QString &url); + DiscordMessage &setEmbedAuthor(const QString &name, const QString &url = "", const QString &icon_url = ""); + DiscordMessage &addEmbedField(const QString &name, const QString &value, bool inline_field = false); + DiscordMessage &endEmbed(); + + QJsonObject toJson() const; + + private: + QMap m_fields; + QVector m_embeds; + QVariantMap m_current_embed; + bool m_building_embed = false; +}; + +struct DiscordMultipart +{ + QByteArray data; + QString name; + QString filename; + QString mime_type; + QString charset; + + template + requires std::convertible_to + DiscordMultipart(T data, QString name, QString filename = "", + QString mime_type = "", QString charset = "") : data(std::move(data)), + name(std::move(name)), + filename(std::move(filename)), + mime_type(std::move(mime_type)), + charset(std::move(charset)) + {} +}; + +class AKASHI_ADDON_EXPORT DiscordMultipartMessage : public DiscordMessageCommon +{ + public: + template + DiscordMultipartMessage &addPart(T data, QString name, QString filename = "", QString mime_type = "", QString charset = "") + { + m_parts.append(DiscordMultipart(std::move(data), std::move(name), std::move(filename), std::move(mime_type), std::move(charset))); + return *this; + } + + DiscordMultipartMessage &setRequestUrl(const QString &url); + DiscordMultipartMessage &setPayloadJson(const QJsonObject &payload); + + int size() const { return m_parts.size(); } + const DiscordMultipart &partAt(int index) const { return m_parts.at(index); } + const QVector &parts() const { return m_parts; } + const QJsonObject &payloadJson() const { return m_payload_json; } + + private: + QVector m_parts; + QJsonObject m_payload_json; +}; diff --git a/library/src/service.cpp b/library/src/service.cpp index 65cb8823..1526bb30 100644 --- a/library/src/service.cpp +++ b/library/src/service.cpp @@ -13,12 +13,12 @@ void Service::setServiceRegistry(ServiceRegistry *f_registry) void Service::setState(State f_state) { m_state = f_state; - qCDebug(akashiService) << "ServiceState of" << getServiceProperty("identifier") << "is set to" << m_state; + qCDebug(akashiService) << "Status of" << getServiceProperty("identifier") << "is set to" << m_state; } Service::State Service::getState() { - qCDebug(akashiService) << "ServiceState of" << getServiceProperty("identifier") << "is" << m_state; + qCDebug(akashiService) << "Status of" << getServiceProperty("identifier") << "is" << m_state; return m_state; } diff --git a/src/commands/moderation.cpp b/src/commands/moderation.cpp index ce68d229..d7073a32 100644 --- a/src/commands/moderation.cpp +++ b/src/commands/moderation.cpp @@ -22,6 +22,7 @@ #include "config_manager.h" #include "db_manager.h" #include "discordhook.h" +#include "discordmessagehelper.h" #include "server.h" #include "serviceregistry.h" @@ -88,14 +89,10 @@ void AOClient::cmdBan(int argc, QStringList argv) emit logBan(l_ban.moderator, l_ban.ipid, l_ban_duration, l_ban.reason); if (ConfigManager::discordBanWebhookEnabled() && m_service_registry->exists(DiscordHook::SERVICE_ID)) { - DiscordMessage l_message; - l_message.setRequestUrl(ConfigManager::discordBanWebhookUrl()) - .beginEmbed() - .setEmbedColor(ConfigManager::discordWebhookColor()) - .setEmbedTitle("Ban issued by " + l_ban.moderator) - .setEmbedDescription("Client IPID : " + l_ban.ipid + "\nBan ID: " + QString::number(l_ban.id) + "\nBan reason : " + l_ban.reason + "\nBanned until : " + QString::number(l_ban.duration)) - .endEmbed(); - m_service_registry->get(DiscordHook::SERVICE_ID).value()->post(l_message); + DiscordMessage l_message = DiscordMessageHelper::banMessage(l_ban.ipid, l_ban.moderator, l_ban_duration, l_ban.reason, l_ban_id); + if (std::optional l_hook = m_service_registry->get(DiscordHook::SERVICE_ID); l_hook.has_value()) { + l_hook.value()->post(l_message); + } } } diff --git a/src/discordmessagehelper.h b/src/discordmessagehelper.h new file mode 100644 index 00000000..bb715db7 --- /dev/null +++ b/src/discordmessagehelper.h @@ -0,0 +1,42 @@ +#pragma once + +#include "config_manager.h" +#include "discordtypes.h" + +#include + +/** + * @brief DiscordMessageHelper contains an assortment of helper function to create specific Webhook messages. + */ +namespace DiscordMessageHelper { + +inline const DiscordMessage banMessage(const QString &f_ipid, const QString &f_moderator, const QString &f_duration, const QString &f_reason, const int &f_banID) +{ + DiscordMessage l_message; + + return l_message; +} + +inline const DiscordMultipartMessage modcallMessage(const QString &f_name, const QString &f_area, const QString &f_reason, const QQueue &f_buffer) +{ + DiscordMultipartMessage l_multi_message; + + DiscordMessage l_message; + l_message.setContent(ConfigManager::discordModcallWebhookContent()) + .beginEmbed() + .setEmbedColor(ConfigManager::discordWebhookColor()) + .setEmbedTitle(f_name + " filed a modcall in " + f_area) + .setEmbedDescription(f_reason) + .endEmbed(); + + QString l_log; + for (const QString &l_entry : f_buffer) { + l_log.append(l_entry); + } + l_multi_message.setRequestUrl(ConfigManager::discordModcallWebhookUrl()); + l_multi_message.addPart(l_log.toUtf8(), "file", "log.txt", "text/plain", "utf-8").setPayloadJson(l_message.toJson()); + + return l_multi_message; +} + +} diff --git a/src/main.cpp b/src/main.cpp index bbeaf67a..680dec10 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,7 @@ int main(int argc, char *argv[]) { + qputenv("QT_LOGGING_CONF", "config/qtlogging.ini"); QCoreApplication app(argc, argv); QCoreApplication::setApplicationName("akashi"); QCoreApplication::setApplicationVersion("jackfruit (1.9)"); diff --git a/src/packet/packet_ma.cpp b/src/packet/packet_ma.cpp index f49c05ed..7b4db380 100644 --- a/src/packet/packet_ma.cpp +++ b/src/packet/packet_ma.cpp @@ -3,6 +3,7 @@ #include "config_manager.h" #include "db_manager.h" #include "discordhook.h" +#include "discordmessagehelper.h" #include "server.h" #include "serviceregistry.h" @@ -106,14 +107,10 @@ void PacketMA::handlePacket(AreaData *area, AOClient &client) const int ban_id = client.getServer()->getDatabaseManager()->getBanID(ban.ip); if (ConfigManager::discordBanWebhookEnabled() && client.m_service_registry->exists(DiscordHook::SERVICE_ID)) { - DiscordMessage l_message; - l_message.setRequestUrl(ConfigManager::discordBanWebhookUrl()) - .beginEmbed() - .setEmbedColor(ConfigManager::discordWebhookColor()) - .setEmbedTitle("Ban issued by " + ban.moderator) - .setEmbedDescription("Client IPID : " + ban.ipid + "\nBan ID: " + QString::number(ban_id) + "\nBan reason : " + ban.reason + "\nBanned until : " + QString::number(ban.duration)) - .endEmbed(); - client.m_service_registry->get(DiscordHook::SERVICE_ID).value()->post(l_message); + DiscordMessage l_message = DiscordMessageHelper::banMessage(ban.ipid, ban.moderator, timestamp, ban.reason, ban_id); + if (std::optional l_hook = client.m_service_registry->get(DiscordHook::SERVICE_ID); l_hook.has_value()) { + l_hook.value()->post(l_message); + } } } } diff --git a/src/packet/packet_zz.cpp b/src/packet/packet_zz.cpp index beae9c4c..90abf540 100644 --- a/src/packet/packet_zz.cpp +++ b/src/packet/packet_zz.cpp @@ -1,6 +1,7 @@ #include "packet/packet_zz.h" #include "config_manager.h" #include "discordhook.h" +#include "discordmessagehelper.h" #include "packet/packet_factory.h" #include "server.h" #include "serviceregistry.h" @@ -62,24 +63,9 @@ void PacketZZ::handlePacket(AreaData *area, AOClient &client) const } } - DiscordMessage l_message; - l_message.setContent(ConfigManager::discordModcallWebhookContent()) - .beginEmbed() - .setEmbedColor(ConfigManager::discordWebhookColor()) - .setEmbedTitle(l_name + " filed a modcall in " + l_areaName) - .setEmbedDescription(webhook_reason) - .endEmbed(); - - const auto l_data = client.getServer()->getAreaBuffer(l_areaName); - QString l_log; - - for (const QString &l_entry : l_data) { - l_log.append(l_entry); + DiscordMultipartMessage l_message = DiscordMessageHelper::modcallMessage(l_name, l_areaName, webhook_reason, client.getServer()->getAreaBuffer(l_areaName)); + if (std::optional l_hook = client.m_service_registry->get(DiscordHook::SERVICE_ID); l_hook.has_value()) { + l_hook.value()->post(l_message); } - DiscordMultipartMessage l_multi_message; - l_multi_message.setRequestUrl(ConfigManager::discordModcallWebhookUrl()); - l_multi_message.addPart(l_log.toUtf8(), "file", "log.txt", "text/plain", "utf-8").setPayloadJson(l_message.toJson()); - - client.m_service_registry->get(DiscordHook::SERVICE_ID).value()->post(l_multi_message); } } From be1b77ab10eb185777c75e01ca4dc4d79acbb8a9 Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:20:28 +0100 Subject: [PATCH 5/8] Bump clang-format --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af0f8cfc..b9e5bb16 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,9 +18,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run clang-format style check. - uses: jidicula/clang-format-action@v4.5.0 + uses: jidicula/clang-format-action@v4.16.0 with: - clang-format-version: '14' + clang-format-version: '21' check-path: '.' build-linux: From a7b6076744462b22d2aaeb742916b620315b0b64 Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:23:22 +0100 Subject: [PATCH 6/8] Revert "Bump clang-format" This reverts commit be1b77ab10eb185777c75e01ca4dc4d79acbb8a9. --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b9e5bb16..af0f8cfc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,9 +18,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run clang-format style check. - uses: jidicula/clang-format-action@v4.16.0 + uses: jidicula/clang-format-action@v4.5.0 with: - clang-format-version: '21' + clang-format-version: '14' check-path: '.' build-linux: From b957834a4e2e24953411bac8f916b4f06c65ec51 Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:57:18 +0100 Subject: [PATCH 7/8] change clang to 21 to match QtCreator --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af0f8cfc..4fa0e983 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: - name: Run clang-format style check. uses: jidicula/clang-format-action@v4.5.0 with: - clang-format-version: '14' + clang-format-version: '21' check-path: '.' build-linux: From 06e19bfd502df0a9313930e26749c2cc06cefc51 Mon Sep 17 00:00:00 2001 From: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:02:46 +0100 Subject: [PATCH 8/8] Politely, fuck you clang. --- src/akashiutils.h | 2 +- src/serverpublisher.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/akashiutils.h b/src/akashiutils.h index fa359398..a2fd7f02 100644 --- a/src/akashiutils.h +++ b/src/akashiutils.h @@ -24,7 +24,7 @@ class AkashiUtils { private: - AkashiUtils(){}; + AkashiUtils() {}; public: template diff --git a/src/serverpublisher.h b/src/serverpublisher.h index b0a1b16f..7ca335d9 100644 --- a/src/serverpublisher.h +++ b/src/serverpublisher.h @@ -32,7 +32,7 @@ class ServerPublisher : public QObject public: explicit ServerPublisher(int port, int *player_count, QObject *parent = nullptr); - virtual ~ServerPublisher(){}; + virtual ~ServerPublisher() {}; public slots: