From 4392c72206b3a7c5f294fc187b2f6904e660890e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Mon, 27 Nov 2023 13:25:30 -0500
Subject: [PATCH] conversation_module: avoid double conversation for same
 contact

If we receive a conversation request for a contact where we
already have a conversation, we can just accept the new one.

GitLab: #924
Change-Id: I9e5d0f7302567ce488c52b45377117f034cd5da5
---
 src/jamidht/account_manager.cpp               | 30 +++++-----
 src/jamidht/account_manager.h                 |  2 +-
 src/jamidht/archive_account_manager.cpp       |  4 +-
 src/jamidht/archive_account_manager.h         |  2 +-
 src/jamidht/conversation_module.cpp           | 44 ++++++++++----
 src/jamidht/jamiaccount.cpp                   |  3 +-
 src/jamidht/jamiaccount.h                     |  5 ++
 .../conversation/conversationRequest.cpp      | 59 +++++++++++++++++++
 8 files changed, 117 insertions(+), 32 deletions(-)

diff --git a/src/jamidht/account_manager.cpp b/src/jamidht/account_manager.cpp
index 66e28c3749..8074a284d5 100644
--- a/src/jamidht/account_manager.cpp
+++ b/src/jamidht/account_manager.cpp
@@ -272,24 +272,26 @@ AccountManager::announceFromReceipt(const std::string& receipt)
 }
 
 void
