Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -111,13 +111,15 @@ qt_add_executable(akashi
src/serverpublisher.h
src/testimony_recorder.cpp
src/typedefs.h
src/discordmessagehelper.h
)

target_link_libraries(akashi PRIVATE
Qt6::Core
Qt6::Sql
Qt6::Network
Qt6::WebSockets
akashi_addon
)

target_include_directories(akashi PRIVATE src src/logger src/network src/packet)
Expand Down
2 changes: 2 additions & 0 deletions bin/config_sample/qtlogging.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[Rules]
*.debug=false
32 changes: 32 additions & 0 deletions library/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)

target_link_libraries(akashi_addon
PUBLIC
Qt6::Core
Qt6::Network
)
9 changes: 9 additions & 0 deletions library/include/akashi_addon_global.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <QtCore/QtGlobal>

#if defined(AKASHI_ADDON_LIBRARY)
#define AKASHI_ADDON_EXPORT Q_DECL_EXPORT
#else
#define AKASHI_ADDON_EXPORT Q_DECL_IMPORT
#endif
33 changes: 33 additions & 0 deletions library/include/discordhook.h
Original file line number Diff line number Diff line change
@@ -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;
};
92 changes: 92 additions & 0 deletions library/include/discordtypes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#pragma once

#include "akashi_addon_global.h"

#include <QByteArray>
#include <QJsonObject>
#include <QMap>
#include <QString>
#include <QVector>
#include <concepts>

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 &timestamp);
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<QString, QString> m_fields;
QVector<QVariantMap> 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 <typename T>
requires std::convertible_to<T, QByteArray>
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 <typename T>
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<DiscordMultipart> &parts() const { return m_parts; }
const QJsonObject &payloadJson() const { return m_payload_json; }

private:
QVector<DiscordMultipart> m_parts;
QJsonObject m_payload_json;
};
39 changes: 39 additions & 0 deletions library/include/service.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#include "akashi_addon_global.h"

#include <QLoggingCategory>
#include <QMap>
#include <QObject>
#include <QVariant>

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<QString, QString> m_service_properties;
ServiceRegistry *m_registry;
};
99 changes: 99 additions & 0 deletions library/include/serviceregistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#pragma once

// Welcome to absolute hell :)

#include "akashi_addon_global.h"
#include "servicewrapper.h"

#include <QLoggingCategory>
#include <QMap>
#include <QObject>

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 <class T>
requires std::is_base_of_v<Service, T>
inline void create()
{
T *l_ptr = new T(this);
l_ptr->setServiceRegistry(this);
insertService<T>(l_ptr);
}

template <class T>
requires std::is_base_of_v<QObject, T>
inline void createWrapped(const QString &f_identifier,
const QString &f_version, const QString &f_author)
{
T *l_ptr = new T(this);
ServiceWrapper<T> *l_wrapper = new ServiceWrapper<T>(l_ptr, f_identifier, f_version, f_author, this);
insertService<ServiceWrapper<T>>(l_wrapper);
}

template <class T>
requires std::is_base_of_v<Service, T>
inline std::optional<T *> 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<T *>(static_cast<T *>(l_service));
}

inline bool exists(const QString &f_identifier)
{
return m_services.contains(f_identifier);
}

private:
template <class T>
requires std::is_base_of_v<Service, T>
void insertService(T *f_ptr)
{
std::unique_ptr<T> 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<QString, Service *> m_services;
};
Empty file added library/include/servicetypes.h
Empty file.
36 changes: 36 additions & 0 deletions library/include/servicewrapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once

#include "service.h"
#include <QObject>
#include <QPointer>
#include <type_traits>

// This is a non-owning wrapper. Because I say so. - Salanto

Q_DECLARE_EXPORTED_LOGGING_CATEGORY(akashiServiceWrapper, AKASHI_ADDON_EXPORT)

template <class T>
requires std::is_base_of_v<QObject, T>
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<T> m_ptr;
};
Loading