From 9b256c6868dccdacf1bab7d1cea94e81731216f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Mon, 2 Aug 2021 13:51:09 -0400
Subject: [PATCH] conversation: add signal to detect declined requests

The API doesn't provide any signal to detect when a device
discards a conversation's request. So, there is no way to correctly
update the UI when needed. This patch introduces a signal for this.

Change-Id: I78e39c65ff1ed037ff38b92368c770f73f3e5006
GitLab: #600
---
 .../cx.ring.Ring.ConfigurationManager.xml     | 17 ++++
 bin/dbus/dbusclient.cpp                       |  2 +
 bin/jni/conversation.i                        |  2 +
 bin/jni/jni_interface.i                       |  1 +
 bin/nodejs/callback.h                         | 20 +++++
 bin/nodejs/conversation.i                     |  2 +
 bin/nodejs/nodejs_interface.i                 |  1 +
 src/client/ring_signal.cpp                    |  1 +
 src/jami/conversation_interface.h             |  6 ++
 src/jamidht/account_manager.cpp               | 17 ++--
 src/jamidht/account_manager.h                 |  2 +-
 src/jamidht/conversation.cpp                  |  2 +
 src/jamidht/conversation.h                    |  7 ++
 src/jamidht/jamiaccount.cpp                   | 19 +++-
 test/unitTest/syncHistory/syncHistory.cpp     | 89 +++++++++++++++++++
 15 files changed, 178 insertions(+), 10 deletions(-)

diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
index 145ab26582..0071e04706 100644
--- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
+++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
@@ -1946,6 +1946,23 @@
            </arg>
        </signal>
 
+       <signal name="conversationRequestDeclined" tp:name-for-bindings="conversationRequestDeclined">
+           <tp:added version="10.1.0"/>
+           <tp:docstring>
+               Notify clients when a new conversation request is declined
+           </tp:docstring>
+           <arg type="s" name="account_id">
+               <tp:docstring>
+                   Account id related
+               </tp:docstring>
+           </arg>
+           <arg type="s" name="conversation_id">
+               <tp:docstring>
+                   Conversation id
+               </tp:docstring>
+           </arg>
+       </signal>
+
        <signal name="conversationReady" tp:name-for-bindings="conversationReady">
            <tp:added version="10.0.0"/>
            <tp:docstring>
diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp
index 3a4b6af132..248585343f 100644
--- a/bin/dbus/dbusclient.cpp
+++ b/bin/dbus/dbusclient.cpp
@@ -303,6 +303,8 @@ DBusClient::initLibrary(int flags)
             bind(&DBusConfigurationManager::messageReceived, confM, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestReceived>(
             bind(&DBusConfigurationManager::conversationRequestReceived, confM, _1, _2, _3)),
+        exportable_callback<ConversationSignal::ConversationRequestDeclined>(
+            bind(&DBusConfigurationManager::conversationRequestDeclined, confM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationReady>(
             bind(&DBusConfigurationManager::conversationReady, confM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationRemoved>(
diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i
index 7858045b84..ba31e46e9f 100644
--- a/bin/jni/conversation.i
+++ b/bin/jni/conversation.i
@@ -28,6 +28,7 @@ public:
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
+    virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
@@ -66,6 +67,7 @@ public:
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
+    virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i
index 5e83fcac81..010ad84cc6 100644
--- a/bin/jni/jni_interface.i
+++ b/bin/jni/jni_interface.i
@@ -320,6 +320,7 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM
         exportable_callback<ConversationSignal::ConversationLoaded>(bind(&ConversationCallback::conversationLoaded, convM, _1, _2, _3, _4)),
         exportable_callback<ConversationSignal::MessageReceived>(bind(&ConversationCallback::messageReceived, convM, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&ConversationCallback::conversationRequestReceived, convM, _1, _2, _3)),
+        exportable_callback<ConversationSignal::ConversationRequestDeclined>(bind(&ConversationCallback::conversationRequestDeclined, convM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationReady>(bind(&ConversationCallback::conversationReady, convM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationRemoved>(bind(&ConversationCallback::conversationRemoved, convM, _1, _2)),
         exportable_callback<ConversationSignal::ConversationMemberEvent>(bind(&ConversationCallback::conversationMemberEvent, convM, _1, _2, _3, _4)),
diff --git a/bin/nodejs/callback.h b/bin/nodejs/callback.h
index c1da6535c2..480a7c88f9 100644
--- a/bin/nodejs/callback.h
+++ b/bin/nodejs/callback.h
@@ -30,6 +30,7 @@ Persistent<Function> incomingCallWithMediaCb;
 Persistent<Function> conversationLoadedCb;
 Persistent<Function> messageReceivedCb;
 Persistent<Function> conversationRequestReceivedCb;
+Persistent<Function> conversationRequestDeclinedCb;
 Persistent<Function> conversationReadyCb;
 Persistent<Function> conversationRemovedCb;
 Persistent<Function> conversationMemberEventCb;
@@ -93,6 +94,8 @@ getPresistentCb(std::string_view signal)
         return &conversationRemovedCb;
     else if (signal == "ConversationRequestReceived")
         return &conversationRequestReceivedCb;
+    else if (signal == "ConversationRequestDeclined")
+        return &conversationRequestDeclinedCb;
     else if (signal == "ConversationMemberEvent")
         return &conversationMemberEventCb;
     else if (signal == "OnConversationError")
@@ -569,6 +572,23 @@ conversationRequestReceived(const std::string& accountId, const std::string& con
     uv_async_send(&signalAsync);
 }
 
+void
+conversationRequestDeclined(const std::string& accountId, const std::string& conversationId)
+{
+    std::lock_guard<std::mutex> lock(pendingSignalsLock);
+    pendingSignals.emplace([accountId, conversationId, message]() {
+        Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), conversationRequestDeclinedCb);
+        if (!func.IsEmpty()) {
+            SWIGV8_VALUE callback_args[] = {
+                V8_STRING_NEW_LOCAL(accountId),
+                V8_STRING_NEW_LOCAL(conversationId)
+            };
+            func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 2, callback_args);
+        }
+    });
+    uv_async_send(&signalAsync);
+}
+
 void
 conversationReady(const std::string& accountId, const std::string& conversationId)
 {
diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i
index 45605ae27b..5a143ad151 100644
--- a/bin/nodejs/conversation.i
+++ b/bin/nodejs/conversation.i
@@ -28,6 +28,7 @@ public:
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
+    virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
@@ -67,6 +68,7 @@ public:
     virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){}
     virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){}
     virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){}
+    virtual void conversationRequestDeclined(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){}
     virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){}
diff --git a/bin/nodejs/nodejs_interface.i b/bin/nodejs/nodejs_interface.i
index d64d875ee7..1dd9511dc8 100644
--- a/bin/nodejs/nodejs_interface.i
+++ b/bin/nodejs/nodejs_interface.i
@@ -148,6 +148,7 @@ void init(const SWIGV8_VALUE& funcMap){
         exportable_callback<ConversationSignal::ConversationLoaded>(bind(&conversationLoaded, _1, _2, _3, _4)),
         exportable_callback<ConversationSignal::MessageReceived>(bind(&messageReceived, _1, _2, _3)),
         exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&conversationRequestReceived, _1, _2, _3)),
+        exportable_callback<ConversationSignal::ConversationRequestDeclined>(bind(&conversationRequestDeclined, _1, _2)),
         exportable_callback<ConversationSignal::ConversationReady>(bind(&conversationReady, _1, _2)),
         exportable_callback<ConversationSignal::ConversationRemoved>(bind(&conversationRemoved, _1, _2)),
         exportable_callback<ConversationSignal::ConversationMemberEvent>(bind(&conversationMemberEvent, _1, _2, _3, _4)),
diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp
index a306c07654..dcccdedbd7 100644
--- a/src/client/ring_signal.cpp
+++ b/src/client/ring_signal.cpp
@@ -131,6 +131,7 @@ getSignalHandlers()
         exported_callback<DRing::ConversationSignal::ConversationLoaded>(),
         exported_callback<DRing::ConversationSignal::MessageReceived>(),
         exported_callback<DRing::ConversationSignal::ConversationRequestReceived>(),
+        exported_callback<DRing::ConversationSignal::ConversationRequestDeclined>(),
         exported_callback<DRing::ConversationSignal::ConversationReady>(),
         exported_callback<DRing::ConversationSignal::ConversationRemoved>(),
         exported_callback<DRing::ConversationSignal::ConversationMemberEvent>(),
diff --git a/src/jami/conversation_interface.h b/src/jami/conversation_interface.h
index ada5792746..69890a9a54 100644
--- a/src/jami/conversation_interface.h
+++ b/src/jami/conversation_interface.h
@@ -98,6 +98,12 @@ struct DRING_PUBLIC ConversationSignal
                              const std::string& /* conversationId */,
                              std::map<std::string, std::string> /*metadatas*/);
     };
+    struct DRING_PUBLIC ConversationRequestDeclined
+    {
+        constexpr static const char* name = "ConversationRequestDeclined";
+        using cb_type = void(const std::string& /*accountId*/,
+                             const std::string& /* conversationId */);
+    };
     struct DRING_PUBLIC ConversationReady
     {
         constexpr static const char* name = "ConversationReady";
diff --git a/src/jamidht/account_manager.cpp b/src/jamidht/account_manager.cpp
index 0272f6c558..ec8a9494c0 100644
--- a/src/jamidht/account_manager.cpp
+++ b/src/jamidht/account_manager.cpp
@@ -532,14 +532,22 @@ AccountManager::getRequest(const std::string& id) const
     return std::nullopt;
 }
 
-void
+bool
 AccountManager::addConversationRequest(const std::string& id, const ConversationRequest& req)
 {
     if (info_) {
         std::lock_guard<std::mutex> lk(conversationsRequestsMtx);
+        auto it = info_->conversationsRequests.find(id);
+        if (it != info_->conversationsRequests.end()) {
+            // Check if updated
+            if (req == it->second)
+                return false;
+        }
         info_->conversationsRequests[id] = req;
         saveConvRequests();
+        return true;
     }
+    return false;
 }
 
 void
@@ -724,7 +732,8 @@ AccountManager::forEachDevice(
 
     struct State
     {
-        unsigned remaining {1}; // Note: state is initialized to 1, because we need to wait that the get is finished
+        unsigned remaining {
+            1}; // Note: state is initialized to 1, because we need to wait that the get is finished
         std::set<dht::PkId> treatedDevices {};
         std::function<void(const std::shared_ptr<dht::crypto::PublicKey>&)> onDevice;
         std::function<void(bool)> onEnd;
@@ -767,9 +776,7 @@ AccountManager::forEachDevice(
             });
             return true;
         },
-        [state](bool /*ok*/) {
-            state->found({});
-        });
+        [state](bool /*ok*/) { state->found({}); });
 }
 
 void
