diff --git a/contrib/src/opendht/package.json b/contrib/src/opendht/package.json
index 7367cff3dc5462b4dc85854347b859a15f1ca315..10736475d0d9350ca380843f5478306c6a0d3f02 100644
--- a/contrib/src/opendht/package.json
+++ b/contrib/src/opendht/package.json
@@ -18,7 +18,8 @@
         "OPENDHT_PUSH_NOTIFICATIONS=1",
         "OPENDHT_TOOLS=0"
     ],
-    "patches": [],
+    "patches": [
+    ],
     "win_patches": [],
     "project_paths": [],
     "with_env" : "",
diff --git a/src/dring/account_const.h b/src/dring/account_const.h
index f5f689373413d0278e519614031ea601f634a822..f810064121cbff33aac0633327adc6b1634b6d5b 100644
--- a/src/dring/account_const.h
+++ b/src/dring/account_const.h
@@ -275,6 +275,7 @@ namespace TrustRequest {
 constexpr static const char FROM[] = "from";
 constexpr static const char RECEIVED[] = "received";
 constexpr static const char PAYLOAD[] = "payload";
+constexpr static const char CONVERSATIONID[] = "conversationId";
 
 } // namespace TrustRequest
 
diff --git a/src/jamidht/account_manager.cpp b/src/jamidht/account_manager.cpp
index 166fa19c6824eeb21cdbb4d98e72830859a39569..8eb2960a9f8c34f6b812947b9ec9a250111bd471 100644
--- a/src/jamidht/account_manager.cpp
+++ b/src/jamidht/account_manager.cpp
@@ -233,16 +233,18 @@ AccountManager::startSync(const OnNewDeviceCb& cb)
                       true,
                       [this, v](const std::shared_ptr<dht::crypto::Certificate>&,
                                 dht::InfoHash peer_account) mutable {
-                          JAMI_WARN("Got trust request from: %s / %s",
+                          JAMI_WARN("Got trust request from: %s / %s. ConversationId: %s",
                                     peer_account.toString().c_str(),
-                                    v.from.toString().c_str());
+                                    v.from.toString().c_str(),
+                                    v.conversationId.c_str());
                           if (info_)
                               if (info_->contacts->onTrustRequest(peer_account,
                                                                   v.from,
                                                                   time(nullptr),
                                                                   v.confirm,
+                                                                  v.conversationId,
                                                                   std::move(v.payload))) {
-                                  sendTrustRequestConfirm(peer_account);
+                                  sendTrustRequestConfirm(peer_account, v.conversationId);
                                   info_->contacts->saveTrustRequests();
                               }
                       });
@@ -306,7 +308,7 @@ AccountManager::foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>&
     }
 
     // Check cached OCSP response
-    if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD){
+    if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD) {
         JAMI_ERR("Certificate %s is disabled by cached OCSP response", crt->getId().to_c_str());
         return false;
     }
@@ -490,9 +492,12 @@ bool
 AccountManager::acceptTrustRequest(const std::string& from)
 {
     dht::InfoHash f(from);
-    if (info_ and info_->contacts->acceptTrustRequest(f)) {
-        sendTrustRequestConfirm(f);
-        syncDevices();
+    if (info_) {
+        auto req = info_->contacts->getTrustRequest(dht::InfoHash(from));
+        if (info_->contacts->acceptTrustRequest(f)) {
+            sendTrustRequestConfirm(f, req[DRing::Account::TrustRequest::CONVERSATIONID]);
+            syncDevices();
+        }
         return true;
     }
     return false;
@@ -506,7 +511,9 @@ AccountManager::discardTrustRequest(const std::string& from)
 }
 
 void
-AccountManager::sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload)
+AccountManager::sendTrustRequest(const std::string& to,
+                                 const std::string& convId,
+                                 const std::vector<uint8_t>& payload)
 {
     JAMI_WARN("AccountManager::sendTrustRequest");
     auto toH = dht::InfoHash(to);
@@ -521,22 +528,27 @@ AccountManager::sendTrustRequest(const std::string& to, const std::vector<uint8_
     if (info_->contacts->addContact(toH)) {
         syncDevices();
     }
-    forEachDevice(toH, [this, toH, payload](const dht::InfoHash& dev) {
+    forEachDevice(toH, [this, toH, convId, payload](const dht::InfoHash& dev) {
         JAMI_WARN("sending trust request to: %s / %s",
                   toH.toString().c_str(),
                   dev.toString().c_str());
         dht_->putEncrypted(dht::InfoHash::get("inbox:" + dev.toString()),
                            dev,
-                           dht::TrustRequest(DHT_TYPE_NS, "", payload));
+                           dht::TrustRequest(DHT_TYPE_NS, convId, payload));
     });
 }
 
 void
-AccountManager::sendTrustRequestConfirm(const dht::InfoHash& toH)
+AccountManager::sendTrustRequestConfirm(const dht::InfoHash& toH, const std::string& convId)
 {
     JAMI_WARN("AccountManager::sendTrustRequestConfirm");
-    dht::TrustRequest answer {DHT_TYPE_NS};
+    dht::TrustRequest answer {DHT_TYPE_NS, ""};
     answer.confirm = true;
+    answer.conversationId = convId;
+
+    if (!convId.empty() && info_)
+        info_->contacts->acceptConversation(convId);
+
     forEachDevice(toH, [this, toH, answer](const dht::InfoHash& dev) {
         JAMI_WARN("sending trust request reply: %s / %s",
                   toH.toString().c_str(),
diff --git a/src/jamidht/account_manager.h b/src/jamidht/account_manager.h
index 4a9cdd593ffd630b3e359a7370b62b32b8c2708f..b6b90deb4df634a71331dd4681a48edd8c0f2bb6 100644
--- a/src/jamidht/account_manager.h
+++ b/src/jamidht/account_manager.h
@@ -195,8 +195,11 @@ public:
     bool acceptTrustRequest(const std::string& from);
     bool discardTrustRequest(const std::string& from);
 
-    void sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload);
-    void sendTrustRequestConfirm(const dht::InfoHash& to);
+    void sendTrustRequest(const std::string& to,
+                          const std::string& convId,
+                          const std::vector<uint8_t>& payload);
+    void sendTrustRequestConfirm(const dht::InfoHash& to,
+                                 const std::string& conversationId); // TODO ideally no convId here
 
     // Contact
 
diff --git a/src/jamidht/archive_account_manager.cpp b/src/jamidht/archive_account_manager.cpp
index 561fef03d273d1f6996ceb35436558d58b567649..96326528c6108328cd654c36352007659c97ef2f 100644
--- a/src/jamidht/archive_account_manager.cpp
+++ b/src/jamidht/archive_account_manager.cpp
@@ -501,7 +501,12 @@ ArchiveAccountManager::onSyncData(DeviceSync&& sync)
 
     // Sync trust requests
     for (const auto& tr : sync.trust_requests)
-        info_->contacts->onTrustRequest(tr.first, tr.second.device, tr.second.received, false, {});
+        info_->contacts->onTrustRequest(tr.first,
+                                        tr.second.device,
+                                        tr.second.received,
+                                        false,
+                                        tr.second.conversationId,
+                                        {});
     info_->contacts->saveTrustRequests();
 }
 
diff --git a/src/jamidht/contact_list.cpp b/src/jamidht/contact_list.cpp
index 7dd66f81ad021b454e111bce5bcdaef9f523eb47..bcf8338432beb5c1bd9ff16c500faa9820b399f0 100644
--- a/src/jamidht/contact_list.cpp
+++ b/src/jamidht/contact_list.cpp
@@ -237,6 +237,7 @@ ContactList::loadTrustRequests()
                        tr.second.device,
                        tr.second.received,
                        false,
+                       tr.second.conversationId,
                        std::move(tr.second.payload));
 }
 
@@ -245,6 +246,7 @@ ContactList::onTrustRequest(const dht::InfoHash& peer_account,
                             const dht::InfoHash& peer_device,
                             time_t received,
                             bool confirm,
+                            const std::string& conversationId,
                             std::vector<uint8_t>&& payload)
 {
     bool accept = false;
@@ -273,7 +275,10 @@ ContactList::onTrustRequest(const dht::InfoHash& peer_account,
             // Add trust request
             req = trustRequests_
                       .emplace(peer_account,
-                               TrustRequest {peer_device, received, std::move(payload)})
+                               TrustRequest {peer_device,
+                                             conversationId,
+                                             received,
+                                             std::move(payload)})
                       .first;
         } else {
             // Update trust request
@@ -286,8 +291,10 @@ ContactList::onTrustRequest(const dht::InfoHash& peer_account,
                          peer_account.toString().c_str());
             }
         }
-        callbacks_.trustRequest(req->first.toString(), req->second.payload, received);
+        saveTrustRequests();
     }
+    // Note: call JamiAccount's callback to build ConversationRequest anyway
+    callbacks_.trustRequest(peer_account.toString(), conversationId, payload, received);
     return accept;
 }
 
@@ -303,12 +310,27 @@ ContactList::getTrustRequests() const
         ret.emplace_back(
             Map {{DRing::Account::TrustRequest::FROM, r.first.toString()},
                  {DRing::Account::TrustRequest::RECEIVED, std::to_string(r.second.received)},
+                 {DRing::Account::TrustRequest::CONVERSATIONID, r.second.conversationId},
                  {DRing::Account::TrustRequest::PAYLOAD,
                   std::string(r.second.payload.begin(), r.second.payload.end())}});
     }
     return ret;
 }
 
+std::map<std::string, std::string>
+ContactList::getTrustRequest(const dht::InfoHash& from) const
+{
+    using Map = std::map<std::string, std::string>;
+    auto r = trustRequests_.find(from);
+    if (r == trustRequests_.end())
+        return {};
+    return Map {{DRing::Account::TrustRequest::FROM, r->first.toString()},
+                {DRing::Account::TrustRequest::RECEIVED, std::to_string(r->second.received)},
+                {DRing::Account::TrustRequest::CONVERSATIONID, r->second.conversationId},
+                {DRing::Account::TrustRequest::PAYLOAD,
+                 std::string(r->second.payload.begin(), r->second.payload.end())}};
+}
+
 bool
 ContactList::acceptTrustRequest(const dht::InfoHash& from)
 {
@@ -320,15 +342,18 @@ ContactList::acceptTrustRequest(const dht::InfoHash& from)
         return false;
 
     // Clear trust request
-    auto treq = std::move(i->second);
     trustRequests_.erase(i);
     saveTrustRequests();
-
-    // Send confirmation
-    // account_.get().sendTrustRequestConfirm(from);
     return true;
 }
 