-AccountManager::startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb)
+AccountManager::startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb, bool publishPresence)
 {
     // Put device announcement
     if (info_->announce) {
         auto h = dht::InfoHash(info_->accountId);
-        dht_->put(
-            h,
-            info_->announce,
-            [dcb = std::move(dcb), h](bool ok) {
-                if (ok)
-                    JAMI_DEBUG("device announced at {}", h.toString());
-                // We do not care about the status, it's a permanent put, if this fail,
-                // this means the DHT is disconnected but the put will be retried when connected.
-                if (dcb)
-                    dcb();
-            },
-            {},
-            true);
+        if (publishPresence) {
+            dht_->put(
+                h,
+                info_->announce,
+                [dcb = std::move(dcb), h](bool ok) {
+                    if (ok)
+                        JAMI_DEBUG("device announced at {}", h.toString());
+                    // We do not care about the status, it's a permanent put, if this fail,
+                    // this means the DHT is disconnected but the put will be retried when connected.
+                    if (dcb)
+                        dcb();
+                },
+                {},
+                true);
+        }
         for (const auto& crl : info_->identity.second->issuer->getRevocationLists())
             dht_->put(h, crl, dht::DoneCallback {}, {}, true);
         dht_->listen<DeviceAnnouncement>(h, [this, cb = std::move(cb)](DeviceAnnouncement&& dev) {
diff --git a/src/jamidht/account_manager.h b/src/jamidht/account_manager.h
index cd8e5afcd3..9703012f43 100644
--- a/src/jamidht/account_manager.h
+++ b/src/jamidht/account_manager.h
@@ -140,7 +140,7 @@ public:
 
     void setDht(const std::shared_ptr<dht::DhtRunner>& dht) { dht_ = dht; }
 
-    virtual void startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb);
+    virtual void startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb, bool publishPresence = true);
 
     const AccountInfo* getInfo() const { return info_.get(); }
 
diff --git a/src/jamidht/archive_account_manager.cpp b/src/jamidht/archive_account_manager.cpp
index 0b399f9919..7cf0edb0f4 100644
--- a/src/jamidht/archive_account_manager.cpp
+++ b/src/jamidht/archive_account_manager.cpp
@@ -537,9 +537,9 @@ ArchiveAccountManager::syncDevices()
 }
 
 void
-ArchiveAccountManager::startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb)
+ArchiveAccountManager::startSync(const OnNewDeviceCb& cb, const OnDeviceAnnouncedCb& dcb, bool publishPresence)
 {
-    AccountManager::startSync(std::move(cb), std::move(dcb));
+    AccountManager::startSync(std::move(cb), std::move(dcb), publishPresence);
 
     dht_->listen<DeviceSync>(
         dht::InfoHash::get("inbox:" + info_->devicePk->getId().toString()),
diff --git a/src/jamidht/archive_account_manager.h b/src/jamidht/archive_account_manager.h
index 4437dc5e9f..55d9b30aea 100644
--- a/src/jamidht/archive_account_manager.h
+++ b/src/jamidht/archive_account_manager.h
@@ -49,7 +49,7 @@ public:
                             AuthFailureCallback onFailure,
                             const OnChangeCallback& onChange) override;
 
-    void startSync(const OnNewDeviceCb&, const OnDeviceAnnouncedCb& dcb = {}) override;
+    void startSync(const OnNewDeviceCb&, const OnDeviceAnnouncedCb& dcb = {}, bool publishPresence = true) override;
 
     bool changePassword(const std::string& password_old, const std::string& password_new) override;
 
diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp
index 40a043f6bd..704e57a485 100644
--- a/src/jamidht/conversation_module.cpp
+++ b/src/jamidht/conversation_module.cpp
@@ -1573,20 +1573,20 @@ ConversationModule::onTrustRequest(const std::string& uri,
     if (getOneToOneConversation(uri) != "") {
         // If there is already an active one to one conversation here, it's an active
         // contact and the contact will reclone this activeConv, so ignore the request
-        JAMI_WARN("Contact is sending a request for a non active conversation. Ignore. They will "
+        JAMI_WARNING("Contact is sending a request for a non active conversation. Ignore. They will "
                   "clone the old one");
         return;
     }
     if (pimpl_->isAcceptedConversation(conversationId)) {
-        JAMI_INFO("[Account %s] Received a request for a conversation "
+        JAMI_DEBUG("[Account {}] Received a request for a conversation "
                   "already handled. Ignore",
-                  pimpl_->accountId_.c_str());
+                  pimpl_->accountId_);
         return;
     }
     if (pimpl_->getRequest(conversationId) != std::nullopt) {
-        JAMI_INFO("[Account %s] Received a request for a conversation "
+        JAMI_DEBUG("[Account {}] Received a request for a conversation "
                   "already existing. Ignore",
-                  pimpl_->accountId_.c_str());
+                  pimpl_->accountId_);
         return;
     }
     ConversationRequest req;
@@ -1612,26 +1612,44 @@ void
 ConversationModule::onConversationRequest(const std::string& from, const Json::Value& value)
 {
     ConversationRequest req(value);
-    JAMI_INFO("[Account %s] Receive a new conversation request for conversation %s from %s",
-              pimpl_->accountId_.c_str(),
-              req.conversationId.c_str(),
-              from.c_str());
+    JAMI_DEBUG("[Account {}] Receive a new conversation request for conversation {} from {}",
+              pimpl_->accountId_,
+              req.conversationId,
+              from);
     auto convId = req.conversationId;
     req.from = from;
 
     // Already accepted request, do nothing
-    if (pimpl_->isAcceptedConversation(convId)) {
+    if (pimpl_->isAcceptedConversation(convId))
         return;
-    }
     if (pimpl_->getRequest(convId) != std::nullopt) {
-        JAMI_INFO("[Account %s] Received a request for a conversation already existing. "
+        JAMI_DEBUG("[Account {}] Received a request for a conversation already existing. "
                   "Ignore",
-                  pimpl_->accountId_.c_str());
+                  pimpl_->accountId_);
         return;
     }
     req.received = std::time(nullptr);
     auto reqMap = req.toMap();
     auto isOneToOne = req.isOneToOne();
+    std::string oldConv;
+    auto acc = pimpl_->account_.lock();
+    if (acc && isOneToOne) {
+        auto oldConv = getOneToOneConversation(from);
+        if (!oldConv.empty()) {
+            // Already a conversation with the contact.
+            if (oldConv != convId && acc->updateConvForContact(from, oldConv, convId)) {
+                initReplay(oldConv, convId);
+                cloneConversationFrom(convId, from, oldConv);
+                return;
+            }
+            // If there is already an active one to one conversation here, it's an active
+            // contact and the contact will reclone this activeConv, so ignore the request
+            JAMI_WARNING("Contact is sending a request for a non active conversation. Ignore. They will "
+                    "clone the old one");
+            return;
+        }
+    }
+
     if (pimpl_->addConversationRequest(convId, std::move(req))) {
         // Note: no need to sync here because other connected devices should receive
         // the same conversation request. Will sync when the conversation will be added
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index 6f64e3ef02..55efcf4dca 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -1926,7 +1926,8 @@ JamiAccount::doRegister_()
                         emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(
                             accountID_, getVolatileAccountDetails());
                     }
-                });
+                },
+                publishPresence_);
         };
 
         setRegistrationState(RegistrationState::TRYING);
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index edbb3dc78d..2747ce442b 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -438,6 +438,10 @@ public:
     {
         noSha3sumVerification_ = newValue;
     }
