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: diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bf69976..b581b7f6 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 @@ -95,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 @@ -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 @@ -118,6 +119,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/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 new file mode 100644 index 00000000..1ddc522f --- /dev/null +++ b/library/CMakeLists.txt @@ -0,0 +1,32 @@ +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 + include/discordtypes.h +) +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..4b37c4aa --- /dev/null +++ b/library/include/discordhook.h @@ -0,0 +1,33 @@ +#pragma once + +#include "akashi_addon_global.h" +#include "discordtypes.h" +#include "service.h" + +class QNetworkAccessManager; +class QNetworkReply; + +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiDiscordHook, AKASHI_ADDON_EXPORT) +class AKASHI_ADDON_EXPORT DiscordHook : public Service +{ + Q_OBJECT + + public: + DiscordHook(QObject *parent = nullptr); + ~DiscordHook() = default; + + inline const static QString SERVICE_ID = "akashi.addon.discordook"; + + 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/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/include/service.h b/library/include/service.h new file mode 100644 index 00000000..48cf78db --- /dev/null +++ b/library/include/service.h @@ -0,0 +1,39 @@ +#pragma once + +#include "akashi_addon_global.h" + +#include +#include +#include +#include + +class ServiceRegistry; + +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiService, AKASHI_ADDON_EXPORT) +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); + void setState(Service::State f_state); + Service::State getState(); + + QString getServiceProperty(QString f_key) const; + + 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..3c2fb179 --- /dev/null +++ b/library/include/serviceregistry.h @@ -0,0 +1,99 @@ +#pragma once + +// Welcome to absolute hell :) + +#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 + +class AKASHI_ADDON_EXPORT ServiceRegistry : public QObject +{ + Q_OBJECT + + public: + ServiceRegistry(QObject *parent = nullptr); + ~ServiceRegistry(); + + 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)) { + qCCritical(akashiServiceRegistry) << qUtf8Printable(QString("Unable to get service with identifier %1").arg(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) { + 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: + 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/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..05c45563 --- /dev/null +++ b/library/include/servicewrapper.h @@ -0,0 +1,36 @@ +#pragma once + +#include "service.h" +#include +#include +#include + +// 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 +{ + 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..94d61624 --- /dev/null +++ b/library/src/discordhook.cpp @@ -0,0 +1,343 @@ +#include "discordhook.h" +#include "serviceregistry.h" +#include "servicewrapper.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(akashiDiscordHook, "akashi.addon.discordhook") + +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(QString 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", SERVICE_ID}}; +} + +void DiscordHook::setServiceRegistry(ServiceRegistry *f_registry) +{ + m_registry = f_registry; + + auto l_service = m_registry->get>("qt.network.manager"); + if (!l_service.has_value()) { + setState(State::FAILED); + } + + setState(State::OK); + m_network_manager = l_service.value()->get(); +} + +void DiscordHook::post(const DiscordMessage &message) +{ + if (!m_network_manager) { + qCWarning(akashiDiscordHook) << "Cannot post DiscordMessage: QNetworkAccessManager not installed"; + return; + } + + QUrl url(message.requestUrl()); + if (!url.isValid()) { + qCWarning(akashiDiscordHook) << "Failed to post DiscordMessage: Invalid URL" << qUtf8Printable(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) +{ + QUrl url(message.requestUrl()); + if (!url.isValid()) { + qCWarning(akashiDiscordHook) << "Failed to post DiscordMultipartMessage: Invalid URL" << qUtf8Printable(message.requestUrl()); + return; + } + + auto *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + 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); + } + + 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); + multipart->setParent(reply); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + onDiscordResponse(reply); + }); +} + +void DiscordHook::onDiscordResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + 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 new file mode 100644 index 00000000..1526bb30 --- /dev/null +++ b/library/src/service.cpp @@ -0,0 +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) << "Status of" << getServiceProperty("identifier") << "is set to" << m_state; +} + +Service::State Service::getState() +{ + qCDebug(akashiService) << "Status 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 new file mode 100644 index 00000000..c1985fd7 --- /dev/null +++ b/library/src/serviceregistry.cpp @@ -0,0 +1,13 @@ +#include "serviceregistry.h" +#include "service.h" + +#include + +Q_LOGGING_CATEGORY(akashiServiceRegistry, "akashi.addon.serviceregistry") + +ServiceRegistry::ServiceRegistry(QObject *parent) : QObject{parent} +{ + qCDebug(akashiServiceRegistry) << "Created at" << this; +} + +ServiceRegistry::~ServiceRegistry() {} diff --git a/library/src/servicewrapper.cpp b/library/src/servicewrapper.cpp new file mode 100644 index 00000000..5541cfe1 --- /dev/null +++ b/library/src/servicewrapper.cpp @@ -0,0 +1,3 @@ +#include "servicewrapper.h" + +Q_LOGGING_CATEGORY(akashiServiceWrapper, "akashi.addon.servicewrapper") 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/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..d7073a32 100644 --- a/src/commands/moderation.cpp +++ b/src/commands/moderation.cpp @@ -21,7 +21,10 @@ #include "command_extension.h" #include "config_manager.h" #include "db_manager.h" +#include "discordhook.h" +#include "discordmessagehelper.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 +88,12 @@ 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 = 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); + } + } } 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/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 888fe4a1..680dec10 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,25 +17,21 @@ ////////////////////////////////////////////////////////////////////////////////////// #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[]) { + qputenv("QT_LOGGING_CONF", "config/qtlogging.ini"); 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 +40,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..7b4db380 100644 --- a/src/packet/packet_ma.cpp +++ b/src/packet/packet_ma.cpp @@ -2,7 +2,10 @@ #include "config_manager.h" #include "db_manager.h" +#include "discordhook.h" +#include "discordmessagehelper.h" #include "server.h" +#include "serviceregistry.h" PacketMA::PacketMA(QStringList &contents) : AOPacket(contents) @@ -103,8 +106,11 @@ 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 = 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 a308e6b7..90abf540 100644 --- a/src/packet/packet_zz.cpp +++ b/src/packet/packet_zz.cpp @@ -1,7 +1,10 @@ #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" #include @@ -45,7 +48,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(); @@ -60,6 +63,9 @@ void PacketZZ::handlePacket(AreaData *area, AOClient &client) const } } - emit client.getServer()->modcallWebhookRequest(l_name, l_areaName, webhook_reason, client.getServer()->getAreaBuffer(l_areaName)); + 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); + } } } diff --git a/src/server.cpp b/src/server.cpp index 2a3260e6..29f24613 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -23,17 +23,21 @@ #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); @@ -47,9 +51,6 @@ 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); @@ -78,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); @@ -151,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); @@ -282,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"); @@ -490,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]); @@ -555,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 6f94a38d..474cd45e 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. @@ -58,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. @@ -67,6 +68,11 @@ class Server : public QObject */ ~Server(); + /** + * @brief Initialises service instances. + */ + bool initServices(); + /** * @brief Starts the server. * @@ -350,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. */ @@ -385,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. @@ -419,11 +398,6 @@ class Server : public QObject */ QWebSocketServer *server; - /** - * @brief Handles Discord webhooks. - */ - Discord *discord; - /** * @brief Handles HTTP server advertising. */ @@ -439,6 +413,8 @@ class Server : public QObject */ MusicManager *music_manager; + ServiceRegistry *m_service_registry; + /** * @brief The port through which the server will accept WebSocket connections. */ 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: