diff --git a/src/account.h b/src/account.h index 99c960e49e1d74e843e18a080a11623c87e71f88..40cbf6507e612783096e70b77f4fc217aa501c0e 100644 --- a/src/account.h +++ b/src/account.h @@ -324,7 +324,9 @@ public: const std::string& /*commitId*/) {}; // Invites - virtual void onConversationRequest(const std::string& /* from */, const Json::Value&) {}; + virtual void onConversationRequest(const std::string& /*from*/, const Json::Value&) {}; + virtual void onNeedConversationRequest(const std::string& /*from*/, + const std::string& /*conversationId*/) {}; /** * Helper function used to load the default codec order from the codec factory diff --git a/src/jamidht/account_manager.h b/src/jamidht/account_manager.h index b6b90deb4df634a71331dd4681a48edd8c0f2bb6..945f916f428b242ee2145214008d2eed26240d20 100644 --- a/src/jamidht/account_manager.h +++ b/src/jamidht/account_manager.h @@ -21,6 +21,7 @@ #include "config.h" #endif +#include "jamidht/conversation.h" #include "contact_list.h" #include "logger.h" #if HAVE_RINGNS @@ -45,6 +46,8 @@ struct AccountInfo { dht::crypto::Identity identity; std::unique_ptr<ContactList> contacts; + std::vector<ConvInfo> conversations; + std::map<std::string, ConversationRequest> conversationsRequests; std::string accountId; std::string deviceId; std::shared_ptr<dht::Value> announce; @@ -211,6 +214,20 @@ public: void removeContact(const std::string& uri, bool banned = true); std::vector<std::map<std::string, std::string>> getContacts() const; + // Conversations + void setConversations(const std::vector<ConvInfo>& newConv) + { + if (info_) { + info_->conversations = newConv; + } + } + void setConversationsRequests(const std::map<std::string, ConversationRequest>& newConvReq) + { + if (info_) { + info_->conversationsRequests = newConvReq; + } + } + /** Obtain details about one account contact in serializable form. */ std::map<std::string, std::string> getContactDetails(const std::string& uri) const; diff --git a/src/jamidht/accountarchive.cpp b/src/jamidht/accountarchive.cpp index c7b6bc0ab00e3018066ef403cab649a26fff37f1..4c54fe611bb4ed613c8400a864a0a96ab818b656 100644 --- a/src/jamidht/accountarchive.cpp +++ b/src/jamidht/accountarchive.cpp @@ -73,6 +73,15 @@ AccountArchive::deserialize(const std::vector<uint8_t>& dat) if (h != dht::InfoHash {}) contacts.emplace(h, Contact {*citr}); } + } else if (key.compare(Conf::CONVERSATIONS_KEY) == 0) { + for (Json::ValueIterator citr = itr->begin(); citr != itr->end(); citr++) { + conversations.emplace_back(ConvInfo(*citr)); + } + } else if (key.compare(Conf::CONVERSATIONS_REQUESTS_KEY) == 0) { + for (Json::ValueIterator citr = itr->begin(); citr != itr->end(); citr++) { + conversationsRequests.emplace(citr.key().asString(), + ConversationRequest(*citr)); + } } else if (key.compare(Conf::ETH_KEY) == 0) { eth_key = base64::decode(itr->asString()); } else if (key.compare(Conf::RING_ACCOUNT_CRL) == 0) { @@ -120,6 +129,20 @@ AccountArchive::serialize() const jsonContacts[c.first.toString()] = c.second.toJson(); } + if (not conversations.empty()) { + Json::Value& jsonConversations = root[Conf::CONVERSATIONS_KEY]; + for (const auto& c : conversations) { + jsonConversations.append(c.toJson()); + } + } + + if (not conversationsRequests.empty()) { + Json::Value& jsonConversationsReqs = root[Conf::CONVERSATIONS_REQUESTS_KEY]; + for (const auto& [key, value] : conversationsRequests) { + jsonConversationsReqs[key] = value.toJson(); + } + } + Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; wbuilder["indentation"] = ""; diff --git a/src/jamidht/accountarchive.h b/src/jamidht/accountarchive.h index f1b442059165b3162589d1a72f01f71cd6b35078..e03bd3c6b3956fda1f3ac8a6f27c1a5796717e34 100644 --- a/src/jamidht/accountarchive.h +++ b/src/jamidht/accountarchive.h @@ -18,6 +18,7 @@ #pragma once #include "jami_contact.h" +#include "jamidht/jamiaccount.h" #include "fileutils.h" #include <opendht/crypto.h> @@ -49,6 +50,10 @@ struct AccountArchive /** Contacts */ std::map<dht::InfoHash, Contact> contacts; + // Conversations + std::vector<ConvInfo> conversations; + std::map<std::string, ConversationRequest> conversationsRequests; + /** Account configuration */ std::map<std::string, std::string> config; diff --git a/src/jamidht/archive_account_manager.cpp b/src/jamidht/archive_account_manager.cpp index 96326528c6108328cd654c36352007659c97ef2f..9e35d3157180be1218e839ef7f5cc722d61ed503 100644 --- a/src/jamidht/archive_account_manager.cpp +++ b/src/jamidht/archive_account_manager.cpp @@ -348,6 +348,8 @@ ArchiveAccountManager::onArchiveLoaded(AuthContext& ctx, AccountArchive&& a) info->contacts = std::make_unique<ContactList>(a.id.second, path_, onChange_); info->contacts->setContacts(a.contacts); info->contacts->foundAccountDevice(deviceCertificate, ctx.deviceName, clock::now()); + info->conversations = a.conversations; + info->conversationsRequests = a.conversationsRequests; info->ethAccount = ethAccount; info->announce = std::move(receipt.second); info_ = std::move(info); @@ -559,6 +561,8 @@ ArchiveAccountManager::updateArchive(AccountArchive& archive) const archive.config[it.first] = it.second; } archive.contacts = info_->contacts->getContacts(); + archive.conversations = info_->conversations; + archive.conversationsRequests = info_->conversationsRequests; } void diff --git a/src/jamidht/configkeys.h b/src/jamidht/configkeys.h index c9a4d9da5d66687e8776f4950fdf52cf140c91d7..5d0889a2c3a7e60984ad3c4053f9780836a33bce 100644 --- a/src/jamidht/configkeys.h +++ b/src/jamidht/configkeys.h @@ -38,6 +38,8 @@ constexpr const char* const RING_ACCOUNT_RECEIPT = "ringAccountReceipt"; constexpr const char* const RING_ACCOUNT_RECEIPT_SIG = "ringAccountReceiptSignature"; constexpr const char* const RING_ACCOUNT_CRL = "ringAccountCRL"; constexpr const char* const RING_ACCOUNT_CONTACTS = "ringAccountContacts"; +constexpr const char* const CONVERSATIONS_KEY = "conversations"; +constexpr const char* const CONVERSATIONS_REQUESTS_KEY = "conversationsRequests"; constexpr const char* const PROXY_ENABLED_KEY = "proxyEnabled"; constexpr const char* const PROXY_SERVER_KEY = "proxyServer"; constexpr const char* const PROXY_PUSH_TOKEN_KEY = "proxyPushToken"; diff --git a/src/jamidht/contact_list.cpp b/src/jamidht/contact_list.cpp index bcf8338432beb5c1bd9ff16c500faa9820b399f0..e238b94c61cb2d090a74f23eac48937dbd4e913c 100644 --- a/src/jamidht/contact_list.cpp +++ b/src/jamidht/contact_list.cpp @@ -275,17 +275,14 @@ ContactList::onTrustRequest(const dht::InfoHash& peer_account, // Add trust request req = trustRequests_ .emplace(peer_account, - TrustRequest {peer_device, - conversationId, - received, - std::move(payload)}) + TrustRequest {peer_device, conversationId, received, payload}) .first; } else { // Update trust request if (received < req->second.received) { req->second.device = peer_device; req->second.received = received; - req->second.payload = std::move(payload); + req->second.payload = payload; } else { JAMI_DBG("[Contacts] Ignoring outdated trust request from %s", peer_account.toString().c_str()); @@ -294,7 +291,7 @@ ContactList::onTrustRequest(const dht::InfoHash& peer_account, saveTrustRequests(); } // Note: call JamiAccount's callback to build ConversationRequest anyway - callbacks_.trustRequest(peer_account.toString(), conversationId, payload, received); + callbacks_.trustRequest(peer_account.toString(), conversationId, std::move(payload), received); return accept; } diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index 6d4a6cc53b374c9578b4f7a64dd93fe64a72cda1..8a2120b044e7718fb9e56653e6fc9a61a9acf836 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -31,6 +31,75 @@ namespace jami { +ConvInfo::ConvInfo(const Json::Value& json) +{ + id = json["id"].asString(); + created = json["created"].asLargestUInt(); + removed = json["removed"].asLargestUInt(); + erased = json["erased"].asLargestUInt(); + for (const auto& v : json["members"]) { + members.emplace_back(v["uri"].asString()); + } +} + +Json::Value +ConvInfo::toJson() const +{ + Json::Value json; + json["id"] = id; + json["created"] = Json::Int64(created); + if (removed) { + json["removed"] = Json::Int64(removed); + } + if (erased) { + json["erased"] = Json::Int64(erased); + } + for (const auto& m : members) { + Json::Value member; + member["uri"] = m; + json["members"].append(member); + } + return json; +} + +// ConversationRequest +ConversationRequest::ConversationRequest(const Json::Value& json) +{ + received = json["received"].asLargestUInt(); + declined = json["declined"].asLargestUInt(); + from = json["from"].asString(); + conversationId = json["conversationId"].asString(); + auto& md = json["metadatas"]; + for (const auto& member : md.getMemberNames()) { + metadatas.emplace(member, md[member].asString()); + } +} + +Json::Value +ConversationRequest::toJson() const +{ + Json::Value json; + json["conversationId"] = conversationId; + json["from"] = from; + json["received"] = static_cast<uint32_t>(received); + if (declined) + json["declined"] = static_cast<uint32_t>(declined); + for (const auto& [key, value] : metadatas) { + json["metadatas"][key] = value; + } + return json; +} + +std::map<std::string, std::string> +ConversationRequest::toMap() const +{ + auto result = metadatas; + result["id"] = conversationId; + result["from"] = from; + result["received"] = std::to_string(received); + return result; +} + class Conversation::Impl { public: @@ -128,7 +197,9 @@ Conversation::Impl::convCommitToMap(const std::vector<ConversationCommit>& commi repository_->pinCertificates(); // Get certificate from repo try { - auto certPath = fileutils::getFullPath(repoPath(), std::string("devices") + DIR_SEPARATOR_STR + authorDevice + ".crt"); + auto certPath = fileutils::getFullPath(repoPath(), + std::string("devices") + DIR_SEPARATOR_STR + + authorDevice + ".crt"); auto deviceCert = fileutils::loadTextFile(certPath); cert = std::make_shared<crypto::Certificate>(deviceCert); if (!cert) { @@ -429,15 +500,18 @@ Conversation::mergeHistory(const std::string& uri) std::unique_lock<std::mutex> lk(pimpl_->writeMtx_); // Validate commit - auto newCommits = pimpl_->repository_->validFetch(uri); + auto [newCommits, err] = pimpl_->repository_->validFetch(uri); if (newCommits.empty()) { - JAMI_ERR("Could not validate history with %s", uri.c_str()); + if (err) + JAMI_ERR("Could not validate history with %s", uri.c_str()); + pimpl_->repository_->removeBranchWith(uri); return {}; } // If validated, merge if (!pimpl_->repository_->merge(remoteHead)) { JAMI_ERR("Could not merge history with %s", uri.c_str()); + pimpl_->repository_->removeBranchWith(uri); return {}; } lk.unlock(); diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index 52bd889e21f981dd347271fa9af55fe49a0bf4c6..f8f3e4c684d00d6f55893a2d55a8f09640f0a6bd 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -25,9 +25,50 @@ #include <map> #include <memory> #include <json/json.h> +#include <msgpack.hpp> namespace jami { +/** + * A ConversationRequest is a request which corresponds to a trust request, but for conversations + * It's signed by the sender and contains the members list, the conversationId, and the metadatas + * such as the conversation's vcard, etc. (TODO determine) + * Transmitted via the UDP DHT + */ +struct ConversationRequest +{ + std::string conversationId; + std::string from; + std::map<std::string, std::string> metadatas; + + time_t received {0}; + time_t declined {0}; + + ConversationRequest() = default; + ConversationRequest(const Json::Value& json); + + Json::Value toJson() const; + std::map<std::string, std::string> toMap() const; + + MSGPACK_DEFINE_MAP(from, conversationId, metadatas, received, declined) +}; + +struct ConvInfo +{ + std::string id {}; + time_t created {0}; + time_t removed {0}; + time_t erased {0}; + std::vector<std::string> members; + + ConvInfo() = default; + ConvInfo(const Json::Value& json); + + Json::Value toJson() const; + + MSGPACK_DEFINE_MAP(id, created, removed, erased, members) +}; + class JamiAccount; class ConversationRepository; enum class ConversationMode; diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 69844babf043732f1db1c57bd24c42a77c04b9e7..e86e549d45e335c58ba4e31b71db89dff7c45a41 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -2714,18 +2714,18 @@ ConversationRepository::resolveVote(const std::string& uri, bool isDevice) return {}; } -std::vector<ConversationCommit> +std::pair<std::vector<ConversationCommit>, bool> ConversationRepository::validFetch(const std::string& remoteDevice) const { auto newCommit = remoteHead(remoteDevice); if (not pimpl_ or newCommit.empty()) - return {}; + return {{}, false}; auto commitsToValidate = pimpl_->behind(newCommit); std::reverse(std::begin(commitsToValidate), std::end(commitsToValidate)); auto isValid = pimpl_->validCommits(commitsToValidate); if (isValid) - return commitsToValidate; - return {}; + return {commitsToValidate, false}; + return {{}, true}; } bool @@ -2734,6 +2734,20 @@ ConversationRepository::validClone() const return pimpl_->validCommits(logN("", 0)); } +void +ConversationRepository::removeBranchWith(const std::string& remoteDevice) +{ + git_remote* remote_ptr = nullptr; + auto repo = pimpl_->repository(); + if (git_remote_lookup(&remote_ptr, repo.get(), remoteDevice.c_str()) < 0) { + JAMI_WARN("No remote found with id: %s", remoteDevice.c_str()); + return; + } + GitRemote remote {remote_ptr, git_remote_free}; + + git_remote_prune(remote.get(), nullptr); +} + std::vector<std::string> ConversationRepository::getInitialMembers() const { diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index 1e8449a2d7b46dea12d3817d82fa7e2d9c36efa8..71457148a6417e153ccfe77b530ba393c6383b21 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -240,9 +240,21 @@ public: std::string voteKick(const std::string& uri, bool isDevice); std::string resolveVote(const std::string& uri, bool isDevice); - std::vector<ConversationCommit> validFetch(const std::string& remoteDevice) const; + /** + * Validate a fetch with remote device + * @param remotedevice + * @return the validated commits and if an error occurs + */ + std::pair<std::vector<ConversationCommit>, bool> validFetch( + const std::string& remoteDevice) const; bool validClone() const; + /** + * Delete branch with remote + * @param remoteDevice + */ + void removeBranchWith(const std::string& remoteDevice); + /** * One to one util, get initial members * @return initial members @@ -260,7 +272,6 @@ public: */ void refreshMembers() const; - /** * Because conversations can contains non contacts certificates, this methods * loads certificates in conversations into the cert store diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 5d5c850e28b6ac5886d8723964a54a67553ba4c1..b3a553290b9591395e9697f4fa93015654008a84 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -136,77 +136,6 @@ struct VCardMessageCtx std::string path; }; -struct ConvInfo -{ - std::string id {}; - time_t created {0}; - time_t removed {0}; - time_t erased {0}; - - ConvInfo() = default; - ConvInfo(const Json::Value& json) - { - id = json["id"].asString(); - created = json["created"].asLargestUInt(); - removed = json["removed"].asLargestUInt(); - erased = json["erased"].asLargestUInt(); - } - - Json::Value toJson() const - { - Json::Value json; - json["id"] = id; - json["created"] = Json::Int64(created); - if (removed) { - json["removed"] = Json::Int64(removed); - } - if (erased) { - json["erased"] = Json::Int64(erased); - } - return json; - } - - MSGPACK_DEFINE_MAP(id, created, removed, erased) -}; - -// ConversationRequest -ConversationRequest::ConversationRequest(const Json::Value& json) -{ - received = json["received"].asLargestUInt(); - declined = json["declined"].asLargestUInt(); - from = json["from"].asString(); - conversationId = json["conversationId"].asString(); - auto& md = json["metadatas"]; - for (const auto& member : md.getMemberNames()) { - metadatas.emplace(member, md[member].asString()); - } -} - -Json::Value -ConversationRequest::toJson() const -{ - Json::Value json; - json["conversationId"] = conversationId; - json["from"] = from; - json["received"] = static_cast<uint32_t>(received); - if (declined) - json["declined"] = static_cast<uint32_t>(declined); - for (const auto& [key, value] : metadatas) { - json["metadatas"][key] = value; - } - return json; -} - -std::map<std::string, std::string> -ConversationRequest::toMap() const -{ - auto result = metadatas; - result["id"] = conversationId; - result["from"] = from; - result["received"] = std::to_string(received); - return result; -} - namespace Migration { enum class State { // Contains all the Migration states @@ -1144,6 +1073,7 @@ JamiAccount::addDevice(const std::string& password) bool JamiAccount::exportArchive(const std::string& destinationPath, const std::string& password) { + saveConvInfos(); // Refresh members known if (auto manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) { return manager->exportArchive(destinationPath, password); } @@ -1238,6 +1168,7 @@ JamiAccount::loadAccount(const std::string& archive_password, req.metadatas = ConversationRepository::infosFromVCard(details); acc->conversationsRequests_[conversationId] = std::move(req); } + acc->saveConvRequests(); emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>( acc->getAccountID(), uri, payload, received); } @@ -1389,6 +1320,17 @@ JamiAccount::loadAccount(const std::string& archive_password, details[key] = value; setAccountDetails(details); + { + std::lock_guard<std::mutex> lkConv(conversationsMtx_); + convInfos_ = info.conversations; + } + { + std::lock_guard<std::mutex> lkReq(conversationsRequestsMtx_); + conversationsRequests_ = info.conversationsRequests; + } + saveConvInfos(); + saveConvRequests(); + if (not info.photo.empty() or not displayName_.empty()) emitSignal<DRing::ConfigurationSignal::AccountProfileReceived>(getAccountID(), displayName_, @@ -2697,6 +2639,33 @@ JamiAccount::doRegister_() }); } } + + for (const auto& c : convInfos_) { + if (!c.removed) { + auto it = conversations_.find(c.id); + if (it == conversations_.end()) { + std::shared_ptr<std::atomic_bool> willClone + = std::make_shared<std::atomic_bool>(false); + for (const auto& member : c.members) { + if (member != getUsername()) { + // Try to clone from first other members device found + accountManager_ + ->forEachDevice(dht::InfoHash(member), + [w = weak(), convId = c.id, willClone]( + const dht::InfoHash& dev) { + if (willClone->exchange(true)) + return; + auto shared = w.lock(); + if (!shared) + return; + shared->cloneConversation(dev.toString(), + convId); + }); + } + } + } + } + } } } catch (const std::exception& e) { JAMI_ERR("Error registering DHT account: %s", e.what()); @@ -3393,6 +3362,16 @@ JamiAccount::acceptTrustRequest(const std::string& from) bool JamiAccount::discardTrustRequest(const std::string& from) { + // Remove 1:1 generated conv requests + auto requests = getTrustRequests(); + for (const auto& req : requests) { + if (req.at(DRing::Account::TrustRequest::FROM) == from) { + declineConversationRequest(req.at(DRing::Account::TrustRequest::CONVERSATIONID)); + } + } + saveConvRequests(); + + // Remove trust request std::lock_guard<std::mutex> lock(configurationMutex_); if (accountManager_) return accountManager_->discardTrustRequest(from); @@ -3909,7 +3888,9 @@ JamiAccount::acceptConversationRequest(const std::string& conversationId) std::unique_lock<std::mutex> lk(conversationsRequestsMtx_); auto request = conversationsRequests_.find(conversationId); if (request == conversationsRequests_.end()) { - JAMI_WARN("Request not found for conversation %s", conversationId.c_str()); + JAMI_WARN("[Account %s] Request not found for conversation %s", + getAccountID().c_str(), + conversationId.c_str()); return; } { @@ -3918,7 +3899,7 @@ JamiAccount::acceptConversationRequest(const std::string& conversationId) } auto memberHash = dht::InfoHash(request->second.from); if (!memberHash) { - JAMI_WARN("Invalid member detected"); + JAMI_WARN("Invalid member detected: %s", request->second.from.c_str()); return; } forEachDevice(memberHash, [this, request = request->second](const dht::InfoHash& dev) { @@ -3976,45 +3957,66 @@ JamiAccount::handlePendingConversations() std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); for (auto it = pendingConversationsFetch_.begin(); it != pendingConversationsFetch_.end();) { if (it->second.ready) { - // Clone and store conversation - auto conversationId = it->first; - try { - auto conversation = std::make_shared<Conversation>(weak(), - it->second.deviceId, - conversationId); - if (conversation) { - auto commitId = conversation->join(); - ConvInfo info; - info.id = conversationId; - info.created = std::time(nullptr); - convInfos_.emplace_back(info); - { - std::lock_guard<std::mutex> lk(conversationsMtx_); - conversations_.emplace(conversationId, std::move(conversation)); + dht::ThreadPool::io().run([w = weak(), + conversationId = it->first, + deviceId = it->second.deviceId]() { + auto shared = w.lock(); + if (!shared) + return; + // Clone and store conversation + try { + auto conversation = std::make_shared<Conversation>(w, deviceId, conversationId); + if (!conversation->isMember(shared->getUsername(), true)) { + JAMI_ERR("Conversation cloned but doesn't seems to be a valid member"); + conversation->erase(); + return; } - if (!commitId.empty()) { - runOnMainThread([w = weak(), conversationId, commitId]() { - if (auto shared = w.lock()) { - std::lock_guard<std::mutex> lk(shared->conversationsMtx_); - auto it = shared->conversations_.find(conversationId); - // Do not sync as it's synched by convInfos - if (it != shared->conversations_.end()) - shared->sendMessageNotification(*it->second, commitId, false); + if (conversation) { + auto commitId = conversation->join(); + // TODO change convInfos to map<id, ConvInfo> + auto found = false; + for (const auto& ci : shared->convInfos_) { + if (ci.id == conversationId) { + found = true; + break; } - }); + } + if (!found) { + ConvInfo info; + info.id = conversationId; + info.created = std::time(nullptr); + shared->convInfos_.emplace_back(info); + } + { + std::lock_guard<std::mutex> lk(shared->conversationsMtx_); + shared->conversations_.emplace(conversationId, std::move(conversation)); + } + if (!commitId.empty()) { + runOnMainThread([w, conversationId, commitId]() { + if (auto shared = w.lock()) { + std::lock_guard<std::mutex> lk(shared->conversationsMtx_); + auto it = shared->conversations_.find(conversationId); + // Do not sync as it's synched by convInfos + if (it != shared->conversations_.end()) + shared->sendMessageNotification(*it->second, + commitId, + false); + } + }); + } + shared->saveConvInfos(); + // Inform user that the conversation is ready + emitSignal<DRing::ConversationSignal::ConversationReady>(shared->accountID_, + conversationId); } - saveConvInfos(); - // Inform user that the conversation is ready - emitSignal<DRing::ConversationSignal::ConversationReady>(accountID_, - conversationId); + } catch (const std::exception& e) { + emitSignal<DRing::ConversationSignal::OnConversationError>(shared->accountID_, + conversationId, + EFETCH, + e.what()); + JAMI_WARN("Something went wrong when cloning conversation: %s", e.what()); } - } catch (const std::exception& e) { - emitSignal<DRing::ConversationSignal::OnConversationError>(getAccountID(), - conversationId, - EFETCH, - e.what()); - JAMI_WARN("Something went wrong when cloning conversation: %s", e.what()); - } + }); it = pendingConversationsFetch_.erase(it); } else { ++it; @@ -4052,19 +4054,18 @@ JamiAccount::removeConversation(const std::string& conversationId) for (auto& info : convInfos_) { if (info.id == conversationId) { info.removed = std::time(nullptr); - saveConvInfos(); - if (hasMembers) { - // Sync now, because it can take some time to really removes the datas - runOnMainThread([w = weak()]() { - // Invite connected devices for the same user - auto shared = w.lock(); - if (!shared or !shared->accountManager_) - return; + // Sync now, because it can take some time to really removes the datas + runOnMainThread([w = weak(), hasMembers]() { + // Invite connected devices for the same user + auto shared = w.lock(); + if (!shared or !shared->accountManager_) + return; - // Send to connected devices + shared->saveConvInfos(); + // Send to connected devices + if (hasMembers) shared->syncWithConnected(); - }); - } + }); break; } } @@ -4194,7 +4195,7 @@ JamiAccount::addConversationMember(const std::string& conversationId, const std::string& contactUri, bool sendRequest) { - std::lock_guard<std::mutex> lk(conversationsMtx_); + std::unique_lock<std::mutex> lk(conversationsMtx_); // Add a new member in the conversation auto it = conversations_.find(conversationId); if (it == conversations_.end()) { @@ -4208,7 +4209,9 @@ JamiAccount::addConversationMember(const std::string& conversationId, conversationId.c_str()); // Note: This should not be necessary, but if for whatever reason the other side didn't join // we should not forbid new invites - sendTextMessage(contactUri, it->second->generateInvitation()); + auto invite = it->second->generateInvitation(); + lk.unlock(); + sendTextMessage(contactUri, invite); return true; } @@ -4222,7 +4225,7 @@ JamiAccount::addConversationMember(const std::string& conversationId, auto shared = w.lock(); if (not shared or messages.empty()) return; // should not happen - std::lock_guard<std::mutex> lk(shared->conversationsMtx_); + std::unique_lock<std::mutex> lk(shared->conversationsMtx_); // Add a new member in the conversation auto it = shared->conversations_.find(conversationId); if (it == shared->conversations_.end()) { @@ -4234,9 +4237,12 @@ JamiAccount::addConversationMember(const std::string& conversationId, emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), conversationId, message); - if (sendRequest) - shared->sendTextMessage(contactUri, it->second->generateInvitation()); shared->sendMessageNotification(*it->second, commitId, true); + if (sendRequest) { + auto invite = it->second->generateInvitation(); + lk.unlock(); + shared->sendTextMessage(contactUri, invite); + } }, commitId, 1); @@ -4284,7 +4290,7 @@ JamiAccount::removeConversationMember(const std::string& conversationId, } std::vector<std::map<std::string, std::string>> -JamiAccount::getConversationMembers(const std::string& conversationId) +JamiAccount::getConversationMembers(const std::string& conversationId) const { std::lock_guard<std::mutex> lk(conversationsMtx_); auto conversation = conversations_.find(conversationId); @@ -4541,9 +4547,16 @@ JamiAccount::fetchNewCommits(const std::string& peer, if (pendingConversationsFetch_.find(conversationId) != pendingConversationsFetch_.end()) return; } - JAMI_WARN("[Account %s] Could not find conversation %s", + for (const auto& ci : convInfos_) { + if (ci.id == conversationId) { + cloneConversation(deviceId, conversationId); + return; + } + } + JAMI_WARN("[Account %s] Could not find conversation %s, ask for an invite", getAccountID().c_str(), conversationId.c_str()); + sendTextMessage(peer, {{"application/invite", conversationId}}); } } @@ -4551,9 +4564,10 @@ void JamiAccount::onConversationRequest(const std::string& from, const Json::Value& value) { ConversationRequest req(value); - JAMI_INFO("[Account %s] Receive a new conversation request for conversation %s", + JAMI_INFO("[Account %s] Receive a new conversation request for conversation %s from %s", getAccountID().c_str(), - req.conversationId.c_str()); + req.conversationId.c_str(), + from.c_str()); auto convId = req.conversationId; req.from = from; @@ -4578,6 +4592,29 @@ JamiAccount::onConversationRequest(const std::string& from, const Json::Value& v req.toMap()); } +void +JamiAccount::onNeedConversationRequest(const std::string& from, const std::string& conversationId) +{ + // Check if conversation exists + std::unique_lock<std::mutex> lk(conversationsMtx_); + auto itConv = conversations_.find(conversationId); + if (itConv != conversations_.end() && !itConv->second->isRemoving()) { + // Check if isMember + if (!itConv->second->isMember(from, true)) { + JAMI_WARN("%s is asking a new invite for %s, but not a member", + from.c_str(), + conversationId.c_str()); + return; + } + + // Send new invite + auto invite = itConv->second->generateInvitation(); + lk.unlock(); + JAMI_DBG("%s is asking a new invite for %s", from.c_str(), conversationId.c_str()); + sendTextMessage(from, invite); + } +} + void JamiAccount::cacheTurnServers() { @@ -5007,49 +5044,7 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket, conversationsRequests_.erase(convId); } if (not removed) { - if (!isConversation(convId)) { - { - std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); - auto it = pendingConversationsFetch_.find(convId); - if (it != pendingConversationsFetch_.end()) // Already pending - return len; - pendingConversationsFetch_[convId] = PendingConversationFetch {}; - } - std::lock_guard<std::mutex> lkCM(connManagerMtx_); - if (!connectionManager_) - return len; - connectionManager_->connectDevice( - DeviceId(deviceId), - std::string("git://").append(deviceId).append("/").append(convId), - [this, convId](std::shared_ptr<ChannelSocket> socket, - const DeviceId& deviceId) { - if (socket) { - std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_); - auto& pending = pendingConversationsFetch_[convId]; - if (!pending.ready) { - pending.ready = true; - pending.deviceId = deviceId.toString(); - lk.unlock(); - // Save the git socket - addGitSocket(deviceId.toString(), convId, socket); - checkConversationsEvents(); - } else { - lk.unlock(); - socket->shutdown(); - } - } - }); - - JAMI_INFO( - "[Account %s] New conversation detected: %s. Ask device %s to clone it", - getAccountID().c_str(), - convId.c_str(), - deviceId.c_str()); - } else { - JAMI_INFO("[Account %s] Already have conversation %s", - getAccountID().c_str(), - convId.c_str()); - } + cloneConversation(deviceId, convId); } else { { std::lock_guard<std::mutex> lk(conversationsMtx_); @@ -5179,14 +5174,13 @@ JamiAccount::loadConvInfos() auto file = fileutils::loadFile("convInfo", idPath_); // load values msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size()); - oh.get().convert(convInfo); + oh.get().convert(convInfos_); } catch (const std::exception& e) { JAMI_WARN("[convInfo] error loading convInfo: %s", e.what()); return; } for (auto& info : convInfo) { - convInfos_.emplace_back(info); std::lock_guard<std::mutex> lk(conversationsMtx_); auto itConv = conversations_.find(info.id); if (itConv != conversations_.end() && info.removed) { @@ -5200,6 +5194,20 @@ JamiAccount::saveConvInfos() const { std::ofstream file(idPath_ + DIR_SEPARATOR_STR "convInfo", std::ios::trunc | std::ios::binary); msgpack::pack(file, convInfos_); + + // Update infos + // TODO avoid to do this for all conversations, just last updated if possible + for (auto& c : convInfos_) { + c.members.clear(); + auto members = getConversationMembers(c.id); + for (const auto& member : members) { + auto uri = member.find("uri"); + if (uri != member.end()) { + c.members.emplace_back(uri->second); + } + } + } + accountManager_->setConversations(convInfos_); } void @@ -5224,6 +5232,9 @@ JamiAccount::saveConvRequests() std::ofstream file(idPath_ + DIR_SEPARATOR_STR "convRequests", std::ios::trunc | std::ios::binary); msgpack::pack(file, conversationsRequests_); + + // Update infos + accountManager_->setConversationsRequests(conversationsRequests_); } void @@ -5242,11 +5253,12 @@ JamiAccount::removeRepository(const std::string& conversationId, bool sync, bool for (auto& info : convInfos_) { if (info.id == conversationId) { info.erased = std::time(nullptr); - saveConvInfos(); runOnMainThread([w = weak()]() { // Send to connected devices - if (auto shared = w.lock()) + if (auto shared = w.lock()) { + shared->saveConvInfos(); // will lock conversationsMtx_ shared->syncWithConnected(); + } }); break; } @@ -5345,4 +5357,51 @@ JamiAccount::monitor() const connectionManager_->monitor(); } +void +JamiAccount::cloneConversation(const std::string& deviceId, const std::string& convId) +{ + if (!isConversation(convId)) { + { + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + auto it = pendingConversationsFetch_.find(convId); + if (it != pendingConversationsFetch_.end()) // Already pending + return; + pendingConversationsFetch_[convId] = PendingConversationFetch {}; + } + std::lock_guard<std::mutex> lkCM(connManagerMtx_); + if (!connectionManager_) + return; + connectionManager_ + ->connectDevice(DeviceId(deviceId), + "git://" + deviceId + "/" + convId, + [this, convId](std::shared_ptr<ChannelSocket> socket, + const DeviceId& deviceId) { + if (socket) { + std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_); + auto& pending = pendingConversationsFetch_[convId]; + if (!pending.ready) { + pending.ready = true; + pending.deviceId = deviceId.toString(); + lk.unlock(); + // Save the git socket + addGitSocket(deviceId.toString(), convId, socket); + checkConversationsEvents(); + } else { + lk.unlock(); + socket->shutdown(); + } + } + }); + + JAMI_INFO("[Account %s] New conversation detected: %s. Ask device %s to clone it", + getAccountID().c_str(), + convId.c_str(), + deviceId.c_str()); + } else { + JAMI_INFO("[Account %s] Already have conversation %s", + getAccountID().c_str(), + convId.c_str()); + } +} + } // namespace jami diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 6a801e8d2c3b730b4eeb902c4d524202cd047a23..98e7fffb325fa8ef322352cbee475d271241133c 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -31,6 +31,7 @@ #include "security/diffie-hellman.h" #include "sip/sipaccountbase.h" #include "dring/datatransfer_interface.h" +#include "jamidht/conversation.h" #include "multiplexed_socket.h" #include "noncopyable.h" @@ -84,32 +85,6 @@ class AccountManager; struct AccountInfo; class SipTransport; class ChanneledOutgoingTransfer; -class Conversation; -struct ConvInfo; - -/** - * A ConversationRequest is a request which corresponds to a trust request, but for conversations - * It's signed by the sender and contains the members list, the conversationId, and the metadatas - * such as the conversation's vcard, etc. (TODO determine) - * Transmitted via the UDP DHT - */ -struct ConversationRequest -{ - std::string conversationId; - std::string from; - std::map<std::string, std::string> metadatas; - - time_t received {0}; - time_t declined {0}; - - ConversationRequest() = default; - ConversationRequest(const Json::Value& json); - - Json::Value toJson() const; - std::map<std::string, std::string> toMap() const; - - MSGPACK_DEFINE_MAP(conversationId, metadatas, received, declined) -}; using SipConnectionKey = std::pair<std::string /* accountId */, DeviceId>; using GitSocketList = std::map<std::string, /* device Id */ @@ -527,7 +502,7 @@ public: const std::string& contactUri, bool isDevice = false); std::vector<std::map<std::string, std::string>> getConversationMembers( - const std::string& conversationId); + const std::string& conversationId) const; // Message send/load void sendMessage(const std::string& conversationId, @@ -568,9 +543,18 @@ public: // Invites void onConversationRequest(const std::string& from, const Json::Value&) override; + void onNeedConversationRequest(const std::string& from, + const std::string& conversationId) override; void monitor() const; + /** + * Clone a conversation (initial) from device + * @param deviceId + * @param convId + */ + void cloneConversation(const std::string& deviceId, const std::string& convId); + private: NON_COPYABLE(JamiAccount); @@ -773,7 +757,7 @@ private: std::lock_guard<std::mutex> lk(conversationsMtx_); return conversations_.find(convId) != conversations_.end(); } - std::vector<ConvInfo> convInfos_; + mutable std::vector<ConvInfo> convInfos_; mutable std::mutex dhtValuesMtx_; bool dhtPublicInCalls_ {true}; diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index f575615fadf8f4ab8a54edd9398c494bc7e112c5..93930760329291e8b7a17a51b9aec7f4af5164f9 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -62,6 +62,7 @@ namespace jami { static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"}; static constexpr const char MIME_TYPE_GIT[] {"application/im-gitmessage-id"}; static constexpr const char MIME_TYPE_INVITE_JSON[] {"application/invite+json"}; +static constexpr const char MIME_TYPE_INVITE[] {"application/invite"}; static constexpr const char MIME_TYPE_IM_COMPOSING[] {"application/im-iscomposing+xml"}; static constexpr std::chrono::steady_clock::duration COMPOSING_TIMEOUT {std::chrono::seconds(12)}; @@ -589,6 +590,8 @@ SIPAccountBase::onTextMessage(const std::string& id, return; } onConversationRequest(from, json); + } else if (m.first == MIME_TYPE_INVITE) { + onNeedConversationRequest(from, m.second); } } diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index ddc452576a6cff20493eb777754531833328a150..5bc91e99c2cb0119fa1eac3b36fc00c56895e9e2 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -675,8 +675,7 @@ ConversationTest::testAddOfflineMemberThenConnects() CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri)); Manager::instance().sendRegister(carlaId, true); - cv.wait_for(lk, std::chrono::seconds(60)); - CPPUNIT_ASSERT(requestReceived); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return requestReceived; })); carlaAccount->acceptConversationRequest(convId); cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }); @@ -995,19 +994,21 @@ ConversationTest::testSendMessageToMultipleParticipants() std::mutex mtx; std::unique_lock<std::mutex> lk {mtx}; std::condition_variable cv; + bool carlaConnected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; if (daemonStatus == "REGISTERED") { + carlaConnected = true; cv.notify_one(); } })); DRing::registerSignalHandlers(confHandlers); Manager::instance().sendRegister(carlaId, true); - cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return carlaConnected; })); confHandlers.clear(); DRing::unregisterSignalHandlers(); @@ -1225,14 +1226,14 @@ ConversationTest::testMemberBanNoBadFile() auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); auto carlaUri = carlaAccount->getUsername(); aliceAccount->trackBuddyPresence(carlaUri, true); - Manager::instance().sendRegister(carlaId, 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, - voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false, + carlaConnected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -1241,6 +1242,16 @@ ConversationTest::testMemberBanNoBadFile() requestReceived = true; cv.notify_one(); })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( [&](const std::string& accountId, const std::string& conversationId) { if (accountId == bobId && conversationId == convId) { @@ -1272,6 +1283,8 @@ ConversationTest::testMemberBanNoBadFile() cv.notify_one(); })); DRing::registerSignalHandlers(confHandlers); + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return carlaConnected; })); CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived && memberMessageGenerated; @@ -2051,22 +2064,32 @@ ConversationTest::testMemberCannotBanOther() auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); auto carlaUri = carlaAccount->getUsername(); aliceAccount->trackBuddyPresence(carlaUri, true); - Manager::instance().sendRegister(carlaId, 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, - voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false, + carlaConnected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( - [&](const std::string& /*accountId*/, + [&](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::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( [&](const std::string& accountId, const std::string& conversationId) { if (accountId == bobId && conversationId == convId) { @@ -2098,6 +2121,8 @@ ConversationTest::testMemberCannotBanOther() cv.notify_one(); })); DRing::registerSignalHandlers(confHandlers); + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return carlaConnected; })); CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived && memberMessageGenerated; @@ -2107,6 +2132,7 @@ ConversationTest::testMemberCannotBanOther() CPPUNIT_ASSERT( cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); requestReceived = false; + memberMessageGenerated = false; CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived && memberMessageGenerated; @@ -2140,14 +2166,14 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected() auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); auto carlaUri = carlaAccount->getUsername(); aliceAccount->trackBuddyPresence(carlaUri, true); - Manager::instance().sendRegister(carlaId, 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, - voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false, + carlaConnected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -2177,6 +2203,16 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected() } cv.notify_one(); })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( [&](const std::string& /* accountId */, const std::string& /* conversationId */, @@ -2187,6 +2223,8 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected() cv.notify_one(); })); DRing::registerSignalHandlers(confHandlers); + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return carlaConnected; })); CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived && memberMessageGenerated; @@ -2224,14 +2262,14 @@ ConversationTest::testVoteNonEmpty() auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); auto carlaUri = carlaAccount->getUsername(); aliceAccount->trackBuddyPresence(carlaUri, true); - Manager::instance().sendRegister(carlaId, 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, - voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false, + carlaConnected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -2247,6 +2285,16 @@ ConversationTest::testVoteNonEmpty() cv.notify_one(); } })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( [&](const std::string& accountId, const std::string& conversationId, @@ -2271,6 +2319,8 @@ ConversationTest::testVoteNonEmpty() cv.notify_one(); })); DRing::registerSignalHandlers(confHandlers); + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return carlaConnected; })); CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived && memberMessageGenerated; @@ -2493,7 +2543,6 @@ ConversationTest::testNoBadFileInInitialCommit() Manager::instance().sendRegister(carlaId, true); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; })); - JAMI_ERR("@@@@@@@@@@@22"); CPPUNIT_ASSERT(carlaAccount->addConversationMember(convId, aliceUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); @@ -2590,14 +2639,15 @@ ConversationTest::testVoteNoBadFile() auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); auto carlaUri = carlaAccount->getUsername(); aliceAccount->trackBuddyPresence(carlaUri, true); - Manager::instance().sendRegister(carlaId, 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, - voteMessageGenerated = false, messageBobReceived = false, messageCarlaReceived = false; + voteMessageGenerated = false, messageBobReceived = false, messageCarlaReceived = false, + carlaConnected = true; + ; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -2613,6 +2663,16 @@ ConversationTest::testVoteNoBadFile() cv.notify_one(); } })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( [&](const std::string& accountId, const std::string& conversationId, @@ -2635,6 +2695,9 @@ ConversationTest::testVoteNoBadFile() cv.notify_one(); })); DRing::registerSignalHandlers(confHandlers); + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return carlaConnected; })); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived && memberMessageGenerated; @@ -2880,6 +2943,7 @@ ConversationTest::testMemberJoinsNoBadFile() + DIR_SEPARATOR_STR + "convInfo"; auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "convInfo"; + std::remove(ciPathCarla.c_str()); std::filesystem::copy(ciPathAlice, ciPathCarla); // Accept for alice and makes different heads @@ -2952,6 +3016,7 @@ ConversationTest::testMemberAddedNoCertificate() + DIR_SEPARATOR_STR + "convInfo"; auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "convInfo"; + std::remove(ciPathCarla.c_str()); std::filesystem::copy(ciPathAlice, ciPathCarla); // Remove invite but do not add member certificate @@ -3033,6 +3098,7 @@ ConversationTest::testMemberJoinsInviteRemoved() + DIR_SEPARATOR_STR + "convInfo"; auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "convInfo"; + std::remove(ciPathCarla.c_str()); std::filesystem::copy(ciPathAlice, ciPathCarla); // Let invited, but add /members + /devices @@ -3840,6 +3906,7 @@ END:VCARD"; const std::string& /*from*/, const std::vector<uint8_t>& payload, time_t /*received*/) { + auto pstr = std::string(payload.begin(), payload.begin() + payload.size()); if (account_id == bobId && std::string(payload.data(), payload.data() + payload.size()) == vcard) requestReceived = true; @@ -3867,7 +3934,7 @@ END:VCARD"; aliceAccount->addContact(bobUri); std::vector<uint8_t> payload(vcard.begin(), vcard.end()); aliceAccount->sendTrustRequest(bobUri, payload); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&]() { + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&]() { return !convId.empty() && requestReceived; })); } diff --git a/test/unitTest/syncHistory/syncHistory.cpp b/test/unitTest/syncHistory/syncHistory.cpp index 8fec5e149ccb9782ebf6317160c068a3396f68a5..9a1c93c9d11d9f0be3acab55b1bd7f8749832af7 100644 --- a/test/unitTest/syncHistory/syncHistory.cpp +++ b/test/unitTest/syncHistory/syncHistory.cpp @@ -23,6 +23,7 @@ #include <condition_variable> #include <filesystem> +#include "fileutils.h" #include "manager.h" #include "jamidht/connectionmanager.h" #include "jamidht/multiplexed_socket.h" @@ -59,15 +60,23 @@ private: void testCreateConversationThenSync(); void testCreateConversationWithOnlineDevice(); void testCreateConversationWithMessagesThenAddDevice(); + void testCreateMultipleConversationThenAddDevice(); void testReceivesInviteThenAddDevice(); void testRemoveConversationOnAllDevices(); + void testSyncCreateAccountExportDeleteReimportOldBackup(); + void testSyncCreateAccountExportDeleteReimportWithConvId(); + void testSyncCreateAccountExportDeleteReimportWithConvReq(); CPPUNIT_TEST_SUITE(SyncHistoryTest); CPPUNIT_TEST(testCreateConversationThenSync); CPPUNIT_TEST(testCreateConversationWithOnlineDevice); CPPUNIT_TEST(testCreateConversationWithMessagesThenAddDevice); + CPPUNIT_TEST(testCreateMultipleConversationThenAddDevice); CPPUNIT_TEST(testReceivesInviteThenAddDevice); CPPUNIT_TEST(testRemoveConversationOnAllDevices); + CPPUNIT_TEST(testSyncCreateAccountExportDeleteReimportOldBackup); + CPPUNIT_TEST(testSyncCreateAccountExportDeleteReimportWithConvId); + CPPUNIT_TEST(testSyncCreateAccountExportDeleteReimportWithConvReq); CPPUNIT_TEST_SUITE_END(); }; @@ -333,6 +342,77 @@ SyncHistoryTest::testCreateConversationWithMessagesThenAddDevice() CPPUNIT_ASSERT(messages[2]["body"] == "Message 1"); } +void +SyncHistoryTest::testCreateMultipleConversationThenAddDevice() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + // Start conversation + auto convId = aliceAccount->startConversation(); + aliceAccount->sendMessage(convId, std::string("Message 1")); + aliceAccount->sendMessage(convId, std::string("Message 2")); + aliceAccount->sendMessage(convId, std::string("Message 3")); + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto convId2 = aliceAccount->startConversation(); + aliceAccount->sendMessage(convId2, std::string("Message 1")); + aliceAccount->sendMessage(convId2, std::string("Message 2")); + aliceAccount->sendMessage(convId2, std::string("Message 3")); + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto convId3 = aliceAccount->startConversation(); + aliceAccount->sendMessage(convId3, std::string("Message 1")); + aliceAccount->sendMessage(convId3, std::string("Message 2")); + aliceAccount->sendMessage(convId3, std::string("Message 3")); + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto convId4 = aliceAccount->startConversation(); + aliceAccount->sendMessage(convId4, std::string("Message 1")); + aliceAccount->sendMessage(convId4, std::string("Message 2")); + aliceAccount->sendMessage(convId4, std::string("Message 3")); + + // Now create alice2 + auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz"; + std::remove(aliceArchive.c_str()); + aliceAccount->exportArchive(aliceArchive); + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE2"; + details[ConfProperties::ALIAS] = "ALICE2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = aliceArchive; + alice2Id = Manager::instance().addAccount(details); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::atomic_int conversationReady = 0; + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto alice2Account = Manager::instance().getAccount<JamiAccount>(alice2Id); + if (!alice2Account) + return; + auto details = alice2Account->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == alice2Id) { + conversationReady += 1; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + confHandlers.clear(); + + // Check if conversation is ready + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(60), [&]() { return conversationReady == 4; })); + DRing::unregisterSignalHandlers(); +} + void SyncHistoryTest::testReceivesInviteThenAddDevice() { @@ -463,6 +543,321 @@ SyncHistoryTest::testRemoveConversationOnAllDevices() DRing::unregisterSignalHandlers(); } +void +SyncHistoryTest::testSyncCreateAccountExportDeleteReimportOldBackup() +{ + std::this_thread::sleep_for(std::chrono::seconds(10)); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + + // Backup alice before start conversation, worst scenario for invites + auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz"; + std::remove(aliceArchive.c_str()); + aliceAccount->exportArchive(aliceArchive); + + // Start conversation + auto convId = aliceAccount->startConversation(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + bool aliceReady = false; + 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 == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + 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(); + } + + if (accountId == alice2Id && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto alice2Account = Manager::instance().getAccount<JamiAccount>(alice2Id); + if (!alice2Account) + return; + auto details = alice2Account->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + aliceReady = 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; })); + + // Wait that alice sees Bob + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; }); + + // disable account (same as removed) + Manager::instance().sendRegister(aliceId, false); + std::this_thread::sleep_for(std::chrono::seconds(5)); + + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE2"; + details[ConfProperties::ALIAS] = "ALICE2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = aliceArchive; + requestReceived = false; + conversationReady = false; + alice2Id = Manager::instance().addAccount(details); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return aliceReady; })); + std::this_thread::sleep_for(std::chrono::seconds(10)); + aliceAccount = Manager::instance().getAccount<JamiAccount>(alice2Id); + + // This will trigger a conversation request. Cause alice2 can't know first conversation + bobAccount->sendMessage(convId, std::string("hi")); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return requestReceived; })); + + aliceAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return conversationReady; })); + + messageBobReceived = 0; + aliceAccount->sendMessage(convId, std::string("hi")); + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }); +} + +void +SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvId() +{ + std::this_thread::sleep_for(std::chrono::seconds(10)); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + + // Start conversation + auto convId = aliceAccount->startConversation(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + bool aliceReady = false; + 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 == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + 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(); + } + + if (accountId == alice2Id && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto alice2Account = Manager::instance().getAccount<JamiAccount>(alice2Id); + if (!alice2Account) + return; + auto details = alice2Account->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + aliceReady = 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; })); + + // Wait that alice sees Bob + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; }); + + // Backup alice after startConversation with member + auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz"; + std::remove(aliceArchive.c_str()); + aliceAccount->exportArchive(aliceArchive); + + // disable account (same as removed) + Manager::instance().sendRegister(aliceId, false); + std::this_thread::sleep_for(std::chrono::seconds(5)); + + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE2"; + details[ConfProperties::ALIAS] = "ALICE2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = aliceArchive; + requestReceived = false; + conversationReady = false; + alice2Id = Manager::instance().addAccount(details); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return aliceReady; })); + std::this_thread::sleep_for(std::chrono::seconds(10)); + aliceAccount = Manager::instance().getAccount<JamiAccount>(alice2Id); + + // Should retrieve conversation, no need for action as the convInfos is in the archive + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return conversationReady; })); + + messageBobReceived = 0; + aliceAccount->sendMessage(convId, std::string("hi")); + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }); +} + +void +SyncHistoryTest::testSyncCreateAccountExportDeleteReimportWithConvReq() +{ + std::this_thread::sleep_for(std::chrono::seconds(10)); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto aliceUri = aliceAccount->getUsername(); + + // Start conversation + auto convId = bobAccount->startConversation(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + bool aliceReady = false; + 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 == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + 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(); + } + + if (accountId == alice2Id && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto alice2Account = Manager::instance().getAccount<JamiAccount>(alice2Id); + if (!alice2Account) + return; + auto details = alice2Account->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + aliceReady = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + CPPUNIT_ASSERT(bobAccount->addConversationMember(convId, aliceUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + + // Backup alice after startConversation with member + auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz"; + std::remove(aliceArchive.c_str()); + aliceAccount->exportArchive(aliceArchive); + + // disable account (same as removed) + Manager::instance().sendRegister(aliceId, false); + std::this_thread::sleep_for(std::chrono::seconds(5)); + + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE2"; + details[ConfProperties::ALIAS] = "ALICE2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = aliceArchive; + conversationReady = false; + alice2Id = Manager::instance().addAccount(details); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return aliceReady; })); + std::this_thread::sleep_for(std::chrono::seconds(10)); + aliceAccount = Manager::instance().getAccount<JamiAccount>(alice2Id); + + // Should get the same request as before. + aliceAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return conversationReady && messageBobReceived == 1; + })); +} + } // namespace test } // namespace jami