From ce61749408e47a7870a4e015af44ee649fc97db4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Tue, 17 Nov 2020 13:55:05 -0500
Subject: [PATCH] swarm: remove a user/device from the conversation

TODO

Change-Id: I23b5d00e9b69dcc667cb52543d528171f9f34fdc
GitLab: #298
GitLab: #299
---
 src/jamidht/conversation.cpp                |  77 +++-
 src/jamidht/conversation.h                  |   5 +-
 src/jamidht/conversationrepository.cpp      | 146 ++++++-
 src/jamidht/conversationrepository.h        |   3 +
 src/jamidht/jamiaccount.cpp                 |  53 ++-
 src/jamidht/jamiaccount.h                   |   4 +-
 test/unitTest/conversation/conversation.cpp | 460 +++++++++++++++++++-
 7 files changed, 710 insertions(+), 38 deletions(-)

diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
index f10ff171ff..e32f1a87e4 100644
--- a/src/jamidht/conversation.cpp
+++ b/src/jamidht/conversation.cpp
@@ -59,6 +59,7 @@ public:
     }
     ~Impl() = default;
 
+    bool isAdmin() const;
     std::string repoPath() const;
 
     std::unique_ptr<ConversationRepository> repository_;
@@ -73,6 +74,24 @@ public:
     std::deque<std::tuple<std::string, std::string, OnPullCb>> pullcbs_ {};
 };
 
+bool
+Conversation::Impl::isAdmin() const
+{
+    auto shared = account_.lock();
+    if (!shared)
+        return false;
+
+    auto adminsPath = repoPath() + DIR_SEPARATOR_STR + "admins";
+    auto cert = shared->identity().second;
+    auto parentCert = cert->issuer;
+    if (!parentCert) {
+        JAMI_ERR("Parent cert is null!");
+        return false;
+    }
+    auto uri = parentCert->getId().toString();
+    return fileutils::isFile(fileutils::getFullPath(adminsPath, uri + ".crt"));
+}
+
 std::string
 Conversation::Impl::repoPath() const
 {
@@ -164,14 +183,37 @@ Conversation::id() const
 std::string
 Conversation::addMember(const std::string& contactUri)
 {
+    if (isMember(contactUri, true)) {
+        JAMI_WARN("Could not add member %s because it's already a member", contactUri.c_str());
+        return {};
+    }
+    if (isBanned(contactUri)) {
+        JAMI_WARN("Could not add member %s because this member is banned", contactUri.c_str());
+        return {};
+    }
     // Add member files and commit
     return pimpl_->repository_->addMember(contactUri);
 }
 
 bool
-Conversation::removeMember(const std::string& contactUri)
+Conversation::removeMember(const std::string& contactUri, bool isDevice)
 {
-    // TODO
+    // Check if admin
+    if (!pimpl_->isAdmin()) {
+        JAMI_WARN("You're not an admin of this repo. Cannot ban %s", contactUri.c_str());
+        return false;
+    }
+    // Vote for removal
+    if (pimpl_->repository_->voteKick(contactUri, isDevice).empty()) {
+        JAMI_WARN("Kicking %s failed", contactUri.c_str());
+        return false;
+    }
+    // If admin, check vote
+    if (!pimpl_->repository_->resolveVote(contactUri, isDevice).empty()) {
+        JAMI_WARN("Vote solved for %s. %s banned",
+                  contactUri.c_str(),
+                  isDevice ? "Device" : "Member");
+    }
     return true;
 }
 
@@ -183,12 +225,9 @@ Conversation::getMembers(bool includeInvited) const
     if (!shared)
         return result;
 
-    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID()
-                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR
-                    + pimpl_->repository_->id();
-    auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins";
-    auto membersPath = repoPath + DIR_SEPARATOR_STR + "members";
     auto invitedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "invited";
+    auto adminsPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "admins";
+    auto membersPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "members";
     for (const auto& certificate : fileutils::readDirectory(adminsPath)) {
         if (certificate.find(".crt") == std::string::npos) {
             JAMI_WARN("Incorrect file found: %s/%s", adminsPath.c_str(), certificate.c_str());
@@ -229,18 +268,15 @@ Conversation::join()
 }
 
 bool
-Conversation::isMember(const std::string& uri, bool includeInvited)
+Conversation::isMember(const std::string& uri, bool includeInvited) const
 {
     auto shared = pimpl_->account_.lock();
     if (!shared)
         return false;
 
-    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID()
-                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR
-                    + pimpl_->repository_->id();
-    auto invitedPath = repoPath + DIR_SEPARATOR_STR + "invited";
-    auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins";
-    auto membersPath = repoPath + DIR_SEPARATOR_STR + "members";
+    auto invitedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "invited";
+    auto adminsPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "admins";
+    auto membersPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "members";
     std::vector<std::string> pathsToCheck = {adminsPath, membersPath};
     if (includeInvited)
         pathsToCheck.emplace_back(invitedPath);
@@ -259,6 +295,19 @@ Conversation::isMember(const std::string& uri, bool includeInvited)
     return false;
 }
 
+bool
+Conversation::isBanned(const std::string& uri, bool isDevice) const
+{
+    auto shared = pimpl_->account_.lock();
+    if (!shared)
+        return true;
+
+    auto type = isDevice ? "devices" : "members";
+    auto bannedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + type
+                      + DIR_SEPARATOR_STR + uri + ".crt";
+    return fileutils::isFile(bannedPath);
+}
+
 std::string
 Conversation::sendMessage(const std::string& message,
                           const std::string& type,
diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h
index aab97c5a46..f3d93a2d12 100644
--- a/src/jamidht/conversation.h
+++ b/src/jamidht/conversation.h
@@ -52,7 +52,7 @@ public:
      * @return Commit id or empty if fails
      */
     std::string addMember(const std::string& contactUri);
-    bool removeMember(const std::string& contactUri);
+    bool removeMember(const std::string& contactUri, bool isDevice);
     /**
      * @param includeInvited        If we want invited members
      * @return a vector of member details:
@@ -76,7 +76,8 @@ public:
      * @param uri       URI to test
      * @return true if uri is a member
      */
-    bool isMember(const std::string& uri, bool includInvited = false);
+    bool isMember(const std::string& uri, bool includeInvited = false) const;
+    bool isBanned(const std::string& uri, bool isDevice = false) const;
 
     // Message send
     std::string sendMessage(const std::string& message,
diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index 53e8974068..76c5243530 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -1210,7 +1210,7 @@ ConversationRepository::join()
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
-    return pimpl_->commit(Json::writeString(wbuilder, json));
+    return commitMessage(Json::writeString(wbuilder, json));
 }
 
 std::string
@@ -1281,7 +1281,7 @@ ConversationRepository::leave()
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
-    return pimpl_->commit(Json::writeString(wbuilder, json));
+    return commitMessage(Json::writeString(wbuilder, json));
 }
 
 void
@@ -1294,4 +1294,146 @@ ConversationRepository::erase()
     fileutils::removeAll(repoPath, true);
 }
 
+std::string
+ConversationRepository::voteKick(const std::string& uri, bool isDevice)
+{
+    // Add vote + commit
+    // TODO simplify
+    std::string repoPath = git_repository_workdir(pimpl_->repository_.get());
+    auto account = pimpl_->account_.lock();
+    if (!account)
+        return {};
+    auto cert = account->identity().second;
+    auto parentCert = cert->issuer;
+    if (!parentCert) {
+        JAMI_ERR("Parent cert is null!");
+        return {};
+    }
+    auto adminUri = parentCert->getId().toString();
+    if (adminUri == uri) {
+        JAMI_WARN("Admin tried to ban theirself");
+        return {};
+    }
+
+    // TODO avoid duplicate
+    auto relativeVotePath = std::string("votes") + DIR_SEPARATOR_STR
+                            + (isDevice ? "devices" : "members") + DIR_SEPARATOR_STR + uri;
+    auto voteDirectory = repoPath + DIR_SEPARATOR_STR + relativeVotePath;
+    if (!fileutils::recursive_mkdir(voteDirectory, 0700)) {
+        JAMI_ERR("Error when creating %s. Abort vote", voteDirectory.c_str());
+        return {};
+    }
+    auto votePath = fileutils::getFullPath(voteDirectory, adminUri);
+    auto voteFile = fileutils::ofstream(votePath, std::ios::trunc | std::ios::binary);
+    if (!voteFile.is_open()) {
+        JAMI_ERR("Could not write data to %s", votePath.c_str());
+        return {};
+    }
+    voteFile.close();
+
+    auto toAdd = fileutils::getFullPath(relativeVotePath, adminUri);
+    if (!pimpl_->add(toAdd.c_str()))
+        return {};
+
+    Json::Value json;
+    json["uri"] = uri;
+    json["type"] = "vote";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    return commitMessage(Json::writeString(wbuilder, json));
+}
+
+std::string
+ConversationRepository::resolveVote(const std::string& uri, bool isDevice)
+{
+    // Count ratio admin/votes
+    auto nbAdmins = 0, nbVote = 0;
+    // For each admin, check if voted
+    std::string repoPath = git_repository_workdir(pimpl_->repository_.get());
+    std::string adminsPath = repoPath + "admins";
+    std::string membersPath = repoPath + "members";
+    std::string devicesPath = repoPath + "devices";
+    std::string bannedPath = repoPath + "banned";
+    auto isAdmin = fileutils::isFile(fileutils::getFullPath(adminsPath, uri + ".crt"));
+    std::string type = "members";
+    if (isDevice)
+        type = "devices";
+    else if (isAdmin)
+        type = "admins";
+
+    auto voteDirectory = repoPath + DIR_SEPARATOR_STR + "votes" + DIR_SEPARATOR_STR
+                         + (isDevice ? "devices" : "members") + DIR_SEPARATOR_STR + uri;
+    for (const auto& certificate : fileutils::readDirectory(adminsPath)) {
+        if (certificate.find(".crt") == std::string::npos) {
+            JAMI_WARN("Incorrect file found: %s/%s", adminsPath.c_str(), certificate.c_str());
+            continue;
+        }
+        auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
+        nbAdmins += 1;
+        if (fileutils::isFile(fileutils::getFullPath(voteDirectory, adminUri)))
+            nbVote += 1;
+    }
+
+    if (nbAdmins > 0 && (static_cast<double>(nbVote) / static_cast<double>(nbAdmins)) > .5) {
+        JAMI_WARN("More than half of the admins voted to ban %s, apply the ban", uri.c_str());
+
+        // Remove vote directory
+        fileutils::removeAll(voteDirectory, true);
+
+        // Move from device or members file into banned
+        std::string originFilePath = membersPath;
+        if (isDevice)
+            originFilePath = devicesPath;
+        else if (isAdmin)
+            originFilePath = adminsPath;
+        originFilePath += DIR_SEPARATOR_STR + uri + ".crt";
+        auto destPath = bannedPath + DIR_SEPARATOR_STR + (isDevice ? "devices" : "members");
+        auto destFilePath = destPath + DIR_SEPARATOR_STR + uri + ".crt";
+        if (!fileutils::recursive_mkdir(destPath, 0700)) {
+            JAMI_ERR("Error when creating %s. Abort resolving vote", destPath.c_str());
+            return {};
+        }
+
+        if (std::rename(originFilePath.c_str(), destFilePath.c_str())) {
+            JAMI_ERR("Error when moving %s to %s. Abort resolving vote",
+                     originFilePath.c_str(),
+                     destFilePath.c_str());
+            return {};
+        }
+
+        // If members, remove related devices
+        if (!isDevice) {
+            for (const auto& certificate : fileutils::readDirectory(devicesPath)) {
+                auto certPath = fileutils::getFullPath(devicesPath, certificate);
+                auto deviceCert = fileutils::loadTextFile(certPath);
+                try {
+                    crypto::Certificate cert(deviceCert);
+                    if (auto issuer = cert.issuer)
+                        if (issuer->toString() == uri)
+                            fileutils::remove(certPath, true);
+                } catch (...) {
+                    continue;
+                }
+            }
+        }
+
+        // Commit
+        if (!git_add_all(pimpl_->repository_.get()))
+            return {};
+
+        Json::Value json;
+        json["action"] = "ban";
+        json["uri"] = uri;
+        json["type"] = "member";
+        Json::StreamWriterBuilder wbuilder;
+        wbuilder["commentStyle"] = "None";
+        wbuilder["indentation"] = "";
+        return commitMessage(Json::writeString(wbuilder, json));
+    }
+
+    // If vote nok
+    return {};
+}
+
 } // namespace jami
diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h
index a19a46d1ef..b5492f4027 100644
--- a/src/jamidht/conversationrepository.h
+++ b/src/jamidht/conversationrepository.h
@@ -187,6 +187,9 @@ public:
      */
     void erase();
 
+    std::string voteKick(const std::string& uri, bool isDevice);
+    std::string resolveVote(const std::string& uri, bool isDevice);
+
 private:
     ConversationRepository() = delete;
     class Impl;
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index bfda853359..b9a658f5b6 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -4105,6 +4105,15 @@ JamiAccount::addConversationMember(const std::string& conversationId,
         JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str());
         return false;
     }
+
+    if (it->second->isMember(contactUri, true)) {
+        JAMI_DBG("%s is already a member of %s, resend invite", contactUri.c_str(), conversationId.c_str());
+        // Note: This should not be necessary, but if for whatever reason the other side didn't join
+        // we should not forbid new invites
+        sendTextMessage(contactUri, it->second->generateInvitation());
+        return true;
+    }
+
     auto commitId = it->second->addMember(contactUri);
     if (commitId.empty()) {
         JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str());
@@ -4131,11 +4140,34 @@ JamiAccount::addConversationMember(const std::string& conversationId,
 
 bool
 JamiAccount::removeConversationMember(const std::string& conversationId,
-                                      const std::string& contactUri)
+                                      const std::string& contactUri,
+                                      bool isDevice)
 {
     std::lock_guard<std::mutex> lk(conversationsMtx_);
-    conversations_[conversationId]->removeMember(contactUri);
-    return true;
+    auto conversation = conversations_.find(conversationId);
+    if (conversation != conversations_.end() && conversation->second) {
+        auto lastCommit = conversation->second->lastCommitId();
+        if (conversation->second->removeMember(contactUri, isDevice)) {
+            conversation->second->loadMessages([w=weak(), lastCommit, conversationId] (auto&& messages) {
+                if (auto shared = w.lock()) {
+                    std::reverse(messages.begin(), messages.end());
+                    // Announce new commits
+                    for (const auto& msg : messages) {
+                        emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(),
+                                                                                conversationId,
+                                                                                msg);
+                    }
+                    std::lock_guard<std::mutex> lk(shared->conversationsMtx_);
+                    auto conversation = shared->conversations_.find(conversationId);
+                    // Send notification for others
+                    if (conversation != shared->conversations_.end() && conversation->second)
+                        shared->sendMessageNotification(*conversation->second, lastCommit, true);
+                }
+            }, "", lastCommit);
+            return true;
+        }
+    }
+    return false;
 }
 
 std::vector<std::map<std::string, std::string>>