diff --git a/src/jamidht/account_manager.h b/src/jamidht/account_manager.h
index 0fb6177b69..1fe1fc379f 100644
--- a/src/jamidht/account_manager.h
+++ b/src/jamidht/account_manager.h
@@ -231,7 +231,7 @@ public:
     void addConversation(const ConvInfo& info);
     void setConversationsRequests(const std::map<std::string, ConversationRequest>& newConvReq);
     std::optional<ConversationRequest> getRequest(const std::string& id) const;
-    void addConversationRequest(const std::string& id, const ConversationRequest& req);
+    bool addConversationRequest(const std::string& id, const ConversationRequest& req);
     void rmConversationRequest(const std::string& id);
     mutable std::mutex conversationsRequestsMtx;
 
diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
index b802bd87d3..3e873b002a 100644
--- a/src/jamidht/conversation.cpp
+++ b/src/jamidht/conversation.cpp
@@ -103,6 +103,8 @@ ConversationRequest::toMap() const
     auto result = metadatas;
     result["id"] = conversationId;
     result["from"] = from;
+    if (declined)
+        result["declined"] = std::to_string(declined);
     result["received"] = std::to_string(received);
     return result;
 }
diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h
index fa0fa2047e..80555c666a 100644
--- a/src/jamidht/conversation.h
+++ b/src/jamidht/conversation.h
@@ -52,6 +52,13 @@ struct ConversationRequest
     Json::Value toJson() const;
     std::map<std::string, std::string> toMap() const;
 
+    bool operator==(const ConversationRequest& o) const
+    {
+        auto m = toMap();
+        auto om = o.toMap();
+        return m.size() == om.size() && std::equal(m.begin(), m.end(), om.begin());
+    }
+
     MSGPACK_DEFINE_MAP(from, conversationId, metadatas, received, declined)
 };
 
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index 566427027c..5910a1c50d 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -3888,6 +3888,8 @@ JamiAccount::declineConversationRequest(const std::string& conversationId)
         return;
     request->declined = std::time(nullptr);
     accountManager_->addConversationRequest(conversationId, std::move(*request));