+
+    void publishPresence(bool newValue) {
+        publishPresence_  = newValue;
+    }
 #endif
 
     /**
@@ -868,6 +872,7 @@ private:
     std::atomic_bool deviceAnnounced_ {false};
 
     bool noSha3sumVerification_ {false};
+    bool publishPresence_ {true};
 
     std::map<Uri::Scheme, std::unique_ptr<ChannelHandlerInterface>> channelHandlers_ {};
 
diff --git a/test/unitTest/conversation/conversationRequest.cpp b/test/unitTest/conversation/conversationRequest.cpp
index 984f961d4f..646ddfc7a3 100644
--- a/test/unitTest/conversation/conversationRequest.cpp
+++ b/test/unitTest/conversation/conversationRequest.cpp
@@ -77,6 +77,7 @@ public:
     void testCacheRequestFromClient();
     void testNeedsSyncingWithForCloning();
     void testRemoveContactRemoveTrustRequest();
+    void testAddConversationNoPresenceThenConnects();
     std::string aliceId;
     std::string bobId;
     std::string bob2Id;
@@ -107,6 +108,7 @@ private:
     CPPUNIT_TEST(testCacheRequestFromClient);
     CPPUNIT_TEST(testNeedsSyncingWithForCloning);
     CPPUNIT_TEST(testRemoveContactRemoveTrustRequest);
+    CPPUNIT_TEST(testAddConversationNoPresenceThenConnects);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -1591,6 +1593,63 @@ ConversationRequestTest::testRemoveContactRemoveTrustRequest()
     CPPUNIT_ASSERT(bob2Account->getTrustRequests().size() == 0);
 }
 
+void
+ConversationRequestTest::testAddConversationNoPresenceThenConnects()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    carlaAccount->publishPresence(false);
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
+    std::string convId = "";
+    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            }
+            cv.notify_one();
+        }));
+    auto carlaConnected = false;
+    confHandlers.insert(
+        libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = carlaAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[libjami::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED") {
+                    carlaConnected = true;
+                } else if (daemonStatus == "UNREGISTERED") {
+                    carlaConnected = false;
+                }
+                cv.notify_one();
+            }));
+    libjami::registerSignalHandlers(confHandlers);
+    aliceAccount->addContact(carlaUri);
+    cv.wait_for(lk, 5s); // Wait 5 secs for the put to happen
+    Manager::instance().sendRegister(carlaId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return carlaConnected; }));
+    carlaAccount->addContact(aliceUri);
+    cv.wait_for(lk, 5s); // Wait 5 secs for the put to happen
+    carlaAccount->publishPresence(true);
+
+    Manager::instance().sendRegister(carlaId, false);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return !carlaConnected; }));
+    convId.clear();
+    Manager::instance().sendRegister(carlaId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return carlaConnected; }));
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, 30s, [&]() { return !convId.empty(); }));
+
+    auto carlaDetails = carlaAccount->getContactDetails(aliceUri);
+    auto aliceDetails = aliceAccount->getContactDetails(carlaUri);
+    CPPUNIT_ASSERT(carlaDetails["conversationId"] == aliceDetails["conversationId"] && carlaDetails["conversationId"] == convId);
+}
+
 } // namespace test
 } // namespace jami
 
-- 
GitLab