mirror of
https://github.com/logos-co/logos-messaging-module.git
synced 2026-08-30 12:11:14 +00:00
453 lines
16 KiB
C++
453 lines
16 KiB
C++
#include "delivery_module_plugin.h"
|
|
#include <QDebug>
|
|
#include <QVariantList>
|
|
#include <QDateTime>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <QJsonArray>
|
|
#include <semaphore>
|
|
#include <unordered_map>
|
|
|
|
#include "api_call_handler.h"
|
|
// Include the liblogosdelivery header from logos-delivery
|
|
// liblogosdelivery provides a high-level message-delivery API
|
|
extern "C" {
|
|
#include <liblogosdelivery.h>
|
|
}
|
|
|
|
DeliveryModulePlugin::DeliveryModulePlugin() : deliveryCtx(nullptr)
|
|
{
|
|
qDebug() << "DeliveryModulePlugin: Initializing...";
|
|
qDebug() << "DeliveryModulePlugin: Initialized successfully";
|
|
}
|
|
|
|
DeliveryModulePlugin::~DeliveryModulePlugin()
|
|
{
|
|
// Clean up resources, this is not done in PluginInterface destructor
|
|
if (logosAPI) {
|
|
delete logosAPI;
|
|
logosAPI = nullptr;
|
|
}
|
|
|
|
// Clean up delivery context if it exists
|
|
if (deliveryCtx) {
|
|
logosdelivery_destroy(deliveryCtx, nullptr, nullptr);
|
|
deliveryCtx = nullptr;
|
|
}
|
|
}
|
|
|
|
void DeliveryModulePlugin::emitEvent(const QString& eventName, const QVariantList& data) {
|
|
if (!logosAPI) {
|
|
qWarning() << "DeliveryModulePlugin: LogosAPI not available, cannot emit" << eventName;
|
|
return;
|
|
}
|
|
|
|
LogosAPIClient* client = logosAPI->getClient("delivery_module");
|
|
if (!client) {
|
|
qWarning() << "DeliveryModulePlugin: Failed to get delivery_module client for event" << eventName;
|
|
return;
|
|
}
|
|
|
|
client->onEventResponse(this, eventName, data);
|
|
}
|
|
|
|
// Static callback function for liblogosdelivery events, this one is one time registered
|
|
// on initialization and will be called for all events from the Nim FFI side.
|
|
void DeliveryModulePlugin::event_callback(int callerRet, const char* msg, size_t len, void* userData)
|
|
{
|
|
qDebug() << "DeliveryModulePlugin::event_callback called with ret:" << callerRet;
|
|
|
|
DeliveryModulePlugin* plugin = static_cast<DeliveryModulePlugin*>(userData);
|
|
if (!plugin) {
|
|
qWarning() << "DeliveryModulePlugin::event_callback: Invalid userData";
|
|
return;
|
|
}
|
|
|
|
if (msg && len > 0) {
|
|
QString message = QString::fromUtf8(msg, len);
|
|
qDebug() << "DeliveryModulePlugin::event_callback message:" << message;
|
|
|
|
// Parse JSON to determine event type
|
|
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
|
|
if (!doc.isObject()) {
|
|
qWarning() << "DeliveryModulePlugin::event_callback: Invalid JSON";
|
|
return;
|
|
}
|
|
|
|
QJsonObject jsonObj = doc.object();
|
|
QString eventType = jsonObj["eventType"].toString();
|
|
QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);
|
|
|
|
if (eventType == "message_sent") {
|
|
// MessageSentEvent: requestId, messageHash
|
|
QVariantList eventData;
|
|
eventData << jsonObj["requestId"].toString();
|
|
eventData << jsonObj["messageHash"].toString();
|
|
eventData << timestamp;
|
|
plugin->emitEvent("messageSent", eventData);
|
|
|
|
} else if (eventType == "message_error") {
|
|
// MessageErrorEvent: requestId, messageHash, error
|
|
QVariantList eventData;
|
|
eventData << jsonObj["requestId"].toString();
|
|
eventData << jsonObj["messageHash"].toString();
|
|
eventData << jsonObj["error"].toString();
|
|
eventData << timestamp;
|
|
plugin->emitEvent("messageError", eventData);
|
|
|
|
} else if (eventType == "message_propagated") {
|
|
// MessagePropagatedEvent: requestId, messageHash
|
|
QVariantList eventData;
|
|
eventData << jsonObj["requestId"].toString();
|
|
eventData << jsonObj["messageHash"].toString();
|
|
eventData << timestamp;
|
|
plugin->emitEvent("messagePropagated", eventData);
|
|
|
|
} else if (eventType == "message_received") {
|
|
// MessageReceivedEvent: messageHash, message (WakuMessage)
|
|
QJsonObject msgObj = jsonObj["message"].toObject();
|
|
QVariantList eventData;
|
|
eventData << jsonObj["messageHash"].toString();
|
|
eventData << msgObj["contentTopic"].toString();
|
|
|
|
// The waku API returns payload as a JSON byte array (e.g. [106,106,106,106]).
|
|
// Convert it to a base64 string to match the documented event contract.
|
|
QJsonValue payloadValue = msgObj["payload"];
|
|
if (payloadValue.isArray()) {
|
|
QJsonArray payloadArray = payloadValue.toArray();
|
|
QByteArray payloadBytes;
|
|
payloadBytes.reserve(payloadArray.size());
|
|
for (const QJsonValue& val : payloadArray) {
|
|
payloadBytes.append(static_cast<char>(val.toInt()));
|
|
}
|
|
eventData << QString::fromLatin1(payloadBytes.toBase64());
|
|
} else {
|
|
eventData << payloadValue.toString();
|
|
}
|
|
|
|
eventData << QString::number(msgObj["timestamp"].toDouble(), 'f', 0);
|
|
plugin->emitEvent("messageReceived", eventData);
|
|
|
|
} else if (eventType == "connection_status_change") {
|
|
QVariantList eventData;
|
|
eventData << jsonObj["connectionStatus"].toString();
|
|
eventData << timestamp;
|
|
plugin->emitEvent("connectionStateChanged", eventData);
|
|
|
|
} else {
|
|
qWarning() << "DeliveryModulePlugin::event_callback: Unknown event type:" << eventType;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DeliveryModulePlugin::initLogos(LogosAPI* logosAPIInstance) {
|
|
if (logosAPI) {
|
|
delete logosAPI;
|
|
}
|
|
logosAPI = logosAPIInstance;
|
|
}
|
|
|
|
bool DeliveryModulePlugin::createNode(const QString &cfg)
|
|
{
|
|
std::lock_guard<std::mutex> createNodeLock(createNodeMutex);
|
|
|
|
if (deliveryCtx != nullptr) {
|
|
qWarning() << "DeliveryModulePlugin: createNode rejected - context already initialized";
|
|
return false;
|
|
}
|
|
|
|
qDebug() << "DeliveryModulePlugin::createNode called with cfg:" << cfg;
|
|
|
|
// Convert QString to UTF-8 byte array
|
|
QByteArray cfgUtf8 = cfg.toUtf8();
|
|
|
|
// Create callback context for synchronous createNode result.
|
|
// The context is kept in a pending map so late callbacks can be safely ignored.
|
|
struct CallbackContext {
|
|
std::binary_semaphore sem{0};
|
|
int callerRet{RET_ERR};
|
|
QString message;
|
|
};
|
|
|
|
static std::mutex pendingMutex;
|
|
static std::unordered_map<void*, std::shared_ptr<CallbackContext>> pendingContexts;
|
|
|
|
auto callbackCtx = std::make_shared<CallbackContext>();
|
|
void* callbackKey = static_cast<void*>(callbackCtx.get());
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(pendingMutex);
|
|
pendingContexts[callbackKey] = callbackCtx;
|
|
}
|
|
|
|
// Callback is expected in both success and error cases.
|
|
auto callback = +[](int callerRet, const char* msg, size_t len, void* userData) {
|
|
qDebug() << "DeliveryModulePlugin::createNode callback called with ret:" << callerRet;
|
|
|
|
std::shared_ptr<CallbackContext> callbackCtx;
|
|
{
|
|
std::lock_guard<std::mutex> lock(pendingMutex);
|
|
auto it = pendingContexts.find(userData);
|
|
if (it == pendingContexts.end()) {
|
|
return;
|
|
}
|
|
callbackCtx = it->second;
|
|
pendingContexts.erase(it);
|
|
}
|
|
|
|
if (!callbackCtx) {
|
|
return;
|
|
}
|
|
|
|
callbackCtx->callerRet = callerRet;
|
|
if (msg && len > 0) {
|
|
callbackCtx->message = QString::fromUtf8(msg, len);
|
|
qDebug() << "DeliveryModulePlugin::createNode callback message:" << callbackCtx->message;
|
|
}
|
|
|
|
// Release semaphore to unblock the createNode method
|
|
callbackCtx->sem.release();
|
|
};
|
|
|
|
// Call logosdelivery_create_node with the configuration
|
|
// Important: Keep deliveryCtx assignment from the call,
|
|
// creating of the context is immediate and not depends on the callback.
|
|
deliveryCtx = logosdelivery_create_node(cfgUtf8.constData(), callback, callbackKey);
|
|
|
|
qDebug() << "DeliveryModulePlugin: Waiting for createNode callback...";
|
|
|
|
// Wait for callback result regardless of immediate pointer value.
|
|
// Callback ensures that the underlying node object is properly created.
|
|
if (!callbackCtx->sem.try_acquire_for(CALLBACK_TIMEOUT)) {
|
|
std::lock_guard<std::mutex> lock(pendingMutex);
|
|
pendingContexts.erase(callbackKey);
|
|
|
|
deliveryCtx = nullptr;
|
|
|
|
qWarning() << "DeliveryModulePlugin: Timeout waiting for createNode callback";
|
|
return false;
|
|
}
|
|
|
|
// Any issue happened during node creation means the context is destroyed and must not be user.
|
|
if (callbackCtx->callerRet != RET_OK || deliveryCtx == nullptr) {
|
|
if (!callbackCtx->message.isEmpty()) {
|
|
qWarning() << "DeliveryModulePlugin: createNode callback error:" << callbackCtx->message;
|
|
}
|
|
|
|
deliveryCtx = nullptr;
|
|
|
|
qWarning() << "DeliveryModulePlugin: Failed to create Messaging context";
|
|
return false;
|
|
}
|
|
|
|
// Success case - deliveryCtx is valid and callback returned RET_OK.
|
|
qDebug() << "DeliveryModulePlugin: Messaging context created successfully";
|
|
|
|
// Set up event callback
|
|
logosdelivery_set_event_callback(deliveryCtx, event_callback, this);
|
|
return true;
|
|
}
|
|
|
|
bool DeliveryModulePlugin::start()
|
|
{
|
|
qDebug() << "DeliveryModulePlugin::start called";
|
|
|
|
if (!deliveryCtx) {
|
|
qWarning() << "DeliveryModulePlugin: Cannot start Messaging - context not initialized. Call createNode first.";
|
|
return false;
|
|
}
|
|
|
|
auto outcome = callApiRetVoid(
|
|
"start",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_start_node, deliveryCtx));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Start failed:" << outcome.error();
|
|
return false;
|
|
}
|
|
|
|
qDebug() << "DeliveryModulePlugin: Messaging start completed with success: true";
|
|
return true;
|
|
}
|
|
|
|
bool DeliveryModulePlugin::stop()
|
|
{
|
|
qDebug() << "DeliveryModulePlugin::stop called";
|
|
|
|
if (!deliveryCtx) {
|
|
qWarning() << "DeliveryModulePlugin: Cannot stop Messaging - context not initialized.";
|
|
return false;
|
|
}
|
|
|
|
auto outcome = callApiRetVoid(
|
|
"stop",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_stop_node, deliveryCtx));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Stop failed:" << outcome.error();
|
|
return false;
|
|
}
|
|
|
|
qDebug() << "DeliveryModulePlugin: Messaging stop completed with success: true";
|
|
return true;
|
|
}
|
|
LogosResult DeliveryModulePlugin::send(const QString &contentTopic, const QString &payload)
|
|
{
|
|
qDebug() << "DeliveryModulePlugin::send called with contentTopic:" << contentTopic;
|
|
qDebug() << "DeliveryModulePlugin::send payload:" << payload;
|
|
|
|
if (!deliveryCtx) {
|
|
qWarning() << "DeliveryModulePlugin: Cannot send message - context not initialized. Call createNode first.";
|
|
return {false, QVariant(), QStringLiteral("Context not initialized")};
|
|
}
|
|
|
|
// Construct JSON message according to logosdelivery_send API
|
|
// The payload should be base64-encoded as per the API spec
|
|
QJsonObject messageObj;
|
|
messageObj["contentTopic"] = contentTopic;
|
|
messageObj["payload"] = QString::fromUtf8(payload.toUtf8().toBase64());
|
|
messageObj["ephemeral"] = false;
|
|
|
|
QJsonDocument doc(messageObj);
|
|
QByteArray messageJson = doc.toJson(QJsonDocument::Compact);
|
|
|
|
auto outcome = callApiRetValue<QString>(
|
|
"send",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_send, deliveryCtx, messageJson.constData()));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Send failed for topic:" << contentTopic << ", reason:" << outcome.error();
|
|
return {false, QVariant(), outcome.error()};
|
|
}
|
|
|
|
const QString responseMessage = outcome.value();
|
|
qDebug() << "DeliveryModulePlugin: Send initiated for topic:" << contentTopic << ", with success: true";
|
|
return {true, responseMessage};
|
|
}
|
|
|
|
bool DeliveryModulePlugin::subscribe(const QString &contentTopic)
|
|
{
|
|
qDebug() << "DeliveryModulePlugin::subscribe called with contentTopic:" << contentTopic;
|
|
|
|
if (!deliveryCtx) {
|
|
qWarning() << "DeliveryModulePlugin: Cannot subscribe - context not initialized. Call createNode first.";
|
|
return false;
|
|
}
|
|
|
|
// Convert QString to UTF-8 byte array
|
|
QByteArray topicUtf8 = contentTopic.toUtf8();
|
|
|
|
auto outcome = callApiRetVoid(
|
|
"subscribe",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_subscribe, deliveryCtx, topicUtf8.constData()));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Subscribe failed for topic:" << contentTopic << ", reason:" << outcome.error();
|
|
return false;
|
|
}
|
|
|
|
qDebug() << "DeliveryModulePlugin: Subscribe completed for topic:" << contentTopic << " with success: true";
|
|
return true;
|
|
}
|
|
|
|
bool DeliveryModulePlugin::unsubscribe(const QString &contentTopic)
|
|
{
|
|
qDebug() << "DeliveryModulePlugin::unsubscribe called with contentTopic:" << contentTopic;
|
|
|
|
if (!deliveryCtx) {
|
|
qWarning() << "DeliveryModulePlugin: Cannot unsubscribe - context not initialized.";
|
|
return false;
|
|
}
|
|
|
|
// Convert QString to UTF-8 byte array
|
|
QByteArray topicUtf8 = contentTopic.toUtf8();
|
|
|
|
auto outcome = callApiRetVoid(
|
|
"unsubscribe",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_unsubscribe, deliveryCtx, topicUtf8.constData()));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Unsubscribe failed for topic:" << contentTopic << ", reason:" << outcome.error();
|
|
return false;
|
|
}
|
|
|
|
qDebug() << "DeliveryModulePlugin: Unsubscribe completed for topic:" << contentTopic << " with success: true";
|
|
return true;
|
|
}
|
|
|
|
QString DeliveryModulePlugin::version() const {
|
|
QString moduleVersion = "1.0.0";
|
|
if (!deliveryCtx) {
|
|
qWarning() << "DeliveryModulePlugin: Cannot subscribe - context not initialized. Call createNode first.";
|
|
return moduleVersion + " (liblogosdelivery version unknown, context not initialized)";
|
|
}
|
|
|
|
auto attributeName = "Version";
|
|
auto liblogosDeliveryVersion = callApiRetValue<QString>(
|
|
"get_node_info",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_get_node_info, deliveryCtx, attributeName));
|
|
|
|
if (liblogosDeliveryVersion.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Get node info failed getting version, reason:" <<
|
|
liblogosDeliveryVersion.error();
|
|
return moduleVersion + " (liblogosdelivery version unknown)";
|
|
}
|
|
|
|
const QString version = liblogosDeliveryVersion.value();
|
|
qDebug() << "DeliveryModulePlugin: Get node info completed for attribute:" <<
|
|
attributeName << ", with success: " << version;
|
|
|
|
return moduleVersion + " (liblogosdelivery version: " + version + ")";
|
|
}
|
|
|
|
QString DeliveryModulePlugin::getAvailableNodeInfoIDs() {
|
|
auto outcome = callApiRetValue<QString>(
|
|
"get_available_node_info_ids",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_get_available_node_info_ids, deliveryCtx));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Get available node info IDs failed, reason:" << outcome.error();
|
|
return QString();
|
|
}
|
|
|
|
return outcome.value();
|
|
}
|
|
|
|
QString DeliveryModulePlugin::getNodeInfo(const QString &nodeInfoId) {
|
|
auto outcome = callApiRetValue<QString>(
|
|
"get_node_info",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_get_node_info, deliveryCtx, nodeInfoId.toUtf8().constData()));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Get node info failed for ID:" << nodeInfoId <<
|
|
", reason:" << outcome.error();
|
|
return QString();
|
|
}
|
|
|
|
return outcome.value();
|
|
}
|
|
|
|
QString DeliveryModulePlugin::getAvailableConfigs() {
|
|
auto outcome = callApiRetValue<QString>(
|
|
"get_available_configs",
|
|
CALLBACK_TIMEOUT,
|
|
bindApiCall(logosdelivery_get_available_configs, deliveryCtx));
|
|
|
|
if (outcome.isErr()) {
|
|
qWarning() << "DeliveryModulePlugin: Get available configs failed, reason:" << outcome.error();
|
|
return QString();
|
|
}
|
|
|
|
return outcome.value();
|
|
}
|