+void
+ContactList::acceptConversation(const std::string& convId)
+{
+    if (callbacks_.acceptConversation)
+        callbacks_.acceptConversation(convId);
+}
+
 bool
 ContactList::discardTrustRequest(const dht::InfoHash& from)
 {
@@ -506,16 +531,22 @@ ContactList::getSyncData() const
     static constexpr size_t MAX_TRUST_REQUESTS = 20;
     if (trustRequests_.size() <= MAX_TRUST_REQUESTS)
         for (const auto& req : trustRequests_)
-            sync_data.trust_requests
-                .emplace(req.first, TrustRequest {req.second.device, req.second.received, {}});
+            sync_data.trust_requests.emplace(req.first,
+                                             TrustRequest {req.second.device,
+                                                           req.second.conversationId,
+                                                           req.second.received,
+                                                           {}});
     else {
         size_t inserted = 0;
         auto req = trustRequests_.lower_bound(dht::InfoHash::getRandom());
         while (inserted++ < MAX_TRUST_REQUESTS) {
             if (req == trustRequests_.end())
                 req = trustRequests_.begin();
-            sync_data.trust_requests
-                .emplace(req->first, TrustRequest {req->second.device, req->second.received, {}});
+            sync_data.trust_requests.emplace(req->first,
+                                             TrustRequest {req->second.device,
+                                                           req->second.conversationId,
+                                                           req->second.received,
+                                                           {}});
             ++req;
         }
     }
@@ -542,42 +573,4 @@ ContactList::syncDevice(const dht::InfoHash& device, const time_point& syncDate)
     return true;
 }
 
-/*
-void
-ContactList::onSyncData(DeviceSync&& sync)
-{
-    auto it = knownDevices_.find(sync.from);
-    if (it == knownDevices_.end()) {
-        JAMI_WARN("[Contacts] dropping sync data from unknown device");
-        return;
-    }
-    auto sync_date = clock::time_point(clock::duration(sync.date));
-    if (it->second.last_sync >= sync_date) {
-        JAMI_DBG("[Contacts] dropping outdated sync data");
-        return;
-    }
-
-    // Sync known devices
-    JAMI_DBG("[Contacts] received device sync data (%lu devices, %lu contacts)",
-sync.devices_known.size(), sync.peers.size()); for (const auto& d : sync.devices_known) {
-        account_.get().findCertificate(d.first, [this,d](const
-std::shared_ptr<dht::crypto::Certificate>& crt) { if (not crt) return;
-            //std::lock_guard<std::mutex> lock(deviceListMutex_);
-            foundAccountDevice(crt, d.second);
-        });
-    }
-    saveKnownDevices();
-
-    // Sync contacts
-    for (const auto& peer : sync.peers)
-        updateContact(peer.first, peer.second);
-    saveContacts();
-
-    // Sync trust requests
-    for (const auto& tr : sync.trust_requests)
-        onTrustRequest(tr.first, tr.second.device, tr.second.received, false, {});
-
-    it->second.last_sync = sync_date;
-}
-*/
 } // namespace jami
diff --git a/src/jamidht/contact_list.h b/src/jamidht/contact_list.h
index 0fe48aa80de9953fefbd96030098d276f0a59f36..4340ef2d836fc768f785469d9cac7308f44ade6a 100644
--- a/src/jamidht/contact_list.h
+++ b/src/jamidht/contact_list.h
@@ -40,8 +40,9 @@ public:
 
     using OnContactAdded = std::function<void(const std::string&, bool)>;
     using OnContactRemoved = std::function<void(const std::string&, bool)>;
-    using OnIncomingTrustRequest
-        = std::function<void(const std::string&, const std::vector<uint8_t>&, time_t)>;
+    using OnIncomingTrustRequest = std::function<
+        void(const std::string&, const std::string&, const std::vector<uint8_t>&, time_t)>;
+    using OnAcceptConversation = std::function<void(const std::string&)>;
     using OnDevicesChanged = std::function<void(const std::map<dht::InfoHash, KnownDevice>&)>;
 
     struct OnChangeCallback
@@ -50,6 +51,7 @@ public:
         OnContactRemoved contactRemoved;
         OnIncomingTrustRequest trustRequest;
         OnDevicesChanged devicesChanged;
+        OnAcceptConversation acceptConversation;
     };
 
     ContactList(const std::shared_ptr<crypto::Certificate>& cert,
@@ -106,8 +108,11 @@ public:
                         const dht::InfoHash& peer_device,
                         time_t received,
                         bool confirm,
+                        const std::string& conversationId,
                         std::vector<uint8_t>&& payload);
     std::vector<std::map<std::string, std::string>> getTrustRequests() const;
+    std::map<std::string, std::string> getTrustRequest(const dht::InfoHash& from) const;
+    void acceptConversation(const std::string& convId); // ToDO this is a bit dirty imho
     bool acceptTrustRequest(const dht::InfoHash& from);
     bool discardTrustRequest(const dht::InfoHash& from);
 
diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
index 1bd55baea15c441ed7b2dcbbdc23faabbd1db21d..72b0782684d2daf7246bb1294f2808c2a89c0d4d 100644
--- a/src/jamidht/conversation.cpp
+++ b/src/jamidht/conversation.cpp
@@ -22,6 +22,7 @@
 #include "fileutils.h"
 #include "jamiaccount.h"
 #include "conversationrepository.h"
+#include "client/ring_signal.h"
 
 #include <json/json.h>
 #include <string_view>
@@ -33,13 +34,21 @@ namespace jami {
 class Conversation::Impl
 {
 public:
+    Impl(const std::weak_ptr<JamiAccount>& account,
+         ConversationMode mode,
+         const std::string& otherMember = "")
+        : account_(account)
+    {
+        repository_ = ConversationRepository::createConversation(account, mode, otherMember);
+        if (!repository_) {
+            throw std::logic_error("Couldn't create repository");
+        }
+    }
+
     Impl(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId)
         : account_(account)
     {
-        if (conversationId.empty())
-            repository_ = ConversationRepository::createConversation(account);
-        else
-            repository_ = std::make_unique<ConversationRepository>(account, conversationId);
+        repository_ = std::make_unique<ConversationRepository>(account, conversationId);
         if (!repository_) {
             throw std::logic_error("Couldn't create repository");
         }
@@ -65,6 +74,8 @@ public:
     std::unique_ptr<ConversationRepository> repository_;
     std::weak_ptr<JamiAccount> account_;
     std::atomic_bool isRemoving_ {false};
+    std::vector<std::map<std::string, std::string>> convCommitToMap(
+        const std::vector<ConversationCommit>& commits) const;
     std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "",
                                                                  const std::string& toMessage = "",
                                                                  size_t n = 0);
@@ -72,6 +83,9 @@ public:
     std::mutex pullcbsMtx_ {};
     std::set<std::string> fetchingRemotes_ {}; // store current remote in fetch
     std::deque<std::tuple<std::string, std::string, OnPullCb>> pullcbs_ {};
+
+    // Mutex used to protect write index (one commit at a time)
+    std::mutex writeMtx_ {};
 };
 
 bool
@@ -103,19 +117,10 @@ Conversation::Impl::repoPath() const
 }
 
 std::vector<std::map<std::string, std::string>>
