From b3d8577db4d65bd713a4232dc7a888a2871e1ace Mon Sep 17 00:00:00 2001 From: Fadi SHEHADEH <fadi.shehadeh@savoirfairelinux.com> Date: Fri, 31 Mar 2023 13:59:31 -0400 Subject: [PATCH] DRT: integrate DRT components to conversation This patches avoid swarm members to connect to all devices of the conversation by using a better routing mechanism. The idea is to add a component called "DRT" that will store, like a DHT, a routing table containing the known devices of the conversation. The DRT is bootstraped with the list of the devices that already wrote a message in the conversation, else with some other members that have devices present (because lot of devices may never have written any message). Moreover, because a user can import an old backup without the conversation and without other devices present, we also try to connect to 2 other random members because they may never connect if they are on an old back-up. Once bootstraped, the DRT is used to decide to who a user should be connected. And it will use approximately log(N) connections (instead of N like before). So, when sending a message, a user will send to connected peers (and some mobiles). Then, some peers in the conversation will get the message, pull it, then announce to their nearest peers (and some mobiles) until all peers got the new message. To avoid infinite loop, we check if we already got the commit and if we have something to announce. Finally, if somebody disconnect, this will cause the DRT to try to re-fill its bucket and try to connect to a new device. GitLab: #297 Change-Id: I137788c77219fe74287585882260547cc5628784 --- src/account.h | 19 +- src/connectivity/connectionmanager.cpp | 19 +- src/connectivity/multiplexed_socket.cpp | 34 +- src/connectivity/multiplexed_socket.h | 2 + src/im/message_engine.cpp | 51 +- src/im/message_engine.h | 25 +- src/jamidht/conversation.cpp | 293 ++++++++-- src/jamidht/conversation.h | 49 +- src/jamidht/conversation_module.cpp | 34 +- src/jamidht/conversation_module.h | 4 +- src/jamidht/conversationrepository.cpp | 61 ++- src/jamidht/conversationrepository.h | 16 +- src/jamidht/jamiaccount.cpp | 515 ++++++++++-------- src/jamidht/jamiaccount.h | 27 +- src/jamidht/swarm/routing_table.cpp | 29 + src/jamidht/swarm/routing_table.h | 12 +- src/jamidht/swarm/swarm_manager.cpp | 240 ++++---- src/jamidht/swarm/swarm_manager.h | 123 ++++- src/manager.cpp | 22 +- src/manager.h | 13 +- src/meson.build | 4 + src/sip/sipaccount.cpp | 29 +- src/sip/sipaccount.h | 20 +- src/sip/sipaccountbase.h | 6 +- test/unitTest/Makefile.am | 18 + test/unitTest/actors/account_list.yml | 112 ++++ test/unitTest/conversation/conversation.cpp | 20 +- .../conversation/conversationMembersEvent.cpp | 1 + .../conversation/conversationRequest.cpp | 5 +- .../conversationRepository.cpp | 361 +----------- test/unitTest/fileTransfer/fileTransfer.cpp | 92 +--- test/unitTest/swarm/bootstrap.cpp | 392 +++++++++++++ test/unitTest/swarm/routing_table.cpp | 4 +- test/unitTest/swarm/swarm_conversation.cpp | 223 ++++++++ test/unitTest/swarm/swarm_spread.cpp | 8 +- 35 files changed, 1911 insertions(+), 972 deletions(-) create mode 100644 test/unitTest/actors/account_list.yml create mode 100644 test/unitTest/swarm/bootstrap.cpp create mode 100644 test/unitTest/swarm/swarm_conversation.cpp diff --git a/src/account.h b/src/account.h index 949c6dffdc..bd7a99bfc2 100644 --- a/src/account.h +++ b/src/account.h @@ -112,12 +112,16 @@ public: */ virtual void loadConfig(); - const AccountConfig& config() const { - if (config_) return *config_; - else throw std::runtime_error("Account doesn't have a configuration"); + const AccountConfig& config() const + { + if (config_) + return *config_; + else + throw std::runtime_error("Account doesn't have a configuration"); } - inline void editConfig(std::function<void(AccountConfig& config)>&& edit) { + inline void editConfig(std::function<void(AccountConfig& config)>&& edit) + { std::lock_guard<std::recursive_mutex> lock(configurationMutex_); edit(*config_); saveConfig(); @@ -125,7 +129,8 @@ public: virtual void saveConfig() const; - void setAccountDetails(const std::map<std::string, std::string>& details) { + void setAccountDetails(const std::map<std::string, std::string>& details) + { std::lock_guard<std::recursive_mutex> lock(configurationMutex_); if (not config_) config_ = buildConfig(); @@ -134,7 +139,8 @@ public: saveConfig(); } - std::map<std::string, std::string> getAccountDetails() const { + std::map<std::string, std::string> getAccountDetails() const + { std::lock_guard<std::recursive_mutex> lock(configurationMutex_); return config().toMap(); } @@ -186,6 +192,7 @@ public: * @return a token to query the message status */ virtual uint64_t sendTextMessage(const std::string& /*to*/, + const std::string& /*deviceId*/, const std::map<std::string, std::string>& /*payloads*/, uint64_t /*refreshToken*/ = 0, bool /*onlyConnected*/ = false) diff --git a/src/connectivity/connectionmanager.cpp b/src/connectivity/connectionmanager.cpp index 9457e404ca..61f507f752 100644 --- a/src/connectivity/connectionmanager.cpp +++ b/src/connectivity/connectionmanager.cpp @@ -60,7 +60,6 @@ struct ConnectionInfo std::function<void(bool)> onConnected_; std::unique_ptr<asio::steady_timer> waitForAnswer_ {}; - }; class ConnectionManager::Impl : public std::enable_shared_from_this<ConnectionManager::Impl> @@ -344,12 +343,17 @@ ConnectionManager::Impl::connectDeviceStartIce( } info->onConnected_ = std::move(onConnected); - info->waitForAnswer_ = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext(), std::chrono::steady_clock::now() + DHT_MSG_TIMEOUT); - info->waitForAnswer_->async_wait(std::bind(&ConnectionManager::Impl::onResponse, this, std::placeholders::_1, deviceId, vid)); + info->waitForAnswer_ = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext(), + std::chrono::steady_clock::now() + + DHT_MSG_TIMEOUT); + info->waitForAnswer_->async_wait( + std::bind(&ConnectionManager::Impl::onResponse, this, std::placeholders::_1, deviceId, vid)); } void -ConnectionManager::Impl::onResponse(const asio::error_code& ec, const DeviceId& deviceId, const dht::Value::Id& vid) +ConnectionManager::Impl::onResponse(const asio::error_code& ec, + const DeviceId& deviceId, + const dht::Value::Id& vid) { if (ec == asio::error::operation_aborted) return; @@ -384,7 +388,6 @@ ConnectionManager::Impl::onResponse(const asio::error_code& ec, const DeviceId& info->onConnected_(true); } - bool ConnectionManager::Impl::connectDeviceOnNegoDone( const DeviceId& deviceId, @@ -706,7 +709,11 @@ ConnectionManager::Impl::onPeerResponse(const PeerConnectionRequest& req) info->responseReceived_ = true; info->response_ = std::move(req); info->waitForAnswer_->expires_at(std::chrono::steady_clock::now()); - info->waitForAnswer_->async_wait(std::bind(&ConnectionManager::Impl::onResponse, this, std::placeholders::_1, device, req.id)); + info->waitForAnswer_->async_wait(std::bind(&ConnectionManager::Impl::onResponse, + this, + std::placeholders::_1, + device, + req.id)); } else { JAMI_WARN() << account << " respond received, but cannot find request"; } diff --git a/src/connectivity/multiplexed_socket.cpp b/src/connectivity/multiplexed_socket.cpp index 2670f71626..3c48f3db2c 100644 --- a/src/connectivity/multiplexed_socket.cpp +++ b/src/connectivity/multiplexed_socket.cpp @@ -127,19 +127,15 @@ public: { auto& channelSocket = sockets[channel]; if (not channelSocket) - channelSocket = std::make_shared<ChannelSocket>(parent_.weak(), - name, - channel, - isInitiator, - [w = parent_.weak(), channel]() { - // Remove socket in another thread to avoid any lock - dht::ThreadPool::io().run([w, channel]() { - if (auto shared = w.lock()) { - shared->eraseChannel(channel); - } - }); - - }); + channelSocket = std::make_shared<ChannelSocket>( + parent_.weak(), name, channel, isInitiator, [w = parent_.weak(), channel]() { + // Remove socket in another thread to avoid any lock + dht::ThreadPool::io().run([w, channel]() { + if (auto shared = w.lock()) { + shared->eraseChannel(channel); + } + }); + }); else { JAMI_WARN("A channel is already present on that socket, accepting " "the request will close the previous one %s", @@ -669,7 +665,11 @@ MultiplexedSocket::monitor() const std::lock_guard<std::mutex> lk(pimpl_->socketsMutex); for (const auto& [_, channel] : pimpl_->sockets) { if (channel) - JAMI_DEBUG("\t\t- Channel {} (count: {}) with name {:s} Initiator: {}", fmt::ptr(channel.get()), channel.use_count(), channel->name(), channel->isInitiator()); + JAMI_DEBUG("\t\t- Channel {} (count: {}) with name {:s} Initiator: {}", + fmt::ptr(channel.get()), + channel.use_count(), + channel->name(), + channel->isInitiator()); } } @@ -791,8 +791,7 @@ ChannelSocketTest::ChannelSocketTest(const DeviceId& deviceId, , ioCtx_(*Manager::instance().ioContext()) {} -ChannelSocketTest::~ChannelSocketTest() -{} +ChannelSocketTest::~ChannelSocketTest() {} void ChannelSocketTest::link(const std::shared_ptr<ChannelSocketTest>& socket1, @@ -925,7 +924,8 @@ ChannelSocket::ChannelSocket(std::weak_ptr<MultiplexedSocket> endpoint, const uint16_t& channel, bool isInitiator, std::function<void()> rmFromMxSockCb) - : pimpl_ {std::make_unique<Impl>(endpoint, name, channel, isInitiator, std::move(rmFromMxSockCb))} + : pimpl_ { + std::make_unique<Impl>(endpoint, name, channel, isInitiator, std::move(rmFromMxSockCb))} {} ChannelSocket::~ChannelSocket() {} diff --git a/src/connectivity/multiplexed_socket.h b/src/connectivity/multiplexed_socket.h index 1b6858b386..a414adf26c 100644 --- a/src/connectivity/multiplexed_socket.h +++ b/src/connectivity/multiplexed_socket.h @@ -115,10 +115,12 @@ public: * This will close all channels and send a TLS EOF on the main socket. */ void shutdown(); + /** * This will wait that eventLoop is stopped and stop it if necessary */ void join(); + /** * Will trigger that callback when shutdown() is called */ diff --git a/src/im/message_engine.cpp b/src/im/message_engine.cpp index 1f49da6e3b..d75bcd77b2 100644 --- a/src/im/message_engine.cpp +++ b/src/im/message_engine.cpp @@ -44,6 +44,7 @@ MessageEngine::MessageEngine(SIPAccountBase& acc, const std::string& path) MessageToken MessageEngine::sendMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t refreshToken) { @@ -52,7 +53,8 @@ MessageEngine::sendMessage(const std::string& to, MessageToken token; { std::lock_guard<std::mutex> lock(messagesMutex_); - auto& peerMessages = messages_[to]; + + auto& peerMessages = deviceId.empty() ? messages_[to] : messagesDevices_[deviceId]; auto previousIt = peerMessages.find(refreshToken); if (previousIt != peerMessages.end() && previousIt->second.status != MessageStatus::SENT) { JAMI_DEBUG("[message {:d}] Replace content", refreshToken); @@ -70,18 +72,20 @@ MessageEngine::sendMessage(const std::string& to, } save_(); } - runOnMainThread([this, to]() { retrySend(to); }); + runOnMainThread([this, to, deviceId]() { retrySend(to, true, deviceId); }); return token; } void -MessageEngine::onPeerOnline(const std::string& peer, bool retryOnTimeout) +MessageEngine::onPeerOnline(const std::string& peer, + bool retryOnTimeout, + const std::string& deviceId) { - retrySend(peer, retryOnTimeout); + retrySend(peer, retryOnTimeout, deviceId); } void -MessageEngine::retrySend(const std::string& peer, bool retryOnTimeout) +MessageEngine::retrySend(const std::string& peer, bool retryOnTimeout, const std::string& deviceId) { if (account_.getRegistrationState() != RegistrationState::REGISTERED) return; @@ -94,10 +98,13 @@ MessageEngine::retrySend(const std::string& peer, bool retryOnTimeout) std::vector<PendingMsg> pending {}; { std::lock_guard<std::mutex> lock(messagesMutex_); - auto p = messages_.find(peer); - if (p == messages_.end()) + + auto& m = deviceId.empty() ? messages_ : messagesDevices_; + auto p = m.find(deviceId.empty() ? peer : deviceId); + if (p == m.end()) return; auto& messages = p->second; + for (auto m = messages.begin(); m != messages.end(); ++m) { if (m->second.status == MessageStatus::UNKNOWN || m->second.status == MessageStatus::IDLE) { @@ -118,7 +125,7 @@ MessageEngine::retrySend(const std::string& peer, bool retryOnTimeout) p.to, std::to_string(p.token), (int) libjami::Account::MessageStates::SENDING); - account_.sendMessage(p.to, p.payloads, p.token, retryOnTimeout); + account_.sendMessage(p.to, deviceId, p.payloads, p.token, retryOnTimeout, false); } } @@ -159,15 +166,19 @@ MessageEngine::cancel(MessageToken t) } void -MessageEngine::onMessageSent(const std::string& peer, MessageToken token, bool ok) +MessageEngine::onMessageSent(const std::string& peer, + MessageToken token, + bool ok, + const std::string& deviceId) { JAMI_DEBUG("[message {:d}] Message sent: {:s}", token, ok ? "success"sv : "failure"sv); std::lock_guard<std::mutex> lock(messagesMutex_); - auto p = messages_.find(peer); - if (p == messages_.end()) { - JAMI_DEBUG("[message {:d}] Can't find peer", token); + auto& m = deviceId.empty() ? messages_ : messagesDevices_; + + auto p = m.find(deviceId.empty() ? peer : deviceId); + if (p == m.end()) return; - } + auto f = p->second.find(token); if (f != p->second.end()) { auto emit = f->second.payloads.find("application/im-gitmessage-id") @@ -304,7 +315,8 @@ MessageEngine::save_() const payloads[p.first] = p.second; peerRoot[to_hex_string(m.first)] = std::move(msg); } - if (peerRoot.size() == 0) continue; + if (peerRoot.size() == 0) + continue; root[c.first] = std::move(peerRoot); } @@ -325,14 +337,11 @@ MessageEngine::save_() const writer->write(root, &file); } catch (const std::exception& e) { JAMI_ERROR("[Account {:s}] Couldn't save messages to {:s}: {:s}", - accountID, - path, - e.what()); + accountID, + path, + e.what()); } - JAMI_DEBUG("[Account {:s}] saved {:d} messages to {:s}", - accountID, - root.size(), - path); + JAMI_DEBUG("[Account {:s}] saved {:d} messages to {:s}", accountID, root.size(), path); }); } catch (const std::exception& e) { JAMI_ERR("[Account %s] couldn't save messages to %s: %s", diff --git a/src/im/message_engine.h b/src/im/message_engine.h index 9f93fc80d6..80de0d3eab 100644 --- a/src/im/message_engine.h +++ b/src/im/message_engine.h @@ -41,7 +41,15 @@ class MessageEngine public: MessageEngine(SIPAccountBase&, const std::string& path); + /** + * Add a message to the engine and try to send it + * @param to Uri of the peer + * @param deviceId (Optional) if we want to send to a specific device + * @param payloads The message + * @param refreshToken The token of the message + */ MessageToken sendMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t refreshToken); @@ -51,14 +59,20 @@ public: bool isSent(MessageToken t) const { return getStatus(t) == MessageStatus::SENT; } - void onMessageSent(const std::string& peer, MessageToken t, bool success); + void onMessageSent(const std::string& peer, + MessageToken t, + bool success, + const std::string& deviceId = {}); + void onMessageDisplayed(const std::string& peer, MessageToken t, bool displayed); /** * @TODO change MessageEngine by a queue, * @NOTE retryOnTimeout is used for failing SIP messages (jamiAccount::sendTextMessage) */ - void onPeerOnline(const std::string& peer, bool retryOnTimeout = true); + void onPeerOnline(const std::string& peer, + bool retryOnTimeout = true, + const std::string& deviceId = {}); /** * Load persisted messages @@ -75,7 +89,10 @@ private: static const std::chrono::minutes RETRY_PERIOD; using clock = std::chrono::steady_clock; - void retrySend(const std::string& peer, bool retryOnTimeout = true); + void retrySend(const std::string& peer, + bool retryOnTimeout = true, + const std::string& deviceId = {}); + void save_() const; struct Message @@ -91,6 +108,8 @@ private: const std::string savePath_; std::map<std::string, std::map<MessageToken, Message>> messages_; + std::map<std::string, std::map<MessageToken, Message>> messagesDevices_; + std::set<MessageToken> sentMessages_; mutable std::mutex messagesMutex_ {}; diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index f62fc51f41..dbb736bf93 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -39,7 +39,7 @@ namespace jami { static const char* const LAST_MODIFIED = "lastModified"; -static const auto jsonBuilder = []{ +static const auto jsonBuilder = [] { Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; wbuilder["indentation"] = ""; @@ -122,7 +122,7 @@ ConversationRequest::toMap() const class Conversation::Impl { public: - Impl(const std::weak_ptr<JamiAccount>& account, + Impl(const std::shared_ptr<JamiAccount>& account, ConversationMode mode, const std::string& otherMember = "") : repository_(ConversationRepository::createConversation(account, mode, otherMember)) @@ -134,7 +134,7 @@ public: init(); } - Impl(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId) + Impl(const std::shared_ptr<JamiAccount>& account, const std::string& conversationId) : account_(account) { repository_ = std::make_unique<ConversationRepository>(account, conversationId); @@ -144,7 +144,7 @@ public: init(); } - Impl(const std::weak_ptr<JamiAccount>& account, + Impl(const std::shared_ptr<JamiAccount>& account, const std::string& remoteDevice, const std::string& conversationId) : account_(account) @@ -153,10 +153,8 @@ public: remoteDevice, conversationId); if (!repository_) { - if (auto shared = account.lock()) { - emitSignal<libjami::ConversationSignal::OnConversationError>( - shared->getAccountID(), conversationId, EFETCH, "Couldn't clone repository"); - } + emitSignal<libjami::ConversationSignal::OnConversationError>( + account->getAccountID(), conversationId, EFETCH, "Couldn't clone repository"); throw std::logic_error("Couldn't clone repository"); } init(); @@ -171,7 +169,10 @@ public: void init() { if (auto shared = account_.lock()) { + ioContext_ = Manager::instance().ioContext(); + fallbackTimer_ = std::make_unique<asio::steady_timer>(*ioContext_); swarmManager_ = std::make_shared<SwarmManager>(NodeId(shared->currentDeviceId())); + swarmManager_->setMobility(shared->isMobile()); accountId_ = shared->getAccountID(); transferManager_ = std::make_shared<TransferManager>(shared->getAccountID(), repository_->id()); @@ -194,6 +195,27 @@ public: } } + const std::string& toString() const + { + if (fmtStr_.empty()) { + if (repository_->mode() == ConversationMode::ONE_TO_ONE) { + if (auto acc = account_.lock()) { + auto peer = acc->getUsername(); + for (const auto& member : repository_->getInitialMembers()) { + if (member != acc->getUsername()) { + peer = member; + } + } + fmtStr_ = fmt::format("[Conversation (1:1) {}]", peer); + } + } else { + fmtStr_ = fmt::format("[Conversation {}]", repository_->id()); + } + } + return fmtStr_; + } + mutable std::string fmtStr_; + /** * If, for whatever reason, the daemon is stopped while hosting a conference, * we need to announce the end of this call when restarting. @@ -201,7 +223,15 @@ public: */ std::vector<std::string> announceEndedCalls(); - ~Impl() = default; + ~Impl() + { + try { + if (fallbackTimer_) + fallbackTimer_->cancel(); + } catch (const std::exception& e) { + JAMI_ERROR("[Conversation {:s}] {:s}", toString(), e.what()); + } + } std::vector<std::string> refreshActiveCalls(); bool isAdmin() const; @@ -272,11 +302,12 @@ public: auto device = commit.at("device"); std::lock_guard<std::mutex> lk(activeCallsMtx_); auto itActive = std::find_if(activeCalls_.begin(), - activeCalls_.end(), - [&](const auto& value) { - return value.at("id") == confId && value.at("uri") == uri - && value.at("device") == device; - }); + activeCalls_.end(), + [&](const auto& value) { + return value.at("id") == confId + && value.at("uri") == uri + && value.at("device") == device; + }); if (commit.find("duration") == commit.end()) { if (itActive == activeCalls_.end() && !eraseOnly) { JAMI_DEBUG( @@ -300,18 +331,16 @@ public: itActive = activeCalls_.erase(itActive); // Unlikely, but we must ensure that no duplicate exists while (itActive != activeCalls_.end()) { - itActive = std::find_if(itActive, activeCalls_.end(), - [&](const auto& value) { - return value.at("id") == confId && value.at("uri") == uri - && value.at("device") == device; - }); + itActive = std::find_if(itActive, activeCalls_.end(), [&](const auto& value) { + return value.at("id") == confId && value.at("uri") == uri + && value.at("device") == device; + }); if (itActive != activeCalls_.end()) { JAMI_ERROR("Duplicate call found. (This is a bug)"); itActive = activeCalls_.erase(itActive); } } - if (eraseOnly) { JAMI_WARNING("previous swarm:{:s} call finished detected: {:s} on device " "{:s}, account {:s}", @@ -523,7 +552,8 @@ public: void voteUnban(const std::string& contactUri, const std::string_view type, const OnDoneCb& cb); std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited, - bool includeLeft, bool includeBanned) const; + bool includeLeft, + bool includeBanned) const; std::string_view bannedType(const std::string& uri) const { @@ -562,12 +592,15 @@ public: std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited, bool includeLeft) const; - std::shared_ptr<SwarmManager> swarmManager_; std::set<std::string> checkedMembers_; // Store members we tried std::function<void()> bootstrapCb_; +#ifdef LIBJAMI_TESTABLE + std::function<void(std::string, BootstrapStatus)> bootstrapCbTest_; +#endif std::mutex writeMtx_ {}; std::unique_ptr<ConversationRepository> repository_; + std::shared_ptr<SwarmManager> swarmManager_; std::weak_ptr<JamiAccount> account_; std::atomic_bool isRemoving_ {false}; std::vector<std::map<std::string, std::string>> loadMessages(const LogOptions& options); @@ -604,6 +637,12 @@ public: mutable std::vector<std::map<std::string, std::string>> activeCalls_ {}; GitSocketList gitSocketList_ {}; + + // Bootstrap + std::shared_ptr<asio::io_context> ioContext_; + std::unique_ptr<asio::steady_timer> fallbackTimer_; + + bool isMobile {false}; }; bool @@ -719,18 +758,18 @@ Conversation::Impl::loadMessages(const LogOptions& options) return repository_->convCommitToMap(convCommits); } -Conversation::Conversation(const std::weak_ptr<JamiAccount>& account, +Conversation::Conversation(const std::shared_ptr<JamiAccount>& account, ConversationMode mode, const std::string& otherMember) : pimpl_ {new Impl {account, mode, otherMember}} {} -Conversation::Conversation(const std::weak_ptr<JamiAccount>& account, +Conversation::Conversation(const std::shared_ptr<JamiAccount>& account, const std::string& conversationId) : pimpl_ {new Impl {account, conversationId}} {} -Conversation::Conversation(const std::weak_ptr<JamiAccount>& account, +Conversation::Conversation(const std::shared_ptr<JamiAccount>& account, const std::string& remoteDevice, const std::string& conversationId) : pimpl_ {new Impl {account, remoteDevice, conversationId}} @@ -807,11 +846,7 @@ Conversation::gitSocket(const DeviceId& deviceId) const { return pimpl_->gitSocket(deviceId); } -/*bool -Conversation::hasGitSocket(const DeviceId& deviceId) const -{ - return pimpl_->hasGitSocket(deviceId); -}*/ + void Conversation::addGitSocket(const DeviceId& deviceId, const std::shared_ptr<ChannelSocket>& socket) { @@ -828,6 +863,20 @@ void Conversation::removeGitSockets() { pimpl_->gitSocketList_.clear(); + pimpl_->swarmManager_->shutdown(); + pimpl_->checkedMembers_.clear(); +} + +void +Conversation::connectivityChanged() +{ + pimpl_->swarmManager_->maintainBuckets(); +} + +bool +Conversation::hasSwarmChannel(const std::string& deviceId) +{ + return pimpl_->swarmManager_->isConnectedWith(DeviceId(deviceId)); } void @@ -928,7 +977,23 @@ Conversation::removeMember(const std::string& contactUri, bool isDevice, const O JAMI_WARN("Vote solved for %s. %s banned", contactUri.c_str(), isDevice ? "Device" : "Member"); + + const auto nodes = sthis->pimpl_->swarmManager_->getRoutingTable().getAllNodes(); + // Remove nodes from swarmManager + std::vector<NodeId> toRemove; + for (const auto node : nodes) + if (contactUri == sthis->uriFromDevice(node.toString())) + toRemove.emplace_back(node); + sthis->pimpl_->swarmManager_->deleteNode(toRemove); + // Remove git sockets with this member + std::vector<DeviceId> gitToRm; + for (const auto& [deviceId, _] : sthis->pimpl_->gitSocketList_) + if (contactUri == sthis->uriFromDevice(deviceId.toString())) + gitToRm.emplace_back(deviceId); + for (const auto& did: gitToRm) + sthis->removeGitSocket(did); } + sthis->pimpl_->announce(commits); lk.unlock(); cb(!lastId.empty(), lastId); @@ -958,9 +1023,31 @@ Conversation::peersToSyncWith() const s.reserve(nodes.size() + mobiles.size()); s.insert(s.end(), nodes.begin(), nodes.end()); s.insert(s.end(), mobiles.begin(), mobiles.end()); + for (const auto& [deviceId, _] : pimpl_->gitSocketList_) + if (std::find(s.cbegin(), s.cend(), deviceId) == s.cend()) + s.emplace_back(deviceId); return s; } +bool +Conversation::isBoostraped() const +{ + const auto& routingTable = pimpl_->swarmManager_->getRoutingTable(); + return !routingTable.getNodes().empty(); +} + +std::string +Conversation::uriFromDevice(const std::string& deviceId) const +{ + return pimpl_->repository_->uriFromDevice(deviceId); +} + +void +Conversation::monitor() +{ + pimpl_->swarmManager_->getRoutingTable().printRoutingTable(); +} + std::string Conversation::join() { @@ -1280,11 +1367,9 @@ Conversation::sync(const std::string& member, if (auto account = pimpl_->account_.lock()) { // For waiting request, downloadFile for (const auto& wr : dataTransfer()->waitingRequests()) { - auto path = fileutils::get_data_dir() + DIR_SEPARATOR_STR - + account->getAccountID() + DIR_SEPARATOR_STR - + "conversation_data" + DIR_SEPARATOR_STR - + id() + DIR_SEPARATOR_STR - + wr.fileId; + auto path = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID() + + DIR_SEPARATOR_STR + "conversation_data" + DIR_SEPARATOR_STR + id() + + DIR_SEPARATOR_STR + wr.fileId; auto start = fileutils::size(path); if (start < 0) start = 0; @@ -1595,7 +1680,9 @@ Conversation::setMessageDisplayed(const std::string& uri, const std::string& int void Conversation::updateLastDisplayed(const std::map<std::string, std::string>& map) { - auto filePath = fmt::format("{}/{}", pimpl_->conversationDataPath_, ConversationMapKeys::LAST_DISPLAYED); + auto filePath = fmt::format("{}/{}", + pimpl_->conversationDataPath_, + ConversationMapKeys::LAST_DISPLAYED); auto prefs = map; auto itLast = prefs.find(LAST_MODIFIED); if (itLast != prefs.end()) { @@ -1611,7 +1698,7 @@ Conversation::updateLastDisplayed(const std::map<std::string, std::string>& map) prefs.erase(itLast); } - for (const auto& [uri, id]: prefs) + for (const auto& [uri, id] : prefs) setMessageDisplayed(uri, id); } @@ -1670,7 +1757,9 @@ Conversation::displayed() const { try { std::map<std::string, std::string> lastDisplayed; - auto filePath = fmt::format("{}/{}", pimpl_->conversationDataPath_, ConversationMapKeys::LAST_DISPLAYED); + auto filePath = fmt::format("{}/{}", + pimpl_->conversationDataPath_, + ConversationMapKeys::LAST_DISPLAYED); auto file = fileutils::loadFile(filePath); msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size()); oh.get().convert(lastDisplayed); @@ -1681,19 +1770,145 @@ Conversation::displayed() const return {}; } +#ifdef LIBJAMI_TESTABLE +void +Conversation::onBootstrapStatus(const std::function<void(std::string, BootstrapStatus)>& cb) +{ + pimpl_->bootstrapCbTest_ = cb; +} +#endif + +void +Conversation::checkBootstrapMember(const asio::error_code& ec, + std::vector<std::map<std::string, std::string>> members) +{ + if (ec == asio::error::operation_aborted + or pimpl_->swarmManager_->getRoutingTable().getNodes().size() > 0) + return; + // We bootstrap the DRT with devices who already wrote in the repository. + // However, in a conversation, a large number of devices may just watch + // the conversation, but never write any message. + auto acc = pimpl_->account_.lock(); + std::string uri; + while (!members.empty()) { + auto member = members.back(); + members.pop_back(); + auto& uri = member.at("uri"); + if (uri != acc->getUsername() + && pimpl_->checkedMembers_.find(uri) == pimpl_->checkedMembers_.end()) + break; + } + // If members is empty, we finished the fallback un-successfully + if (members.empty() && uri.empty()) { + JAMI_WARNING("{}[SwarmManager {}] Bootstrap: Fallback failed. Wait for remote connections.", + pimpl_->toString(), + fmt::ptr(pimpl_->swarmManager_.get())); +#ifdef LIBJAMI_TESTABLE + if (pimpl_->bootstrapCbTest_) + pimpl_->bootstrapCbTest_(id(), BootstrapStatus::FAILED); +#endif + return; + } + + // Fallback, check devices of a member (we didn't check yet) in the conversation + pimpl_->checkedMembers_.emplace(uri); + auto devices = std::make_shared<std::vector<NodeId>>(); + acc->forEachDevice( + dht::InfoHash(uri), + [w = weak(), devices](const std::shared_ptr<dht::crypto::PublicKey>& dev) { + // Test if already sent + if (auto sthis = w.lock()) { + if (!sthis->pimpl_->swarmManager_->getRoutingTable().hasKnownNode(dev->getLongId())) + devices->emplace_back(dev->getLongId()); + } + }, + [w = weak(), devices, members = std::move(members), uri](bool ok) { + auto sthis = w.lock(); + if (!sthis) + return; + if (ok && devices->size() != 0) { +#ifdef LIBJAMI_TESTABLE + if (sthis->pimpl_->bootstrapCbTest_) + sthis->pimpl_->bootstrapCbTest_(sthis->id(), BootstrapStatus::FALLBACK); +#endif + JAMI_WARNING("{}[SwarmManager {}] Bootstrap: Fallback with member: {}", + sthis->pimpl_->toString(), + fmt::ptr(sthis->pimpl_->swarmManager_.get()), + uri); + sthis->pimpl_->swarmManager_->setKnownNodes(*devices); + } else { + // Check next member + sthis->pimpl_->fallbackTimer_->expires_at(std::chrono::steady_clock::now()); + sthis->pimpl_->fallbackTimer_->async_wait( + std::bind(&Conversation::checkBootstrapMember, + sthis.get(), + std::placeholders::_1, + std::move(members))); + } + }); +} + void Conversation::bootstrap(std::function<void()> onBootstraped) { if (!pimpl_ || !pimpl_->repository_ || !pimpl_->swarmManager_) return; + // Here, we bootstrap the DRT with devices who already wrote in the conversation + // If this doesn't work, it will try to fallback with checkBootstrapMember + // If it works, the callback onConnectionChanged will be called with ok=true pimpl_->bootstrapCb_ = std::move(onBootstraped); std::vector<DeviceId> devices; for (const auto& m : pimpl_->repository_->devices()) devices.insert(devices.end(), m.second.begin(), m.second.end()); // Add known devices - JAMI_DEBUG("[SwarmManager {}] Bootstrap with {} devices", + if (auto acc = pimpl_->account_.lock()) { + for (const auto& [id, _] : acc->getKnownDevices()) { + devices.emplace_back(id); + } + } + JAMI_DEBUG("{}[SwarmManager {}] Bootstrap with {} devices", + pimpl_->toString(), fmt::ptr(pimpl_->swarmManager_.get()), devices.size()); + // set callback + pimpl_->swarmManager_->onConnectionChanged([w = weak()](bool ok) { + // This will call methods from accounts, so trigger on another thread. + dht::ThreadPool::io().run([w, ok] { + auto sthis = w.lock(); + if (!sthis) + return; + if (ok) { + // Bootstrap succeeded! + sthis->pimpl_->checkedMembers_.clear(); + if (sthis->pimpl_->bootstrapCb_) + sthis->pimpl_->bootstrapCb_(); +#ifdef LIBJAMI_TESTABLE + if (sthis->pimpl_->bootstrapCbTest_) + sthis->pimpl_->bootstrapCbTest_(sthis->id(), BootstrapStatus::SUCCESS); +#endif + return; + } + // Fallback + auto acc = sthis->pimpl_->account_.lock(); + if (!acc) + return; + auto members = sthis->getMembers(false, false); + std::shuffle(members.begin(), members.end(), acc->rand); + // TODO decide a formula + auto timeForBootstrap = std::min(static_cast<size_t>(8), members.size()); + JAMI_DEBUG("{}[SwarmManager {}] Fallback in {} seconds", + sthis->pimpl_->toString(), + fmt::ptr(sthis->pimpl_->swarmManager_.get()), + (20 - timeForBootstrap)); + sthis->pimpl_->fallbackTimer_->expires_at(std::chrono::steady_clock::now() + 20s + - std::chrono::seconds(timeForBootstrap)); + sthis->pimpl_->fallbackTimer_->async_wait(std::bind(&Conversation::checkBootstrapMember, + sthis.get(), + std::placeholders::_1, + std::move(members))); + }); + }); + pimpl_->checkedMembers_.clear(); pimpl_->swarmManager_->setKnownNodes(devices); } diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index 10dd08b451..2f32a3b444 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -33,6 +33,8 @@ #include <memory> #include <set> +#include <asio.hpp> + namespace jami { namespace ConversationMapKeys { @@ -126,18 +128,32 @@ using NeedSocketCb class Conversation : public std::enable_shared_from_this<Conversation> { public: - Conversation(const std::weak_ptr<JamiAccount>& account, + Conversation(const std::shared_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, + Conversation(const std::shared_ptr<JamiAccount>& account, + const std::string& conversationId = ""); + Conversation(const std::shared_ptr<JamiAccount>& account, const std::string& remoteDevice, const std::string& conversationId); ~Conversation(); + /** + * Print the state of the DRT linked to the conversation + */ + void monitor(); + +#ifdef LIBJAMI_TESTABLE + enum class BootstrapStatus { FAILED, FALLBACK, SUCCESS }; + /** + * Used by the tests to get whenever the DRT is connected/disconnected + */ + void onBootstrapStatus(const std::function<void(std::string, BootstrapStatus)>& cb); +#endif + /** * Bootstrap swarm manager to other peers - * @param onBootstrapped Callback called when connection is successfully established + * @param onBootstraped Callback called when connection is successfully established */ void bootstrap(std::function<void()> onBootstraped); @@ -486,10 +502,26 @@ public: */ std::vector<std::map<std::string, std::string>> currentCalls() const; + /** + * Git operations will need a ChannelSocket for cloning/fetching commits + * Because libgit2 is a C library, we store the pointer in the corresponding conversation + * and the GitTransport will inject to libgit2 whenever needed + */ std::shared_ptr<ChannelSocket> gitSocket(const DeviceId& deviceId) const; void addGitSocket(const DeviceId& deviceId, const std::shared_ptr<ChannelSocket>& socket); void removeGitSocket(const DeviceId& deviceId); void removeGitSockets(); + /** + * Used to avoid multiple connections, we just check if we got a swarm channel with a specific + * device + * @param deviceId + */ + bool hasSwarmChannel(const std::string& deviceId); + + /** + * If we change from one network to one another, we will need to update the state of the connections + */ + void connectivityChanged(); private: std::shared_ptr<Conversation> shared() @@ -509,6 +541,15 @@ private: return std::static_pointer_cast<Conversation const>(shared_from_this()); } + // Private because of weak() + /** + * Used by bootstrap() to launch the fallback + * @param ec + * @param members Members to try to connect + */ + void checkBootstrapMember(const asio::error_code& ec, + std::vector<std::map<std::string, std::string>> members); + class Impl; std::unique_ptr<Impl> pimpl_; }; diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp index d1d8736aca..84f0e06cd5 100644 --- a/src/jamidht/conversation_module.cpp +++ b/src/jamidht/conversation_module.cpp @@ -898,8 +898,9 @@ ConversationModule::Impl::sendMessageNotification(Conversation& conversation, { std::lock_guard<std::mutex> lk(notSyncedNotificationMtx_); devices = conversation.peersToSyncWith(); - auto members = conversation.memberUris(username_, {}); + auto members = conversation.memberUris(username_, {MemberRole::BANNED}); std::vector<std::string> connectedMembers; + // print all members for (const auto& device : devices) { auto cert = tls::CertificateStore::instance().getCertificate(device.toString()); if (cert && cert->issuer) @@ -1142,10 +1143,10 @@ ConversationModule::loadConversations() auto convFromDetails = getOneToOneConversation(otherUri); if (convFromDetails != repository) { if (convFromDetails.empty()) { - JAMI_ERROR( - "No conversation detected for {} but one exists ({}). Update details", - otherUri, - repository); + JAMI_ERROR("No conversation detected for {} but one exists ({}). " + "Update details", + otherUri, + repository); acc->updateConvForContact(otherUri, convFromDetails, repository); } else { JAMI_ERROR("Multiple conversation detected for {} but ({} & {})", @@ -1292,12 +1293,12 @@ ConversationModule::clearPendingFetch() { if (!pimpl_->pendingConversationsFetch_.empty()) { // Note: This is a fallback. convModule() is kept if account is disabled/re-enabled. - // iOS uses setAccountActive() a lot, and if for some reason the previous pending fetch is - // not erased (callback not called), it will block the new messages as it will not sync. The - // best way to debug this is to get logs from the last ICE connection for syncing the - // conversation. It may have been killed in some un-expected way avoiding to call the - // callbacks. This should never happen, but if it's the case, this will allow new messages - // to be synced correctly. + // iOS uses setAccountActive() a lot, and if for some reason the previous pending fetch + // is not erased (callback not called), it will block the new messages as it will not + // sync. The best way to debug this is to get logs from the last ICE connection for + // syncing the conversation. It may have been killed in some un-expected way avoiding to + // call the callbacks. This should never happen, but if it's the case, this will allow + // new messages to be synced correctly. JAMI_ERR("This is a bug, seems to still fetch to some device on initializing"); pimpl_->pendingConversationsFetch_.clear(); } @@ -1839,7 +1840,8 @@ ConversationModule::onSyncData(const SyncMsg& msg, auto itConv = pimpl_->convInfos_.find(convId); if (itConv != pimpl_->convInfos_.end() && itConv->second.removed) { if (itConv->second.removed > convInfo.created) { - // Only reclone if re-added, else the peer is not synced yet (could be offline before) + // Only reclone if re-added, else the peer is not synced yet (could be + // offline before) continue; } JAMI_DEBUG("Re-add previously removed conversation {:s}", convId); @@ -2020,8 +2022,8 @@ ConversationModule::addConversationMember(const std::string& conversationId, if (it->second->isMember(contactUri, true)) { JAMI_DEBUG("{:s} is already a member of {:s}, resend invite", contactUri, conversationId); - // Note: This should not be necessary, but if for whatever reason the other side didn't join - // we should not forbid new invites + // Note: This should not be necessary, but if for whatever reason the other side didn't + // join we should not forbid new invites auto invite = it->second->generateInvitation(); lk.unlock(); pimpl_->sendMsgCb_(contactUri, {}, std::move(invite), 0); @@ -2524,8 +2526,8 @@ ConversationModule::hostConference(const std::string& conversationId, }); // When conf finished = remove host & commit - // Master call, so when it's stopped, the conference will be stopped (as we use the hold state - // for detaching the call) + // Master call, so when it's stopped, the conference will be stopped (as we use the hold + // state for detaching the call) conf->onShutdown( [w = pimpl_->weak(), accountUri = pimpl_->username_, confId, conversationId, call]( int duration) { diff --git a/src/jamidht/conversation_module.h b/src/jamidht/conversation_module.h index e02fb46d00..9b7d68fa58 100644 --- a/src/jamidht/conversation_module.h +++ b/src/jamidht/conversation_module.h @@ -370,8 +370,8 @@ public: */ void setConversationPreferences(const std::string& conversationId, const std::map<std::string, std::string>& prefs); - std::map<std::string, std::string> getConversationPreferences( - const std::string& conversationId, bool includeCreated = false) const; + std::map<std::string, std::string> getConversationPreferences(const std::string& conversationId, + bool includeCreated = false) const; /** * Retrieve all conversation preferences to sync with other devices */ diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 7f6174de06..79909c13d8 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -93,7 +93,8 @@ public: return {std::move(repo), git_repository_free}; } - std::string getDisplayName() const { + std::string getDisplayName() const + { auto shared = account_.lock(); if (!shared) return {}; @@ -2060,7 +2061,6 @@ ConversationRepository::Impl::log(const LogOptions& options) const if (options.skipMerge && git_commit_parentcount(commit.get()) > 1) { return CallbackResult::Skip; } - if ((options.nbOfCommits != 0 && commits.size() == options.nbOfCommits)) return CallbackResult::Break; // Stop logging if (breakLogging) @@ -2455,17 +2455,14 @@ ConversationRepository::Impl::diffStats(const GitDiff& diff) const ////////////////////////////////// std::unique_ptr<ConversationRepository> -ConversationRepository::createConversation(const std::weak_ptr<JamiAccount>& account, +ConversationRepository::createConversation(const std::shared_ptr<JamiAccount>& account, ConversationMode mode, const std::string& otherMember) { - auto shared = account.lock(); - if (!shared) - return {}; // Create temporary directory because we can't know the first hash for now std::uniform_int_distribution<uint64_t> dist {0, std::numeric_limits<uint64_t>::max()}; random_device rdev; - auto conversationsPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() + auto conversationsPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID() + DIR_SEPARATOR_STR + "conversations"; fileutils::check_dir(conversationsPath.c_str()); auto tmpPath = conversationsPath + DIR_SEPARATOR_STR + std::to_string(dist(rdev)); @@ -2483,14 +2480,14 @@ ConversationRepository::createConversation(const std::weak_ptr<JamiAccount>& acc } // Add initial files - if (!add_initial_files(repo, shared, mode, otherMember)) { + if (!add_initial_files(repo, account, mode, otherMember)) { JAMI_ERR("Error when adding initial files"); fileutils::removeAll(tmpPath, true); return {}; } // Commit changes - auto id = initial_commit(repo, shared, mode, otherMember); + auto id = initial_commit(repo, account, mode, otherMember); if (id.empty()) { JAMI_ERR("Couldn't create initial commit in %s", tmpPath.c_str()); fileutils::removeAll(tmpPath, true); @@ -2511,20 +2508,15 @@ ConversationRepository::createConversation(const std::weak_ptr<JamiAccount>& acc } std::unique_ptr<ConversationRepository> -ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& account, +ConversationRepository::cloneConversation(const std::shared_ptr<JamiAccount>& account, const std::string& deviceId, const std::string& conversationId) { - auto shared = account.lock(); - if (!shared) - return {}; - auto conversationsPath = fileutils::get_data_dir() + "/" + shared->getAccountID() + "/" + auto conversationsPath = fileutils::get_data_dir() + "/" + account->getAccountID() + "/" + "conversations"; fileutils::check_dir(conversationsPath.c_str()); auto path = conversationsPath + "/" + conversationId; - git_repository* rep = nullptr; - std::stringstream url; - url << "git://" << deviceId << '/' << conversationId; + auto url = fmt::format("git://{}/{}", deviceId, conversationId); git_clone_options clone_options; git_clone_options_init(&clone_options, GIT_CLONE_OPTIONS_VERSION); @@ -2533,11 +2525,14 @@ ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& acco void*) { // Uncomment to get advancment // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects) - // JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects, stats->received_bytes/1024); + // JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects, + // stats->received_bytes/1024); // If a pack is more than 256Mb, it's anormal. if (stats->received_bytes > MAX_FETCH_SIZE) { JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})", - stats->received_bytes, stats->received_objects, stats->total_objects); + stats->received_bytes, + stats->received_objects, + stats->total_objects); return -1; } return 0; @@ -2549,11 +2544,13 @@ ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& acco fileutils::removeAll(path, true); } - JAMI_INFO("Start clone in %s", path.c_str()); - if (git_clone(&rep, url.str().c_str(), path.c_str(), nullptr) < 0) { - const git_error* err = giterr_last(); - if (err) - JAMI_ERR("Error when retrieving remote conversation: %s %s", err->message, path.c_str()); + JAMI_DEBUG("Start clone of {:s} to {:s}", url, path); + git_repository* rep = nullptr; + if (auto err = git_clone(&rep, url.c_str(), path.c_str(), nullptr)) { + if (const git_error* gerr = giterr_last()) + JAMI_ERROR("Error when retrieving remote conversation: {:s} {:s}", gerr->message, path); + else + JAMI_ERROR("Unknown error {:d} when retrieving remote conversation", err); return nullptr; } git_repository_free(rep); @@ -2930,11 +2927,14 @@ ConversationRepository::fetch(const std::string& remoteDeviceId) fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void*) { // Uncomment to get advancment // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects) - // JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects, stats->received_bytes/1024); + // JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects, + // stats->received_bytes/1024); // If a pack is more than 256Mb, it's anormal. if (stats->received_bytes > MAX_FETCH_SIZE) { JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})", - stats->received_bytes, stats->received_objects, stats->total_objects); + stats->received_bytes, + stats->received_objects, + stats->total_objects); return -1; } return 0; @@ -3702,6 +3702,12 @@ ConversationRepository::pinCertificates(bool blocking) } } +std::string +ConversationRepository::uriFromDevice(const std::string& deviceId) const +{ + return pimpl_->uriFromDevice(deviceId); +} + std::string ConversationRepository::updateInfos(const std::map<std::string, std::string>& profile) { @@ -3799,7 +3805,8 @@ ConversationRepository::infos() const std::map<std::string, std::string> result; if (fileutils::isFile(profilePath)) { auto content = fileutils::loadFile(profilePath); - result = ConversationRepository::infosFromVCard(vCard::utils::toMap(std::string_view {(const char*)content.data(), content.size()})); + result = ConversationRepository::infosFromVCard(vCard::utils::toMap( + std::string_view {(const char*) content.data(), content.size()})); } result["mode"] = std::to_string(static_cast<int>(mode())); return result; diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index 0178c7f7ab..d497777110 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -59,8 +59,9 @@ struct LogOptions { std::string from {}; std::string to {}; - uint64_t nbOfCommits {0}; // maximum number of commits wanted - bool skipMerge {false}; // Do not include merge commits in the log. Used by the module to get last interaction without potential merges + uint64_t nbOfCommits {0}; // maximum number of commits wanted + bool skipMerge {false}; // Do not include merge commits in the log. Used by the module to get + // last interaction without potential merges bool includeTo {false}; // If we want or not the "to" commit [from-to] or [from-to) bool fastLog {false}; // Do not parse content, used mostly to count bool logIfNotFound {true}; // Add a warning in the log if commit is not found @@ -140,7 +141,7 @@ public: * @return the conversation repository object */ static LIBJAMI_TESTABLE std::unique_ptr<ConversationRepository> createConversation( - const std::weak_ptr<JamiAccount>& account, + const std::shared_ptr<JamiAccount>& account, ConversationMode mode = ConversationMode::INVITES_ONLY, const std::string& otherMember = ""); @@ -153,7 +154,7 @@ public: * @param socket Socket used to clone */ static LIBJAMI_TESTABLE std::unique_ptr<ConversationRepository> cloneConversation( - const std::weak_ptr<JamiAccount>& account, + const std::shared_ptr<JamiAccount>& account, const std::string& deviceId, const std::string& conversationId); @@ -372,6 +373,13 @@ public: * @param blocking if we need to wait that certificates are pinned */ void pinCertificates(bool blocking = false); + /** + * Retrieve the uri from a deviceId + * @note used by swarm manager (peersToSyncWith) + * @param deviceId + * @return corresponding issuer + */ + std::string uriFromDevice(const std::string& deviceId) const; /** * Change repository's infos diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 58446e3e74..ad6e850127 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -1356,7 +1356,6 @@ JamiAccount::getVolatileAccountDetails() const return a; } - #if HAVE_RINGNS void JamiAccount::lookupName(const std::string& name) @@ -1496,8 +1495,8 @@ JamiAccount::registerAsyncOps() const auto& accId = accPtr->getAccountID(); JAMI_LOG("[Account {:s}] DHT UPNP mapping changed to {:s}", - accId, - mapRes->toString(true)); + accId, + mapRes->toString(true)); if (*update) { // Check if we need to update the mapping and the registration. @@ -1512,10 +1511,11 @@ JamiAccount::registerAsyncOps() // Update the mapping and restart the registration. dhtMap.updateFrom(mapRes); - JAMI_WARNING("[Account {:s}] Allocated port changed to {}. Restarting the " - "registration", - accId, - accPtr->dhtPortUsed()); + JAMI_WARNING( + "[Account {:s}] Allocated port changed to {}. Restarting the " + "registration", + accId, + accPtr->dhtPortUsed()); accPtr->dht_->connectivityChanged(); @@ -1529,14 +1529,15 @@ JamiAccount::registerAsyncOps() // Set connection info and load the account. if (mapRes->getState() == upnp::MappingState::OPEN) { dhtMap.updateFrom(mapRes); - JAMI_LOG("[Account {:s}] Mapping {:s} successfully allocated: starting the DHT", - accId, - dhtMap.toString()); - } else { - JAMI_WARNING( - "[Account {:s}] Mapping request is in {:s} state: starting the DHT anyway", + JAMI_LOG( + "[Account {:s}] Mapping {:s} successfully allocated: starting the DHT", accId, - mapRes->getStateStr()); + dhtMap.toString()); + } else { + JAMI_WARNING("[Account {:s}] Mapping request is in {:s} state: starting " + "the DHT anyway", + accId, + mapRes->getStateStr()); } // Load the account and start the DHT. @@ -1564,7 +1565,8 @@ JamiAccount::doRegister() { std::lock_guard<std::recursive_mutex> lock(configurationMutex_); if (not isUsable()) { - JAMI_WARNING("[Account {:s}] Account must be enabled and active to register, ignoring", getAccountID()); + JAMI_WARNING("[Account {:s}] Account must be enabled and active to register, ignoring", + getAccountID()); return; } @@ -1816,11 +1818,14 @@ JamiAccount::doRegister_() config.proxy_server = proxyServerCached_; if (not config.proxy_server.empty()) { - JAMI_LOG("[Account {}] using proxy server {}", - getAccountID(), - config.proxy_server); + JAMI_LOG("[Account {}] using proxy server {}", getAccountID(), config.proxy_server); if (not config.push_token.empty()) { - JAMI_LOG("[Account %s] using push notifications with platform: {}, topic: {}, token: {}", getAccountID(), config.push_platform, config.push_topic, config.push_token); + JAMI_LOG( + "[Account %s] using push notifications with platform: {}, topic: {}, token: {}", + getAccountID(), + config.push_platform, + config.push_topic, + config.push_token); } } @@ -1939,6 +1944,14 @@ JamiAccount::doRegister_() }, [this] { deviceAnnounced_ = true; + + // Bootstrap at the end to avoid to be long to load. + dht::ThreadPool::io().run([w = weak()] { + if (auto shared = w.lock()) { + std::lock_guard<std::recursive_mutex> lock(shared->configurationMutex_); + shared->convModule()->bootstrap(); + } + }); emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>( accountID_, getVolatileAccountDetails()); }); @@ -2023,12 +2036,13 @@ JamiAccount::doRegister_() // Check if pull from banned device if (convModule()->isBannedDevice(conversationId, remoteDevice)) { - JAMI_WARNING("[Account {:s}] Git server requested for conversation {:s}, but the " - "device is " - "unauthorized ({:s}) ", - getAccountID(), - conversationId, - remoteDevice); + JAMI_WARNING( + "[Account {:s}] Git server requested for conversation {:s}, but the " + "device is " + "unauthorized ({:s}) ", + getAccountID(), + conversationId, + remoteDevice); channel->shutdown(); return; } @@ -2039,12 +2053,13 @@ JamiAccount::doRegister_() // So it's not the server socket return; } - JAMI_WARNING("[Account {:s}] Git server requested for conversation {:s}, device {:s}, " - "channel {}", - accountID_, - conversationId, - deviceId.toString(), - channel->channel()); + JAMI_WARNING( + "[Account {:s}] Git server requested for conversation {:s}, device {:s}, " + "channel {}", + accountID_, + conversationId, + deviceId.toString(), + channel->channel()); auto gs = std::make_unique<GitServer>(accountID_, conversationId, channel); gs->setOnFetched( [w = weak(), conversationId, deviceId](const std::string& commit) { @@ -2142,11 +2157,12 @@ JamiAccount::convModule() shared->syncModule()->syncWithConnected(syncMsg); }); }, - [this](auto&& uri, auto&& msg, auto token = 0) { + [this](auto&& uri, auto&& device, auto&& msg, auto token = 0) { // No need to retrigger, sendTextMessage will call // messageEngine_.sendMessage, already retriggering on // main thread. - return sendTextMessage(uri, msg, token); + auto deviceId = device ? device.toString() : ""; + return sendTextMessage(uri, deviceId, msg, token); }, [this](const auto& convId, const auto& deviceId, auto cb, const auto& type) { runOnMainThread([w = weak(), convId, deviceId, cb = std::move(cb), type] { @@ -2186,16 +2202,41 @@ JamiAccount::convModule() }); }, [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType) { - std::lock_guard<std::mutex> lkCM(connManagerMtx_); - if (!connectionManager_) { - Manager::instance().ioContext()->post([cb = std::move(cb)] { cb({}); }); - return; - } - connectionManager_->connectDevice(DeviceId(deviceId), - fmt::format("swarm://{}", convId), - [cb = std::move( - cb)](std::shared_ptr<ChannelSocket> socket, - const DeviceId&) { cb(socket); }); + runOnMainThread([w = weak(), convId, deviceId, cb, connectionType] { + auto shared = w.lock(); + if (!shared) + return; + std::lock_guard<std::mutex> lkCM(shared->connManagerMtx_); + if (!shared->connectionManager_) { + Manager::instance().ioContext()->post([cb] { cb({}); }); + return; + } + if (!shared->connectionManager_->isConnecting(DeviceId(deviceId), fmt::format("swarm://{}", convId))) { + shared->connectionManager_->connectDevice( + DeviceId(deviceId), + fmt::format("swarm://{}", convId), + [w, cb](std::shared_ptr<ChannelSocket> socket, + const DeviceId& deviceId) { + if (socket) { + auto shared = w.lock(); + if (!shared) + return; + auto remoteCert = socket->peerCertificate(); + auto uri = remoteCert->issuer->getId().toString(); + std::unique_lock<std::mutex> lk(shared->sipConnsMtx_); + // Verify that the connection is not already cached + SipConnectionKey key(uri, deviceId.toString()); + auto it = shared->sipConns_.find(key); + if (it == shared->sipConns_.end()) { + lk.unlock(); + shared->requestSIPConnection(uri, deviceId, ""); + } + } + + cb(socket); + }); + } + }); }, [this](auto&& convId, auto&& contactUri, bool accept) { // NOTE: do not reschedule as the conversation's requests @@ -2328,6 +2369,7 @@ JamiAccount::connectivityChanged() // nothing to do return; } + convModule()->connectivityChanged(); dht_->connectivityChanged(); { std::lock_guard<std::mutex> lkCM(connManagerMtx_); @@ -2911,7 +2953,7 @@ JamiAccount::declineConversationRequest(const std::string& conversationId) { auto peerId = convModule()->peerFromConversationRequest(conversationId); convModule()->declineConversationRequest(conversationId); - if (!peerId.empty()) { + if (!peerId.empty()) { std::lock_guard<std::recursive_mutex> lock(configurationMutex_); if (auto info = accountManager_->getInfo()) { // Verify if we have a trust request with this peer + convId @@ -2919,7 +2961,9 @@ JamiAccount::declineConversationRequest(const std::string& conversationId) if (req.find(libjami::Account::TrustRequest::CONVERSATIONID) != req.end() && req.at(libjami::Account::TrustRequest::CONVERSATIONID) == conversationId) { accountManager_->discardTrustRequest(peerId); - JAMI_DEBUG("[Account {:s}] declined trust request with {:s}", getAccountID(), peerId); + JAMI_DEBUG("[Account {:s}] declined trust request with {:s}", + getAccountID(), + peerId); } } } @@ -2969,6 +3013,7 @@ JamiAccount::forEachDevice(const dht::InfoHash& to, uint64_t JamiAccount::sendTextMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t refreshToken, bool onlyConnected) @@ -2983,18 +3028,19 @@ JamiAccount::sendTextMessage(const std::string& to, try { toUri = parseJamiUri(to); } catch (...) { - JAMI_ERR("Failed to send a text message due to an invalid URI %s", to.c_str()); + JAMI_ERROR("Failed to send a text message due to an invalid URI {}", to); return 0; } if (payloads.size() != 1) { - JAMI_ERR("Multi-part im is not supported yet by JamiAccount"); + JAMI_ERROR("Multi-part im is not supported yet by JamiAccount"); return 0; } - return SIPAccountBase::sendTextMessage(toUri, payloads, refreshToken, onlyConnected); + return SIPAccountBase::sendTextMessage(toUri, deviceId, payloads, refreshToken, onlyConnected); } void JamiAccount::sendMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t token, bool retryOnTimeout, @@ -3006,7 +3052,7 @@ JamiAccount::sendMessage(const std::string& to, } catch (...) { JAMI_ERR("Failed to send a text message due to an invalid URI %s", to.c_str()); if (!onlyConnected) - messageEngine_.onMessageSent(to, token, false); + messageEngine_.onMessageSent(to, token, false, deviceId); return; } if (payloads.size() != 1) { @@ -3018,9 +3064,6 @@ JamiAccount::sendMessage(const std::string& to, return; } - auto toH = dht::InfoHash(toUri); - auto now = clock::to_time_t(clock::now()); - auto confirm = std::make_shared<PendingConfirmation>(); if (onlyConnected) { confirm->replied = true; @@ -3035,6 +3078,10 @@ JamiAccount::sendMessage(const std::string& to, ++it; continue; } + if (!deviceId.empty() && key.second.toString() != deviceId) { + ++it; + continue; + } auto& conn = value.back(); auto& channel = conn.channel; @@ -3042,7 +3089,7 @@ JamiAccount::sendMessage(const std::string& to, auto ctx = std::make_unique<TextMessageCtx>(); ctx->acc = weak(); ctx->to = to; - ctx->deviceId = key.second; + ctx->deviceId = DeviceId(deviceId); ctx->id = token; ctx->onlyConnected = onlyConnected; ctx->retryOnTimeout = retryOnTimeout; @@ -3070,14 +3117,14 @@ JamiAccount::sendMessage(const std::string& to, }); if (!res) { if (!onlyConnected) - messageEngine_.onMessageSent(to, token, false); + messageEngine_.onMessageSent(to, token, false, deviceId); ++it; continue; } } catch (const std::runtime_error& ex) { JAMI_WARN("%s", ex.what()); if (!onlyConnected) - messageEngine_.onMessageSent(to, token, false); + messageEngine_.onMessageSent(to, token, false, deviceId); ++it; // Remove connection in incorrect state shutdownSIPConnection(channel, to, key.second); @@ -3086,137 +3133,153 @@ JamiAccount::sendMessage(const std::string& to, devices->emplace(key.second); ++it; + if (key.second.toString() == deviceId) { + return; + } } lk.unlock(); if (onlyConnected) return; - // Find listening devices for this account - accountManager_->forEachDevice( - toH, - [this, confirm, to, token, payloads, now, devices]( - const std::shared_ptr<dht::crypto::PublicKey>& dev) { - // Test if already sent - auto deviceId = dev->getLongId(); - if (devices->find(deviceId) != devices->end()) { - return; - } - if (deviceId.toString() == currentDeviceId()) { - devices->emplace(deviceId); - return; - } + if (deviceId.empty()) { + auto toH = dht::InfoHash(toUri); + // Find listening devices for this account + accountManager_->forEachDevice( + toH, + [this, confirm, to, token, payloads, devices]( + const std::shared_ptr<dht::crypto::PublicKey>& dev) { + // Test if already sent + auto deviceId = dev->getLongId(); + if (devices->find(deviceId) != devices->end()) { + return; + } + if (deviceId.toString() == currentDeviceId()) { + devices->emplace(deviceId); + return; + } - // Else, ask for a channel and send a DHT message - auto payload_type = payloads.cbegin()->first; - requestSIPConnection(to, deviceId, payload_type); - { - std::lock_guard<std::mutex> lock(messageMutex_); - sentMessages_[token].to.emplace(deviceId); - } + // Else, ask for a channel and send a DHT message + auto payload_type = payloads.cbegin()->first; + requestSIPConnection(to, deviceId, payload_type); + { + std::lock_guard<std::mutex> lock(messageMutex_); + sentMessages_[token].to.emplace(deviceId); + } - auto h = dht::InfoHash::get("inbox:" + dev->getId().toString()); - std::lock_guard<std::mutex> l(confirm->lock); - auto list_token - = dht_->listen<dht::ImMessage>(h, [this, to, token, confirm](dht::ImMessage&& msg) { - // check expected message confirmation - if (msg.id != token) - return true; - - { - std::lock_guard<std::mutex> lock(messageMutex_); - auto e = sentMessages_.find(msg.id); - if (e == sentMessages_.end() - or e->second.to.find(msg.owner->getLongId()) == e->second.to.end()) { - JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token - << "] Message not found"; - return true; - } - sentMessages_.erase(e); - JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token - << "] Received text message reply"; - - // add treated message - auto res = treatedMessages_.emplace(to_hex_string(msg.id)); - if (!res.second) - return true; - } - saveTreatedMessages(); - - // report message as confirmed received - { - std::lock_guard<std::mutex> l(confirm->lock); - for (auto& t : confirm->listenTokens) - dht_->cancelListen(t.first, std::move(t.second)); - confirm->listenTokens.clear(); - confirm->replied = true; - } - messageEngine_.onMessageSent(to, token, true); - return false; - }); - confirm->listenTokens.emplace(h, std::move(list_token)); - dht_->putEncrypted(h, - dev, - dht::ImMessage(token, - std::string(payloads.begin()->first), - std::string(payloads.begin()->second), - now), - [this, to, token, confirm, h](bool ok) { - JAMI_DBG() - << "[Account " << getAccountID() << "] [message " << token - << "] Put encrypted " << (ok ? "ok" : "failed"); - if (not ok && connectionManager_ /* Check if not joining */) { - std::unique_lock<std::mutex> l(confirm->lock); - auto lt = confirm->listenTokens.find(h); - if (lt != confirm->listenTokens.end()) { - std::shared_future<size_t> tok = std::move(lt->second); - confirm->listenTokens.erase(lt); - dht_->cancelListen(h, tok); - } - if (confirm->listenTokens.empty() and not confirm->replied) { - l.unlock(); - messageEngine_.onMessageSent(to, token, false); + auto h = dht::InfoHash::get("inbox:" + dev->getId().toString()); + std::lock_guard<std::mutex> l(confirm->lock); + auto list_token = dht_->listen< + dht::ImMessage>(h, [this, to, token, confirm](dht::ImMessage&& msg) { + // check expected message confirmation + if (msg.id != token) + return true; + + { + std::lock_guard<std::mutex> lock(messageMutex_); + auto e = sentMessages_.find(msg.id); + if (e == sentMessages_.end() + or e->second.to.find(msg.owner->getLongId()) == e->second.to.end()) { + JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token + << "] Message not found"; + return true; + } + sentMessages_.erase(e); + JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token + << "] Received text message reply"; + + // add treated message + auto res = treatedMessages_.emplace(to_hex_string(msg.id)); + if (!res.second) + return true; + } + saveTreatedMessages(); + + // report message as confirmed received + { + std::lock_guard<std::mutex> l(confirm->lock); + for (auto& t : confirm->listenTokens) + dht_->cancelListen(t.first, std::move(t.second)); + confirm->listenTokens.clear(); + confirm->replied = true; + } + messageEngine_.onMessageSent(to, token, true); + return false; + }); + confirm->listenTokens.emplace(h, std::move(list_token)); + auto now = clock::to_time_t(clock::now()); + dht_->putEncrypted(h, + dev, + dht::ImMessage(token, + std::string(payloads.begin()->first), + std::string(payloads.begin()->second), + now), + [this, to, token, confirm, h](bool ok) { + JAMI_DBG() + << "[Account " << getAccountID() << "] [message " + << token << "] Put encrypted " << (ok ? "ok" : "failed"); + if (not ok + && connectionManager_ /* Check if not joining */) { + std::unique_lock<std::mutex> l(confirm->lock); + auto lt = confirm->listenTokens.find(h); + if (lt != confirm->listenTokens.end()) { + std::shared_future<size_t> tok = std::move( + lt->second); + confirm->listenTokens.erase(lt); + dht_->cancelListen(h, tok); + } + if (confirm->listenTokens.empty() + and not confirm->replied) { + l.unlock(); + messageEngine_.onMessageSent(to, token, false); + } } - } - }); + }); - JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token - << "] Sending message for device " << deviceId.toString(); - }, - [this, to, token, devices, confirm](bool ok) { - if (devices->size() == 1 && devices->begin()->toString() == currentDeviceId()) { - // Current user only have devices, so no message are sent - { - std::lock_guard<std::mutex> l(confirm->lock); - for (auto& t : confirm->listenTokens) - dht_->cancelListen(t.first, std::move(t.second)); - confirm->listenTokens.clear(); - confirm->replied = true; + JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token + << "] Sending message for device " << deviceId.toString(); + }, + [this, to, token, devices, confirm](bool ok) { + if (devices->size() == 1 && devices->begin()->toString() == currentDeviceId()) { + // Current user only have devices, so no message are sent + { + std::lock_guard<std::mutex> l(confirm->lock); + for (auto& t : confirm->listenTokens) + dht_->cancelListen(t.first, std::move(t.second)); + confirm->listenTokens.clear(); + confirm->replied = true; + } + messageEngine_.onMessageSent(to, token, true); + } else if (not ok) { + messageEngine_.onMessageSent(to, token, false); } - messageEngine_.onMessageSent(to, token, true); - } else if (not ok) { - messageEngine_.onMessageSent(to, token, false); - } - }); + }); - // Timeout cleanup - Manager::instance().scheduleTaskIn( - [w = weak(), confirm, to, token]() { - std::unique_lock<std::mutex> l(confirm->lock); - if (not confirm->replied) { - if (auto this_ = w.lock()) { - JAMI_DBG() << "[Account " << this_->getAccountID() << "] [message " << token - << "] Timeout"; - for (auto& t : confirm->listenTokens) - this_->dht_->cancelListen(t.first, std::move(t.second)); - confirm->listenTokens.clear(); - confirm->replied = true; - l.unlock(); - this_->messageEngine_.onMessageSent(to, token, false); + // Timeout cleanup + Manager::instance().scheduleTaskIn( + [w = weak(), confirm, to, token]() { + std::unique_lock<std::mutex> l(confirm->lock); + if (not confirm->replied) { + if (auto this_ = w.lock()) { + JAMI_DBG() << "[Account " << this_->getAccountID() << "] [message " << token + << "] Timeout"; + for (auto& t : confirm->listenTokens) + this_->dht_->cancelListen(t.first, std::move(t.second)); + confirm->listenTokens.clear(); + confirm->replied = true; + l.unlock(); + this_->messageEngine_.onMessageSent(to, token, false); + } } - } - }, - std::chrono::minutes(1)); + }, + std::chrono::minutes(1)); + + } else { + // Set message as not sent in order to be re-triggered + messageEngine_.onMessageSent(to, token, false, deviceId); + auto payload_type = payloads.cbegin()->first; + requestSIPConnection(to, DeviceId(deviceId), payload_type); + } } void @@ -3227,7 +3290,7 @@ JamiAccount::onSIPMessageSent(const std::shared_ptr<TextMessageCtx>& ctx, int co ctx->confirmation->replied = true; l.unlock(); if (!ctx->onlyConnected) - messageEngine_.onMessageSent(ctx->to, ctx->id, true); + messageEngine_.onMessageSent(ctx->to, ctx->id, true, ctx->deviceId.toString()); } else { // Note: This can be called from pjsip's eventloop while // sipConnsMtx_ is locked. So we should retrigger the shutdown. @@ -3245,7 +3308,7 @@ JamiAccount::onSIPMessageSent(const std::shared_ptr<TextMessageCtx>& ctx, int co // After closing sockets with that peer, we try to re-connect to // that peer one time. if (ctx->retryOnTimeout) - messageEngine_.onPeerOnline(ctx->to, false); + messageEngine_.onPeerOnline(ctx->to, false, ctx->deviceId.toString()); } } @@ -3344,7 +3407,8 @@ JamiAccount::setPushNotificationTopic(const std::string& topic) } bool -JamiAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data) { +JamiAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data) +{ if (SIPAccountBase::setPushNotificationConfig(data)) { if (dht_) { dht_->setPushNotificationPlatform(config_->platform); @@ -3356,7 +3420,6 @@ JamiAccount::setPushNotificationConfig(const std::map<std::string, std::string>& return false; } - /** * To be called by clients with relevant data when a push notification is received. */ @@ -3471,16 +3534,16 @@ JamiAccount::sendInstantMessage(const std::string& convId, const std::map<std::string, std::string>& msg) { auto members = convModule()->getConversationMembers(convId); - if (members.empty()) { + if (convId.empty() && members.empty()) { // TODO remove, it's for old API for contacts - sendTextMessage(convId, msg); + sendTextMessage(convId, "", msg); return; } for (const auto& m : members) { const auto& uri = m.at("uri"); auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand); // Announce to all members that a new message is sent - sendMessage(uri, msg, token, false, true); + sendMessage(uri, "", msg, token, false, true); } } @@ -3497,13 +3560,16 @@ JamiAccount::handleMessage(const std::string& from, const std::pair<std::string, return false; } - JAMI_WARN("Received indication for new commit available in conversation %s", - json["id"].asString().c_str()); - - convModule()->onNewCommit(from, - json["deviceId"].asString(), - json["id"].asString(), - json["commit"].asString()); + std::string deviceId = json["deviceId"].asString(); + std::string id = json["id"].asString(); + std::string commit = json["commit"].asString(); + // onNewCommit will do heavy stuff like fetching, avoid to block SIP socket + dht::ThreadPool::io().run([w = weak(), from, deviceId, id, commit] { + if (auto shared = w.lock()) { + if (auto cm = shared->convModule()) + cm->onNewCommit(from, deviceId, id, commit); + } + }); return true; } else if (m.first == MIME_TYPE_INVITE) { convModule()->onNeedConversationRequest(from, m.second); @@ -3682,7 +3748,9 @@ JamiAccount::requestSIPConnection(const std::string& peerId, } void -JamiAccount::sendProfile(const std::string& convId, const std::string& peerUri, const std::string& deviceId) +JamiAccount::sendProfile(const std::string& convId, + const std::string& peerUri, + const std::string& deviceId) { if (not fileutils::isFile(profilePath())) return; @@ -3709,7 +3777,9 @@ JamiAccount::sendProfile(const std::string& convId, const std::string& peerUri, } bool -JamiAccount::needToSendProfile(const std::string& peerUri, const std::string& deviceId, const std::string& sha3Sum) +JamiAccount::needToSendProfile(const std::string& peerUri, + const std::string& deviceId, + const std::string& sha3Sum) { std::string previousSha3 {}; auto vCardPath = fmt::format("{}/vcard", cachePath_); @@ -3878,12 +3948,15 @@ JamiAccount::cacheSIPConnection(std::shared_ptr<ChannelSocket>&& socket, // Store the connection connections.emplace_back(SipConnection {sip_tr, socket}); JAMI_WARNING("[Account {:s}] New SIP channel opened with {:s}", - getAccountID(), deviceId.to_c_str()); + getAccountID(), + deviceId.to_c_str()); lk.unlock(); convModule()->syncConversations(peerId, deviceId.toString()); // Retry messages + messageEngine_.onPeerOnline(peerId); + messageEngine_.onPeerOnline(peerId, true, deviceId.toString()); // Connect pending calls forEachPendingCall(deviceId, [&](const auto& pc) { @@ -3946,14 +4019,15 @@ JamiAccount::dataTransfer(const std::string& id) } void -JamiAccount::monitor() const +JamiAccount::monitor() { JAMI_DEBUG("[Account {:s}] Monitor connections", getAccountID()); JAMI_DEBUG("[Account {:s}] Using proxy: {:s}", getAccountID(), proxyServerCached_); - std::lock_guard<std::mutex> lkCM(connManagerMtx_); + convModule()->monitor(); + /*std::lock_guard<std::mutex> lkCM(connManagerMtx_); if (connectionManager_) - connectionManager_->monitor(); + connectionManager_->monitor();*/ } void @@ -4032,27 +4106,40 @@ JamiAccount::transferFile(const std::string& conversationId, std::lock_guard<std::mutex> lkCM(connManagerMtx_); if (!connectionManager_) return; - connectionManager_->connectDevice( - DeviceId(deviceId), - channelName, - [this, conversationId, path = std::move(path), fileId, interactionId, start, end, onFinished = std::move(onFinished)]( - std::shared_ptr<ChannelSocket> socket, const DeviceId&) { - if (!socket) - return; - dht::ThreadPool::io().run([w = weak(), - path = std::move(path), - socket = std::move(socket), - conversationId = std::move(conversationId), - fileId, - interactionId, - start, - end, - onFinished = std::move(onFinished)] { - if (auto shared = w.lock()) - if (auto dt = shared->dataTransfer(conversationId)) - dt->transferFile(socket, fileId, interactionId, path, start, end, std::move(onFinished)); - }); - }); + connectionManager_ + ->connectDevice(DeviceId(deviceId), + channelName, + [this, + conversationId, + path = std::move(path), + fileId, + interactionId, + start, + end, + onFinished = std::move(onFinished)](std::shared_ptr<ChannelSocket> socket, + const DeviceId&) { + if (!socket) + return; + dht::ThreadPool::io().run([w = weak(), + path = std::move(path), + socket = std::move(socket), + conversationId = std::move(conversationId), + fileId, + interactionId, + start, + end, + onFinished = std::move(onFinished)] { + if (auto shared = w.lock()) + if (auto dt = shared->dataTransfer(conversationId)) + dt->transferFile(socket, + fileId, + interactionId, + path, + start, + end, + std::move(onFinished)); + }); + }); } void diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index a2b62d3656..021697ebbc 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -311,11 +311,14 @@ public: void sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload); void sendMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t id, bool retryOnTimeout = true, bool onlyConnected = false) override; + uint64_t sendTextMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t refreshToken = 0, bool onlyConnected = false) override; @@ -460,7 +463,7 @@ public: bool handleMessage(const std::string& from, const std::pair<std::string, std::string>& message) override; - void monitor() const; + void monitor(); // File transfer void sendFile(const std::string& conversationId, @@ -511,14 +514,18 @@ public: * @param sha3Sum SHA3 hash of the profile */ // Note: when swarm will be merged, this can be moved in transferManager - bool needToSendProfile(const std::string& peerUri, const std::string& deviceId, const std::string& sha3Sum); + bool needToSendProfile(const std::string& peerUri, + const std::string& deviceId, + const std::string& sha3Sum); /** * Send Profile via cached SIP connection * @param convId Conversation's identifier (can be empty for self profile on sync) * @param peerUri Uri that will receive the profile * @param deviceId Device that will receive the profile */ - void sendProfile(const std::string& convId, const std::string& peerUri, const std::string& deviceId); + void sendProfile(const std::string& convId, + const std::string& peerUri, + const std::string& deviceId); /** * Send profile via cached SIP connection * @param peerUri Uri that will receive the profile @@ -572,14 +579,22 @@ public: void handleIncomingConversationCall(const std::string& callId, const std::string& destination); /** - * The DRT component is composed on some special nodes, that are usually present but not connected. - * This kind of node corresponds to devices with push notifications & proxy and are + * The DRT component is composed on some special nodes, that are usually present but not + * connected. This kind of node corresponds to devices with push notifications & proxy and are * stored in the mobile nodes */ - bool isMobile() const { + bool isMobile() const + { return config().proxyEnabled and not config().deviceKey.empty(); } +#ifdef LIBJAMI_TESTABLE + std::map<Uri::Scheme, std::unique_ptr<ChannelHandlerInterface>>& channelHandlers() + { + return channelHandlers_; + }; +#endif + private: NON_COPYABLE(JamiAccount); diff --git a/src/jamidht/swarm/routing_table.cpp b/src/jamidht/swarm/routing_table.cpp index c3dc468511..7f88bd0e36 100644 --- a/src/jamidht/swarm/routing_table.cpp +++ b/src/jamidht/swarm/routing_table.cpp @@ -494,6 +494,35 @@ RoutingTable::contains(const std::list<Bucket>::iterator& bucket, const NodeId& || NodeId::cmp(nodeId, std::next(bucket)->getLowerLimit()) < 0); } +std::vector<NodeId> +RoutingTable::getAllNodes() const +{ + std::vector<NodeId> ret; + for (const auto& b : buckets) { + const auto& nodes = b.getNodeIds(); + const auto& knownNodes = b.getKnownNodes(); + const auto& mobileNodes = b.getMobileNodes(); + const auto& connectingNodes = b.getConnectingNodes(); + + ret.insert(ret.end(), nodes.begin(), nodes.end()); + ret.insert(ret.end(), knownNodes.begin(), knownNodes.end()); + ret.insert(ret.end(), mobileNodes.begin(), mobileNodes.end()); + ret.insert(ret.end(), connectingNodes.begin(), connectingNodes.end()); + } + return ret; +} + +void +RoutingTable::deleteNode(const NodeId& nodeId) +{ + auto bucket = findBucket(nodeId); + shutdownNode(nodeId); + bucket->removeNode(nodeId); + bucket->removeConnectingNode(nodeId); + bucket->removeKnownNode(nodeId); + bucket->removeMobileNode(nodeId); +} + NodeId RoutingTable::middle(std::list<Bucket>::iterator& it) const { diff --git a/src/jamidht/swarm/routing_table.h b/src/jamidht/swarm/routing_table.h index c20d5a9cb5..3265b050d4 100644 --- a/src/jamidht/swarm/routing_table.h +++ b/src/jamidht/swarm/routing_table.h @@ -60,7 +60,7 @@ class Bucket { public: - static constexpr int BUCKET_MAX_SIZE = 4; + static constexpr int BUCKET_MAX_SIZE = 2; Bucket() = delete; Bucket(const Bucket&) = delete; @@ -525,6 +525,16 @@ public: */ bool contains(const std::list<Bucket>::iterator& it, const NodeId& nodeId) const; + /** + * Return every node from each bucket + */ + std::vector<NodeId> getAllNodes() const; + + /** + * Delete node from every table in bucket + */ + void deleteNode(const NodeId& nodeId); + private: RoutingTable(const RoutingTable&) = delete; RoutingTable& operator=(const RoutingTable&) = delete; diff --git a/src/jamidht/swarm/swarm_manager.cpp b/src/jamidht/swarm/swarm_manager.cpp index c6d52a5954..e0e56d05d2 100644 --- a/src/jamidht/swarm/swarm_manager.cpp +++ b/src/jamidht/swarm/swarm_manager.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2023 Savoir-faire Linux Inc. + * * Author: Fadi Shehadeh <fadi.shehadeh@savoirfairelinux.com> * * This program is free software; you can redistribute it and/or modify @@ -13,11 +14,10 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include <iostream> - #include "swarm_manager.h" #include "connectivity/multiplexed_socket.h" #include <opendht/thread_pool.h> @@ -37,48 +37,91 @@ SwarmManager::SwarmManager(const NodeId& id) SwarmManager::~SwarmManager() { - shutdown(); + if (!isShutdown_) + shutdown(); } void -SwarmManager::removeNode(const NodeId& nodeId) +SwarmManager::setKnownNodes(const std::vector<NodeId>& known_nodes) { + isShutdown_ = false; { std::lock_guard<std::mutex> lock(mutex); - removeNodeInternal(nodeId); + for (const auto& NodeId : known_nodes) + addKnownNodes(std::move(NodeId)); } maintainBuckets(); } void -SwarmManager::removeNodeInternal(const NodeId& nodeId) +SwarmManager::setMobileNodes(const std::vector<NodeId>& mobile_nodes) { - routing_table.removeNode(nodeId); + { + std::lock_guard<std::mutex> lock(mutex); + for (const auto& nodeId : mobile_nodes) + addMobileNodes(nodeId); + } } void -SwarmManager::setKnownNodes(const std::vector<NodeId>& known_nodes) +SwarmManager::addChannel(const std::shared_ptr<ChannelSocketInterface>& channel) { - isShutdown_ = false; - { - std::lock_guard<std::mutex> lock(mutex); - for (const auto& NodeId : known_nodes) - addKnownNodes(std::move(NodeId)); + // JAMI_WARNING("[SwarmManager {}] addChannel! with {}", fmt::ptr(this), channel->deviceId().to_view()); + if (channel) { + auto emit = false; + { + std::lock_guard<std::mutex> lock(mutex); + emit = routing_table.findBucket(getId())->getNodeIds().size() == 0; + auto bucket = routing_table.findBucket(channel->deviceId()); + if (routing_table.addNode(channel, bucket)) { + std::error_code ec; + resetNodeExpiry(ec, channel, id_); + } + } + receiveMessage(channel); + if (emit && onConnectionChanged_) { + // If it's the first channel we add, we're now connected! + JAMI_DEBUG("[SwarmManager {}] Bootstrap: Connected!", fmt::ptr(this)); + onConnectionChanged_(true); + } } - maintainBuckets(); } void -SwarmManager::setMobileNodes(const std::vector<NodeId>& mobile_nodes) +SwarmManager::removeNode(const NodeId& nodeId) { { std::lock_guard<std::mutex> lock(mutex); - for (const auto& nodeId : mobile_nodes) - addMobileNodes(nodeId); + removeNodeInternal(nodeId); } maintainBuckets(); } +void +SwarmManager::changeMobility(const NodeId& nodeId, bool isMobile) +{ + std::lock_guard<std::mutex> lock(mutex); + auto bucket = routing_table.findBucket(nodeId); + bucket->changeMobility(nodeId, isMobile); +} + +bool +SwarmManager::isConnectedWith(const NodeId& deviceId) +{ + return routing_table.hasNode(deviceId); +} + +void +SwarmManager::shutdown() +{ + if (isShutdown_) { + return; + } + isShutdown_ = true; + std::lock_guard<std::mutex> lock(mutex); + routing_table.shutdownAllNodes(); +} + void SwarmManager::addKnownNodes(const NodeId& nodeId) { @@ -95,6 +138,31 @@ SwarmManager::addMobileNodes(const NodeId& nodeId) } } +void +SwarmManager::maintainBuckets() +{ + std::set<NodeId> nodes; + std::unique_lock<std::mutex> lock(mutex); + auto& buckets = routing_table.getBuckets(); + for (auto it = buckets.begin(); it != buckets.end(); ++it) { + auto& bucket = *it; + bool myBucket = routing_table.contains(it, id_); + auto connecting_nodes = myBucket ? bucket.getConnectingNodesSize() + : bucket.getConnectingNodesSize() + bucket.getNodesSize(); + if (connecting_nodes < Bucket::BUCKET_MAX_SIZE) { + auto nodesToTry = bucket.getKnownNodesRandom(Bucket::BUCKET_MAX_SIZE - connecting_nodes, + rd); + for (auto& node : nodesToTry) + routing_table.addConnectingNode(node); + + nodes.insert(nodesToTry.begin(), nodesToTry.end()); + } + } + lock.unlock(); + for (auto& node : nodes) + tryConnect(node); +} + void SwarmManager::sendRequest(const std::shared_ptr<ChannelSocketInterface>& socket, NodeId& nodeId, @@ -135,17 +203,21 @@ SwarmManager::sendAnswer(const std::shared_ptr<ChannelSocketInterface>& socket, msg.is_mobile = isMobile_; msg.response = std::move(toResponse); - msgpack::sbuffer buffer((size_t) 1024); + msgpack::sbuffer buffer((size_t) 60000); msgpack::packer<msgpack::sbuffer> pk(&buffer); pk.pack(msg); std::error_code ec; + socket->write(reinterpret_cast<const unsigned char*>(buffer.data()), buffer.size(), ec); if (ec) { JAMI_ERROR("{}", ec.message()); return; } } + + else { + } } void @@ -170,7 +242,7 @@ SwarmManager::receiveMessage(const std::shared_ptr<ChannelSocketInterface>& sock auto shared = w.lock(); auto socket = wsocket.lock(); if (!shared || !socket) - return 0lu; + return size_t {0}; try { Message msg; @@ -179,9 +251,10 @@ SwarmManager::receiveMessage(const std::shared_ptr<ChannelSocketInterface>& sock if (msg.is_mobile) shared->changeMobility(socket->deviceId(), msg.is_mobile); - if (msg.request) + if (msg.request) { shared->sendAnswer(socket, msg); - else if (msg.response) { + + } else if (msg.response) { shared->setKnownNodes(msg.response->nodes); shared->setMobileNodes(msg.response->mobile_nodes); } @@ -204,33 +277,35 @@ SwarmManager::receiveMessage(const std::shared_ptr<ChannelSocketInterface>& sock } void -SwarmManager::maintainBuckets() +SwarmManager::resetNodeExpiry(const asio::error_code& ec, + const std::shared_ptr<ChannelSocketInterface>& socket, + NodeId node) { - std::set<NodeId> nodes; - std::unique_lock<std::mutex> lock(mutex); - auto& buckets = routing_table.getBuckets(); - for (auto it = buckets.begin(); it != buckets.end(); ++it) { - auto& bucket = *it; - bool myBucket = routing_table.contains(it, id_); - auto connecting_nodes = myBucket ? bucket.getConnectingNodesSize() - : bucket.getConnectingNodesSize() + bucket.getNodesSize(); - if (connecting_nodes < Bucket::BUCKET_MAX_SIZE) { - auto nodesToTry = bucket.getKnownNodesRandom(Bucket::BUCKET_MAX_SIZE - connecting_nodes, - rd); - for (auto& node : nodesToTry) - routing_table.addConnectingNode(node); + NodeId idToFind; + std::list<Bucket>::iterator bucket; - nodes.insert(nodesToTry.begin(), nodesToTry.end()); - } + if (ec == asio::error::operation_aborted) + return; + + if (!node) { + bucket = routing_table.findBucket(socket->deviceId()); + idToFind = bucket->randomId(rd); + } else { + bucket = routing_table.findBucket(node); + idToFind = node; + } + + sendRequest(socket, idToFind, Query::FIND, Bucket::BUCKET_MAX_SIZE); + + if (!node) { + auto& nodeTimer = bucket->getNodeTimer(socket); + nodeTimer.expires_after(FIND_PERIOD); + nodeTimer.async_wait(std::bind(&jami::SwarmManager::resetNodeExpiry, + shared_from_this(), + std::placeholders::_1, + socket, + NodeId {})); } - lock.unlock(); - for (auto& node : nodes) - tryConnect(node); -} -bool -SwarmManager::hasChannel(const NodeId& deviceId) -{ - return routing_table.hasNode(deviceId); } void @@ -263,71 +338,32 @@ SwarmManager::tryConnect(const NodeId& nodeId) } void -SwarmManager::addChannel(std::shared_ptr<ChannelSocketInterface> channel) +SwarmManager::removeNodeInternal(const NodeId& nodeId) { - auto emit = false; - { - std::lock_guard<std::mutex> lock(mutex); - emit = routing_table.findBucket(getId())->getNodeIds().size() == 0; - auto bucket = routing_table.findBucket(channel->deviceId()); - if (routing_table.addNode(channel, bucket)) { - std::error_code ec; - resetNodeExpiry(ec, channel, id_); - } - } - receiveMessage(channel); - if (emit && onConnectionChanged_) { - // If it's the first channel we add, we're now connected! - JAMI_DEBUG("[SwarmManager {}] Bootstrap: Connected!", fmt::ptr(this)); - onConnectionChanged_(true); - } + routing_table.removeNode(nodeId); } -void -SwarmManager::resetNodeExpiry(const asio::error_code& ec, - const std::shared_ptr<ChannelSocketInterface>& socket, - NodeId node) +std::vector<NodeId> +SwarmManager::getAllNodes() const { - NodeId idToFind; - std::list<Bucket>::iterator bucket; - - if (ec == asio::error::operation_aborted) - return; - - if (!node) { - bucket = routing_table.findBucket(socket->deviceId()); - idToFind = bucket->randomId(rd); - } else { - bucket = routing_table.findBucket(node); - idToFind = node; - } - - sendRequest(socket, idToFind, Query::FIND, Bucket::BUCKET_MAX_SIZE); + std::lock_guard<std::mutex> lock(mutex); + std::vector<NodeId> nodes; + const auto& rtNodes = routing_table.getAllNodes(); + nodes.insert(nodes.end(), rtNodes.begin(), rtNodes.end()); - if (!node) { - auto& nodeTimer = bucket->getNodeTimer(socket); - nodeTimer.expires_after(FIND_PERIOD); - nodeTimer.async_wait(std::bind(&jami::SwarmManager::resetNodeExpiry, - shared_from_this(), - std::placeholders::_1, - socket, - NodeId {})); - } + return nodes; } void -SwarmManager::changeMobility(const NodeId& nodeId, bool isMobile) +SwarmManager::deleteNode(std::vector<NodeId> nodes) { - std::lock_guard<std::mutex> lock(mutex); - auto bucket = routing_table.findBucket(nodeId); - bucket->changeMobility(nodeId, isMobile); + { + std::lock_guard<std::mutex> lock(mutex); + for (const auto& node : nodes) { + routing_table.deleteNode(node); + } + } + maintainBuckets(); } -void -SwarmManager::shutdown() -{ - isShutdown_ = true; - std::lock_guard<std::mutex> lock(mutex); - routing_table.shutdownAllNodes(); -} } // namespace jami diff --git a/src/jamidht/swarm/swarm_manager.h b/src/jamidht/swarm/swarm_manager.h index df1d247226..50f58cc74c 100644 --- a/src/jamidht/swarm/swarm_manager.h +++ b/src/jamidht/swarm/swarm_manager.h @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ + #pragma once #include "routing_table.h" @@ -41,52 +42,107 @@ public: NeedSocketCb needSocketCb_; - std::weak_ptr<SwarmManager> weak() - { - return std::static_pointer_cast<SwarmManager>(shared_from_this()); - } + std::weak_ptr<SwarmManager> weak() { return weak_from_this(); } + /** + * Get swarm manager id + * @return NodeId + */ const NodeId& getId() const { return id_; } /** - * Add list of nodes to the known nodes list - * @param vector<NodeId>& known_nodes + * Set list of nodes to the routing table known_nodes + * @param known_nodes */ void setKnownNodes(const std::vector<NodeId>& known_nodes); /** - * Add list of nodes to the mobile nodes list - * @param vector<NodeId>& mobile_nodes + * Set list of nodes to the routing table mobile_nodes + * @param mobile_nodes */ void setMobileNodes(const std::vector<NodeId>& mobile_nodes); /** * Add channel to routing table - * @param shared_ptr<ChannelSocketInterface>& channel + * @param channel */ - void addChannel(std::shared_ptr<ChannelSocketInterface> channel); + void addChannel(const std::shared_ptr<ChannelSocketInterface>& channel); + /** + * Remove channel from routing table + * @param channel + */ void removeNode(const NodeId& nodeId); + + /** + * Change mobility of specific node + * @param nodeId + * @param isMobile + */ void changeMobility(const NodeId& nodeId, bool isMobile); - /** For testing */ + /** + * get all nodes from the different tables in bucket + */ + std::vector<NodeId> getAllNodes() const; + + /** + * Delete nodes from the different tables in bucket + */ + void deleteNode(std::vector<NodeId> nodes); + + // For tests + + /** + * Get routing table + * @return RoutingTable + */ RoutingTable& getRoutingTable() { return routing_table; }; + + /** + * Get buckets of routing table + * @return buckets list + */ std::list<Bucket>& getBuckets() { return routing_table.getBuckets(); }; + /** + * Shutdown swarm manager + */ void shutdown(); + /** + * Display swarm manager info + */ void display() { JAMI_DEBUG("SwarmManager {:s} has {:d} nodes in table [P = {}]", getId().to_c_str(), routing_table.getRoutingTableNodeCount(), isMobile_); + // print nodes of routingtable + for (auto& bucket : routing_table.getBuckets()) { + for (auto& node : bucket.getNodes()) { + JAMI_DEBUG("Node {:s}", node.first.toString()); + } + } } + /* + * Callback for connection changed + * @param cb + */ void onConnectionChanged(OnConnectionChanged cb) { onConnectionChanged_ = std::move(cb); } + /** + * Set mobility of swarm manager + * @param isMobile + */ void setMobility(bool isMobile) { isMobile_ = isMobile; } + /** + * Get mobility of swarm manager + * @return true if mobile, false if not + */ bool isMobile() const { return isMobile_; } /** @@ -94,27 +150,38 @@ public: */ void maintainBuckets(); - bool hasChannel(const NodeId& deviceId); + /** + * Check if we're connected with a specific device + * @param deviceId + * @return true if connected, false if not + */ + bool isConnectedWith(const NodeId& deviceId); + + /** + * Check if swarm manager is shutdown + * @return true if shutdown, false if not + */ + bool isShutdown() { return isShutdown_; }; private: /** * Add node to the known_nodes list - * @param NodeId nodeId + * @param nodeId */ void addKnownNodes(const NodeId& nodeId); /** * Add node to the mobile_Nodes list - * @param NodeId nodeId + * @param nodeId */ void addMobileNodes(const NodeId& nodeId); /** * Send nodes request to fill known_nodes list - * @param shared_ptr<ChannelSocketInterface>& socket - * @param NodeId& nodeId - * @param Query q - * @param int numberNodes + * @param socket + * @param nodeId + * @param q + * @param numberNodes */ void sendRequest(const std::shared_ptr<ChannelSocketInterface>& socket, NodeId& nodeId, @@ -123,22 +190,22 @@ private: /** * Send answer to request - * @param std::shared_ptr<ChannelSocketInterface>& socket - * @param Message msg + * @param socket + * @param msg */ void sendAnswer(const std::shared_ptr<ChannelSocketInterface>& socket, const Message& msg_); /** * Interpret received message - * @param std::shared_ptr<ChannelSocketInterface>& socket + * @param socket */ void receiveMessage(const std::shared_ptr<ChannelSocketInterface>& socket); /** - * Add list of nodes to the known nodes list - * @param asio::error_code& ec - * @param shared_ptr<ChannelSocketInterface>& socket - * @param NodeId node + * Reset node's timer expiry + * @param ec + * @param socket + * @param node */ void resetNodeExpiry(const asio::error_code& ec, const std::shared_ptr<ChannelSocketInterface>& socket, @@ -146,10 +213,14 @@ private: /** * Try to establich connexion with specific node - * @param NodeId nodeId + * @param nodeId */ void tryConnect(const NodeId& nodeId); + /** + * Remove node from routing table + * @param nodeId + */ void removeNodeInternal(const NodeId& nodeId); const NodeId id_; diff --git a/src/manager.cpp b/src/manager.cpp index cf9d3efaf2..279458fccd 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -679,12 +679,7 @@ Manager::ManagerPimpl::bindCallToConference(Call& call, Conference& conf) if (call.isConferenceParticipant()) base_.detachParticipant(callId); - - - JAMI_DEBUG("[call:{}] bind to conference {} (callState={})", - callId, - confId, - state); + JAMI_DEBUG("[call:{}] bind to conference {} (callState={})", callId, confId, state); base_.getRingBufferPool().unBindAll(callId); @@ -702,9 +697,7 @@ Manager::ManagerPimpl::bindCallToConference(Call& call, Conference& conf) conf.bindParticipant(callId); base_.answerCall(call); } else - JAMI_WARNING("[call:{}] call state {} not recognized for conference", - callId, - state); + JAMI_WARNING("[call:{}] call state {} not recognized for conference", callId, state); } //============================================================================== @@ -2918,10 +2911,10 @@ Manager::sendTextMessage(const std::string& accountID, if (pluginChatManager.hasHandlers()) { auto cm = std::make_shared<JamiMessage>(accountID, to, false, payloads, fromPlugin); pluginChatManager.publishMessage(cm); - return acc->sendTextMessage(cm->peerId, cm->data, 0, onlyConnected); + return acc->sendTextMessage(cm->peerId, "", cm->data, 0, onlyConnected); } else #endif // ENABLE_PLUGIN - return acc->sendTextMessage(to, payloads, 0, onlyConnected); + return acc->sendTextMessage(to, "", payloads, 0, onlyConnected); } catch (const std::exception& e) { JAMI_ERR("Exception during text message sending: %s", e.what()); } @@ -3051,10 +3044,9 @@ Manager::createSinkClients( } if (participant.w && participant.h && !participant.videoMuted) { auto currentSink = getSinkClient(sinkId); - if (!accountId.empty() && - currentSink && - string_remove_suffix(participant.uri, '@') == getAccount(accountId)->getUsername() && - participant.device == getAccount<JamiAccount>(accountId)->currentDeviceId()) { + if (!accountId.empty() && currentSink + && string_remove_suffix(participant.uri, '@') == getAccount(accountId)->getUsername() + && participant.device == getAccount<JamiAccount>(accountId)->currentDeviceId()) { // This is a local sink that must already exist continue; } diff --git a/src/manager.h b/src/manager.h index e75ac960ba..735273be6b 100644 --- a/src/manager.h +++ b/src/manager.h @@ -818,11 +818,12 @@ public: * @param videoStream the the VideoFrameActiveWriter to which the sinks should be attached * @param sinksMap A map between sink ids and the respective shared pointer. */ - void createSinkClients(const std::string& callId, - const ConfInfo& infos, - const std::vector<std::shared_ptr<video::VideoFrameActiveWriter>>& videoStreams, - std::map<std::string, std::shared_ptr<video::SinkClient>>& sinksMap, - const std::string& accountId = ""); + void createSinkClients( + const std::string& callId, + const ConfInfo& infos, + const std::vector<std::shared_ptr<video::VideoFrameActiveWriter>>& videoStreams, + std::map<std::string, std::shared_ptr<video::SinkClient>>& sinksMap, + const std::string& accountId = ""); /** * Return an existing SinkClient instance as a shared_ptr associated to the given identifier. @@ -839,7 +840,7 @@ public: AccountFactory accountFactory; std::vector<libjami::Message> getLastMessages(const std::string& accountID, - const uint64_t& base_timestamp); + const uint64_t& base_timestamp); SIPVoIPLink& sipVoIPLink() const; #ifdef ENABLE_PLUGIN diff --git a/src/meson.build b/src/meson.build index 8cee032ff1..a1e503aeba 100644 --- a/src/meson.build +++ b/src/meson.build @@ -55,6 +55,10 @@ libjami_sources = files( 'jamidht/sync_channel_handler.cpp', 'jamidht/sync_module.cpp', 'jamidht/transfer_channel_handler.cpp', + 'jamidht/swarm/routing_table.cpp', + 'jamidht/swarm/swarm_manager.cpp', + 'jamidht/swarm/swarm_protocol.cpp', + 'jamidht/swarm/swarm_channel_handler.cpp', 'media/audio/audio-processing/null_audio_processor.cpp', 'media/audio/sound/audiofile.cpp', 'media/audio/sound/dtmf.cpp', diff --git a/src/sip/sipaccount.cpp b/src/sip/sipaccount.cpp index 38287eaf09..08c005b892 100644 --- a/src/sip/sipaccount.cpp +++ b/src/sip/sipaccount.cpp @@ -268,8 +268,8 @@ SIPAccount::onTransportStateChanged(pjsip_transport_state state, { pj_status_t currentStatus = transportStatus_; JAMI_DEBUG("Transport state changed to {:s} for account {:s}!", - SipTransport::stateToStr(state), - accountID_); + SipTransport::stateToStr(state), + accountID_); if (!SipTransport::isAlive(state)) { if (info) { transportStatus_ = info->status; @@ -301,7 +301,8 @@ SIPAccount::setTransport(const std::shared_ptr<SipTransport>& t) return; if (transport_) { JAMI_DEBUG("Removing old transport [{}] from account", fmt::ptr(transport_.get())); - // NOTE: do not call destroyRegistrationInfo() there as we must call the registration callback if needed + // NOTE: do not call destroyRegistrationInfo() there as we must call the registration + // callback if needed if (regc_) pjsip_regc_release_transport(regc_); transport_->removeStateListener(reinterpret_cast<uintptr_t>(this)); @@ -482,9 +483,10 @@ SIPAccount::mapPortUPnP() or mapRes->getState() == upnp::MappingState::IN_PROGRESS; auto newPort = success ? mapRes->getExternalPort() : accPtr->config().publishedPort; if (not success and not accPtr->isRegistered()) { - JAMI_WARNING("[Account {:s}] Failed to open port {}: registering SIP account anyway", - accPtr->getAccountID(), - oldPort); + JAMI_WARNING( + "[Account {:s}] Failed to open port {}: registering SIP account anyway", + accPtr->getAccountID(), + oldPort); accPtr->doRegister1_(); return; } @@ -492,12 +494,13 @@ SIPAccount::mapPortUPnP() or (accPtr->getRegistrationState() != RegistrationState::REGISTERED)) { if (not accPtr->isRegistered()) JAMI_WARNING("[Account {:s}] SIP port {} opened: registering SIP account", - accPtr->getAccountID(), - newPort); + accPtr->getAccountID(), + newPort); else - JAMI_WARNING("[Account {:s}] SIP port changed to {}: re-registering SIP account", - accPtr->getAccountID(), - newPort); + JAMI_WARNING( + "[Account {:s}] SIP port changed to {}: re-registering SIP account", + accPtr->getAccountID(), + newPort); accPtr->publishedPortUsed_ = newPort; } else { accPtr->connectivityChanged(); @@ -528,7 +531,8 @@ SIPAccount::setPushNotificationToken(const std::string& pushDeviceToken) } bool -SIPAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data) { +SIPAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data) +{ if (SIPAccountBase::setPushNotificationConfig(data)) { if (config().enabled) doUnregister([&](bool /* transport_free */) { doRegister(); }); @@ -1795,6 +1799,7 @@ static pjsip_accept_hdr* im_create_accept(pj_pool_t *pool) void SIPAccount::sendMessage(const std::string& to, + const std::string&, const std::map<std::string, std::string>& payloads, uint64_t id, bool, diff --git a/src/sip/sipaccount.h b/src/sip/sipaccount.h index 4caf32fb18..a29f7db724 100644 --- a/src/sip/sipaccount.h +++ b/src/sip/sipaccount.h @@ -81,17 +81,19 @@ public: ~SIPAccount() noexcept; - const SipAccountConfig& config() const { + const SipAccountConfig& config() const + { return *static_cast<const SipAccountConfig*>(&Account::config()); } - std::unique_ptr<AccountConfig> buildConfig() const override { + std::unique_ptr<AccountConfig> buildConfig() const override + { return std::make_unique<SipAccountConfig>(getAccountID()); } - inline void editConfig(std::function<void(SipAccountConfig& conf)>&& edit) { - Account::editConfig([&](AccountConfig& conf) { - edit(*static_cast<SipAccountConfig*>(&conf)); - }); + inline void editConfig(std::function<void(SipAccountConfig& conf)>&& edit) + { + Account::editConfig( + [&](AccountConfig& conf) { edit(*static_cast<SipAccountConfig*>(&conf)); }); } std::string_view getAccountType() const override { return ACCOUNT_TYPE; } @@ -396,6 +398,7 @@ public: void onRegister(pjsip_regc_cbparam* param); virtual void sendMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t id, bool retryOnTimeout = true, @@ -412,7 +415,10 @@ public: IpAddr createBindingAddress(); void setActiveCodecs(const std::vector<unsigned>& list) override; - bool isSrtpEnabled() const override { return config().srtpKeyExchange != KeyExchangeProtocol::NONE; } + bool isSrtpEnabled() const override + { + return config().srtpKeyExchange != KeyExchangeProtocol::NONE; + } bool setPushNotificationToken(const std::string& pushDeviceToken = "") override; bool setPushNotificationConfig(const std::map<std::string, std::string>& data) override; diff --git a/src/sip/sipaccountbase.h b/src/sip/sipaccountbase.h index d05de5d886..92acd00149 100644 --- a/src/sip/sipaccountbase.h +++ b/src/sip/sipaccountbase.h @@ -175,6 +175,7 @@ public: IceTransportOptions getIceOptions() const noexcept; virtual void sendMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t id, bool retryOnTimeout = true, @@ -182,16 +183,17 @@ public: = 0; virtual uint64_t sendTextMessage(const std::string& to, + const std::string& deviceId, const std::map<std::string, std::string>& payloads, uint64_t refreshToken = 0, bool onlyConnected = false) override { if (onlyConnected) { auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand); - sendMessage(to, payloads, token, false, true); + sendMessage(to, deviceId, payloads, token, false, true); return token; } - return messageEngine_.sendMessage(to, payloads, refreshToken); + return messageEngine_.sendMessage(to, deviceId, payloads, refreshToken); } im::MessageStatus getMessageStatus(uint64_t id) const override diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index 03719f78ae..03e97dcd6e 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -243,4 +243,22 @@ ut_plugins_SOURCES = plugins/plugins.cpp common.cpp check_PROGRAMS += ut_routing_table ut_routing_table_SOURCES = swarm/routing_table.cpp +# +# Bootstrap +# +check_PROGRAMS += ut_bootstrap +ut_bootstrap_SOURCES = swarm/bootstrap.cpp common.cpp + +# +# SwarmConversation +# +check_PROGRAMS += ut_swarm_conversation +ut_swarm_conversation_SOURCES = swarm/swarm_conversation.cpp common.cpp + +# +# SwarmSpreadTest +# +check_PROGRAMS += ut_swarm_spread +ut_swarm_spread_SOURCES = swarm/swarm_spread.cpp common.cpp + TESTS = $(check_PROGRAMS) diff --git a/test/unitTest/actors/account_list.yml b/test/unitTest/actors/account_list.yml new file mode 100644 index 0000000000..be6c387f85 --- /dev/null +++ b/test/unitTest/actors/account_list.yml @@ -0,0 +1,112 @@ +default-account: + type: RING + upnpEnabled: "true" + archivePassword: "" + archivePIN: "" + archivePath: "" + +accounts: + + alice: + displayName: ALICE + alias: ALICE + + bob: + displayName: BOB + alias: BOB + + charlie: + displayName: CHARLIE + alias: CHARLIE + + dave: + displayName: DAVE + alias: DAVE + + emily: + displayName: EMILY + alias: EMILY + + frank: + displayName: FRANK + alias: FRANK + + greg: + displayName: GREG + alias: GREG + + holly: + displayName: HOLLY + alias: HOLLY + + ian: + displayName: IAN + alias: IAN + + jenna: + displayName: JENNA + alias: JENNA + + kevin: + displayName: KEVIN + alias: KEVIN + + lucy: + displayName: LUCY + alias: LUCY + + mike: + displayName: MIKE + alias: MIKE + + nora: + displayName: NORA + alias: NORA + + olivia: + displayName: OLIVIA + alias: OLIVIA + + pete: + displayName: PETE + alias: PETE + + quinn: + displayName: QUINN + alias: QUINN + + rachel: + displayName: RACHEL + alias: RACHEL + + sam: + displayName: SAM + alias: SAM + + tom: + displayName: TOM + alias: TOM + + uma: + displayName: UMA + alias: UMA + + victor: + displayName: VICTOR + alias: VICTOR + + wendy: + displayName: WENDY + alias: WENDY + + xander: + displayName: XANDER + alias: XANDER + + yvonne: + displayName: YVONNE + alias: YVONNE + + zoe: + displayName: ZOE + alias: ZOE \ No newline at end of file diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 4bebdf2d01..a3fe35c2d8 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -1409,8 +1409,8 @@ ConversationTest::testSetMessageDisplayedAfterClone() std::unique_lock<std::mutex> lk {mtx}; std::condition_variable cv; std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; - bool requestReceived = false, memberMessageGenerated = false, - msgDisplayed = false, aliceRegistered = false; + bool requestReceived = false, memberMessageGenerated = false, msgDisplayed = false, + aliceRegistered = false; confHandlers.insert( libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -1504,9 +1504,6 @@ ConversationTest::testSetMessageDisplayedAfterClone() }) != membersInfos.end()); - - - libjami::unregisterSignalHandlers(); } @@ -2010,7 +2007,7 @@ ConversationTest::testVoteNoBadFile() std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; bool conversationReady = false, requestReceived = false, memberMessageGenerated = false, voteMessageGenerated = false, messageBobReceived = false, messageCarlaReceived = false, - carlaConnected = true; + carlaConnected = false; ; confHandlers.insert( libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>( @@ -3535,15 +3532,12 @@ ConversationTest::testConversationPreferencesBeforeClone() aliceAccount->sendTrustRequest(bobUri, {}); CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT( - cv.wait_for(lk, 30s, [&]() { return conversationReadyBob; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReadyBob; })); // Set preferences Manager::instance().sendRegister(aliceId, false); libjami::setConversationPreferences(bobId, convId, {{"foo", "bar"}, {"bar", "foo"}}); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return preferencesBob.size() == 2; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return preferencesBob.size() == 2; })); CPPUNIT_ASSERT(preferencesBob["foo"] == "bar" && preferencesBob["bar"] == "foo"); // Bob2 should sync preferences @@ -3556,7 +3550,9 @@ ConversationTest::testConversationPreferencesBeforeClone() details[ConfProperties::ARCHIVE_PIN] = ""; details[ConfProperties::ARCHIVE_PATH] = bobArchive; bob2Id = Manager::instance().addAccount(details); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bob2Started && conversationReadyBob2 && !preferencesBob2.empty(); })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return bob2Started && conversationReadyBob2 && !preferencesBob2.empty(); + })); CPPUNIT_ASSERT(preferencesBob2["foo"] == "bar" && preferencesBob2["bar"] == "foo"); } diff --git a/test/unitTest/conversation/conversationMembersEvent.cpp b/test/unitTest/conversation/conversationMembersEvent.cpp index 6fe4f3042e..418a864042 100644 --- a/test/unitTest/conversation/conversationMembersEvent.cpp +++ b/test/unitTest/conversation/conversationMembersEvent.cpp @@ -430,6 +430,7 @@ ConversationMembersEventTest::testMemberAddedNoBadFile() generateFakeInvite(aliceAccount, convId, bobUri); // Generate conv request aliceAccount->sendTextMessage(bobUri, + std::string(bobAccount->currentDeviceId()), {{"application/invite+json", "{\"conversationId\":\"" + convId + "\"}"}}); CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); diff --git a/test/unitTest/conversation/conversationRequest.cpp b/test/unitTest/conversation/conversationRequest.cpp index 71d853dbf5..408914e965 100644 --- a/test/unitTest/conversation/conversationRequest.cpp +++ b/test/unitTest/conversation/conversationRequest.cpp @@ -1401,8 +1401,9 @@ ConversationRequestTest::testRemoveContactRemoveTrustRequest() // First, Alice adds Bob aliceAccount->addContact(bobUri); aliceAccount->sendTrustRequest(bobUri, {}); - CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&]() { return !convId.empty(); })); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestB1Received && requestB2Received; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !convId.empty() && requestB1Received && requestB2Received; + })); // Bob1 accepts, both device should get it CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri)); diff --git a/test/unitTest/conversationRepository/conversationRepository.cpp b/test/unitTest/conversationRepository/conversationRepository.cpp index 861c2c6e65..c181dccb15 100644 --- a/test/unitTest/conversationRepository/conversationRepository.cpp +++ b/test/unitTest/conversationRepository/conversationRepository.cpp @@ -52,7 +52,8 @@ public: ConversationRepositoryTest() { // Init daemon - libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); + libjami::init( + libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); if (not Manager::instance().initialized) CPPUNIT_ASSERT(libjami::start("jami-sample.yml")); } @@ -66,16 +67,11 @@ public: private: void testCreateRepository(); - void testCloneViaChannelSocket(); void testAddSomeMessages(); void testLogMessages(); - void testFetch(); void testMerge(); void testFFMerge(); void testDiff(); - // NOTE: Just for debug. the test is a bit complex to write - // due to the clone/fetch verifications (initial commits, size). - // void testCloneHugeRepo(); void testMergeProfileWithConflict(); @@ -90,15 +86,12 @@ private: CPPUNIT_TEST_SUITE(ConversationRepositoryTest); CPPUNIT_TEST(testCreateRepository); - CPPUNIT_TEST(testCloneViaChannelSocket); CPPUNIT_TEST(testAddSomeMessages); CPPUNIT_TEST(testLogMessages); - CPPUNIT_TEST(testFetch); CPPUNIT_TEST(testMerge); CPPUNIT_TEST(testFFMerge); CPPUNIT_TEST(testDiff); CPPUNIT_TEST(testMergeProfileWithConflict); - // CPPUNIT_TEST(testCloneHugeRepo); CPPUNIT_TEST_SUITE_END(); }; @@ -127,7 +120,7 @@ ConversationRepositoryTest::testCreateRepository() auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId())); auto uri = aliceAccount->getUsername(); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + auto repository = ConversationRepository::createConversation(aliceAccount); // Assert that repository exists CPPUNIT_ASSERT(repository != nullptr); @@ -179,135 +172,11 @@ ConversationRepositoryTest::testCreateRepository() CPPUNIT_ASSERT(deviceCrtStr == deviceCert); } -void -ConversationRepositoryTest::testCloneViaChannelSocket() -{ - auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); - auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId())); - auto uri = aliceAccount->getUsername(); - auto bobDeviceId = DeviceId(std::string(bobAccount->currentDeviceId())); - - bobAccount->connectionManager().onICERequest([](const DeviceId&) { return true; }); - aliceAccount->connectionManager().onICERequest([](const DeviceId&) { return true; }); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); - auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository->id(); - auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository->id(); - - std::mutex mtx; - std::unique_lock<std::mutex> lk {mtx}; - std::condition_variable rcv, scv; - bool successfullyConnected = false; - bool successfullyReceive = false; - bool receiverConnected = false; - std::shared_ptr<ChannelSocket> channelSocket = nullptr; - std::shared_ptr<ChannelSocket> sendSocket = nullptr; - - bobAccount->connectionManager().onChannelRequest( - [&successfullyReceive](const std::shared_ptr<dht::crypto::Certificate>&, - const std::string& name) { - successfullyReceive = name == "git://*"; - return true; - }); - - aliceAccount->connectionManager().onChannelRequest( - [&successfullyReceive](const std::shared_ptr<dht::crypto::Certificate>&, - const std::string&) { return true; }); - - bobAccount->connectionManager().onConnectionReady( - [&](const DeviceId&, const std::string& name, std::shared_ptr<ChannelSocket> socket) { - receiverConnected = socket && (name == "git://*"); - channelSocket = socket; - rcv.notify_one(); - }); - - aliceAccount->connectionManager().connectDevice(bobDeviceId, - "git://*", - [&](std::shared_ptr<ChannelSocket> socket, - const DeviceId&) { - if (socket) { - successfullyConnected = true; - sendSocket = socket; - } - scv.notify_one(); - }); - - rcv.wait_for(lk, std::chrono::seconds(10)); - scv.wait_for(lk, std::chrono::seconds(10)); - CPPUNIT_ASSERT(successfullyReceive); - CPPUNIT_ASSERT(successfullyConnected); - CPPUNIT_ASSERT(receiverConnected); - - bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket); - GitServer gs(aliceId, repository->id(), sendSocket); - - auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(), - aliceDeviceId.toString(), - repository->id()); - gs.stop(); - - CPPUNIT_ASSERT(cloned != nullptr); - CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath)); - - // Assert that first commit is signed by alice - git_repository* repo; - CPPUNIT_ASSERT(git_repository_open(&repo, clonedPath.c_str()) == 0); - - // 1. Verify that last commit is correctly signed by alice - git_oid commit_id; - CPPUNIT_ASSERT(git_reference_name_to_id(&commit_id, repo, "HEAD") == 0); - - git_buf signature = {}, signed_data = {}; - git_commit_extract_signature(&signature, &signed_data, repo, &commit_id, "signature"); - auto pk = base64::decode(std::string(signature.ptr, signature.ptr + signature.size)); - auto data = std::vector<uint8_t>(signed_data.ptr, signed_data.ptr + signed_data.size); - git_repository_free(repo); - - CPPUNIT_ASSERT(aliceAccount->identity().second->getPublicKey().checkSignature(data, pk)); - - // 2. Check created files - auto CRLsPath = clonedPath + DIR_SEPARATOR_STR + "CRLs" + DIR_SEPARATOR_STR - + aliceDeviceId.toString(); - CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath)); - - auto adminCrt = clonedPath + DIR_SEPARATOR_STR + "admins" + DIR_SEPARATOR_STR + uri + ".crt"; - CPPUNIT_ASSERT(fileutils::isFile(adminCrt)); - - auto crt = std::ifstream(adminCrt); - std::string adminCrtStr((std::istreambuf_iterator<char>(crt)), std::istreambuf_iterator<char>()); - - auto cert = aliceAccount->identity().second; - auto deviceCert = cert->toString(false); - auto parentCert = cert->issuer->toString(true); - - CPPUNIT_ASSERT(adminCrtStr == parentCert); - - auto deviceCrt = clonedPath + DIR_SEPARATOR_STR + "devices" + DIR_SEPARATOR_STR - + aliceDeviceId.toString() + ".crt"; - CPPUNIT_ASSERT(fileutils::isFile(deviceCrt)); - - crt = std::ifstream(deviceCrt); - std::string deviceCrtStr((std::istreambuf_iterator<char>(crt)), - std::istreambuf_iterator<char>()); - - CPPUNIT_ASSERT(deviceCrtStr == deviceCert); - - // Check cloned messages - auto messages = cloned->log(); - CPPUNIT_ASSERT(messages.size() == 1); - CPPUNIT_ASSERT(messages[0].id == repository->id()); - CPPUNIT_ASSERT( - aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content, - messages[0].signature)); -} - void ConversationRepositoryTest::testAddSomeMessages() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + auto repository = ConversationRepository::createConversation(aliceAccount); auto id1 = repository->commitMessage("Commit 1"); auto id2 = repository->commitMessage("Commit 2"); @@ -346,7 +215,7 @@ void ConversationRepositoryTest::testLogMessages() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + auto repository = ConversationRepository::createConversation(aliceAccount); auto id1 = repository->commitMessage("Commit 1"); auto id2 = repository->commitMessage("Commit 2"); @@ -371,135 +240,6 @@ ConversationRepositoryTest::testLogMessages() CPPUNIT_ASSERT(messages[0].id == repository->id()); } -void -ConversationRepositoryTest::testFetch() -{ - auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); - auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId())); - auto bobDeviceId = DeviceId(std::string(bobAccount->currentDeviceId())); - - bobAccount->connectionManager().onICERequest([](const DeviceId&) { return true; }); - aliceAccount->connectionManager().onICERequest([](const DeviceId&) { return true; }); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); - auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository->id(); - auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository->id(); - - std::mutex mtx; - std::unique_lock<std::mutex> lk {mtx}; - std::condition_variable rcv, scv, ccv; - bool successfullyConnected = false; - bool successfullyReceive = false; - bool receiverConnected = false; - std::shared_ptr<ChannelSocket> channelSocket = nullptr; - std::shared_ptr<ChannelSocket> sendSocket = nullptr; - - bobAccount->connectionManager().onChannelRequest( - [&](const std::shared_ptr<dht::crypto::Certificate>&, const std::string& name) { - successfullyReceive = name == "git://*"; - ccv.notify_one(); - return true; - }); - - aliceAccount->connectionManager().onChannelRequest( - [&](const std::shared_ptr<dht::crypto::Certificate>&, const std::string&) { return true; }); - - bobAccount->connectionManager().onConnectionReady( - [&](const DeviceId&, const std::string& name, std::shared_ptr<ChannelSocket> socket) { - receiverConnected = socket && (name == "git://*"); - channelSocket = socket; - rcv.notify_one(); - }); - - aliceAccount->connectionManager().connectDevice(bobDeviceId, - "git://*", - [&](std::shared_ptr<ChannelSocket> socket, - const DeviceId&) { - if (socket) { - successfullyConnected = true; - sendSocket = socket; - } - scv.notify_one(); - }); - - rcv.wait_for(lk, std::chrono::seconds(10)); - scv.wait_for(lk, std::chrono::seconds(10)); - CPPUNIT_ASSERT(successfullyReceive); - CPPUNIT_ASSERT(successfullyConnected); - CPPUNIT_ASSERT(receiverConnected); - CPPUNIT_ASSERT(repository != nullptr); - - bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket); - GitServer gs(aliceId, repository->id(), sendSocket); - - // Clone repository - auto id1 = repository->commitMessage("Commit 1"); - - auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(), - aliceDeviceId.toString(), - repository->id()); - gs.stop(); - bobAccount->removeGitSocket(aliceDeviceId, repository->id()); - - // Add some new messages to fetch - auto id2 = repository->commitMessage("Commit 2"); - auto id3 = repository->commitMessage("Commit 3"); - - // Open a new channel to simulate the fact that we are later - aliceAccount->connectionManager().connectDevice(bobDeviceId, - "git://*", - [&](std::shared_ptr<ChannelSocket> socket, - const DeviceId&) { - if (socket) { - successfullyConnected = true; - sendSocket = socket; - } - scv.notify_one(); - }); - - rcv.wait_for(lk, std::chrono::seconds(10)); - scv.wait_for(lk, std::chrono::seconds(10)); - ccv.wait_for(lk, std::chrono::seconds(10)); - bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket); - GitServer gs2(aliceId, repository->id(), sendSocket); - - CPPUNIT_ASSERT(cloned->fetch(aliceDeviceId.toString())); - CPPUNIT_ASSERT(id3 == cloned->remoteHead(aliceDeviceId.toString())); - - gs2.stop(); - bobAccount->removeGitSocket(aliceDeviceId, repository->id()); - - auto messages = cloned->log({id3}); - CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */); - CPPUNIT_ASSERT(messages[0].id == id3); - CPPUNIT_ASSERT(messages[0].parents.front() == id2); - CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3"); - CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name); - CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email); - CPPUNIT_ASSERT(messages[1].id == id2); - CPPUNIT_ASSERT(messages[1].parents.front() == id1); - CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2"); - CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name); - CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email); - CPPUNIT_ASSERT(messages[2].id == id1); - CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1"); - CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name); - CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email); - CPPUNIT_ASSERT(messages[2].parents.front() == repository->id()); - // Check sig - CPPUNIT_ASSERT( - aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content, - messages[0].signature)); - CPPUNIT_ASSERT( - aliceAccount->identity().second->getPublicKey().checkSignature(messages[1].signed_content, - messages[1].signature)); - CPPUNIT_ASSERT( - aliceAccount->identity().second->getPublicKey().checkSignature(messages[2].signed_content, - messages[2].signature)); -} - std::string ConversationRepositoryTest::addCommit(git_repository* repo, const std::shared_ptr<JamiAccount> account, @@ -617,7 +357,7 @@ void ConversationRepositoryTest::testMerge() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + auto repository = ConversationRepository::createConversation(aliceAccount); // Assert that repository exists CPPUNIT_ASSERT(repository != nullptr); @@ -652,7 +392,7 @@ void ConversationRepositoryTest::testFFMerge() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + auto repository = ConversationRepository::createConversation(aliceAccount); // Assert that repository exists CPPUNIT_ASSERT(repository != nullptr); @@ -689,7 +429,7 @@ ConversationRepositoryTest::testDiff() auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId())); auto uri = aliceAccount->getUsername(); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + auto repository = ConversationRepository::createConversation(aliceAccount); auto id1 = repository->commitMessage("Commit 1"); auto id2 = repository->commitMessage("Commit 2"); @@ -708,7 +448,7 @@ void ConversationRepositoryTest::testMergeProfileWithConflict() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + auto repository = ConversationRepository::createConversation(aliceAccount); // Assert that repository exists CPPUNIT_ASSERT(repository != nullptr); @@ -763,89 +503,6 @@ ConversationRepositoryTest::testMergeProfileWithConflict() CPPUNIT_ASSERT(repository->log().size() == 5 /* Initial, add, modify 1, modify 2, merge */); } -/* -void -ConversationRepositoryTest::testCloneHugeRepo() -{ - auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); - auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId())); - auto uri = aliceAccount->getUsername(); - auto bobDeviceId = DeviceId(std::string(bobAccount->currentDeviceId())); - - bobAccount->connectionManager().onICERequest([](const DeviceId&) { return true; }); - aliceAccount->connectionManager().onICERequest([](const DeviceId&) { return true; }); - - auto convPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() - + DIR_SEPARATOR_STR + "conversations"; - fileutils::recursive_mkdir(convPath); - const auto copyOptions = std::filesystem::copy_options::overwrite_existing | -std::filesystem::copy_options::recursive; auto repoPath = convPath + DIR_SEPARATOR_STR + -"8d3be095ebff73be1c43f193d02407b946d7895d"; std::filesystem::copy("/home/amarok/daemon/", repoPath, -copyOptions); - - auto repository = ConversationRepository(aliceAccount->weak(), -"8d3be095ebff73be1c43f193d02407b946d7895d"); auto clonedPath = fileutils::get_data_dir() + -DIR_SEPARATOR_STR + bobAccount->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository.id(); - - std::mutex mtx; - std::unique_lock<std::mutex> lk {mtx}; - std::condition_variable rcv, scv; - bool successfullyConnected = false; - bool successfullyReceive = false; - bool receiverConnected = false; - std::shared_ptr<ChannelSocket> channelSocket = nullptr; - std::shared_ptr<ChannelSocket> sendSocket = nullptr; - - bobAccount->connectionManager().onChannelRequest( - [&successfullyReceive](const std::shared_ptr<dht::crypto::Certificate>&, const std::string& -name) { successfullyReceive = name == "git://*"; return true; - }); - - aliceAccount->connectionManager().onChannelRequest( - [&successfullyReceive](const std::shared_ptr<dht::crypto::Certificate>&, const std::string& -name) { return true; }); - - bobAccount->connectionManager().onConnectionReady( - [&](const DeviceId&, const std::string& name, std::shared_ptr<ChannelSocket> socket) { - receiverConnected = socket && (name == "git://*"); - channelSocket = socket; - rcv.notify_one(); - }); - - aliceAccount->connectionManager().connectDevice(bobDeviceId, - "git://*", - [&](std::shared_ptr<ChannelSocket> socket, - const DeviceId&) { - if (socket) { - successfullyConnected = true; - sendSocket = socket; - } - scv.notify_one(); - }); - - ; - scv.wait_for(lk, std::chrono::seconds(10)); - CPPUNIT_ASSERT(rcv.wait_for(lk, std::chrono::seconds(10), [&] { return receiverConnected; })); - CPPUNIT_ASSERT(scv.wait_for(lk, std::chrono::seconds(10), [&] { return successfullyConnected; -})); CPPUNIT_ASSERT(successfullyReceive); - - bobAccount->addGitSocket(aliceDeviceId, repository.id(), channelSocket); - GitServer gs(aliceId, repository.id(), sendSocket); - - JAMI_ERR("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(), - aliceDeviceId, - repository.id()); - gs.stop(); - JAMI_ERR("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ END CLONE"); - - CPPUNIT_ASSERT(cloned != nullptr); - CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath)); -} -*/ - } // namespace test } // namespace jami diff --git a/test/unitTest/fileTransfer/fileTransfer.cpp b/test/unitTest/fileTransfer/fileTransfer.cpp index 2f4b08bd9b..e8cd6c1500 100644 --- a/test/unitTest/fileTransfer/fileTransfer.cpp +++ b/test/unitTest/fileTransfer/fileTransfer.cpp @@ -37,6 +37,7 @@ using namespace std::literals::chrono_literals; using namespace libjami::Account; +using namespace std::literals::chrono_literals; namespace jami { namespace test { @@ -218,9 +219,7 @@ FileTransferTest::testConversationFileTransfer() libjami::acceptConversationRequest(bobId, convId); libjami::acceptConversationRequest(carlaId, convId); - cv.wait_for(lk, 30s, [&]() { - return conversationReady == 3 && memberJoined == 2; - }); + cv.wait_for(lk, 30s, [&]() { return conversationReady == 3 && memberJoined == 2; }); // Send file std::ofstream sendFile(sendPath); @@ -230,15 +229,12 @@ FileTransferTest::testConversationFileTransfer() libjami::sendFile(aliceId, convId, sendPath, "SEND", ""); - CPPUNIT_ASSERT(cv.wait_for(lk, 45s, [&]() { - return !tidBob.empty() && !tidCarla.empty(); - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 45s, [&]() { return !tidBob.empty() && !tidCarla.empty(); })); libjami::downloadFile(bobId, convId, iidBob, tidBob, recvPath); libjami::downloadFile(carlaId, convId, iidCarla, tidCarla, recv2Path); - CPPUNIT_ASSERT( - cv.wait_for(lk, 45s, [&]() { return finished.size() == 3; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 45s, [&]() { return finished.size() == 3; })); libjami::unregisterSignalHandlers(); } @@ -313,9 +309,7 @@ FileTransferTest::testFileTransferInConversation() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return conversationReady && bobJoined; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady && bobJoined; })); // Create file to send std::ofstream sendFile(sendPath); @@ -330,9 +324,7 @@ FileTransferTest::testFileTransferInConversation() transferAFinished = false; transferBFinished = false; libjami::downloadFile(bobId, convId, iidBob, tidBob, recvPath); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return transferAFinished && transferBFinished; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return transferAFinished && transferBFinished; })); libjami::unregisterSignalHandlers(); std::this_thread::sleep_for(5s); @@ -408,9 +400,7 @@ FileTransferTest::testVcfFileTransferInConversation() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return conversationReady && bobJoined; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady && bobJoined; })); // Create file to send std::ofstream sendFile(sendPath); @@ -425,9 +415,7 @@ FileTransferTest::testVcfFileTransferInConversation() transferAFinished = false; transferBFinished = false; libjami::downloadFile(bobId, convId, iidBob, tidBob, recvPath); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return transferAFinished && transferBFinished; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return transferAFinished && transferBFinished; })); libjami::unregisterSignalHandlers(); std::this_thread::sleep_for(5s); @@ -512,9 +500,7 @@ FileTransferTest::testBadSha3sumOut() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return conversationReady && memberJoin; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady && memberJoin; })); // Create file to send std::ofstream sendFile(sendPath); @@ -537,9 +523,7 @@ FileTransferTest::testBadSha3sumOut() libjami::downloadFile(bobId, convId, iid, mid, recvPath); // The file transfer will not be sent as modified - CPPUNIT_ASSERT(!cv.wait_for(lk, 30s, [&]() { - return transferAFinished || transferBFinished; - })); + CPPUNIT_ASSERT(!cv.wait_for(lk, 30s, [&]() { return transferAFinished || transferBFinished; })); libjami::unregisterSignalHandlers(); } @@ -623,9 +607,7 @@ FileTransferTest::testBadSha3sumIn() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { - return conversationReady && memberJoin; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return conversationReady && memberJoin; })); // Create file to send std::ofstream sendFile(sendPath); @@ -738,23 +720,18 @@ FileTransferTest::testAskToMultipleParticipants() libjami::addConversationMember(aliceId, convId, bobUri); CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); + requestReceived = false; + libjami::addConversationMember(aliceId, convId, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { - return conversationReady && memberJoin; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return conversationReady && memberJoin; })); - requestReceived = false; conversationReady = false; memberJoin = false; - libjami::addConversationMember(aliceId, convId, carlaUri); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); - libjami::acceptConversationRequest(carlaId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { - return conversationReady && memberJoin; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return conversationReady && memberJoin; })); // Create file to send std::ofstream sendFile(sendPath); @@ -764,9 +741,7 @@ FileTransferTest::testAskToMultipleParticipants() libjami::sendFile(aliceId, convId, sendPath, "SEND", ""); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return !bobTid.empty() && !carlaTid.empty(); - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return !bobTid.empty() && !carlaTid.empty(); })); transferCFinished = false; libjami::downloadFile(carlaId, convId, iidCarla, carlaTid, recv2Path); @@ -853,9 +828,7 @@ FileTransferTest::testCancelInTransfer() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return conversationReady && bobJoined; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady && bobJoined; })); // Create file to send std::ofstream sendFile(sendPath); @@ -949,9 +922,7 @@ FileTransferTest::testTransferInfo() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); libjami::acceptConversationRequest(bobId, convId); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return conversationReady && bobJoined; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady && bobJoined; })); // Create file to send std::ofstream sendFile(sendPath); @@ -973,9 +944,7 @@ FileTransferTest::testTransferInfo() transferAFinished = false; transferBFinished = false; libjami::downloadFile(bobId, convId, iidBob, tidBob, recvPath); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return transferAFinished && transferBFinished; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return transferAFinished && transferBFinished; })); CPPUNIT_ASSERT(libjami::fileTransferInfo(bobId, convId, tidBob, path, totalSize, bytesProgress) == libjami::DataTransferError::success); @@ -1019,17 +988,16 @@ FileTransferTest::testRemoveHardLink() conversationReady = true; })); auto conversationRemoved = false; - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationRemoved>( - [&](const std::string& /*accountId*/, const std::string& /* conversationId */) { - conversationRemoved = true; - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::ConversationRemoved>( + [&](const std::string& /*accountId*/, const std::string& /* conversationId */) { + conversationRemoved = true; + })); libjami::registerSignalHandlers(confHandlers); auto convId = libjami::startConversation(aliceId); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return conversationReady; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationReady; })); // Send file std::ofstream sendFile(sendPath); @@ -1040,15 +1008,11 @@ FileTransferTest::testRemoveHardLink() libjami::sendFile(aliceId, convId, sendPath, std::filesystem::absolute("SEND"), ""); messageReceived = false; - CPPUNIT_ASSERT(cv.wait_for(lk, 45s, [&]() { - return messageReceived; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 45s, [&]() { return messageReceived; })); CPPUNIT_ASSERT(libjami::removeConversation(aliceId, convId)); - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { - return conversationRemoved; - })); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationRemoved; })); auto content = fileutils::loadTextFile(sendPath); CPPUNIT_ASSERT(content.find("AAA") != std::string::npos); diff --git a/test/unitTest/swarm/bootstrap.cpp b/test/unitTest/swarm/bootstrap.cpp new file mode 100644 index 0000000000..3c5c9b65e8 --- /dev/null +++ b/test/unitTest/swarm/bootstrap.cpp @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <condition_variable> +#include <msgpack.hpp> +#include <filesystem> + +#include "../../test_runner.h" +#include "account_const.h" +#include "common.h" +#include "conversation_interface.h" +#include "fileutils.h" +#include "jami.h" +#include "jamidht/conversation.h" +#include "jamidht/jamiaccount.h" +#include "jamidht/swarm/swarm_channel_handler.h" +#include "manager.h" + +using namespace std::string_literals; +using namespace std::literals::chrono_literals; +using namespace libjami::Account; + +namespace jami { + +struct ConvInfoTest +{ + std::string accountId; + std::string convId; + bool requestReceived = false; + bool conversationReady = false; + std::vector<std::map<std::string, std::string>> messages; + Conversation::BootstrapStatus bootstrap {Conversation::BootstrapStatus::FAILED}; +}; + +namespace test { + +class BootstrapTest : public CppUnit::TestFixture +{ +public: + ~BootstrapTest() { libjami::fini(); } + static std::string name() { return "Bootstrap"; } + void setUp(); + void tearDown(); + + ConvInfoTest aliceData; + ConvInfoTest bobData; + ConvInfoTest bob2Data; + ConvInfoTest carlaData; + + std::mutex mtx; + std::condition_variable cv; + +private: + void connectSignals(); + + void testBootstrapOk(); + void testBootstrapFailed(); + void testBootstrapNeverNewDevice(); + void testBootstrapCompat(); + + CPPUNIT_TEST_SUITE(BootstrapTest); + CPPUNIT_TEST(testBootstrapOk); + CPPUNIT_TEST(testBootstrapFailed); + CPPUNIT_TEST(testBootstrapNeverNewDevice); + CPPUNIT_TEST(testBootstrapCompat); + CPPUNIT_TEST_SUITE_END(); +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(BootstrapTest, BootstrapTest::name()); + +void +BootstrapTest::setUp() +{ + aliceData = {}; + bobData = {}; + bob2Data = {}; + carlaData = {}; + + // Init daemon + libjami::init( + libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(libjami::start("jami-sample.yml")); + + auto actors = load_actors("actors/alice-bob-carla.yml"); + aliceData.accountId = actors["alice"]; + bobData.accountId = actors["bob"]; + carlaData.accountId = actors["carla"]; + + Manager::instance().sendRegister(carlaData.accountId, false); + wait_for_announcement_of({aliceData.accountId, bobData.accountId}); + connectSignals(); +} + +void +BootstrapTest::tearDown() +{ + libjami::unregisterSignalHandlers(); + auto bobArchive = std::filesystem::current_path().string() + "/bob.gz"; + std::remove(bobArchive.c_str()); + + if (bob2Data.accountId.empty()) { + wait_for_removal_of({aliceData.accountId, bobData.accountId, carlaData.accountId}); + } else { + wait_for_removal_of( + {aliceData.accountId, bobData.accountId, carlaData.accountId, bob2Data.accountId}); + } +} + +void +BootstrapTest::connectSignals() +{ + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& convId) { + if (accountId == aliceData.accountId) { + aliceData.convId = convId; + aliceData.conversationReady = true; + } else if (accountId == bobData.accountId) { + bobData.convId = convId; + bobData.conversationReady = true; + } else if (accountId == bob2Data.accountId) { + bob2Data.convId = convId; + bob2Data.conversationReady = true; + } else if (accountId == carlaData.accountId) { + carlaData.convId = convId; + carlaData.conversationReady = true; + } + cv.notify_one(); + })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + if (accountId == aliceData.accountId) { + aliceData.requestReceived = true; + } else if (accountId == bobData.accountId) { + bobData.requestReceived = true; + } else if (accountId == bob2Data.accountId) { + bob2Data.requestReceived = true; + } else if (accountId == carlaData.accountId) { + carlaData.requestReceived = true; + } + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /*conversationId*/, + std::map<std::string, std::string> message) { + if (accountId == aliceData.accountId) { + aliceData.messages.emplace_back(message); + } else if (accountId == bobData.accountId) { + bobData.messages.emplace_back(message); + } else if (accountId == bob2Data.accountId) { + bob2Data.messages.emplace_back(message); + } else if (accountId == carlaData.accountId) { + carlaData.messages.emplace_back(message); + } + cv.notify_one(); + })); + libjami::registerSignalHandlers(confHandlers); + + // Link callback for convModule() + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaData.accountId); + + aliceAccount->convModule()->onBootstrapStatus( + [&](std::string /*convId*/, Conversation::BootstrapStatus status) { + aliceData.bootstrap = status; + cv.notify_one(); + }); + bobAccount->convModule()->onBootstrapStatus( + [&](std::string /*convId*/, Conversation::BootstrapStatus status) { + bobData.bootstrap = status; + cv.notify_one(); + }); + carlaAccount->convModule()->onBootstrapStatus( + [&](std::string /*convId*/, Conversation::BootstrapStatus status) { + carlaData.bootstrap = status; + cv.notify_one(); + }); +} + +void +BootstrapTest::testBootstrapOk() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId); + auto bobUri = bobAccount->getUsername(); + + aliceAccount->convModule()->onBootstrapStatus( + [&](std::string /*convId*/, Conversation::BootstrapStatus status) { + aliceData.bootstrap = status; + cv.notify_one(); + }); + + std::unique_lock<std::mutex> lk {mtx}; + auto convId = libjami::startConversation(aliceData.accountId); + + libjami::addConversationMember(aliceData.accountId, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; })); + + auto aliceMsgSize = aliceData.messages.size(); + libjami::acceptConversationRequest(bobData.accountId, convId); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1 + && aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS + && bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS; + })); +} + +void +BootstrapTest::testBootstrapFailed() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId); + auto bobUri = bobAccount->getUsername(); + + aliceAccount->convModule()->onBootstrapStatus( + [&](std::string /*convId*/, Conversation::BootstrapStatus status) { + aliceData.bootstrap = status; + cv.notify_one(); + }); + + std::unique_lock<std::mutex> lk {mtx}; + auto convId = libjami::startConversation(aliceData.accountId); + + libjami::addConversationMember(aliceData.accountId, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; })); + + auto aliceMsgSize = aliceData.messages.size(); + libjami::acceptConversationRequest(bobData.accountId, convId); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1 + && aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS + && bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS; + })); + + // Now bob goes offline, it should disconnect alice + Manager::instance().sendRegister(bobData.accountId, false); + // Alice will try to maintain before failing (so will take 30secs to fail) + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return aliceData.bootstrap == Conversation::BootstrapStatus::FAILED; + })); +} + +void +BootstrapTest::testBootstrapNeverNewDevice() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId); + auto bobUri = bobAccount->getUsername(); + + aliceAccount->convModule()->onBootstrapStatus( + [&](std::string /*convId*/, Conversation::BootstrapStatus status) { + aliceData.bootstrap = status; + cv.notify_one(); + }); + + std::unique_lock<std::mutex> lk {mtx}; + auto convId = libjami::startConversation(aliceData.accountId); + + libjami::addConversationMember(aliceData.accountId, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; })); + + auto aliceMsgSize = aliceData.messages.size(); + libjami::acceptConversationRequest(bobData.accountId, convId); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1 + && aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS + && bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS; + })); + + // Alice offline + Manager::instance().sendRegister(aliceData.accountId, false); + // Bob will try to maintain before failing (so will take 30secs to fail) + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return bobData.bootstrap == Conversation::BootstrapStatus::FAILED; + })); + + // Create bob2 + auto bobArchive = std::filesystem::current_path().string() + "/bob.gz"; + std::remove(bobArchive.c_str()); + bobAccount->exportArchive(bobArchive); + std::map<std::string, std::string> details = libjami::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "BOB2"; + details[ConfProperties::ALIAS] = "BOB2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = bobArchive; + bob2Data.accountId = Manager::instance().addAccount(details); + auto bob2Account = Manager::instance().getAccount<JamiAccount>(bob2Data.accountId); + + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + bool bob2Connected = false; + confHandlers.insert( + libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = bob2Account->getVolatileAccountDetails(); + auto daemonStatus = details[libjami::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus != "UNREGISTERED") + bob2Connected = true; + cv.notify_one(); + })); + libjami::registerSignalHandlers(confHandlers); + + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bob2Connected; })); + bob2Account->convModule()->onBootstrapStatus( + [&](std::string /*convId*/, Conversation::BootstrapStatus status) { + bob2Data.bootstrap = status; + cv.notify_one(); + }); + + // Disconnect bob2, to create a valid conv betwen Alice and Bob1 + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return bobData.bootstrap == Conversation::BootstrapStatus::SUCCESS + && bob2Data.bootstrap == Conversation::BootstrapStatus::SUCCESS; + })); + + // Bob offline + Manager::instance().sendRegister(bobData.accountId, false); + // Bob2 will try to maintain before failing (so will take 30secs to fail) + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return bob2Data.bootstrap == Conversation::BootstrapStatus::FAILED; + })); + + // Alice bootstrap should go to fallback (because bob2 never wrote into the conversation) & Connected + Manager::instance().sendRegister(aliceData.accountId, true); + // Wait for announcement, ICE fallback + delay + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return aliceData.bootstrap == Conversation::BootstrapStatus::SUCCESS; + })); +} + +void +BootstrapTest::testBootstrapCompat() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceData.accountId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobData.accountId); + auto bobUri = bobAccount->getUsername(); + + dynamic_cast<SwarmChannelHandler*>(aliceAccount->channelHandlers()[Uri::Scheme::SWARM].get()) + ->disableSwarmManager + = true; + + std::unique_lock<std::mutex> lk {mtx}; + auto convId = libjami::startConversation(aliceData.accountId); + + libjami::addConversationMember(aliceData.accountId, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData.requestReceived; })); + + auto aliceMsgSize = aliceData.messages.size(); + libjami::acceptConversationRequest(bobData.accountId, convId); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return bobData.conversationReady && aliceData.messages.size() == aliceMsgSize + 1; + })); + + auto bobMsgSize = bobData.messages.size(); + libjami::sendMessage(aliceData.accountId, convId, "hi"s, ""); + cv.wait_for(lk, 30s, [&]() { + return bobData.messages.size() == bobMsgSize + 1 + && bobData.bootstrap == Conversation::BootstrapStatus::FAILED; + }); +} + +} // namespace test +} // namespace jami + +RING_TEST_RUNNER(jami::test::BootstrapTest::name()) diff --git a/test/unitTest/swarm/routing_table.cpp b/test/unitTest/swarm/routing_table.cpp index f031fe7f1a..e67c5c44e1 100644 --- a/test/unitTest/swarm/routing_table.cpp +++ b/test/unitTest/swarm/routing_table.cpp @@ -1297,7 +1297,7 @@ RoutingTableTest::testSwarmManagersWMobileModes() } } - sleep(5); + sleep(10); { if (!swarmManagers.empty()) { @@ -1306,7 +1306,7 @@ RoutingTableTest::testSwarmManagersWMobileModes() } } - sleep(4); + sleep(10); CPPUNIT_ASSERT_EQUAL_MESSAGE("Supposed to be equal", swarmManagers.size(), diff --git a/test/unitTest/swarm/swarm_conversation.cpp b/test/unitTest/swarm/swarm_conversation.cpp new file mode 100644 index 0000000000..c94dac82db --- /dev/null +++ b/test/unitTest/swarm/swarm_conversation.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * Author: Fadi Shehadeh <fadi.shehadeh@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <condition_variable> +#include <filesystem> +#include <string> + +#include "jami.h" +#include "../common.h" +#include "jamidht/swarm/swarm_manager.h" +#include "connectivity/multiplexed_socket.h" + +#include "connectivity/peer_connection.h" +#include <opendht/thread_pool.h> + +#include "../../test_runner.h" +#include "account_const.h" +#include "conversation/conversationcommon.h" +#include "manager.h" + +using namespace dht; +using NodeId = dht::PkId; +using namespace std::literals::chrono_literals; + +namespace jami { +namespace test { + +struct ConvData +{ + std::string id {}; + bool requestReceived {false}; + bool needsHost {false}; + bool conferenceChanged {false}; + bool conferenceRemoved {false}; + std::string hostState {}; + std::vector<std::map<std::string, std::string>> messages {}; +}; + +class SwarmConversationTest : public CppUnit::TestFixture +{ +public: + ~SwarmConversationTest(); + static std::string name() { return "SwarmConversationTest"; } + void setUp(); + + std::map<std::string, ConvData> accountMap; + std::vector<std::string> accountIds; + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + +private: + void connectSignals(); + + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + + void testSendMessage(); + + CPPUNIT_TEST_SUITE(SwarmConversationTest); + CPPUNIT_TEST(testSendMessage); + CPPUNIT_TEST_SUITE_END(); +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(SwarmConversationTest, SwarmConversationTest::name()); + +void +SwarmConversationTest::setUp() +{ + libjami::init( + libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(libjami::start("jami-sample.yml")); + + auto actors = load_actors("actors/account_list.yml"); + + for (const auto& act : actors) { + auto id = act.second; + accountIds.emplace_back(id); + std::cout << act.second << std::endl; + accountMap.insert({id, {}}); + } + + wait_for_announcement_of(accountIds, 60s); +} + +SwarmConversationTest::~SwarmConversationTest() +{ + wait_for_removal_of(accountIds); + libjami::fini(); +} + +void +SwarmConversationTest::connectSignals() +{ + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + for (const auto& accId : accountIds) { + if (accountId == accId) { + accountMap[accId].id = conversationId; + } + } + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + for (const auto& accId : accountIds) { + if (accountId == accId && accountMap[accId].id == conversationId) { + accountMap[accId].messages.emplace_back(message); + } + } + cv.notify_one(); + })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& accountId, + const std::string&, + std::map<std::string, std::string>) { + for (const auto& accId : accountIds) { + if (accountId == accId) { + accountMap[accId].requestReceived = true; + } + } + cv.notify_one(); + })); + + libjami::registerSignalHandlers(confHandlers); +} + +void +SwarmConversationTest::testSendMessage() +{ + std::map<std::string, std::shared_ptr<JamiAccount>> jamiAccounts; + + for (const auto& accId : accountIds) { + jamiAccounts.insert({accId, Manager::instance().getAccount<JamiAccount>(accId)}); + std::cout << "created account for: " << accId << std::endl; + } + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + + connectSignals(); + + auto aliceId = jamiAccounts.begin()->first; + auto convId = libjami::startConversation(aliceId); + + std::cout << "started conversation: " << convId << std::endl; + + for (auto it = std::next(jamiAccounts.begin()); it != jamiAccounts.end(); ++it) { + auto userUri = it->second->getUsername(); + std::cout << "adding member: " << userUri << std::endl; + libjami::addConversationMember(aliceId, convId, userUri); + + CPPUNIT_ASSERT_EQUAL_MESSAGE("ERROR", true, cv.wait_for(lk, 40s, [&]() { + return accountMap[it->first].requestReceived == true; + })); + + libjami::acceptConversationRequest(it->first, convId); + std::this_thread::sleep_for(std::chrono::seconds(10)); + } + + std::cout << "waiting for conversation ready" << std::endl; + for (size_t i = 1; i < accountIds.size(); i++) { + CPPUNIT_ASSERT( + cv.wait_for(lk, 40s, [&]() { return accountMap[accountIds.at(i)].id == convId; })); + } + std::cout << "messages size " << accountMap[accountIds.at(0)].messages.size() << std::endl; + + CPPUNIT_ASSERT( + cv.wait_for(lk, 70s, [&]() { return accountMap[accountIds.at(0)].messages.size() >= 2; })); + + libjami::sendMessage(aliceId, convId, "hi"s, ""); + + for (size_t i = 1; i < accountIds.size(); i++) { + std::cout << "COUNTER: " << i << " messages size " + << accountMap[accountIds.at(i)].messages.size() << std::endl; + + if (accountMap[accountIds.at(i)].messages.size() >= 105) { + for (const auto& msg : accountMap[accountIds.at(i)].messages) { + std::cout << "Message id: " << msg.at("id") << " type: " << msg.at("type") + << std::endl; + } + } + + CPPUNIT_ASSERT(cv.wait_for(lk, 40s, [&]() { + return accountMap[accountIds.at(i)].messages.size() >= 1; + })); + } + + libjami::unregisterSignalHandlers(); +} + +} // namespace test +} // namespace jami + +RING_TEST_RUNNER(jami::test::SwarmConversationTest::name()) diff --git a/test/unitTest/swarm/swarm_spread.cpp b/test/unitTest/swarm/swarm_spread.cpp index 51c0fc65d8..c5a0e934d7 100644 --- a/test/unitTest/swarm/swarm_spread.cpp +++ b/test/unitTest/swarm/swarm_spread.cpp @@ -45,10 +45,10 @@ using NodeId = dht::PkId; namespace jami { namespace test { -constexpr size_t nNodes = 5000; +constexpr size_t nNodes = 10; constexpr size_t BOOTSTRAP_SIZE = 2; -auto time = 300s; +auto time = 30s; int TOTAL_HOPS = 0; int moyenne = 0; @@ -200,7 +200,7 @@ SwarmMessageSpread::sendMessage(const std::shared_ptr<ChannelSocketInterface>& s dht::ThreadPool::io().run([socket, buffer = std::move(buffer)] { std::error_code ec; - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // std::this_thread::sleep_for(std::chrono::milliseconds(50)); socket->write(reinterpret_cast<const unsigned char*>(buffer->data()), buffer->size(), ec); }); @@ -302,7 +302,7 @@ SwarmMessageSpread::needSocketCallBack(const std::shared_ptr<SwarmManager>& sm) ChannelSocketTest::link(cstMe.second, cstRemote.second); receiveMessage(myId, cstMe.second); receiveMessage(node, cstRemote.second); - std::this_thread::sleep_for(std::chrono::seconds(5)); + // std::this_thread::sleep_for(std::chrono::seconds(5)); ChannelSocketTest::link(cstMe.first, cstRemote.first); smRemote->addChannel(cstRemote.first); onSocket(cstMe.first); -- GitLab