@@ -4225,6 +4257,21 @@ JamiAccount::fetchNewCommits(const std::string& peer,
     std::unique_lock<std::mutex> lk(conversationsMtx_);
     auto conversation = conversations_.find(conversationId);
     if (conversation != conversations_.end() && conversation->second) {
+        if (!conversation->second->isMember(peer, true)) {
+            JAMI_WARN("[Account %s] %s is not a member of %s",
+                      getAccountID().c_str(),
+                      peer.c_str(),
+                      conversationId.c_str());
+            return;
+        }
+        if (conversation->second->isBanned(deviceId)) {
+            JAMI_WARN("[Account %s] %s is a banned device in conversation %s",
+                      getAccountID().c_str(),
+                      deviceId.c_str(),
+                      conversationId.c_str());
+            return;
+        }
+
         // Retrieve current last message
         auto lastMessageId = conversation->second->lastCommitId();
         if (lastMessageId.empty()) {
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index 6fcc1bfb2d..9e7dcbd19e 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -518,7 +518,9 @@ public:
     bool addConversationMember(const std::string& conversationId,
                                const std::string& contactUri,
                                bool sendRequest = true);
-    bool removeConversationMember(const std::string& conversationId, const std::string& contactUri);
+    bool removeConversationMember(const std::string& conversationId,
+                                  const std::string& contactUri,
+                                  bool isDevice = false);
     std::vector<std::map<std::string, std::string>> getConversationMembers(
         const std::string& conversationId);
 
diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp
index 3aff208e79..58f296fe7f 100644
--- a/test/unitTest/conversation/conversation.cpp
+++ b/test/unitTest/conversation/conversation.cpp
@@ -52,6 +52,7 @@ public:
 
     std::string aliceId;
     std::string bobId;
+    std::string bob2Id;
     std::string carlaId;
 
 private:
@@ -71,23 +72,43 @@ private:
     void testDeclineRequest();
     void testSendMessageToMultipleParticipants();
     void testPingPongMessages();
+    void testRemoveMember();
+    //void testBanDevice();
+    void testMemberTryToRemoveAdmin();
+    void testBannedMemberCannotSendMessage();
+    // void testBannedDeviceCannotSendMessageButMemberCan();
+    // void test2AdminsCannotBanEachOthers();
+    void testAddBannedMember();
+    // void testRevokedDeviceCannotSendMessage();
+    // void test2AdminsBanMembers();
+    // void test2AdminsBanOtherAdmin();
+    // void testCheckAdminFakeAVoteIsDetected();
+    void testAdminCannotKickTheirself();
+    // void testDetectionAdminKickedHimself();
+    // void testAdminRemoveConversationShouldPromoteOther();
 
     CPPUNIT_TEST_SUITE(ConversationTest);
-    CPPUNIT_TEST(testCreateConversation);
-    CPPUNIT_TEST(testGetConversation);
-    CPPUNIT_TEST(testGetConversationsAfterRm);
-    CPPUNIT_TEST(testRemoveInvalidConversation);
-    CPPUNIT_TEST(testRemoveConversationNoMember);
-    CPPUNIT_TEST(testRemoveConversationWithMember);
-    CPPUNIT_TEST(testAddMember);
-    CPPUNIT_TEST(testAddOfflineMemberThenConnects);
-    CPPUNIT_TEST(testGetMembers);
-    CPPUNIT_TEST(testSendMessage);
-    CPPUNIT_TEST(testSendMessageTriggerMessageReceived);
-    CPPUNIT_TEST(testGetRequests);
-    CPPUNIT_TEST(testDeclineRequest);
-    CPPUNIT_TEST(testSendMessageToMultipleParticipants);
-    CPPUNIT_TEST(testPingPongMessages);
+    // CPPUNIT_TEST(testCreateConversation);
+    // CPPUNIT_TEST(testGetConversation);
+    // CPPUNIT_TEST(testGetConversationsAfterRm);
+    // CPPUNIT_TEST(testRemoveInvalidConversation);
+    // CPPUNIT_TEST(testRemoveConversationNoMember);
+    // CPPUNIT_TEST(testRemoveConversationWithMember);
+    // CPPUNIT_TEST(testAddMember);
+    // CPPUNIT_TEST(testAddOfflineMemberThenConnects);
+    // CPPUNIT_TEST(testGetMembers);
+    // CPPUNIT_TEST(testSendMessage);
+    // CPPUNIT_TEST(testSendMessageTriggerMessageReceived);
+    // CPPUNIT_TEST(testGetRequests);
+    // CPPUNIT_TEST(testDeclineRequest);
+    // CPPUNIT_TEST(testSendMessageToMultipleParticipants);
+    // CPPUNIT_TEST(testPingPongMessages);
+    // CPPUNIT_TEST(testRemoveMember);
+    // CPPUNIT_TEST(testBanDevice);
+    // CPPUNIT_TEST(testMemberTryToRemoveAdmin);
+    // CPPUNIT_TEST(testBannedMemberCannotSendMessage);
+    CPPUNIT_TEST(testAddBannedMember);
+    // CPPUNIT_TEST(testAdminCannotKickTheirself);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -163,9 +184,15 @@ ConversationTest::tearDown()
     Manager::instance().removeAccount(aliceId, true);
     Manager::instance().removeAccount(bobId, true);
     Manager::instance().removeAccount(carlaId, true);
+    if (!bob2Id.empty())
+        Manager::instance().removeAccount(bob2Id, true);
+
+    auto bobArchive = std::filesystem::current_path().string() + "/bob.gz";
+    std::remove(bobArchive.c_str());
+
     // Because cppunit is not linked with dbus, just poll if removed
     for (int i = 0; i < 40; ++i) {
-        if (Manager::instance().getAccountList().size() <= currentAccSize - 3)
+        if (Manager::instance().getAccountList().size() <= currentAccSize - bob2Id.empty() ? 3 : 4)
             break;
         std::this_thread::sleep_for(std::chrono::milliseconds(100));
     }
@@ -980,6 +1007,407 @@ ConversationTest::testPingPongMessages()
     DRing::unregisterSignalHandlers();
 }
 
+void
+ConversationTest::testRemoveMember()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobUri = bobAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         voteMessageGenerated = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == bobId && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == aliceId && conversationId == convId && message["type"] == "vote") {
+                voteMessageGenerated = true;
+                cv.notify_one();
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+
+    // Now check that alice, has the only admin, can remove bob
+    memberMessageGenerated = false;
+    voteMessageGenerated = false;
+    aliceAccount->removeConversationMember(convId, bobUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return memberMessageGenerated && voteMessageGenerated;
+    }));
+    auto members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 1);
+    CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername());
+    CPPUNIT_ASSERT(members[0]["role"] == "admin");
+}
+
+/*void
+ConversationTest::testBanDevice()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobUri = bobAccount->getUsername();
+    auto bobDeviceId = std::string(bobAccount->currentDeviceId());
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         voteMessageGenerated = false, bob2GetMessage = false, bobGetMessage = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId* /,
+                const std::string& /* conversationId * /,
+                std::map<std::string, std::string> /*metadatas* /) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if ((accountId == bobId || accountId == bob2Id) && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == aliceId && conversationId == convId && message["type"] == "vote") {
+                voteMessageGenerated = true;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+            } else if (accountId == bobId) {
+                bobGetMessage = true;
+            } else if (accountId == bob2Id) {
+                bob2GetMessage = true;
+            }
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+
+    // Add second device for Bob
+    auto bobArchive = std::filesystem::current_path().string() + "/bob.gz";
+    std::remove(bobArchive.c_str());
+    bobAccount->exportArchive(bobArchive);
+    std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "BOB2";
+    details[ConfProperties::ALIAS] = "BOB2";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = bobArchive;
+    bob2Id = Manager::instance().addAccount(details);
+
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto bob2Account = Manager::instance().getAccount<JamiAccount>(bob2Id);
+                auto details = bob2Account->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED")
+                    cv.notify_one();
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    conversationReady = false;
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { return conversationReady; }));
+
+    // Now check that alice, has the only admin, can remove bob
+    memberMessageGenerated = false;
+    voteMessageGenerated = false;
+    auto members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 2);
+    aliceAccount->removeConversationMember(convId, bobDeviceId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return memberMessageGenerated && voteMessageGenerated;
+    }));
+
+    auto bannedFile = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                      + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId
+                      + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + "devices"
+                      + DIR_SEPARATOR_STR + bobDeviceId + ".crt";
+    CPPUNIT_ASSERT(fileutils::isFile(bannedFile));
+    members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 2);
+
+    // Assert that bob2 get the message, not Bob
+    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(10), [&]() { return bobGetMessage; }));
+    CPPUNIT_ASSERT(bob2GetMessage && !bobGetMessage);
+    DRing::unregisterSignalHandlers();
+}*/
+
+void
+ConversationTest::testMemberTryToRemoveAdmin()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobUri = bobAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == bobId && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == aliceId && conversationId == convId && message["type"] == "member") {
+                memberMessageGenerated = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+
+    // Now check that alice, has the only admin, can remove bob
+    memberMessageGenerated = false;
+    bobAccount->removeConversationMember(convId, aliceUri);
+    auto members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 2 && !memberMessageGenerated);
+}
+
+void
+ConversationTest::testBannedMemberCannotSendMessage()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobUri = bobAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         voteMessageGenerated = false, aliceMessageReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == bobId && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == aliceId && conversationId == convId && message["type"] == "vote") {
+                voteMessageGenerated = true;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "plain/text") {
+                aliceMessageReceived = true;
+            }
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+
+    memberMessageGenerated = false;
+    voteMessageGenerated = false;
+    aliceAccount->removeConversationMember(convId, bobUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return memberMessageGenerated && voteMessageGenerated;
+    }));
+    auto members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 1);
+
+    // Now check that alice doesn't receive a message from Bob
+    aliceMessageReceived = false;
+    bobAccount->sendMessage(convId, "hi");
+    CPPUNIT_ASSERT(
+        !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return aliceMessageReceived; }));
+}
+
+void
+ConversationTest::testAddBannedMember()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto bobUri = bobAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         voteMessageGenerated = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == bobId && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == aliceId && conversationId == convId && message["type"] == "vote") {
+                voteMessageGenerated = true;
+                cv.notify_one();
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+
+    // Now check that alice, has the only admin, can remove bob
+    memberMessageGenerated = false;
+    voteMessageGenerated = false;
+    aliceAccount->removeConversationMember(convId, bobUri);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return memberMessageGenerated && voteMessageGenerated;
+    }));
+
+    // Then check that bobUri cannot be re-added
+    CPPUNIT_ASSERT(!aliceAccount->addConversationMember(convId, bobUri));
+}
+
+void
+ConversationTest::testAdminCannotKickTheirself()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto aliceUri = aliceAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false,
+         voteMessageGenerated = false, aliceMessageReceived = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == bobId && conversationId == convId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == aliceId && conversationId == convId && message["type"] == "vote") {
+                voteMessageGenerated = true;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "member") {
+                memberMessageGenerated = true;
+            } else if (accountId == aliceId && conversationId == convId
+                       && message["type"] == "plain/text") {
+                aliceMessageReceived = true;
+            }
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    auto members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 1);
+    aliceAccount->removeConversationMember(convId, aliceUri);
+    members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 1);
+}
+
 } // namespace test
 } // namespace jami
 
-- 
GitLab