+    emitSignal<DRing::ConversationSignal::ConversationRequestDeclined>(getAccountID(),
+                                                                       conversationId);
 
     syncWithConnected();
 }
@@ -5037,10 +5039,19 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket,
             }
 
             // New request
-            accountManager_->addConversationRequest(convId, req);
-
-            if (req.declined != 0)
-                continue; // Request removed, do not emit signal
+            if (!accountManager_->addConversationRequest(convId, req))
+                continue; // Already added
+
+            if (req.declined != 0) {
+                // Request declined
+                JAMI_INFO("[Account %s] Declined request detected for conversation %s (device %s)",
+                          getAccountID().c_str(),
+                          convId.c_str(),
+                          deviceId.to_c_str());
+                emitSignal<DRing::ConversationSignal::ConversationRequestDeclined>(getAccountID(),
+                                                                                   convId);
+                continue;
+            }
 
             JAMI_INFO("[Account %s] New request detected for conversation %s (device %s)",
                       getAccountID().c_str(),
diff --git a/test/unitTest/syncHistory/syncHistory.cpp b/test/unitTest/syncHistory/syncHistory.cpp
index bc4dae93ee..be8e461671 100644
--- a/test/unitTest/syncHistory/syncHistory.cpp
+++ b/test/unitTest/syncHistory/syncHistory.cpp
@@ -68,6 +68,7 @@ private:
     void testSyncCreateAccountExportDeleteReimportWithConvId();
     void testSyncCreateAccountExportDeleteReimportWithConvReq();
     void testSyncOneToOne();
+    void testConversationRequestRemoved();
 
     CPPUNIT_TEST_SUITE(SyncHistoryTest);
     CPPUNIT_TEST(testCreateConversationThenSync);
@@ -80,6 +81,7 @@ private:
     CPPUNIT_TEST(testSyncCreateAccountExportDeleteReimportWithConvId);
     CPPUNIT_TEST(testSyncCreateAccountExportDeleteReimportWithConvReq);
     CPPUNIT_TEST(testSyncOneToOne);
+    CPPUNIT_TEST(testConversationRequestRemoved);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -862,6 +864,93 @@ SyncHistoryTest::testSyncOneToOne()
     std::remove(aliceArchive.c_str());
 }
 
+void
+SyncHistoryTest::testConversationRequestRemoved()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+
+    // Export alice
+    auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz";
+    std::remove(aliceArchive.c_str());
+    aliceAccount->exportArchive(aliceArchive);
+
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto uri = aliceAccount->getUsername();
+
+    // Start conversation for Alice
+    auto convId = bobAccount->startConversation();
+
+    // Check that alice receives the request
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto requestReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& accountId,
+                const std::string& conversationId,
+                std::map<std::string, std::string> /*metadatas*/) {
+                if (accountId == aliceId && conversationId == convId) {
+                    requestReceived = true;
+                    cv.notify_one();
+                }
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    bobAccount->addConversationMember(convId, uri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return requestReceived; }));
+    DRing::unregisterSignalHandlers();
+    confHandlers.clear();
+
+    // Now create alice2
+    std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "ALICE2";
+    details[ConfProperties::ALIAS] = "ALICE2";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = aliceArchive;
+    alice2Id = Manager::instance().addAccount(details);
+
+    requestReceived = false;
+    bool requestDeclined = false, requestDeclined2 = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& accountId,
+                const std::string& conversationId,
+                std::map<std::string, std::string> /*metadatas*/) {
+                if (accountId == alice2Id && conversationId == convId) {
+                    requestReceived = true;
+                    cv.notify_one();
+                }
+            }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestDeclined>(
+            [&](const std::string& accountId, const std::string& conversationId) {
+                if (conversationId != convId)
+                    return;
+                if (accountId == aliceId)
+                    requestDeclined = true;
+                if (accountId == alice2Id)
+                    requestDeclined2 = true;
+                cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return requestReceived; }));
+    // Now decline trust request, this should trigger ConversationRequestDeclined both sides for Alice
+    aliceAccount->declineConversationRequest(convId);
+
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] {
+        return requestDeclined && requestDeclined2;
+    }));
+
+    DRing::unregisterSignalHandlers();
+    std::remove(aliceArchive.c_str());
+}
+
 } // namespace test
 } // namespace jami
 
-- 
GitLab