mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 01:31:10 +00:00
Finish Abstraction & Refactor (Ongoing) - part 1 (#25)
* refactor: abstract connection/transport; and clearly separate qt remote obj and qt local into separate implementations * abstract qt remote registry * add mock implementation; these serves to further test the abstraction but also useful for testing modules later * use LogosObject instead of QObject * abstract provider side * updates to use new api * re-add async api back --------- Co-authored-by: Logos Workspace <logos@workspace.local>
This commit is contained in:
co-authored by
Logos Workspace
parent
4b66dac015
commit
4197ee1830
+47
-345
@@ -1,50 +1,32 @@
|
||||
#include "logos_api_consumer.h"
|
||||
#include "logos_object.h"
|
||||
#include "module_proxy.h"
|
||||
#include "logos_api_client.h"
|
||||
#include "token_manager.h"
|
||||
#include "logos_mode.h"
|
||||
#include "logos_instance.h"
|
||||
#include "plugin_registry.h"
|
||||
#include <QRemoteObjectNode>
|
||||
#include <QRemoteObjectReplica>
|
||||
#include <QRemoteObjectPendingCall>
|
||||
#include <QRemoteObjectPendingCallWatcher>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
#include "logos_transport.h"
|
||||
#include "logos_transport_factory.h"
|
||||
#include <QDebug>
|
||||
#include <QUrl>
|
||||
#include <QMetaObject>
|
||||
#include <QTimer>
|
||||
#include <QTime>
|
||||
|
||||
LogosAPIConsumer::LogosAPIConsumer(const QString& module_to_talk_to, const QString& origin_module, TokenManager* token_manager, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_node(nullptr)
|
||||
, m_registryUrl(LogosInstance::id(module_to_talk_to))
|
||||
, m_connected(false)
|
||||
, m_token_manager(token_manager)
|
||||
{
|
||||
if (LogosModeConfig::isLocal()) {
|
||||
qDebug() << "LogosAPIConsumer: Using Local mode - skipping QRemoteObjectNode";
|
||||
m_connected = true;
|
||||
} else {
|
||||
m_node = new QRemoteObjectNode(this);
|
||||
connectToRegistry();
|
||||
}
|
||||
m_transport = LogosTransportFactory::createConnection(m_registryUrl);
|
||||
m_transport->connectToHost();
|
||||
}
|
||||
|
||||
LogosAPIConsumer::~LogosAPIConsumer()
|
||||
{
|
||||
// Clean up event callbacks and connections
|
||||
for (auto it = m_connections.begin(); it != m_connections.end(); ++it) {
|
||||
QObject::disconnect(it.value());
|
||||
}
|
||||
m_eventCallbacks.clear();
|
||||
m_connections.clear();
|
||||
|
||||
// QRemoteObjectNode will be deleted automatically as it's a child object
|
||||
}
|
||||
|
||||
QObject* LogosAPIConsumer::requestObject(const QString& objectName, Timeout timeout)
|
||||
LogosObject* LogosAPIConsumer::requestObject(const QString& objectName, Timeout timeout)
|
||||
{
|
||||
qDebug() << "LogosAPIConsumer: Requesting object:" << objectName << "at" << QTime::currentTime().toString("hh:mm:ss.zzz");
|
||||
|
||||
@@ -53,45 +35,21 @@ QObject* LogosAPIConsumer::requestObject(const QString& objectName, Timeout time
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (LogosModeConfig::isLocal()) {
|
||||
QObject* plugin = PluginRegistry::getPlugin<QObject>(objectName);
|
||||
if (!plugin) {
|
||||
qWarning() << "LogosAPIConsumer: Plugin not found in registry:" << objectName;
|
||||
return nullptr;
|
||||
}
|
||||
qDebug() << "LogosAPIConsumer: Successfully found plugin:" << objectName;
|
||||
return plugin;
|
||||
}
|
||||
|
||||
if (!m_connected) {
|
||||
if (!m_transport->isConnected()) {
|
||||
qWarning() << "LogosAPIConsumer: Not connected to registry. Cannot request object:" << objectName;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
qDebug() << "LogosAPIConsumer: Requesting object:" << objectName;
|
||||
|
||||
// Acquire the dynamic replica
|
||||
QRemoteObjectReplica* replica = m_node->acquireDynamic(objectName);
|
||||
if (!replica) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to acquire replica for object:" << objectName;
|
||||
return nullptr;
|
||||
LogosObject* object = m_transport->requestObject(objectName, timeout.ms);
|
||||
if (object) {
|
||||
qDebug() << "[LogosObject] LogosAPIConsumer: acquired LogosObject for:" << objectName << "(id:" << object->id() << ")";
|
||||
}
|
||||
|
||||
// Wait for the replica to be initialized
|
||||
if (!replica->waitForSource(timeout.ms)) {
|
||||
qWarning() << "LogosAPIConsumer: Timeout waiting for object replica to be ready:" << objectName;
|
||||
delete replica;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
qDebug() << "LogosAPIConsumer: Successfully acquired replica for object:" << objectName;
|
||||
qDebug() << "LogosAPIConsumer: Replica acquired at" << QTime::currentTime().toString("hh:mm:ss.zzz");
|
||||
return replica;
|
||||
return object;
|
||||
}
|
||||
|
||||
bool LogosAPIConsumer::isConnected() const
|
||||
{
|
||||
return m_connected;
|
||||
return m_transport->isConnected();
|
||||
}
|
||||
|
||||
QString LogosAPIConsumer::registryUrl() const
|
||||
@@ -101,350 +59,94 @@ QString LogosAPIConsumer::registryUrl() const
|
||||
|
||||
bool LogosAPIConsumer::reconnect()
|
||||
{
|
||||
if (LogosModeConfig::isLocal()) {
|
||||
m_connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
qDebug() << "LogosAPIConsumer: Attempting to reconnect to registry:" << m_registryUrl;
|
||||
|
||||
// Disconnect first if already connected
|
||||
if (m_connected) {
|
||||
// Note: QRemoteObjectNode doesn't have a direct disconnect method
|
||||
// We'll create a new node instead
|
||||
m_node->deleteLater();
|
||||
m_node = new QRemoteObjectNode(this);
|
||||
m_connected = false;
|
||||
}
|
||||
|
||||
return connectToRegistry();
|
||||
return m_transport->reconnect();
|
||||
}
|
||||
|
||||
bool LogosAPIConsumer::connectToRegistry()
|
||||
{
|
||||
if (!m_node) {
|
||||
qWarning() << "LogosAPIConsumer: Remote object node is null";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_registryUrl.isEmpty()) {
|
||||
qWarning() << "LogosAPIConsumer: Registry URL is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "LogosAPIConsumer: Connecting to registry:" << m_registryUrl;
|
||||
qDebug() << "LogosAPIConsumer: Connecting to registry at" << QTime::currentTime().toString("hh:mm:ss.zzz");
|
||||
|
||||
// Connect to the registry node
|
||||
QUrl url(m_registryUrl);
|
||||
bool success = m_node->connectToNode(url);
|
||||
|
||||
if (success) {
|
||||
m_connected = true;
|
||||
qDebug() << "LogosAPIConsumer: Successfully connected to registry:" << m_registryUrl;
|
||||
} else {
|
||||
m_connected = false;
|
||||
qWarning() << "LogosAPIConsumer: Failed to connect to registry:" << m_registryUrl;
|
||||
}
|
||||
qDebug() << "LogosAPIConsumer: Connected to registry at" << QTime::currentTime().toString("hh:mm:ss.zzz");
|
||||
|
||||
return m_connected;
|
||||
}
|
||||
|
||||
|
||||
|
||||
QVariant LogosAPIConsumer::invokeRemoteMethod(const QString& authToken, const QString& objectName, const QString& methodName,
|
||||
const QVariantList& args, Timeout timeout)
|
||||
{
|
||||
qDebug() << "LogosAPIConsumer: Calling invokeRemoteMethod:" << objectName << methodName << "args_count:" << args.size() << "timeout:" << timeout.ms;
|
||||
|
||||
// This method handles both ModuleProxy-wrapped modules (template_module, package_manager)
|
||||
// and direct remote object calls for other modules
|
||||
QObject* plugin = requestObject(objectName, timeout);
|
||||
LogosObject* plugin = m_transport->requestObject(objectName, timeout.ms);
|
||||
if (!plugin) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << objectName;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
ModuleProxy* moduleProxy = qobject_cast<ModuleProxy*>(plugin);
|
||||
if (moduleProxy) {
|
||||
QVariant result = moduleProxy->callRemoteMethod(authToken, methodName, args);
|
||||
if (!LogosModeConfig::isLocal()) {
|
||||
delete plugin;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (LogosModeConfig::isLocal()) {
|
||||
qWarning() << "LogosAPIConsumer: Local mode requires ModuleProxy-wrapped objects";
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
// Remote mode: callRemoteMethod returns QRemoteObjectPendingCall, not QVariant
|
||||
QRemoteObjectPendingCall pendingCall;
|
||||
bool success = QMetaObject::invokeMethod(
|
||||
plugin,
|
||||
"callRemoteMethod",
|
||||
Qt::DirectConnection,
|
||||
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
|
||||
Q_ARG(QString, authToken),
|
||||
Q_ARG(QString, methodName),
|
||||
Q_ARG(QVariantList, args)
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to invoke callRemoteMethod on replica for object:" << objectName;
|
||||
delete plugin;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
// Wait for the result
|
||||
pendingCall.waitForFinished(timeout.ms);
|
||||
delete plugin;
|
||||
|
||||
if (!pendingCall.isFinished() || pendingCall.error() != QRemoteObjectPendingCall::NoError) {
|
||||
qWarning() << "LogosAPIConsumer: Remote callRemoteMethod failed or timed out:" << pendingCall.error();
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
return pendingCall.returnValue();
|
||||
qDebug() << "[LogosObject] LogosAPIConsumer: calling via LogosObject::callMethod" << methodName;
|
||||
QVariant result = plugin->callMethod(authToken, methodName, args, timeout.ms);
|
||||
plugin->release();
|
||||
return result;
|
||||
}
|
||||
|
||||
void LogosAPIConsumer::invokeRemoteMethodAsync(const QString& authToken, const QString& objectName, const QString& methodName,
|
||||
const QVariantList& args,
|
||||
AsyncResultCallback callback,
|
||||
Timeout timeout)
|
||||
const QVariantList& args,
|
||||
AsyncResultCallback callback,
|
||||
Timeout timeout)
|
||||
{
|
||||
if (!callback) {
|
||||
qWarning() << "LogosAPIConsumer: invokeRemoteMethodAsync called with null callback";
|
||||
return;
|
||||
}
|
||||
|
||||
QObject* plugin = requestObject(objectName, timeout);
|
||||
LogosObject* plugin = m_transport->requestObject(objectName, timeout.ms);
|
||||
if (!plugin) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << objectName;
|
||||
QTimer::singleShot(0, this, [callback]() { callback(QVariant()); });
|
||||
return;
|
||||
}
|
||||
|
||||
ModuleProxy* moduleProxy = qobject_cast<ModuleProxy*>(plugin);
|
||||
if (moduleProxy) {
|
||||
QVariant result = moduleProxy->callRemoteMethod(authToken, methodName, args);
|
||||
if (!LogosModeConfig::isLocal()) {
|
||||
delete plugin;
|
||||
}
|
||||
QTimer::singleShot(0, this, [callback, result]() { callback(result); });
|
||||
qDebug() << "[LogosObject] LogosAPIConsumer: async calling via LogosObject::callMethod" << methodName;
|
||||
QVariant result = plugin->callMethod(authToken, methodName, args, timeout.ms);
|
||||
plugin->release();
|
||||
QTimer::singleShot(0, this, [callback, result]() { callback(result); });
|
||||
}
|
||||
|
||||
void LogosAPIConsumer::onEvent(LogosObject* originObject, const QString& eventName, std::function<void(const QString&, const QVariantList&)> callback)
|
||||
{
|
||||
qDebug() << "[LogosObject] LogosAPIConsumer::onEvent registering for:" << eventName << "on LogosObject id:" << originObject;
|
||||
|
||||
if (!originObject) {
|
||||
qWarning() << "LogosAPIConsumer: Cannot register event on null object";
|
||||
return;
|
||||
}
|
||||
|
||||
if (LogosModeConfig::isLocal()) {
|
||||
qWarning() << "LogosAPIConsumer: Local mode requires ModuleProxy-wrapped objects";
|
||||
QTimer::singleShot(0, this, [callback]() { callback(QVariant()); });
|
||||
return;
|
||||
}
|
||||
originObject->onEvent(eventName, std::move(callback));
|
||||
|
||||
QRemoteObjectPendingCall pendingCall;
|
||||
bool success = QMetaObject::invokeMethod(
|
||||
plugin,
|
||||
"callRemoteMethod",
|
||||
Qt::DirectConnection,
|
||||
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
|
||||
Q_ARG(QString, authToken),
|
||||
Q_ARG(QString, methodName),
|
||||
Q_ARG(QVariantList, args)
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to invoke callRemoteMethod on replica for object:" << objectName;
|
||||
delete plugin;
|
||||
QTimer::singleShot(0, this, [callback]() { callback(QVariant()); });
|
||||
return;
|
||||
}
|
||||
|
||||
QRemoteObjectPendingCallWatcher* watcher = new QRemoteObjectPendingCallWatcher(pendingCall, this);
|
||||
QPointer<QRemoteObjectPendingCallWatcher> watcherRef(watcher);
|
||||
QObject* pluginToDelete = plugin;
|
||||
// Use QueuedConnection so the callback always runs on the consumer's (e.g. main) thread,
|
||||
// even if the watcher emits finished() from an IO or worker thread. Otherwise the callback
|
||||
// may never run or may run in a context where UI/consumer state is inaccessible.
|
||||
connect(watcher, &QRemoteObjectPendingCallWatcher::finished, this, [watcherRef, callback, pluginToDelete]() {
|
||||
QVariant result;
|
||||
if (!watcherRef.isNull() && watcherRef->error() == QRemoteObjectPendingCall::NoError) {
|
||||
result = watcherRef->returnValue();
|
||||
}
|
||||
if (callback) callback(result);
|
||||
delete pluginToDelete;
|
||||
if (!watcherRef.isNull()) watcherRef->deleteLater();
|
||||
}, Qt::QueuedConnection);
|
||||
QTimer::singleShot(timeout.ms, this, [watcherRef, callback, pluginToDelete]() {
|
||||
if (!watcherRef.isNull() && !watcherRef->isFinished()) {
|
||||
if (callback) callback(QVariant());
|
||||
delete pluginToDelete;
|
||||
watcherRef->deleteLater();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void LogosAPIConsumer::onEvent(QObject* originObject, QObject* destinationObject, const QString& eventName, std::function<void(const QString&, const QVariantList&)> callback)
|
||||
{
|
||||
qDebug() << "LogosAPIConsumer: Registering event listener for event:" << eventName;
|
||||
|
||||
// Store the callback for this event name
|
||||
m_eventCallbacks[eventName].append(callback);
|
||||
|
||||
// Check if we already have a connection for this origin object
|
||||
if (!m_connections.contains(originObject)) {
|
||||
// Create new connection only if it doesn't exist
|
||||
auto connection = QObject::connect(originObject, SIGNAL(eventResponse(QString, QVariantList)),
|
||||
this, SLOT(invokeCallback(QString, QVariantList)));
|
||||
|
||||
if (connection) {
|
||||
m_connections[originObject] = connection;
|
||||
qDebug() << "LogosAPIConsumer: Created new connection for origin object";
|
||||
} else {
|
||||
qWarning() << "LogosAPIConsumer: Failed to create connection for event:" << eventName;
|
||||
}
|
||||
} else {
|
||||
qDebug() << "LogosAPIConsumer: Reusing existing connection for origin object";
|
||||
}
|
||||
|
||||
qDebug() << "LogosAPIConsumer: Registered callback for event:" << eventName;
|
||||
}
|
||||
|
||||
void LogosAPIConsumer::invokeCallback(const QString& eventName, const QVariantList& data)
|
||||
{
|
||||
// qDebug() << "LogosAPIConsumer: invokeCallback called for event:" << eventName;
|
||||
|
||||
// Call all registered callbacks
|
||||
// Note: This will call all callbacks for any event. In a more sophisticated implementation,
|
||||
// you might want to store event names with callbacks to filter them.
|
||||
for (const auto& callback : m_eventCallbacks[eventName]) {
|
||||
try {
|
||||
callback(eventName, data);
|
||||
} catch (...) {
|
||||
qWarning() << "LogosAPIConsumer: Exception in callback for event:" << eventName;
|
||||
}
|
||||
}
|
||||
|
||||
// qDebug() << "LogosAPIConsumer: Called" << m_eventCallbacks[eventName].size() << "callbacks for event:" << eventName;
|
||||
}
|
||||
|
||||
void LogosAPIConsumer::onEvent(QObject* originObject, QObject* destinationObject, const QString& eventName)
|
||||
{
|
||||
qDebug() << "LogosAPIConsumer: Registering event listener for event:" << eventName << "(connecting to destination slot)";
|
||||
|
||||
// connect to the eventResponse signal of the destinationObject's slot
|
||||
QObject::connect(originObject, SIGNAL(eventResponse(QString, QVariantList)),
|
||||
destinationObject, SLOT(onEventResponse(QString, QVariantList)), Qt::AutoConnection);
|
||||
qDebug() << "[LogosObject] LogosAPIConsumer: event callback registered for:" << eventName;
|
||||
}
|
||||
|
||||
bool LogosAPIConsumer::informModuleToken(const QString& authToken, const QString& moduleName, const QString& token)
|
||||
{
|
||||
qDebug() << "LogosAPIConsumer: Informing module token for module:" << moduleName << "with token:" << token;
|
||||
|
||||
QObject* plugin = requestObject("capability_module", Timeout(20000));
|
||||
LogosObject* plugin = m_transport->requestObject("capability_module", 20000);
|
||||
if (!plugin) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object: capability_module";
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleProxy* moduleProxy = qobject_cast<ModuleProxy*>(plugin);
|
||||
if (moduleProxy) {
|
||||
bool result = moduleProxy->informModuleToken(authToken, moduleName, token);
|
||||
qDebug() << "LogosAPIConsumer: informModuleToken completed with result:" << result;
|
||||
if (!LogosModeConfig::isLocal()) {
|
||||
delete plugin;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (LogosModeConfig::isLocal()) {
|
||||
qWarning() << "LogosAPIConsumer: Local mode requires ModuleProxy-wrapped objects";
|
||||
return false;
|
||||
}
|
||||
|
||||
QRemoteObjectPendingCall pendingCall;
|
||||
bool success = QMetaObject::invokeMethod(
|
||||
plugin,
|
||||
"informModuleToken",
|
||||
Qt::DirectConnection,
|
||||
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
|
||||
Q_ARG(QString, authToken),
|
||||
Q_ARG(QString, moduleName),
|
||||
Q_ARG(QString, token)
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to invoke informModuleToken on replica";
|
||||
delete plugin;
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingCall.waitForFinished(20000);
|
||||
delete plugin;
|
||||
|
||||
if (!pendingCall.isFinished() || pendingCall.error() != QRemoteObjectPendingCall::NoError) {
|
||||
qWarning() << "LogosAPIConsumer: Remote informModuleToken failed or timed out:" << pendingCall.error();
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant result = pendingCall.returnValue();
|
||||
qDebug() << "[LogosObject] LogosAPIConsumer: calling LogosObject::informModuleToken for" << moduleName;
|
||||
bool result = plugin->informModuleToken(authToken, moduleName, token, 20000);
|
||||
qDebug() << "LogosAPIConsumer: informModuleToken completed with result:" << result;
|
||||
|
||||
return result.toBool();
|
||||
plugin->release();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool LogosAPIConsumer::informModuleToken_module(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token)
|
||||
{
|
||||
qDebug() << "LogosAPIConsumer: Informing module token for module:" << moduleName << "with token:" << token;
|
||||
|
||||
QObject* plugin = requestObject(originModule, Timeout(20000));
|
||||
LogosObject* plugin = m_transport->requestObject(originModule, 20000);
|
||||
if (!plugin) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to acquire plugin/replica for object:" << originModule;
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleProxy* moduleProxy = qobject_cast<ModuleProxy*>(plugin);
|
||||
if (moduleProxy) {
|
||||
bool result = moduleProxy->informModuleToken(authToken, moduleName, token);
|
||||
qDebug() << "LogosAPIConsumer: informModuleToken completed with result:" << result;
|
||||
if (!LogosModeConfig::isLocal()) {
|
||||
delete plugin;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (LogosModeConfig::isLocal()) {
|
||||
qWarning() << "LogosAPIConsumer: Local mode requires ModuleProxy-wrapped objects";
|
||||
return false;
|
||||
}
|
||||
QRemoteObjectPendingCall pendingCall;
|
||||
bool success = QMetaObject::invokeMethod(
|
||||
plugin,
|
||||
"informModuleToken",
|
||||
Qt::DirectConnection,
|
||||
Q_RETURN_ARG(QRemoteObjectPendingCall, pendingCall),
|
||||
Q_ARG(QString, authToken),
|
||||
Q_ARG(QString, moduleName),
|
||||
Q_ARG(QString, token)
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
qWarning() << "LogosAPIConsumer: Failed to invoke informModuleToken on replica";
|
||||
delete plugin;
|
||||
return false;
|
||||
}
|
||||
|
||||
pendingCall.waitForFinished(20000);
|
||||
delete plugin;
|
||||
|
||||
if (!pendingCall.isFinished() || pendingCall.error() != QRemoteObjectPendingCall::NoError) {
|
||||
qWarning() << "LogosAPIConsumer: Remote informModuleToken failed or timed out:" << pendingCall.error();
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant result = pendingCall.returnValue();
|
||||
qDebug() << "[LogosObject] LogosAPIConsumer: calling LogosObject::informModuleToken for" << moduleName << "on" << originModule;
|
||||
bool result = plugin->informModuleToken(authToken, moduleName, token, 20000);
|
||||
qDebug() << "LogosAPIConsumer: informModuleToken completed with result:" << result;
|
||||
|
||||
return result.toBool();
|
||||
plugin->release();
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user