-Conversation::Impl::loadMessages(const std::string& fromMessage,
-                                 const std::string& toMessage,
-                                 size_t n)
+Conversation::Impl::convCommitToMap(const std::vector<ConversationCommit>& commits) const
 {
-    if (!repository_)
-        return {};
-    std::vector<ConversationCommit> convCommits;
-    if (toMessage.empty())
-        convCommits = repository_->logN(fromMessage, n);
-    else
-        convCommits = repository_->log(fromMessage, toMessage);
     std::vector<std::map<std::string, std::string>> result = {};
-    for (const auto& commit : convCommits) {
+    for (const auto& commit : commits) {
         auto authorDevice = commit.author.email;
         auto cert = tls::CertificateStore::instance().getCertificate(authorDevice);
         if (!cert && cert->issuer) {
@@ -124,7 +129,7 @@ Conversation::Impl::loadMessages(const std::string& fromMessage,
         auto authorId = cert->issuer->getId().toString();
         std::string parents;
         auto parentsSize = commit.parents.size();
-        for (auto i = 0; i < parentsSize; ++i) {
+        for (std::size_t i = 0; i < parentsSize; ++i) {
             parents += commit.parents[i];
             if (i != parentsSize - 1)
                 parents += ",";
@@ -134,6 +139,7 @@ Conversation::Impl::loadMessages(const std::string& fromMessage,
             type = "merge";
         }
         std::string body {};
+        std::map<std::string, std::string> message;
         if (type.empty()) {
             std::string err;
             Json::Value cm;
@@ -143,23 +149,48 @@ Conversation::Impl::loadMessages(const std::string& fromMessage,
                               commit.commit_msg.data() + commit.commit_msg.size(),
                               &cm,
                               &err)) {
-                type = cm["type"].asString();
-                body = cm["body"].asString();
+                for (auto const& id : cm.getMemberNames()) {
+                    if (id == "type") {
+                        type = cm[id].asString();
+                        continue;
+                    }
+                    message.insert({id, cm[id].asString()});
+                }
             } else {
                 JAMI_WARN("%s", err.c_str());
             }
         }
-        std::map<std::string, std::string> message {{"id", commit.id},
-                                                    {"parents", parents},
-                                                    {"author", authorId},
-                                                    {"type", type},
-                                                    {"body", body},
-                                                    {"timestamp", std::to_string(commit.timestamp)}};
+        message["id"] = commit.id;
+        message["parents"] = parents;
+        message["author"] = authorId;
+        message["type"] = type;
+        message["timestamp"] = std::to_string(commit.timestamp);
         result.emplace_back(message);
     }
     return result;
 }
 
+std::vector<std::map<std::string, std::string>>
+Conversation::Impl::loadMessages(const std::string& fromMessage,
+                                 const std::string& toMessage,
+                                 size_t n)
+{
+    if (!repository_)
+        return {};
+    std::vector<ConversationCommit> convCommits;
+    if (toMessage.empty())
+        convCommits = repository_->logN(fromMessage, n);
+    else
+        convCommits = repository_->log(fromMessage, toMessage);
+    return convCommitToMap(convCommits);
+}
+
+Conversation::Conversation(const std::weak_ptr<JamiAccount>& account,
+                           ConversationMode mode,
+                           const std::string& otherMember)
+    : pimpl_ {new Impl {account, mode, otherMember}}
+{}
+
 Conversation::Conversation(const std::weak_ptr<JamiAccount>& account,
                            const std::string& conversationId)
     : pimpl_ {new Impl {account, conversationId}}
@@ -182,6 +213,20 @@ Conversation::id() const
 std::string
 Conversation::addMember(const std::string& contactUri)
 {
+    try {
+        if (mode() == ConversationMode::ONE_TO_ONE) {
+            // Only authorize to add left members
+            auto initialMembers = getInitialMembers();
+            auto it = std::find(initialMembers.begin(), initialMembers.end(), contactUri);
+            if (it == initialMembers.end()) {
+                JAMI_WARN("Cannot add new member in one to one conversation");
+                return {};
+            }
+        }
+    } catch (const std::exception& e) {
+        JAMI_WARN("Cannot get mode: %s", e.what());
+        return {};
+    }
     if (isMember(contactUri, true)) {
         JAMI_WARN("Could not add member %s because it's already a member", contactUri.c_str());
         return {};
@@ -220,40 +265,18 @@ std::vector<std::map<std::string, std::string>>
 Conversation::getMembers(bool includeInvited) const
 {
     std::vector<std::map<std::string, std::string>> result;
+
     auto shared = pimpl_->account_.lock();
     if (!shared)
         return result;
-
-    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());
+    auto members = pimpl_->repository_->members();
+    for (const auto& member : members) {
+        if (member.role == MemberRole::BANNED)
             continue;
-        }
-        std::map<std::string, std::string>
-            details {{"uri", certificate.substr(0, certificate.size() - std::string(".crt").size())},
-                     {"role", "admin"}};
-        result.emplace_back(details);
-    }
-    for (const auto& certificate : fileutils::readDirectory(membersPath)) {
-        if (certificate.find(".crt") == std::string::npos) {
-            JAMI_WARN("Incorrect file found: %s/%s", membersPath.c_str(), certificate.c_str());
+        if (member.role == MemberRole::INVITED && !includeInvited)
             continue;
-        }
-        std::map<std::string, std::string>
-            details {{"uri", certificate.substr(0, certificate.size() - std::string(".crt").size())},
-                     {"role", "member"}};
-        result.emplace_back(details);
+        result.emplace_back(member.map());
     }
-    if (includeInvited) {
-        for (const auto& uri : fileutils::readDirectory(invitedPath)) {
-            std::map<std::string, std::string> details {{"uri", uri}, {"role", "invited"}};
-            result.emplace_back(details);
-        }
-    }
-
     return result;
 }
 
@@ -281,16 +304,25 @@ Conversation::isMember(const std::string& uri, bool includeInvited) const
         pathsToCheck.emplace_back(invitedPath);
     for (const auto& path : pathsToCheck) {
         for (const auto& certificate : fileutils::readDirectory(path)) {
-            if (certificate.find(".crt") == std::string::npos) {
+            if (path != invitedPath && certificate.find(".crt") == std::string::npos) {
                 JAMI_WARN("Incorrect file found: %s/%s", path.c_str(), certificate.c_str());
                 continue;
             }
-            auto crtUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
+            auto crtUri = certificate;
+            if (crtUri.find(".crt") != std::string::npos)
+                crtUri = crtUri.substr(0, crtUri.size() - std::string(".crt").size());
             if (crtUri == uri)
                 return true;
         }
     }
 
+    if (includeInvited && mode() == ConversationMode::ONE_TO_ONE) {
+        for (const auto& member : getInitialMembers()) {
+            if (member == uri)
+                return true;
+        }
+    }
+
     return false;
 }
 
@@ -315,10 +347,17 @@ Conversation::sendMessage(const std::string& message,
     Json::Value json;
     json["body"] = message;
     json["type"] = type;
+    return sendMessage(json, parent);
+}
+
+std::string
+Conversation::sendMessage(const Json::Value& value, const std::string& parent)
+{
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
-    return pimpl_->repository_->commitMessage(Json::writeString(wbuilder, json));
+    std::lock_guard<std::mutex> lk(pimpl_->writeMtx_);
+    return pimpl_->repository_->commitMessage(Json::writeString(wbuilder, value));
 }
 
 void
@@ -359,36 +398,46 @@ Conversation::lastCommitId() const
 bool
 Conversation::fetchFrom(const std::string& uri)
 {
-    // TODO check if device id or account id
     return pimpl_->repository_->fetch(uri);
 }
 
-bool
+std::vector<std::map<std::string, std::string>>
 Conversation::mergeHistory(const std::string& uri)
 {
     if (not pimpl_ or not pimpl_->repository_) {
         JAMI_WARN("Invalid repo. Abort merge");
-        return false;
+        return {};
     }
     auto remoteHead = pimpl_->repository_->remoteHead(uri);
     if (remoteHead.empty()) {
         JAMI_WARN("Could not get HEAD of %s", uri.c_str());
-        return false;
+        return {};
     }
 
+    std::unique_lock<std::mutex> lk(pimpl_->writeMtx_);
     // Validate commit
-    if (!pimpl_->repository_->validFetch(uri)) {
+    auto newCommits = pimpl_->repository_->validFetch(uri);
+    if (newCommits.empty()) {
         JAMI_ERR("Could not validate history with %s", uri.c_str());
-        return false;
+        return {};
     }
 
     // If validated, merge
     if (!pimpl_->repository_->merge(remoteHead)) {
         JAMI_ERR("Could not merge history with %s", uri.c_str());
-        return false;
+        return {};
     }
+    lk.unlock();
+
     JAMI_DBG("Successfully merge history with %s", uri.c_str());
-    return true;
+    auto result = pimpl_->convCommitToMap(newCommits);
+    for (const auto& commit : result) {
+        auto it = commit.find("type");
+        if (it != commit.end() && it->second == "member") {
+            pimpl_->repository_->refreshMembers();
+        }
+    }
+    return result;
 }
 
 void
@@ -458,9 +507,10 @@ Conversation::pull(const std::string& uri, OnPullCb&& cb, std::string commitId)
                 cb(false, {});
                 continue;
             }
-            // auto newCommits = sthis_->mergeHistory(deviceId);
-            // auto ok = newCommits.empty();
-            // if (cb) cb(true, std::move(newCommits));
+            auto newCommits = sthis_->mergeHistory(deviceId);
+            auto ok = !newCommits.empty();
+            if (cb)
+                cb(ok, std::move(newCommits));
         }
     });
 }
@@ -514,4 +564,23 @@ Conversation::erase()
     pimpl_->repository_->erase();
 }
 
+ConversationMode
+Conversation::mode() const
+{
+    return pimpl_->repository_->mode();
+}
+
+std::vector<std::string>
+Conversation::getInitialMembers() const
+{
+    return pimpl_->repository_->getInitialMembers();
+}
+
+bool
+Conversation::isInitialMember(const std::string& uri) const
+{
+    auto members = getInitialMembers();
+    return std::find(members.begin(), members.end(), uri) != members.end();
+}
+
 } // namespace jami
\ No newline at end of file
diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h
index f3d93a2d1283fd8def893ea6c8af48791ee07a3c..eb7e5365b6a7b9ce03656dab571554497a5d5969 100644
--- a/src/jamidht/conversation.h
+++ b/src/jamidht/conversation.h
@@ -24,11 +24,13 @@
 #include <vector>
 #include <map>
 #include <memory>
