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