+#include <json/json.h>
 
 namespace jami {
 
 class JamiAccount;
 class ConversationRepository;
+enum class ConversationMode;
 
 using OnPullCb = std::function<void(bool fetchOk,std::vector<std::map<std::string, std::string>>&& newMessages)>;
 using OnLoadMessages = std::function<void(std::vector<std::map<std::string, std::string>>&& messages)>;
@@ -37,6 +39,7 @@ using OnLoadMessages = std::function<void(std::vector<std::map<std::string, std:
 class Conversation : public std::enable_shared_from_this<Conversation>
 {
 public:
+    Conversation(const std::weak_ptr<JamiAccount>& account, ConversationMode mode, const std::string& otherMember = "");
     Conversation(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId = "");
     Conversation(const std::weak_ptr<JamiAccount>& account,
                  const std::string& remoteDevice,
@@ -58,7 +61,7 @@ public:
      * @return a vector of member details:
      * {
      *  "uri":"xxx",
-     *  "role":"member/admin",
+     *  "role":"member/admin/invited",
      *  "lastRead":"id"
      *  ...
      * }
@@ -83,6 +86,7 @@ public:
     std::string sendMessage(const std::string& message,
                             const std::string& type = "text/plain",
                             const std::string& parent = "");
+    std::string sendMessage(const Json::Value& message, const std::string& parent = "");
     /**
      * Get a range of messages
      * @param cb        The callback when loaded
@@ -112,9 +116,9 @@ public:
     /**
      * Analyze if merge is possible and merge history
      * @param uri       the peer
-     * @return if the operation was successful
+     * @return new commits
      */
-    bool mergeHistory(const std::string& uri);
+    std::vector<std::map<std::string, std::string>> mergeHistory(const std::string& uri);
 
     /**
      * Fetch and merge from peer
@@ -154,6 +158,18 @@ public:
      */
     void erase();
 
+    /**
+     * Get conversation's mode
+     * @return the mode
+     */
+    ConversationMode mode() const;
+
+    /**
+     * One to one util, get initial members
+     * @return initial members
+     */
+    std::vector<std::string> getInitialMembers() const;
+    bool isInitialMember(const std::string& uri) const;
 private:
 
     std::shared_ptr<Conversation> shared()
diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index cf0c15671cd3c479bd751d8d245bfea51f06c6e8..05e000c91b56bc56dda7e530ca52fc09d3275bf6 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -31,10 +31,11 @@ using random_device = dht::crypto::random_device;
 #include <json/json.h>
 #include <regex>
 #include <exception>
+#include <optional>
 
 using namespace std::string_view_literals;
 constexpr auto DIFF_REGEX = " +\\| +[0-9]+.*"sv;
-constexpr size_t MAX_FETCH_SIZE {256*1024*1024}; // 256Mb
+constexpr size_t MAX_FETCH_SIZE {256 * 1024 * 1024}; // 256Mb
 
 namespace jami {
 
@@ -45,16 +46,26 @@ public:
         : account_(account)
         , id_(id)
     {
-        auto shared = account.lock();
+        repository_ = repository();
+        if (!repository_)
+            throw std::logic_error("Couldn't initialize repo");
+
+        initMembers();
+    }
+
+    GitRepository repository() const
+    {
+        // TODO use only one object
+        auto shared = account_.lock();
         if (!shared)
-            throw std::logic_error("No account detected when loading conversation");
+            return {nullptr, git_repository_free};
         auto path = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + id_;
         git_repository* repo = nullptr;
         // TODO share this repo with GitServer
         if (git_repository_open(&repo, path.c_str()) != 0)
-            throw std::logic_error("Couldn't open " + path);
-        repository_ = {std::move(repo), git_repository_free};
+            return {nullptr, git_repository_free};
+        return {std::move(repo), git_repository_free};
     }
 
     GitSignature signature();
@@ -85,6 +96,7 @@ public:
 
     bool add(const std::string& path);
     std::string commit(const std::string& msg);
+    ConversationMode mode() const;
 
     GitDiff diff(const std::string& idNew, const std::string& idOld) const;
     std::string diffStats(const std::string& newId, const std::string& oldId) const;
@@ -99,9 +111,24 @@ public:
     GitTree treeAtCommit(const std::string& commitId) const;
     std::string getCommitType(const std::string& commitMsg) const;
 
+    std::vector<std::string> getInitialMembers() const;
+
+    GitRepository repository_ {nullptr, git_repository_free};
+
     std::weak_ptr<JamiAccount> account_;
     const std::string id_;
-    GitRepository repository_ {nullptr, git_repository_free};
+    mutable std::optional<ConversationMode> mode_ {};
+
+    // Members utils
+    mutable std::mutex membersMtx_ {};
+    std::vector<ConversationMember> members_ {};
+
+    std::vector<ConversationMember> members() const {
+        std::lock_guard<std::mutex> lk(membersMtx_);
+        return members_;
+    }
+
+    void initMembers();
 };
 
 /////////////////////////////////////////////////////////////////////////////////
@@ -115,7 +142,8 @@ GitRepository
 create_empty_repository(const std::string& path)
 {
     git_repository* repo = nullptr;
-    git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
+    git_repository_init_options opts;
+    git_repository_init_options_init(&opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
     opts.flags |= GIT_REPOSITORY_INIT_MKPATH;
     opts.initial_head = "main";
     if (git_repository_init_ext(&repo, path.c_str(), &opts) < 0) {
@@ -134,7 +162,7 @@ git_add_all(git_repository* repo)
 {
     // git add -A
     git_index* index_ptr = nullptr;
-    git_strarray array = {0};
+    git_strarray array {nullptr, 0};
     if (git_repository_index(&index_ptr, repo) < 0) {
         JAMI_ERR("Could not open repository index");
         return false;
@@ -229,12 +257,17 @@ add_initial_files(GitRepository& repo, const std::shared_ptr<JamiAccount>& accou
 
 /**
  * Sign and create the initial commit
- * @param repo      The git repository
- * @param account   The account who signs
+ * @param repo          The git repository
+ * @param account       The account who signs
+ * @param mode          The mode
+ * @param otherMember   If one to one
  * @return          The first commit hash or empty if failed
  */
 std::string
-initial_commit(GitRepository& repo, const std::shared_ptr<JamiAccount>& account)
+initial_commit(GitRepository& repo,
+               const std::shared_ptr<JamiAccount>& account,
+               ConversationMode mode,
+               const std::string& otherMember = "")
 {
     auto deviceId = std::string(account->currentDeviceId());
     auto name = account->getDisplayName();
@@ -271,12 +304,22 @@ initial_commit(GitRepository& repo, const std::shared_ptr<JamiAccount>& account)
     }
     GitTree tree = {tree_ptr, git_tree_free};
 
+    Json::Value json;
+    json["mode"] = static_cast<int>(mode);
+    if (mode == ConversationMode::ONE_TO_ONE) {
+        json["invited"] = otherMember;
+    }
+    json["type"] = "initial";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+
     if (git_commit_create_buffer(&to_sign,
                                  repo.get(),
                                  sig.get(),
                                  sig.get(),
                                  nullptr,
-                                 "Initial commit",
+                                 Json::writeString(wbuilder, json).c_str(),
                                  tree.get(),
                                  0,
                                  nullptr)
@@ -422,7 +465,7 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str
 
     auto account = account_.lock();
     if (!account)
-        false;
+        return false;
     // git commit -S
     auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
     auto signed_buf = account->identity().first->sign(to_sign_vec);
@@ -466,6 +509,7 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is
 {
     // Initialize target
     git_reference* target_ref_ptr = nullptr;
+    auto repo = repository();
     if (is_unborn) {
         git_reference* head_ref_ptr = nullptr;
         // HEAD reference is unborn, lookup manually so we don't try to resolve it
@@ -522,13 +566,14 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is
     }
     git_reference_free(new_target_ref);
 
-    return 0;
+    return true;
 }
 
 bool
 ConversationRepository::Impl::add(const std::string& path)
 {
-    if (!repository_)
+    auto repo = repository();
+    if (!repo)
         return false;
     git_index* index_ptr = nullptr;
     if (git_repository_index(&index_ptr, repository_.get()) < 0) {
@@ -640,8 +685,9 @@ ConversationRepository::Impl::checkVote(const std::string& userDevice,
         return false;
     }
 
-    if (base_match[3] != userUri) {
-        JAMI_ERR("Admin voted for other user: %s vs %s", userUri.c_str(), base_match[3]);
+    std::string matchedUri = base_match[3];
+    if (matchedUri != userUri) {
+        JAMI_ERR("Admin voted for other user: %s vs %s", userUri.c_str(), matchedUri.c_str());
         return false;
     }
     std::string votedUri = base_match[2];
@@ -709,7 +755,17 @@ ConversationRepository::Impl::checkValidAdd(const std::string& userDevice,
         return false;
     auto userUri = cert->issuer->getId().toString();
 
+    auto repo = repository();
     std::string repoPath = git_repository_workdir(repository_.get());
+    if (mode() == ConversationMode::ONE_TO_ONE) {
+        auto initialMembers = getInitialMembers();
+        auto it = std::find(initialMembers.begin(), initialMembers.end(), uriMember);
+        if (it == initialMembers.end()) {
+            JAMI_ERR("Invalid add in one to one conversation: %s", uriMember.c_str());
+            return false;
+        }
+    }
+
     // Check that only /invited/uri.crt is added & deviceFile & CRLs
     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
     if (changedFiles.size() == 0) {
@@ -785,7 +841,9 @@ ConversationRepository::Impl::checkValidJoins(const std::string& userDevice,
     auto userUri = cert->issuer->getId().toString();
     // Check no other files changed
     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
-    if (changedFiles.size() != 3) {
+    auto oneone = mode() == ConversationMode::ONE_TO_ONE;
+    std::size_t wantedChanged = oneone ? 2 : 3;
+    if (changedFiles.size() != wantedChanged) {
         return false;
     }
 
@@ -800,13 +858,15 @@ ConversationRepository::Impl::checkValidJoins(const std::string& userDevice,
         return false;
 
     // Check /invited removed
-    if (fileAtTree(invitedFile, treeNew)) {
-        JAMI_ERR("%s invited not removed", userUri.c_str());
-        return false;
-    }
-    if (!fileAtTree(invitedFile, treeOld)) {
-        JAMI_ERR("%s invited not found", userUri.c_str());
-        return false;
+    if (!oneone) {
+        if (fileAtTree(invitedFile, treeNew)) {
+            JAMI_ERR("%s invited not removed", userUri.c_str());
+            return false;
+        }
+        if (!fileAtTree(invitedFile, treeOld)) {
+            JAMI_ERR("%s invited not found", userUri.c_str());
+            return false;
+        }
     }
 
     // Check /members added
@@ -924,6 +984,7 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
     // If not for self check that vote is valid and not added
     auto nbAdmins = 0;
     auto nbVotes = 0;
+    auto repo = repository();
     std::string repoPath = git_repository_workdir(repository_.get());
     for (const auto& certificate : fileutils::readDirectory(repoPath + "admins")) {
         if (certificate.find(".crt") == std::string::npos) {
@@ -1000,6 +1061,13 @@ ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
     auto userUri = cert->issuer->getId().toString();
     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, ""));
 
+    try {
+        mode();
+    } catch (...) {
+        JAMI_ERR("Invalid mode detected for commit: %s", commitId.c_str());
+        return false;
+    }
+
     auto hasDevice = false, hasAdmin = false;
     std::string adminsFile = std::string("admins") + DIR_SEPARATOR_STR + userUri + ".crt";
     std::string deviceFile = std::string("devices") + DIR_SEPARATOR_STR + userDevice + ".crt";
@@ -1028,9 +1096,6 @@ ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
 std::string
 ConversationRepository::Impl::commit(const std::string& msg)
 {
-    if (!repository_)
-        return {};
-
     auto account = account_.lock();
     if (!account)
         return {};
@@ -1049,6 +1114,7 @@ ConversationRepository::Impl::commit(const std::string& msg)
 
     // Retrieve current index
     git_index* index_ptr = nullptr;
+    auto repo = repository();
     if (git_repository_index(&index_ptr, repository_.get()) < 0) {
         JAMI_ERR("Could not open repository index");
         return {};
@@ -1126,6 +1192,51 @@ ConversationRepository::Impl::commit(const std::string& msg)
     return commit_str ? commit_str : "";
 }
 
+ConversationMode
+ConversationRepository::Impl::mode() const
+{
+    // If already retrieven, return it, else get it from first commit
+    if (mode_ != std::nullopt)
+        return *mode_;
+
+    auto lastMsg = log(id_, "", 1);
+    if (lastMsg.size() == 0) {
+        throw std::logic_error("Can't retrieve first commit");
+    }
+    auto commitMsg = lastMsg[0].commit_msg;
+
+    std::string err;
+    Json::Value root;
+    Json::CharReaderBuilder rbuilder;
+    auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+    if (!reader->parse(commitMsg.data(), commitMsg.data() + commitMsg.size(), &root, &err)) {
+        throw std::logic_error("Can't retrieve first commit");
+    }
+    if (!root.isMember("mode")) {
+        throw std::logic_error("No mode detected for initial commit");
+    }
+    int mode = root["mode"].asInt();
+
+    switch (mode) {
+    case 0:
+        mode_ = ConversationMode::ONE_TO_ONE;
+        break;
+    case 1:
+        mode_ = ConversationMode::ADMIN_INVITES_ONLY;
+        break;
+    case 2:
+        mode_ = ConversationMode::INVITES_ONLY;
+        break;
+    case 3:
+        mode_ = ConversationMode::PUBLIC;
+        break;
+    default:
+        throw std::logic_error("Incorrect mode detected");
+        break;
+    }
+    return *mode_;
+}
+
 std::string
 ConversationRepository::Impl::diffStats(const std::string& newId, const std::string& oldId) const
 {
@@ -1137,8 +1248,10 @@ ConversationRepository::Impl::diffStats(const std::string& newId, const std::str
 GitDiff
 ConversationRepository::Impl::diff(const std::string& idNew, const std::string& idOld) const
 {
-    if (!repository_)
+    if (!repository_) {
+        JAMI_ERR("Cannot get reference for HEAD");
         return {nullptr, git_diff_free};
+    }
 
     // Retrieve tree for commit new
     git_oid oid;
@@ -1206,6 +1319,7 @@ std::vector<ConversationCommit>
 ConversationRepository::Impl::behind(const std::string& from) const
 {
     git_oid oid_local, oid_remote;
+    auto repo = repository();
     if (git_reference_name_to_id(&oid_local, repository_.get(), "HEAD") < 0) {
         JAMI_ERR("Cannot get reference for HEAD");
         return {};
@@ -1233,6 +1347,7 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to
     std::vector<ConversationCommit> commits {};
 
     git_oid oid;
+    auto repo = repository();
     if (from.empty()) {
         if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) {
             JAMI_ERR("Cannot get reference for HEAD");
@@ -1254,8 +1369,7 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to
     GitRevWalker walker {walker_ptr, git_revwalk_free};
     git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL);
 
-    auto x = git_oid_tostr_s(&oid);
-    for (auto idx = 0; !git_revwalk_next(&oid, walker.get()); ++idx) {
+    for (auto idx = 0u; !git_revwalk_next(&oid, walker.get()); ++idx) {
         if (n != 0 && idx == n) {
             break;
         }
@@ -1329,6 +1443,7 @@ ConversationRepository::Impl::treeAtCommit(const std::string& commitId) const
 {
     git_oid oid;
     git_commit* commit = nullptr;
+    auto repo = repository();
     if (git_oid_fromstr(&oid, commitId.c_str()) < 0
         || git_commit_lookup(&commit, repository_.get(), &oid) < 0) {
         JAMI_WARN("Failed to look up commit %s", commitId.c_str());
@@ -1359,6 +1474,82 @@ ConversationRepository::Impl::getCommitType(const std::string& commitMsg) const
     return type;
 }
 
+std::vector<std::string>
+ConversationRepository::Impl::getInitialMembers() const
+{
+    auto firstCommit = log(id_, "", 1);
+    if (firstCommit.size() == 0) {
+        return {};
+    }
+    auto commit = firstCommit[0];
+
+    auto authorDevice = commit.author.email;
+    auto cert = tls::CertificateStore::instance().getCertificate(authorDevice);
+    if (!cert && cert->issuer) {
+        return {};
+    }
+    auto authorId = cert->issuer->getId().toString();
+    if (mode() == ConversationMode::ONE_TO_ONE) {
+        std::string err;
+        Json::Value root;
+        Json::CharReaderBuilder rbuilder;
+        auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+        if (!reader->parse(commit.commit_msg.data(),
+                           commit.commit_msg.data() + commit.commit_msg.size(),
+                           &root,
+                           &err)) {
+            return {authorId};
+        }
+        if (root.isMember("invited") && root["invited"].asString() != authorId)
+            return {authorId, root["invited"].asString()};
+    }
+    return {authorId};
+}
+
+void
+ConversationRepository::Impl::initMembers()
+{
+    if (!repository_)
+        return;
+
+    std::vector<std::string> uris;
+    std::lock_guard<std::mutex> lk(membersMtx_);
+    members_.clear();
+    std::string repoPath = git_repository_workdir(repository_.get());
+    std::vector<std::string> paths = {
+        repoPath + DIR_SEPARATOR_STR + "invited",
+        repoPath + DIR_SEPARATOR_STR + "admins",
+        repoPath + DIR_SEPARATOR_STR + "members",
+        repoPath + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + "members"
+    };
+    std::vector<MemberRole> roles = {
+        MemberRole::INVITED,
+        MemberRole::ADMIN,
+        MemberRole::MEMBER,
+        MemberRole::BANNED,
+    };
+
+    auto i = 0;
+    for (const auto& p: paths) {
+        for (const auto& f : fileutils::readDirectory(p)) {
+            auto pos = f.find(".crt");
+            auto uri = f.substr(0, pos);
+            uris.emplace_back(uri);
+            members_.emplace_back(ConversationMember {uri, roles[i]});
+        }
+        ++i;
+    }
+
+    if (mode() == ConversationMode::ONE_TO_ONE) {
+        for (const auto& member : getInitialMembers()) {
+            auto it = std::find(uris.begin(), uris.end(), member);
+            if (it == uris.end()) {
+                members_.emplace_back(ConversationMember {member, MemberRole::INVITED});
+            }
+        }
+    }
+}
+
 std::string
 ConversationRepository::Impl::diffStats(const GitDiff& diff) const
 {
@@ -1382,7 +1573,9 @@ ConversationRepository::Impl::diffStats(const GitDiff& diff) const
 //////////////////////////////////
 
 std::unique_ptr<ConversationRepository>
-ConversationRepository::createConversation(const std::weak_ptr<JamiAccount>& account)
+ConversationRepository::createConversation(const std::weak_ptr<JamiAccount>& account,
+                                           ConversationMode mode,
+                                           const std::string& otherMember)
 {
     auto shared = account.lock();
     if (!shared)
@@ -1414,7 +1607,7 @@ ConversationRepository::createConversation(const std::weak_ptr<JamiAccount>& acc
     }
 
     // Commit changes
-    auto id = initial_commit(repo, shared);
+    auto id = initial_commit(repo, shared, mode, otherMember);
     if (id.empty()) {
         JAMI_ERR("Couldn't create initial commit in %s", tmpPath.c_str());
         fileutils::removeAll(tmpPath, true);
@@ -1451,13 +1644,15 @@ ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& acco
 
     git_clone_options clone_options;
     git_clone_options_init(&clone_options, GIT_CLONE_OPTIONS_VERSION);
-    clone_options.fetch_opts = GIT_FETCH_OPTIONS_INIT;
+    git_fetch_options_init(&clone_options.fetch_opts, GIT_FETCH_OPTIONS_VERSION);
     size_t received_bytes = 0;
     clone_options.fetch_opts.callbacks.payload = static_cast<void*>(&received_bytes);
-    clone_options.fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void* payload) {
+    clone_options.fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats,
+                                                              void* payload) {
         *(static_cast<size_t*>(payload)) += stats->received_bytes;
         if (*(static_cast<size_t*>(payload)) > MAX_FETCH_SIZE) {
-            JAMI_ERR("Abort fetching repository, the fetch is too big: %lu bytes", *(static_cast<size_t*>(payload)));
+            JAMI_ERR("Abort fetching repository, the fetch is too big: %lu bytes",
+                     *(static_cast<size_t*>(payload)));
             return -1;
         }
         return 0;
@@ -1641,6 +1836,12 @@ ConversationRepository::addMember(const std::string& uri)
     std::string path = "invited/" + uri;
     if (!pimpl_->add(path.c_str()))
         return {};
+
+    {
+        std::lock_guard<std::mutex> lk(pimpl_->membersMtx_);
+        pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::INVITED});
+    }
+
     Json::Value json;
     json["action"] = "add";
     json["uri"] = uri;
@@ -1713,7 +1914,8 @@ ConversationRepository::fetch(const std::string& remoteDeviceId)
 {
     // Fetch distant repository
     git_remote* remote_ptr = nullptr;
-    git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
+    git_fetch_options fetch_opts;
+    git_fetch_options_init(&fetch_opts, GIT_FETCH_OPTIONS_VERSION);
 
     auto lastMsg = logN("", 1);
     if (lastMsg.size() == 0) {
@@ -1746,17 +1948,19 @@ ConversationRepository::fetch(const std::string& remoteDeviceId)
     fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void* payload) {
         *(static_cast<size_t*>(payload)) += stats->received_bytes;
         if (*(static_cast<size_t*>(payload)) > MAX_FETCH_SIZE) {
-            JAMI_ERR("Abort fetching repository, the fetch is too big: %lu bytes", *(static_cast<size_t*>(payload)));
+            JAMI_ERR("Abort fetching repository, the fetch is too big: %lu bytes",
+                     *(static_cast<size_t*>(payload)));
             return -1;
         }
         return 0;
     };
     if (git_remote_fetch(remote.get(), nullptr, &fetch_opts, "fetch") < 0) {
         const git_error* err = giterr_last();
-        if (err)
+        if (err) {
             JAMI_ERR("Could not fetch remote repository for conversation %s: %s",
                      pimpl_->id_.c_str(),
                      err->message);
+        }
         return false;
     }
 
@@ -1892,7 +2096,7 @@ ConversationRepository::merge(const std::string& merge_id)
             JAMI_INFO("Merge analysis result: Fast-forward");
         const auto* target_oid = git_annotated_commit_id(annotated.get());
 
-        if (pimpl_->mergeFastforward(target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN)) < 0) {
+        if (!pimpl_->mergeFastforward(target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN))) {
             const git_error* err = giterr_last();
             if (err)
                 JAMI_ERR("Fast forward merge failed: %s", err->message);
@@ -1900,9 +2104,11 @@ ConversationRepository::merge(const std::string& merge_id)
         }
         return true;
     } else if (analysis & GIT_MERGE_ANALYSIS_NORMAL) {
-        git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT;
+        git_merge_options merge_opts;
+        git_merge_options_init(&merge_opts, GIT_MERGE_OPTIONS_VERSION);
         merge_opts.file_flags = GIT_MERGE_FILE_STYLE_DIFF3;
-        git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+        git_checkout_options checkout_opts;
+        git_checkout_options_init(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION);
         checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_ALLOW_CONFLICTS;
 
         if (preference & GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY) {
@@ -2003,6 +2209,23 @@ ConversationRepository::join()
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
+
+    {
+        std::lock_guard<std::mutex> lk(pimpl_->membersMtx_);
+        auto updated = false;
+
+        for (auto& member : pimpl_->members_) {
+            if (member.uri == uri) {
+                updated = true;
+                member.role = MemberRole::MEMBER;
+                break;
+            }
+        }
+        if (!updated)
+            pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::MEMBER});
+    }
+
+
     return commitMessage(Json::writeString(wbuilder, json));
 }
 
@@ -2074,6 +2297,12 @@ ConversationRepository::leave()
     Json::StreamWriterBuilder wbuilder;
     wbuilder["commentStyle"] = "None";
     wbuilder["indentation"] = "";
+
+    {
+        std::lock_guard<std::mutex> lk(pimpl_->membersMtx_);
+        std::remove_if(pimpl_->members_.begin(), pimpl_->members_.end(), [&](auto& member) { return member.uri == account->getUsername(); });
+    }
+
     return commitMessage(Json::writeString(wbuilder, json));
 }
 
@@ -2087,6 +2316,12 @@ ConversationRepository::erase()
     fileutils::removeAll(repoPath, true);
 }
 
+ConversationMode
+ConversationRepository::mode() const
+{
+    return pimpl_->mode();
+}
+
 std::string
 ConversationRepository::voteKick(const std::string& uri, bool isDevice)
 {
@@ -2222,6 +2457,21 @@ ConversationRepository::resolveVote(const std::string& uri, bool isDevice)
         Json::StreamWriterBuilder wbuilder;
         wbuilder["commentStyle"] = "None";
         wbuilder["indentation"] = "";
+
+        if (!isDevice) {
+            std::lock_guard<std::mutex> lk(pimpl_->membersMtx_);
+            auto updated = false;
+
+            for (auto& member : pimpl_->members_) {
+                if (member.uri == uri) {
+                    updated = true;
+                    member.role = MemberRole::BANNED;
+                    break;
+                }
+            }
+            if (!updated)
+                pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::BANNED});
+        }
         return commitMessage(Json::writeString(wbuilder, json));
     }
 
@@ -2229,16 +2479,18 @@ ConversationRepository::resolveVote(const std::string& uri, bool isDevice)
     return {};
 }
 
-bool
+std::vector<ConversationCommit>
 ConversationRepository::validFetch(const std::string& remoteDevice) const
 {
     auto newCommit = remoteHead(remoteDevice);
-    if (not pimpl_ or newCommit.empty()) {
-        return false;
-    }
+    if (not pimpl_ or newCommit.empty())
+        return {};
     auto commitsToValidate = pimpl_->behind(newCommit);
     std::reverse(std::begin(commitsToValidate), std::end(commitsToValidate));
-    return pimpl_->validCommits(commitsToValidate);
+    auto isValid = pimpl_->validCommits(commitsToValidate);
+    if (isValid)
+        return commitsToValidate;
+    return {};
 }
 
 bool
@@ -2247,4 +2499,22 @@ ConversationRepository::validClone() const
     return pimpl_->validCommits(logN("", 0));
 }
 
+std::vector<std::string>
+ConversationRepository::getInitialMembers() const
+{
+    return pimpl_->getInitialMembers();
+}
+
+std::vector<ConversationMember>
+ConversationRepository::members() const
+{
+    return pimpl_->members();
+}
+
+void
+ConversationRepository::refreshMembers() const
+{
+    return pimpl_->initMembers();
+}
+
 } // namespace jami
diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h
index 8bf88b22fd195a46eb5b017727f875ec8cfbf367..b4a104f199a625f170cf04037da3b64b9a5be8eb 100644
--- a/src/jamidht/conversationrepository.h
+++ b/src/jamidht/conversationrepository.h
@@ -54,6 +54,8 @@ struct GitAuthor
     std::string email {};
 };
 
+enum class ConversationMode : int { ONE_TO_ONE = 0, ADMIN_INVITES_ONLY, INVITES_ONLY, PUBLIC };
+
 struct ConversationCommit
 {
     std::string id {};
@@ -65,6 +67,30 @@ struct ConversationCommit
     int64_t timestamp {0};
 };
 
+enum class MemberRole { ADMIN = 0, MEMBER, INVITED, BANNED };
+
+struct ConversationMember
+{
+    std::string uri;
+    MemberRole role;
+
+    std::map<std::string, std::string> map() const
+    {
+        std::string rolestr;
+        if (role == MemberRole::ADMIN) {
+            rolestr = "admin";
+        } else if (role == MemberRole::MEMBER) {
+            rolestr = "member";
+        } else if (role == MemberRole::INVITED) {
+            rolestr = "invited";
+        } else if (role == MemberRole::BANNED) {
+            rolestr = "banned";
+        }
+
+        return {{"uri", uri}, {"role", rolestr}};
+    }
+};
+
 /**
  * This class gives access to the git repository that represents the conversation
  */
@@ -74,10 +100,14 @@ public:
     /**
      * Creates a new repository, with initial files, where the first commit hash is the conversation id
      * @param account       The related account
+     * @param mode          The wanted mode
+     * @param otherMember   The other uri
      * @return  the conversation repository object
      */
     static DRING_TESTABLE std::unique_ptr<ConversationRepository> createConversation(
-        const std::weak_ptr<JamiAccount>& account);
+        const std::weak_ptr<JamiAccount>& account,
+        ConversationMode mode = ConversationMode::INVITES_ONLY,
+        const std::string& otherMember = "");
 
     /**
      * Clones a conversation on a remote device
@@ -196,12 +226,34 @@ public:
      */
     void erase();
 
+    /**
+     * Get conversation's mode
+     * @return the mode
+     */
+    ConversationMode mode() const;
+
     std::string voteKick(const std::string& uri, bool isDevice);
     std::string resolveVote(const std::string& uri, bool isDevice);
 
-    bool validFetch(const std::string& remoteDevice) const;
+    std::vector<ConversationCommit> validFetch(const std::string& remoteDevice) const;
     bool validClone() const;
-    std::string getCommitType(const std::string& commitMsg) const;
+
+    /**
+     * One to one util, get initial members
+     * @return initial members
+     */
+    std::vector<std::string> getInitialMembers() const;
+
+    /**
+     * Get conversation's members
+     * @return members
+     */
+    std::vector<ConversationMember> members() const;
+
+    /**
+     * To use after a merge with member's events, refresh members knowledge
+     */
+    void refreshMembers() const;
 
 private:
     ConversationRepository() = delete;
diff --git a/src/jamidht/jami_contact.h b/src/jamidht/jami_contact.h
index 184dfd9f3874c5753601c1b7cf21fd4f1211f070..81eb63c3d034b4f4ca64257e0a53d1d7be79284d 100644
--- a/src/jamidht/jami_contact.h
+++ b/src/jamidht/jami_contact.h
@@ -121,9 +121,10 @@ struct Contact
 struct TrustRequest
 {
     dht::InfoHash device;
+    std::string conversationId;
     time_t received;
     std::vector<uint8_t> payload;
-    MSGPACK_DEFINE_MAP(device, received, payload)
+    MSGPACK_DEFINE_MAP(device, conversationId, received, payload)
 };
 
 struct DeviceAnnouncement : public dht::SignedValue<DeviceAnnouncement>
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index b9a658f5b6ad4b0de58d2d77ea2d9437aba66800..e77c5a78cd30ffd1d0da350e41100efe5dbb5d9b 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -1210,14 +1210,38 @@ JamiAccount::loadAccount(const std::string& archive_password,
                 emitSignal<DRing::ConfigurationSignal::ContactRemoved>(id, uri, banned);
             });
         },
-        [this](const std::string& uri, const std::vector<uint8_t>& payload, time_t received) {
-            dht::ThreadPool::computation().run(
-                [id = getAccountID(), uri, payload = std::move(payload), received] {
-                    emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>(id,
+        [this](const std::string& uri,
+               const std::string& conversationId,
+               const std::vector<uint8_t>& payload,
+               time_t received) {
+            dht::ThreadPool::computation().run([w = weak(),
+                                                uri,
+                                                payload = std::move(payload),
+                                                received,
+                                                conversationId] {
+                if (auto acc = w.lock()) {
+                    if (!conversationId.empty()) {
+                        std::lock_guard<std::mutex> lk(acc->conversationsRequestsMtx_);
+                        auto it = acc->conversationsRequests_.find(conversationId);
+                        if (it != acc->conversationsRequests_.end()) {
+                            JAMI_INFO(
+                                "[Account %s] Received a request for a conversation already existing. "
+                                "Ignore",
+                                acc->getAccountID().c_str());
+                            return;
+                        }
+                        ConversationRequest req;
+                        req.from = uri;
+                        req.conversationId = conversationId;
+                        req.received = std::time(nullptr);
+                        acc->conversationsRequests_[conversationId] = std::move(req);
+                    }
+                    emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>(acc->getAccountID(),
                                                                                  uri,
                                                                                  payload,
                                                                                  received);
-                });
+                }
+            });
         },
         [this](const std::map<dht::InfoHash, KnownDevice>& devices) {
             std::map<std::string, std::string> ids;
@@ -1229,6 +1253,13 @@ JamiAccount::loadAccount(const std::string& archive_password,
             dht::ThreadPool::computation().run([id = getAccountID(), devices = std::move(ids)] {
                 emitSignal<DRing::ConfigurationSignal::KnownDevicesChanged>(id, devices);
             });
+        },
+        [this](const std::string& conversationId) {
+            dht::ThreadPool::computation().run([w = weak(), conversationId] {
+                if (auto acc = w.lock()) {
+                    acc->acceptConversationRequest(conversationId);
+                }
+            });
         }};
 
     try {
@@ -2196,9 +2227,22 @@ JamiAccount::getTrackedBuddyPresence() const
 void
 JamiAccount::onTrackedBuddyOnline(const dht::InfoHash& contactId)
 {
-    JAMI_DBG("Buddy %s online", contactId.toString().c_str());
     std::string id(contactId.toString());
+    JAMI_DBG("Buddy %s online", id.c_str());
     emitSignal<DRing::PresenceSignal::NewBuddyNotification>(getAccountID(), id, 1, "");
+
+    auto details = getContactDetails(id);
+    auto it = details.find("confirmed");
+    if (it == details.end() or it->second == "false") {
+        auto convId = getOneToOneConversation(id);
+        if (convId.empty())
+            return;
+        // In this case, the TrustRequest was sent but never confirmed (cause the contact was offline maybe)
+        // To avoid the contact to never receive the conv request, retry there
+        std::lock_guard<std::mutex> lock(configurationMutex_);
+        if (accountManager_)
+            accountManager_->sendTrustRequest(id, convId, {}); /* TODO payload?, MessageEngine not generic and will be able to move to conversation's requests */
+    }
 }
 
 void
@@ -3279,6 +3323,35 @@ JamiAccount::addContact(const std::string& uri, bool confirmed)
 void
 JamiAccount::removeContact(const std::string& uri, bool ban)
 {
+    // Remove related conversation
+    auto isSelf = uri == getUsername();
+    std::vector<std::string> toRm;
+    {
+        std::lock_guard<std::mutex> lk(conversationsMtx_);
+        for (const auto& [key, conv] : conversations_) {
+            try {
+                // Note it's important to check getUsername(), else
+                // removing self can remove all conversations
+                if (conv->mode() == ConversationMode::ONE_TO_ONE) {
+                    auto initMembers = conv->getInitialMembers();
+                    if ((isSelf && initMembers.size() == 1)
+                        || std::find(initMembers.begin(), initMembers.end(), uri) != initMembers.end())
+                        toRm.emplace_back(key);
+                }
+            } catch (const std::exception& e) {
+                JAMI_WARN("%s", e.what());
+            }
+        }
+    }
+    for (const auto& id : toRm) {
+        // Note, if we ban the device, we don't send the leave cause the other peer will just
+        // never got the notifications, so just erase the datas
+        if (!ban)
+            removeConversation(id);
+        else
+            removeRepository(id, false, true);
+    }
+
     {
         std::lock_guard<std::mutex> lock(configurationMutex_);
         if (accountManager_)
@@ -3301,11 +3374,6 @@ JamiAccount::removeContact(const std::string& uri, bool ban)
             }
         }
     }
-
-    for (const auto& device : devices) {
-        if (connectionManager_)
-            connectionManager_->closeConnectionsWith(device);
-    }
 }
 
 std::map<std::string, std::string>
@@ -3361,22 +3429,11 @@ JamiAccount::sendTrustRequest(const std::string& to, const std::vector<uint8_t>&
 {
     std::lock_guard<std::mutex> lock(configurationMutex_);
     if (accountManager_)
-        accountManager_->sendTrustRequest(to, payload);
+        accountManager_->sendTrustRequest(to, {}, payload);
     else
         JAMI_WARN("[Account %s] sendTrustRequest: account not loaded", getAccountID().c_str());
 }
 
-void
-JamiAccount::sendTrustRequestConfirm(const std::string& to)
-{
-    std::lock_guard<std::mutex> lock(configurationMutex_);
-    if (accountManager_)
-        accountManager_->sendTrustRequestConfirm(dht::InfoHash(to));
-    else
-        JAMI_WARN("[Account %s] sendTrustRequestConfirm: account not loaded",
-                  getAccountID().c_str());
-}
-
 void
 JamiAccount::forEachDevice(const dht::InfoHash& to,
                            std::function<void(const dht::InfoHash&)>&& op,
@@ -3845,10 +3902,10 @@ JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
 }
 
 std::string
-JamiAccount::startConversation()
+JamiAccount::startConversation(ConversationMode mode, const std::string& otherMember)
 {
     // Create the conversation object
-    auto conversation = std::make_unique<Conversation>(weak());
+    auto conversation = std::make_shared<Conversation>(weak(), mode, otherMember);
     auto convId = conversation->id();
     {
         std::lock_guard<std::mutex> lk(conversationsMtx_);
@@ -5094,11 +5151,11 @@ JamiAccount::saveConvRequests()
 }
 
 void
-JamiAccount::removeRepository(const std::string& conversationId, bool sync)
+JamiAccount::removeRepository(const std::string& conversationId, bool sync, bool force)
 {
     std::unique_lock<std::mutex> lk(conversationsMtx_);
     auto it = conversations_.find(conversationId);
-    if (it != conversations_.end() && it->second && it->second->isRemoving()) {
+    if (it != conversations_.end() && it->second && (force || it->second->isRemoving())) {
         JAMI_DBG() << "Remove conversation: " << conversationId;
         it->second->erase();
         conversations_.erase(it);
@@ -5143,4 +5200,23 @@ JamiAccount::sendMessageNotification(const Conversation& conversation,
     }
 }
 
+std::string
+JamiAccount::getOneToOneConversation(const std::string& uri) const
+{
+    auto isSelf = uri == getUsername();
+    std::lock_guard<std::mutex> lk(conversationsMtx_);
+    for (const auto& [key, conv] : conversations_) {
+        // Note it's important to check getUsername(), else
+        // removing self can remove all conversations
+        if (conv->mode() == ConversationMode::ONE_TO_ONE) {
+            auto initMembers = conv->getInitialMembers();
+            if (isSelf && initMembers.size() == 1)
+                return key;
+            if (std::find(initMembers.begin(), initMembers.end(), uri) != initMembers.end())
+                return key;
+        }
+    }
+    return {};
+}
+
 } // namespace jami
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index 9e7dcbd19ec85b60bc79cc923e4b07b5bc59d0ac..17a669568f7c90e4b634c0f9fb0aad96b8c94c5a 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -40,6 +40,7 @@
 #include "scheduled_executor.h"
 #include "connectionmanager.h"
 #include "gitserver.h"
+#include "conversationrepository.h"
 
 #include <opendht/dhtrunner.h>
 #include <opendht/default_types.h>
@@ -335,7 +336,6 @@ public:
     std::map<std::string, std::string> getContactDetails(const std::string& uri) const;
 
     void sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload);
-    void sendTrustRequestConfirm(const std::string& to);
     void sendTextMessage(const std::string& to,
                          const std::map<std::string, std::string>& payloads,
                          uint64_t id,
@@ -507,7 +507,7 @@ public:
 
     std::string_view currentDeviceId() const;
     // Conversation management
-    std::string startConversation();
+    std::string startConversation(ConversationMode mode = ConversationMode::INVITES_ONLY, const std::string& otherMember = "");
     void acceptConversationRequest(const std::string& conversationId);
     void declineConversationRequest(const std::string& conversationId);
     std::vector<std::string> getConversations();
@@ -950,8 +950,9 @@ private:
      * Remove a repository and all files
      * @param convId
      * @param sync      If we send an update to other account's devices
+     * @param force     True if ignore the removing flag
      */
-    void removeRepository(const std::string& convId, bool sync);
+    void removeRepository(const std::string& convId, bool sync, bool force = false);
 
     /**
      * Send a message notification to all members
@@ -962,6 +963,12 @@ private:
     void sendMessageNotification(const Conversation& conversation,
                                  const std::string& commitId,
                                  bool sync);
+    /**
+     * Get related conversation with member
+     * @param uri       The member to search for
+     * @return the conversation id if found else empty
+     */
+    std::string getOneToOneConversation(const std::string& uri) const;
 };
 
 static inline std::ostream&
diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp
index 71fc23a26e891c50e9e26af607d2dc8c48e81c76..4855318ddad22799a8030766bbf51b92b8c8a865 100644
--- a/test/unitTest/conversation/conversation.cpp
+++ b/test/unitTest/conversation/conversation.cpp
@@ -53,14 +53,17 @@ public:
     void generateFakeVote(std::shared_ptr<JamiAccount> account,
                           const std::string& convId,
                           const std::string& votedUri);
+    void generateFakeInvite(std::shared_ptr<JamiAccount> account,
+                            const std::string& convId,
+                            const std::string& uri);
     void addFile(std::shared_ptr<JamiAccount> account,
                  const std::string& convId,
                  const std::string& relativePath,
                  const std::string& content = "");
-    void addAll(std::shared_ptr<JamiAccount> account,
-                 const std::string& convId);
+    void addAll(std::shared_ptr<JamiAccount> account, const std::string& convId);
     void commit(std::shared_ptr<JamiAccount> account,
-                 const std::string& convId, Json::Value& message);
+                const std::string& convId,
+                Json::Value& message);
 
     std::string aliceId;
     std::string bobId;
@@ -85,7 +88,7 @@ private:
     void testSendMessageToMultipleParticipants();
     void testPingPongMessages();
     void testRemoveMember();
-    //void testBanDevice();
+    // void testBanDevice();
     void testMemberTryToRemoveAdmin();
     void testBannedMemberCannotSendMessage();
     void testAddBannedMember();
@@ -112,6 +115,15 @@ private:
     // TODO2 void testMemberJoinsNoBadFile();
     // TODO2 testMemberJoinsInviteRemoved
     // TODO2 testMemberBanNoBadFile
+    void testAddContact();
+    void testAddContactDeleteAndReAdd();
+    void testFailAddMemberInOneToOne();
+    void testUnknownModeDetected();
+    void testRemoveContact();
+    void testBanContact();
+    void testOneToOneFetchWithNewMemberRefused();
+    void testAddOfflineContactThenConnect();
+    void testDeclineTrustRequestDoNotGenerateAnother();
 
     CPPUNIT_TEST_SUITE(ConversationTest);
     CPPUNIT_TEST(testCreateConversation);
@@ -140,7 +152,15 @@ private:
     CPPUNIT_TEST(testVoteNoBadFile);
     CPPUNIT_TEST(testETooBigClone);
     CPPUNIT_TEST(testETooBigFetch);
-    CPPUNIT_TEST_SUITE_END();
+    CPPUNIT_TEST(testAddContact);
+    CPPUNIT_TEST(testAddContactDeleteAndReAdd);
+    CPPUNIT_TEST(testFailAddMemberInOneToOne);
+    CPPUNIT_TEST(testUnknownModeDetected);
+    CPPUNIT_TEST(testRemoveContact);
+    CPPUNIT_TEST(testBanContact);
+    CPPUNIT_TEST(testOneToOneFetchWithNewMemberRefused);
+    CPPUNIT_TEST(testAddOfflineContactThenConnect);
+    CPPUNIT_TEST(testDeclineTrustRequestDoNotGenerateAnother);
 };
 
 CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(ConversationTest, ConversationTest::name());
@@ -254,6 +274,8 @@ ConversationTest::testCreateConversation()
     auto convId = aliceAccount->startConversation();
     cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; });
     CPPUNIT_ASSERT(conversationReady);
+    ConversationRepository repo(aliceAccount, convId);
+    CPPUNIT_ASSERT(repo.mode() == ConversationMode::INVITES_ONLY);
 
     // Assert that repository exists
     auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
@@ -445,8 +467,7 @@ ConversationTest::testRemoveConversationWithMember()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
     // Check created files
-    auto bobInvitedFile = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri
-                          + ".crt";
+    auto bobInvitedFile = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri;
     CPPUNIT_ASSERT(fileutils::isFile(bobInvitedFile));
 
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
@@ -456,8 +477,7 @@ ConversationTest::testRemoveConversationWithMember()
     auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
                       + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath));
-    bobInvitedFile = clonedPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri
-                     + ".crt";
+    bobInvitedFile = clonedPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri;
     CPPUNIT_ASSERT(!fileutils::isFile(bobInvitedFile));
     // Remove conversation from alice once member confirmed
     CPPUNIT_ASSERT(
@@ -514,8 +534,7 @@ ConversationTest::testAddMember()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
     // Check created files
-    auto bobInvited = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri
-                      + ".crt";
+    auto bobInvited = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri;
     CPPUNIT_ASSERT(fileutils::isFile(bobInvited));
     CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
     bobAccount->acceptConversationRequest(convId);
@@ -523,7 +542,7 @@ ConversationTest::testAddMember()
     auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
                       + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath));
-    bobInvited = clonedPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri + ".crt";
+    bobInvited = clonedPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri;
     CPPUNIT_ASSERT(!fileutils::isFile(bobInvited));
     auto bobMember = clonedPath + DIR_SEPARATOR_STR + "members" + DIR_SEPARATOR_STR + bobUri
                      + ".crt";
@@ -630,13 +649,13 @@ ConversationTest::testGetMembers()
     CPPUNIT_ASSERT(members[1]["uri"] == bobUri);
     CPPUNIT_ASSERT(members[1]["role"] == "invited");
 
-    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; });
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
     messageReceived = false;
     bobAccount->acceptConversationRequest(convId);
     cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; });
     members = bobAccount->getConversationMembers(convId);
     CPPUNIT_ASSERT(members.size() == 2);
-    cv.wait_for(lk, std::chrono::seconds(60), [&]() { return messageReceived; });
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { return messageReceived; }));
     members = aliceAccount->getConversationMembers(convId);
     CPPUNIT_ASSERT(members.size() == 2);
     CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername());
@@ -1436,8 +1455,7 @@ ConversationTest::generateFakeVote(std::shared_ptr<JamiAccount> account,
     account->sendMessage(convId, "trigger the fake history to be pulled");
 }
 void
-ConversationTest::addAll(std::shared_ptr<JamiAccount> account,
-                          const std::string& convId)
+ConversationTest::addAll(std::shared_ptr<JamiAccount> account, const std::string& convId)
 {
     auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
@@ -1459,7 +1477,83 @@ ConversationTest::addAll(std::shared_ptr<JamiAccount> account,
 
 void
 ConversationTest::commit(std::shared_ptr<JamiAccount> account,
-                 const std::string& convId, Json::Value& message)
+                         const std::string& convId,
+                         Json::Value& message)
+{
+    ConversationRepository cr(account->weak(), convId);
+
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    cr.commitMessage(Json::writeString(wbuilder, message));
+}
+
+void
+ConversationTest::generateFakeInvite(std::shared_ptr<JamiAccount> account,
+                                     const std::string& convId,
+                                     const std::string& uri)
+{
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    // remove from member & add into banned without voting for the ban
+    auto memberFile = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + uri;
+    std::ofstream file(memberFile);
+    if (file.is_open()) {
+        file.close();
+    }
+
+    git_repository* repo = nullptr;
+    if (git_repository_open(&repo, repoPath.c_str()) != 0)
+        return;
+    GitRepository rep = {std::move(repo), git_repository_free};
+
+    // git add -A
+    git_index* index_ptr = nullptr;
+    git_strarray array = {0};
+    if (git_repository_index(&index_ptr, repo) < 0)
+        return;
+    GitIndex index {index_ptr, git_index_free};
+    git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
+    git_index_write(index.get());
+
+    ConversationRepository cr(account->weak(), convId);
+
+    Json::Value json;
+    json["action"] = "add";
+    json["uri"] = uri;
+    json["type"] = "member";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    cr.commitMessage(Json::writeString(wbuilder, json));
+
+    account->sendMessage(convId, "trigger the fake history to be pulled");
+}
+void
+ConversationTest::addAll(std::shared_ptr<JamiAccount> account, const std::string& convId)
+{
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+
+    git_repository* repo = nullptr;
+    if (git_repository_open(&repo, repoPath.c_str()) != 0)
+        return;
+    GitRepository rep = {std::move(repo), git_repository_free};
+
+    // git add -A
+    git_index* index_ptr = nullptr;
+    git_strarray array = {0};
+    if (git_repository_index(&index_ptr, repo) < 0)
+        return;
+    GitIndex index {index_ptr, git_index_free};
+    git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
+    git_index_write(index.get());
+}
+
+void
+ConversationTest::commit(std::shared_ptr<JamiAccount> account,
+                         const std::string& convId,
+                         Json::Value& message)
 {
     ConversationRepository cr(account->weak(), convId);
 
@@ -1893,7 +1987,7 @@ ConversationTest::testETooBigClone()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     std::ofstream bad(repoPath + DIR_SEPARATOR_STR + "BADFILE");
     CPPUNIT_ASSERT(bad.is_open());
-    for (int i = 0; i < 300*1024*1024; ++i)
+    for (int i = 0; i < 300 * 1024 * 1024; ++i)
         bad << "A";
     bad.close();
 
@@ -1964,7 +2058,7 @@ ConversationTest::testETooBigFetch()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
     std::ofstream bad(repoPath + DIR_SEPARATOR_STR + "BADFILE");
     CPPUNIT_ASSERT(bad.is_open());
-    for (int i = 0; i < 300*1024*1024; ++i)
+    for (int i = 0; i < 300 * 1024 * 1024; ++i)
         bad << "A";
     bad.close();
 
@@ -1975,10 +2069,568 @@ ConversationTest::testETooBigFetch()
     commit(aliceAccount, convId, json);
 
     aliceAccount->sendMessage(convId, std::string("hi"));
-    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }));
+    CPPUNIT_ASSERT(
+        !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }));
     DRing::unregisterSignalHandlers();
 }
 
+void
+ConversationTest::testAddContact()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    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;
+    std::string convId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == bobId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == bobId) {
+                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);
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&]() { return !convId.empty(); }));
+    ConversationRepository repo(aliceAccount, convId);
+    // Mode must be one to one
+    CPPUNIT_ASSERT(repo.mode() == ConversationMode::ONE_TO_ONE);
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    // Check created files
+    auto bobInvited = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri;
+    CPPUNIT_ASSERT(fileutils::isFile(bobInvited));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return conversationReady && memberMessageGenerated;
+    }));
+    auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                      + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath));
+    bobInvited = clonedPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri;
+    CPPUNIT_ASSERT(!fileutils::isFile(bobInvited));
+    auto bobMember = clonedPath + DIR_SEPARATOR_STR + "members" + DIR_SEPARATOR_STR + bobUri
+                     + ".crt";
+    CPPUNIT_ASSERT(fileutils::isFile(bobMember));
+}
+
+void
+ConversationTest::testAddContactDeleteAndReAdd()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    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;
+    std::string convId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == bobId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == bobId) {
+                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);
+    requestReceived = false;
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return conversationReady && memberMessageGenerated;
+    }));
+
+    // removeContact
+    aliceAccount->removeContact(bobUri, false);
+    std::this_thread::sleep_for(std::chrono::seconds(5)); // wait a bit that connections are closed
+
+    // re-add
+    requestReceived = false;
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    conversationReady = false;
+    memberMessageGenerated = false;
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return conversationReady && memberMessageGenerated;
+    }));
+}
+
+void
+ConversationTest::testFailAddMemberInOneToOne()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    auto carlaUri = carlaAccount->getUsername();
+    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;
+    std::string convId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == bobId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == bobId) {
+                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);
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&]() { return !convId.empty(); }));
+    CPPUNIT_ASSERT(!aliceAccount->addConversationMember(convId, carlaUri));
+}
+
+void
+ConversationTest::testUnknownModeDetected()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto convId = aliceAccount->startConversation();
+    ConversationRepository repo(aliceAccount, convId);
+    Json::Value json;
+    json["mode"] = 1412;
+    json["type"] = "initial";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    repo.amend(convId, Json::writeString(wbuilder, json));
+    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) {
+                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; }));
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+}
+
+void
+ConversationTest::testRemoveContact()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    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;
+    std::string convId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == bobId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == bobId) {
+                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);
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&]() { return !convId.empty(); }));
+    // Check created files
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return conversationReady && memberMessageGenerated;
+    }));
+
+    memberMessageGenerated = false;
+    bobAccount->removeContact(aliceUri, false);
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+    aliceAccount->removeContact(bobUri, false);
+    cv.wait_for(lk, std::chrono::seconds(20));
+
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(!fileutils::isDirectory(repoPath));
+
+    repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+               + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(!fileutils::isDirectory(repoPath));
+}
+
+void
+ConversationTest::testBanContact()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    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;
+    std::string convId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == bobId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == bobId) {
+                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);
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&]() { return !convId.empty(); }));
+    // Check created files
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+    CPPUNIT_ASSERT(
+        cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+
+    memberMessageGenerated = false;
+    bobAccount->removeContact(aliceUri, true);
+    cv.wait_for(lk, std::chrono::seconds(10));
+    CPPUNIT_ASSERT(
+        !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; }));
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(!fileutils::isDirectory(repoPath));
+}
+
+void
+ConversationTest::testOneToOneFetchWithNewMemberRefused()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    auto carlaUri = carlaAccount->getUsername();
+    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,
+         messageBob = false;
+    std::string convId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == bobId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == bobId) {
+                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;
+            } else if (accountId == bobId && conversationId == convId
+                       && message["type"] == "member") {
+                messageBob = true;
+            }
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return !convId.empty() && requestReceived;
+    }));
+    CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri));
+    memberMessageGenerated = false;
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return conversationReady && memberMessageGenerated;
+    }));
+
+    messageBob = false;
+    generateFakeInvite(aliceAccount, convId, carlaUri);
+    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBob; }));
+}
+
+void
+ConversationTest::testAddOfflineContactThenConnect()
+{
+    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);
+
+    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;
+    std::string convId = "";
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == carlaId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == carlaId) {
+                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);
+    aliceAccount->addContact(carlaUri);
+    aliceAccount->sendTrustRequest(carlaUri, {});
+    cv.wait_for(lk, std::chrono::seconds(5)); // Wait 5 secs for the put to happen
+    CPPUNIT_ASSERT(!convId.empty());
+    Manager::instance().sendRegister(carlaId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { return requestReceived; }));
+    memberMessageGenerated = false;
+    CPPUNIT_ASSERT(carlaAccount->acceptTrustRequest(aliceUri));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() {
+        return conversationReady && memberMessageGenerated;
+    }));
+}
+
+void
+ConversationTest::testDeclineTrustRequestDoNotGenerateAnother()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+    aliceAccount->trackBuddyPresence(bobUri, true);
+
+    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;
+    std::string convId = "";
+    auto bobConnected = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(
+        [&](const std::string& account_id,
+            const std::string& /*from*/,
+            const std::vector<uint8_t>& /*payload*/,
+            time_t /*received*/) {
+            if (account_id == bobId)
+                requestReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& conversationId) {
+            if (accountId == aliceId) {
+                convId = conversationId;
+            } else if (accountId == bobId) {
+                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();
+            }
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                auto details = bobAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                if (daemonStatus == "REGISTERED") {
+                    bobConnected = true;
+                    cv.notify_one();
+                } else if (daemonStatus == "UNREGISTERED") {
+                    bobConnected = false;
+                    cv.notify_one();
+                }
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+    aliceAccount->addContact(bobUri);
+    aliceAccount->sendTrustRequest(bobUri, {});
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    CPPUNIT_ASSERT(bobAccount->discardTrustRequest(aliceUri));
+    cv.wait_for(lk, std::chrono::seconds(10)); // Wait a bit
+    bobConnected = true;
+    Manager::instance().sendRegister(bobId, false);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return !bobConnected; }));
+    // Trigger on peer online
+    requestReceived = false;
+    Manager::instance().sendRegister(bobId, true);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return bobConnected; }));
+    CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+}
+
 } // namespace test
 } // namespace jami