diff --git a/src/account.h b/src/account.h index 4c746aca97c79d086309857a79167525d088ba63..95058f21169fc40f94327d7444c3f3aba3294fee 100644 --- a/src/account.h +++ b/src/account.h @@ -55,6 +55,10 @@ class Emitter; class Node; } // namespace YAML +namespace Json { +class Value; +} + namespace jami { static constexpr uint64_t DRING_ID_MAX_VAL = 9007199254740992; @@ -319,6 +323,9 @@ public: const std::string& /*conversationId*/, const std::string& /*commitId*/) {}; + // Invites + virtual void onConversationRequest(const std::string& from, const Json::Value&) {}; + /** * Helper function used to load the default codec order from the codec factory */ diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index 00860304959e14148df70165216a2ba29c88c82c..426af1b8df8d34b52982e963c1f1b6999ba90170 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -56,6 +56,8 @@ public: } ~Impl() = default; + std::string repoPath() const; + std::unique_ptr<ConversationRepository> repository_; std::weak_ptr<JamiAccount> account_; std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "", @@ -63,6 +65,16 @@ public: size_t n = 0); }; +std::string +Conversation::Impl::repoPath() const +{ + auto shared = account_.lock(); + if (!shared) + return {}; + return fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository_->id(); +} + std::vector<std::map<std::string, std::string>> Conversation::Impl::loadMessages(const std::string& fromMessage, const std::string& toMessage, @@ -156,7 +168,7 @@ Conversation::removeMember(const std::string& contactUri) } std::vector<std::map<std::string, std::string>> -Conversation::getMembers() +Conversation::getMembers(bool includeInvited) const { std::vector<std::map<std::string, std::string>> result; auto shared = pimpl_->account_.lock(); @@ -168,6 +180,7 @@ Conversation::getMembers() + pimpl_->repository_->id(); auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins"; auto membersPath = repoPath + DIR_SEPARATOR_STR + "members"; + auto invitedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "invited"; for (const auto& certificate : fileutils::readDirectory(adminsPath)) { if (certificate.find(".crt") == std::string::npos) { JAMI_WARN("Incorrect file found: %s/%s", adminsPath.c_str(), certificate.c_str()); @@ -188,6 +201,14 @@ Conversation::getMembers() {"role", "member"}}; result.emplace_back(details); } + if (includeInvited) { + for (const auto& uri : fileutils::readDirectory(invitedPath)) { + std::map<std::string, std::string> + details {{"uri", uri }, + {"role", "invited"}}; + result.emplace_back(details); + } + } return result; } @@ -289,4 +310,28 @@ Conversation::mergeHistory(const std::string& uri) return true; } +std::map<std::string, std::string> +Conversation::generateInvitation() const +{ + // Invite the new member to the conversation + std::map<std::string, std::string> invite; + Json::Value root; + root["conversationId"] = id(); + // TODO remove, cause the peer cannot trust? + // Or add signatures? + for (const auto& member : getMembers()) { + Json::Value jsonMember; + for (const auto& [key, value] : member) { + jsonMember[key] = value; + } + root["members"].append(jsonMember); + } + // TODO metadatas + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + invite["application/invite+json"] = Json::writeString(wbuilder, root); + return invite; +} + } // namespace jami \ No newline at end of file diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index 075415e4d5bec2e4c24f0c4b502c2683911df99c..1bea272328fd4d95625874cb17453bc42d46b456 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -49,6 +49,7 @@ public: std::string addMember(const std::string& contactUri); bool removeMember(const std::string& contactUri); /** + * @param includeInvited If we want invited members * @return a vector of member details: * { * "uri":"xxx", @@ -57,7 +58,7 @@ public: * ... * } */ - std::vector<std::map<std::string, std::string>> getMembers(); + std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited = false) const; /** * Join a conversation @@ -107,6 +108,12 @@ public: */ bool mergeHistory(const std::string& uri); + /** + * Generate an invitation to send to new contacts + * @return the invite to send + */ + std::map<std::string, std::string> generateInvitation() const; + private: class Impl; std::unique_ptr<Impl> pimpl_; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 7bfd6dfb1df869271e74aefcc533182a9786dbb9..5df2da730d472d97d121f8aad396924bf40b2198 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -87,7 +87,6 @@ #include <opendht/http.h> #include <yaml-cpp/yaml.h> -#include <json/json.h> #include <unistd.h> @@ -162,6 +161,44 @@ struct ConvInfo MSGPACK_DEFINE_MAP(id, created, removed) }; +// ConversationRequest +ConversationRequest::ConversationRequest(const Json::Value& json) +{ + received = json["received"].asLargestUInt(); + declined = json["declined"].asLargestUInt(); + from = json["from"].asString(); + conversationId = json["conversationId"].asString(); + auto& md = json["metadatas"]; + for (const auto& member : md.getMemberNames()) { + metadatas.emplace(member, md[member].asString()); + } +} + +Json::Value +ConversationRequest::toJson() const +{ + Json::Value json; + json["conversationId"] = conversationId; + json["from"] = from; + json["received"] = static_cast<uint32_t>(received); + if (declined) + json["declined"] = static_cast<uint32_t>(declined); + for (const auto& [key, value] : metadatas) { + json["metadatas"][key] = value; + } + return json; +} + +std::map<std::string, std::string> +ConversationRequest::toMap() const +{ + auto result = metadatas; + result["id"] = conversationId; + result["from"] = from; + result["received"] = std::to_string(received); + return result; +} + namespace Migration { enum class State { // Contains all the Migration states @@ -1977,6 +2014,7 @@ JamiAccount::doRegister() } } loadConvInfos(); + loadConvRequests(); JAMI_INFO("[Account %s] Conversations loaded!", getAccountID().c_str()); // invalid state transitions: @@ -2583,34 +2621,42 @@ JamiAccount::doRegister_() if (!dhtPeerConnector_) dhtPeerConnector_ = std::make_unique<DhtPeerConnector>(*this); - dht_->listen<ConversationRequest>( - inboxDeviceKey, [this, inboxDeviceKey](ConversationRequest&& req) { - // TODO it's a trust request, we need to confirm incoming device - JAMI_INFO("Receive a new conversation request for conversation %s", - req.conversationId.c_str()); - auto convId = req.conversationId; - std::map<std::string, std::string> metadatas = req.metadatas; - { - std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); - auto it = conversationsRequests_.find(convId); - if (it != conversationsRequests_.end()) { - JAMI_INFO("Received a request for a conversation already existing. Ignore"); - return true; + { + std::lock_guard<std::mutex> lock(buddyInfoMtx); + for (auto& buddy : trackedBuddies_) { + buddy.second.devices_cnt = 0; + trackPresence(buddy.first, buddy.second); + } + } + + if (needsConvSync_.exchange(false)) { + // Sync conversations (at startup or when re-enabled) + std::lock_guard<std::mutex> lk(conversationsMtx_); + for (const auto& item : conversations_) { + const auto& convId = item.first; + const auto& conv = item.second; + if (!conv) + continue; + for (const auto& member : conv->getMembers()) { + auto peer = member.at("uri"); + if (username_.find(peer) != std::string::npos) { + // TODO should be handled by device sync + continue; } - conversationsRequests_[convId] = std::move(req); + accountManager_->forEachDevice( + dht::InfoHash(peer), [w = weak(), peer, convId](const dht::InfoHash& dev) { + auto shared = w.lock(); + if (!shared) + return; + JAMI_INFO("[Account %s] Start syncing conversation %s with %s (%s)", + shared->getAccountID().c_str(), + convId.c_str(), + peer.c_str(), + dev.toString().c_str()); + shared->fetchNewCommits(peer, dev.toString(), convId); + }); } - // TODO: store request to be persistent when restarting - - emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(accountID_, - convId, - metadatas); - return true; - }); - - std::lock_guard<std::mutex> lock(buddyInfoMtx); - for (auto& buddy : trackedBuddies_) { - buddy.second.devices_cnt = 0; - trackPresence(buddy.first, buddy.second); + } } } catch (const std::exception& e) { JAMI_ERR("Error registering DHT account: %s", e.what()); @@ -2788,8 +2834,10 @@ JamiAccount::doUnregister(std::function<void(bool)> released_cb) // Stop all current p2p connections if account is disabled // Else, we let the system managing if the co is down or not - if (not isEnabled()) + if (not isEnabled()) { + needsConvSync_ = true; shutdownConnections(); + } dht_->join(); @@ -3803,7 +3851,6 @@ JamiAccount::startConversation() void JamiAccount::acceptConversationRequest(const std::string& conversationId) { - // TODO DRT to optimize connections // For all conversation members, try to open a git channel with this conversation ID std::unique_lock<std::mutex> lk(conversationsRequestsMtx_); auto request = conversationsRequests_.find(conversationId); @@ -3815,41 +3862,39 @@ JamiAccount::acceptConversationRequest(const std::string& conversationId) std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); pendingConversationsFetch_[request->first] = PendingConversationFetch {}; } - for (const auto& member : request->second.members) { - auto memberHash = dht::InfoHash(member); - if (!memberHash) { - // TODO check why some members are 000000 - continue; - } - // TODO cf sync between devices - forEachDevice(memberHash, [this, request = request->second](const dht::InfoHash& dev) { - if (dev == dht()->getId()) - return; - std::string channelName = "git://" + dev.toString() + "/" + request.conversationId; - connectionManager().connectDevice( - dev, - channelName, - [this, request](std::shared_ptr<ChannelSocket> socket, const DeviceId& dev) { - if (socket) { - std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_); - auto& pending = pendingConversationsFetch_[request.conversationId]; - if (!pending.ready) { - pending.ready = true; - pending.deviceId = dev.toString(); - lk.unlock(); - // Save the git socket - addGitSocket(dev.toString(), request.conversationId, socket); - // TODO when do we remove the gitSocket? - } else { - lk.unlock(); - socket->shutdown(); - } - } - }); - }); + auto memberHash = dht::InfoHash(request->second.from); + if (!memberHash) { + JAMI_WARN("Invalid member detected"); + return; } + forEachDevice(memberHash, [this, request = request->second](const dht::InfoHash& dev) { + if (dev == dht()->getId()) + return; + connectionManager().connectDevice( + dev, + "git://" + dev.toString() + "/" + request.conversationId, + [this, request](std::shared_ptr<ChannelSocket> socket, const DeviceId& dev) { + if (socket) { + std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_); + auto& pending = pendingConversationsFetch_[request.conversationId]; + if (!pending.ready) { + pending.ready = true; + pending.deviceId = dev.toString(); + lk.unlock(); + // Save the git socket + addGitSocket(dev.toString(), request.conversationId, socket); + // TODO when do we remove the gitSocket? + } else { + lk.unlock(); + socket->shutdown(); + } + } + }); + }); conversationsRequests_.erase(conversationId); lk.unlock(); + saveConvRequests(); + syncWithConnected(); checkConversationsEvents(); } @@ -3911,7 +3956,16 @@ JamiAccount::handlePendingConversations() void JamiAccount::declineConversationRequest(const std::string& conversationId) -{} +{ + std::unique_lock<std::mutex> lk(conversationsRequestsMtx_); + auto request = conversationsRequests_.find(conversationId); + if (request == conversationsRequests_.end()) + return; + request->second.declined = std::time(nullptr); + lk.unlock(); + saveConvRequests(); + syncWithConnected(); +} bool JamiAccount::removeConversation(const std::string& conversationId) @@ -3934,35 +3988,45 @@ JamiAccount::getConversations() std::vector<std::map<std::string, std::string>> JamiAccount::getConversationRequests() { - // TODO - return {}; + std::vector<std::map<std::string, std::string>> requests; + { + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + requests.reserve(conversationsRequests_.size()); + for (const auto& [id, request] : conversationsRequests_) { + if (request.declined) + continue; // Do not add declined requests + requests.emplace_back(request.toMap()); + } + } + return requests; } // Member management bool -JamiAccount::addConversationMember(const std::string& conversationId, const std::string& contactUri) +JamiAccount::addConversationMember(const std::string& conversationId, + const std::string& contactUri, + bool sendRequest) { std::lock_guard<std::mutex> lk(conversationsMtx_); // Add a new member in the conversation - if (conversations_[conversationId]->addMember(contactUri).empty()) { + auto it = conversations_.find(conversationId); + if (it == conversations_.end()) { + JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str()); + return false; + } + auto commitId = it->second->addMember(contactUri); + if (commitId.empty()) { JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str()); return false; } - // Invite the new member to the conversation - auto toH = dht::InfoHash(contactUri); - ConversationRequest req; - req.conversationId = conversationId; - auto convMembers = conversations_[conversationId]->getMembers(); - for (const auto& member : convMembers) - req.members.emplace_back(member.at("uri")); - req.metadatas = {/* TODO */}; - // TODO message engine - forEachDevice(toH, [this, toH, req](const dht::InfoHash& dev) { - JAMI_INFO("Sending conversation invite %s / %s", - toH.toString().c_str(), - dev.toString().c_str()); - dht_->putEncrypted(dht::InfoHash::get("inbox:" + dev.toString()), dev, req); - }); + auto messages = it->second->loadMessages(commitId, 1); + if (messages.empty()) + return false; // should not happen + emitSignal<DRing::ConversationSignal::MessageReceived>(getAccountID(), + conversationId, + messages.front()); + if (sendRequest) + sendTextMessage(contactUri, it->second->generateInvitation()); return true; } @@ -3981,7 +4045,7 @@ JamiAccount::getConversationMembers(const std::string& conversationId) std::lock_guard<std::mutex> lk(conversationsMtx_); auto conversation = conversations_.find(conversationId); if (conversation != conversations_.end() && conversation->second) - return conversation->second->getMembers(); + return conversation->second->getMembers(true); return {}; } @@ -3990,7 +4054,8 @@ void JamiAccount::sendMessage(const std::string& conversationId, const std::string& message, const std::string& parent, - const std::string& type) + const std::string& type, + bool announce) { std::lock_guard<std::mutex> lk(conversationsMtx_); auto conversation = conversations_.find(conversationId); @@ -4015,7 +4080,8 @@ JamiAccount::sendMessage(const std::string& conversationId, if (username_.find(uri) != std::string::npos) continue; // Announce to all members that a new message is sent - sendTextMessage(uri, {{"application/im-gitmessage-id", text}}); + if (announce) + sendTextMessage(uri, {{"application/im-gitmessage-id", text}}); } } else { JAMI_ERR("Failed to send message to conversation %s", conversationId.c_str()); @@ -4058,6 +4124,14 @@ JamiAccount::onNewGitCommit(const std::string& peer, peer.c_str(), conversationId.c_str(), commitId.c_str()); + fetchNewCommits(peer, deviceId, conversationId); +} + +void +JamiAccount::fetchNewCommits(const std::string& peer, + const std::string& deviceId, + const std::string& conversationId) +{ auto conversation = conversations_.find(conversationId); if (conversation != conversations_.end() && conversation->second) { if (!conversation->second->isMember(peer)) { @@ -4141,10 +4215,53 @@ JamiAccount::onNewGitCommit(const std::string& peer, }); } } else { + { + // Check if the conversation is still a request + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + if (conversationsRequests_.find(conversationId) != conversationsRequests_.end()) + return; + } + { + // Check if the conversation is cloning + std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_); + if (pendingConversationsFetch_.find(conversationId) != pendingConversationsFetch_.end()) + return; + } JAMI_WARN("Could not find conversation %s", conversationId.c_str()); } } +void +JamiAccount::onConversationRequest(const std::string& from, const Json::Value& value) +{ + ConversationRequest req(value); + JAMI_INFO("[Account %s] Receive a new conversation request for conversation %s", + getAccountID().c_str(), + req.conversationId.c_str()); + auto convId = req.conversationId; + req.from = from; + + { + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + auto it = conversationsRequests_.find(convId); + if (it != conversationsRequests_.end()) { + JAMI_INFO("[Account %s] Received a request for a conversation already existing. " + "Ignore", + getAccountID().c_str()); + return; + } + req.received = std::time(nullptr); + conversationsRequests_[convId] = req; + } + saveConvRequests(); + // Note: no need to sync here because over connected devices should receives + // the same conversation request. Will sync when the conversation will be added + + emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(accountID_, + convId, + req.toMap()); +} + void JamiAccount::cacheTurnServers() { @@ -4566,6 +4683,10 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket, for (const auto& jsonConv : value["conversations"]) { auto convId = jsonConv["id"].asString(); auto removed = jsonConv.isMember("removed"); + { + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + conversationsRequests_.erase(convId); + } if (not removed) { if (!isConversation(convId)) { { @@ -4617,8 +4738,39 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket, } } } - saveConvInfos(); } + + if (value.isMember("conversationsRequests")) { + for (const auto& jsonReq : value["conversationsRequests"]) { + auto convId = jsonReq["conversationId"].asString(); + if (isConversation(convId)) { + // Already accepted request + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + conversationsRequests_.erase(convId); + continue; + } + + // New request + ConversationRequest req(jsonReq); + { + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + conversationsRequests_[convId] = req; + } + + if (req.declined != 0) + continue; // Request removed, do not emit signal + + JAMI_INFO("[Account %s] New request detected for conversation %s (device %s)", + getAccountID().c_str(), + convId.c_str(), + deviceId.c_str()); + emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(getAccountID(), + convId, + req.toMap()); + } + } + saveConvInfos(); + saveConvRequests(); return len; }); } @@ -4657,12 +4809,15 @@ void JamiAccount::syncInfos(const std::shared_ptr<ChannelSocket>& socket) { // Sync conversations - if (not socket or convInfos_.empty()) + if (not socket or (convInfos_.empty() and conversationsRequests_.empty())) return; Json::Value syncValue; for (const auto& info : convInfos_) { syncValue["conversations"].append(info.toJson()); } + for (const auto& [_id, request] : conversationsRequests_) { + syncValue["conversationsRequests"].append(request.toJson()); + } Json::StreamWriterBuilder builder; const auto sync = Json::writeString(builder, syncValue); @@ -4707,4 +4862,28 @@ JamiAccount::saveConvInfos() const msgpack::pack(file, convInfos_); } +void +JamiAccount::loadConvRequests() +{ + try { + // read file + auto file = fileutils::loadFile("convRequests", idPath_); + // load values + msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size()); + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + oh.get().convert(conversationsRequests_); + } catch (const std::exception& e) { + JAMI_WARN("Error loading conversationsRequests: %s", e.what()); + } +} + +void +JamiAccount::saveConvRequests() +{ + std::lock_guard<std::mutex> lk(conversationsRequestsMtx_); + std::ofstream file(idPath_ + DIR_SEPARATOR_STR "convRequests", + std::ios::trunc | std::ios::binary); + msgpack::pack(file, conversationsRequests_); +} + } // namespace jami diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 607926a043f8c629c242439604bf98d35ef650dd..a232911b04120bd1cdd80e018a99ef9685df3a11 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -53,6 +53,7 @@ #include <chrono> #include <list> #include <future> +#include <json/json.h> #if HAVE_RINGNS #include "namedirectory.h" @@ -91,14 +92,22 @@ struct ConvInfo; * such as the conversation's vcard, etc. (TODO determine) * Transmitted via the UDP DHT */ -struct ConversationRequest : public dht::EncryptedValue<ConversationRequest> +struct ConversationRequest { - static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA; - dht::Value::Id id = dht::Value::INVALID_ID; std::string conversationId; - std::vector<std::string> members; + std::string from; std::map<std::string, std::string> metadatas; - MSGPACK_DEFINE_MAP(id, conversationId, members, metadatas) + + time_t received {0}; + time_t declined {0}; + + ConversationRequest() = default; + ConversationRequest(const Json::Value& json); + + Json::Value toJson() const; + std::map<std::string, std::string> toMap() const; + + MSGPACK_DEFINE_MAP(conversationId, metadatas, received, declined) }; using SipConnectionKey = std::pair<std::string /* accountId */, DeviceId>; @@ -506,7 +515,9 @@ public: std::vector<std::map<std::string, std::string>> getConversationRequests(); // Member management - bool addConversationMember(const std::string& conversationId, const std::string& contactUri); + bool addConversationMember(const std::string& conversationId, + const std::string& contactUri, + bool sendRequest = true); bool removeConversationMember(const std::string& conversationId, const std::string& contactUri); std::vector<std::map<std::string, std::string>> getConversationMembers( const std::string& conversationId); @@ -515,7 +526,8 @@ public: void sendMessage(const std::string& conversationId, const std::string& message, const std::string& parent = "", - const std::string& type = "text/plain"); + const std::string& type = "text/plain", + bool announce = true); uint32_t loadConversationMessages(const std::string& conversationId, const std::string& fromMessage = "", size_t n = 0); @@ -525,6 +537,12 @@ public: const std::string& deviceId, const std::string& conversationId, const std::string& commitId) override; + void fetchNewCommits(const std::string& peer, + const std::string& deviceId, + const std::string& conversationId); + + // Invites + void onConversationRequest(const std::string& from, const Json::Value&) override; private: NON_COPYABLE(JamiAccount); @@ -669,6 +687,9 @@ private: void loadConvInfos(); void saveConvInfos() const; + void loadConvRequests(); + void saveConvRequests(); + template<class... Args> std::shared_ptr<IceTransport> createIceTransport(const Args&... args); void newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, std::string_view toUri); @@ -912,6 +933,7 @@ private: void syncWith(const std::string& deviceId, const std::shared_ptr<ChannelSocket>& socket); void syncInfos(const std::shared_ptr<ChannelSocket>& socket); void syncWithConnected(); + std::atomic_bool needsConvSync_ {true}; }; static inline std::ostream& diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 8267108dba00dddde7a1643e6db2d57e8000b2cd..3e8bf5b7726d63dbaf0992c4025dd71b8be06bf5 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -58,6 +58,7 @@ namespace jami { static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"}; static constexpr const char MIME_TYPE_GIT[] {"application/im-gitmessage-id"}; +static constexpr const char MIME_TYPE_INVITE_JSON[] {"application/invite+json"}; static constexpr const char MIME_TYPE_IM_COMPOSING[] {"application/im-iscomposing+xml"}; static constexpr std::chrono::steady_clock::duration COMPOSING_TIMEOUT {std::chrono::seconds(12)}; @@ -580,6 +581,16 @@ SIPAccountBase::onTextMessage(const std::string& id, json["deviceId"].asString(), json["id"].asString(), json["commit"].asString()); + } else if (m.first == MIME_TYPE_INVITE_JSON) { + Json::Value json; + std::string err; + Json::CharReaderBuilder rbuilder; + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (!reader->parse(m.second.data(), m.second.data() + m.second.size(), &json, &err)) { + JAMI_ERR("Can't parse server response: %s", err.c_str()); + return; + } + onConversationRequest(from, json); } } diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 6326cd5acfed0260fb392b79271b3fe462b11771..b6259fac94b6c6aad86bfac0ea2ce8798042e72f 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -24,6 +24,7 @@ #include <string> #include <fstream> #include <streambuf> +#include <git2.h> #include <filesystem> #include "manager.h" @@ -35,8 +36,6 @@ #include "fileutils.h" #include "account_const.h" -#include <git2.h> - using namespace std::string_literals; using namespace DRing::Account; @@ -53,23 +52,34 @@ public: std::string aliceId; std::string bobId; + std::string carlaId; private: void testCreateConversation(); void testGetConversation(); void testAddMember(); + void testAddOfflineMemberThenConnects(); void testGetMembers(); void testSendMessage(); void testSendMessageTriggerMessageReceived(); void testMergeTwoDifferentHeads(); + void testGetRequests(); + void testDeclineRequest(); + void testSendMessageToMultipleParticipants(); + void testPingPongMessages(); CPPUNIT_TEST_SUITE(ConversationTest); CPPUNIT_TEST(testCreateConversation); CPPUNIT_TEST(testGetConversation); CPPUNIT_TEST(testAddMember); + CPPUNIT_TEST(testAddOfflineMemberThenConnects); CPPUNIT_TEST(testGetMembers); CPPUNIT_TEST(testSendMessage); CPPUNIT_TEST(testSendMessageTriggerMessageReceived); + CPPUNIT_TEST(testGetRequests); + CPPUNIT_TEST(testDeclineRequest); + CPPUNIT_TEST(testSendMessageToMultipleParticipants); + CPPUNIT_TEST(testPingPongMessages); CPPUNIT_TEST_SUITE_END(); }; @@ -103,9 +113,21 @@ ConversationTest::setUp() details[ConfProperties::ARCHIVE_PATH] = ""; bobId = Manager::instance().addAccount(details); + details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "CARLA"; + details[ConfProperties::ALIAS] = "CARLA"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + carlaId = Manager::instance().addAccount(details); + JAMI_INFO("Initialize account..."); auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + Manager::instance().sendRegister(carlaId, false); std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; std::mutex mtx; std::unique_lock<std::mutex> lk {mtx}; @@ -132,9 +154,10 @@ ConversationTest::tearDown() auto currentAccSize = Manager::instance().getAccountList().size(); Manager::instance().removeAccount(aliceId, true); Manager::instance().removeAccount(bobId, true); + Manager::instance().removeAccount(carlaId, true); // Because cppunit is not linked with dbus, just poll if removed for (int i = 0; i < 40; ++i) { - if (Manager::instance().getAccountList().size() <= currentAccSize - 2) + if (Manager::instance().getAccountList().size() <= currentAccSize - 3) break; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } @@ -258,6 +281,50 @@ ConversationTest::testAddMember() CPPUNIT_ASSERT(!fileutils::isFile(bobInvited)); } +void +ConversationTest::testAddOfflineMemberThenConnects() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + auto convId = aliceAccount->startConversation(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool conversationReady = false, requestReceived = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == carlaId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri)); + Manager::instance().sendRegister(carlaId, true); + cv.wait_for(lk, std::chrono::seconds(60)); + CPPUNIT_ASSERT(requestReceived); + + carlaAccount->acceptConversationRequest(convId); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(conversationReady); + auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath)); +} + void ConversationTest::testGetMembers() { @@ -308,9 +375,11 @@ ConversationTest::testGetMembers() CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); auto members = aliceAccount->getConversationMembers(convId); - CPPUNIT_ASSERT(members.size() == 1); + CPPUNIT_ASSERT(members.size() == 2); CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername()); CPPUNIT_ASSERT(members[0]["role"] == "admin"); + CPPUNIT_ASSERT(members[1]["uri"] == bobUri); + CPPUNIT_ASSERT(members[1]["role"] == "invited"); cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }); messageReceived = false; @@ -321,9 +390,10 @@ ConversationTest::testGetMembers() cv.wait_for(lk, std::chrono::seconds(60), [&]() { return messageReceived; }); members = aliceAccount->getConversationMembers(convId); CPPUNIT_ASSERT(members.size() == 2); - auto hasBob = members[0]["uri"] == bobUri || members[1]["uri"] == bobUri; - auto hasAlice = members[0]["uri"] == aliceUri || members[1]["uri"] == aliceUri; - CPPUNIT_ASSERT(hasAlice && hasBob); + CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername()); + CPPUNIT_ASSERT(members[0]["role"] == "admin"); + CPPUNIT_ASSERT(members[1]["uri"] == bobUri); + CPPUNIT_ASSERT(members[1]["role"] == "member"); } void @@ -394,7 +464,6 @@ void ConversationTest::testSendMessageTriggerMessageReceived() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - std::mutex mtx; std::unique_lock<std::mutex> lk {mtx}; std::condition_variable cv; @@ -486,6 +555,243 @@ ConversationTest::testMergeTwoDifferentHeads() DRing::unregisterSignalHandlers(); } +void +ConversationTest::testGetRequests() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool requestReceived = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(requestReceived); + + auto requests = bobAccount->getConversationRequests(); + CPPUNIT_ASSERT(requests.size() == 1); + CPPUNIT_ASSERT(requests.front()["id"] == convId); +} + +void +ConversationTest::testDeclineRequest() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool requestReceived = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(requestReceived); + + bobAccount->declineConversationRequest(convId); + // Decline request + auto requests = bobAccount->getConversationRequests(); + CPPUNIT_ASSERT(requests.size() == 0); +} + +void +ConversationTest::testSendMessageToMultipleParticipants() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + + // Enable carla + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + Manager::instance().sendRegister(carlaId, true); + cv.wait_for(lk, std::chrono::seconds(30)); + confHandlers.clear(); + DRing::unregisterSignalHandlers(); + + auto messageReceivedAlice = 0; + auto messageReceivedBob = 0; + auto messageReceivedCarla = 0; + auto requestReceived = 0; + auto conversationReady = 0; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == aliceId) + messageReceivedAlice += 1; + if (accountId == bobId) + messageReceivedBob += 1; + if (accountId == carlaId) + messageReceivedCarla += 1; + cv.notify_one(); + })); + + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived += 1; + cv.notify_one(); + })); + + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& /*accountId*/, const std::string& /* conversationId */) { + conversationReady += 1; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri)); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(60), [&]() { return requestReceived == 2; })); + + messageReceivedAlice = 0; + bobAccount->acceptConversationRequest(convId); + carlaAccount->acceptConversationRequest(convId); + // >= because we can have merges cause the accept commits + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { + return conversationReady == 3 && messageReceivedAlice >= 2; + })); + + // Assert that repository exists + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + + aliceAccount->sendMessage(convId, "hi"); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { + return messageReceivedBob >= 1 && messageReceivedCarla >= 1; + })); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testPingPongMessages() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == bobId) { + messageBobReceived += 1; + } + if (accountId == aliceId) { + messageAliceReceived += 1; + } + if (messageAliceReceived == messageBobReceived) + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + auto convId = aliceAccount->startConversation(); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + cv.wait_for(lk, std::chrono::seconds(30)); + CPPUNIT_ASSERT(requestReceived); + messageAliceReceived = 0; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { + return conversationReady && messageAliceReceived == 1; + })); + // Assert that repository exists + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + messageBobReceived = 0; + messageAliceReceived = 0; + aliceAccount->sendMessage(convId, "ping"); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return messageBobReceived == 1 && messageAliceReceived == 1; + })); + bobAccount->sendMessage(convId, "pong"); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return messageBobReceived == 2 && messageAliceReceived == 2; + })); + bobAccount->sendMessage(convId, "ping"); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return messageBobReceived == 3 && messageAliceReceived == 3; + })); + aliceAccount->sendMessage(convId, "pong"); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return messageBobReceived == 4 && messageAliceReceived == 4; + })); + DRing::unregisterSignalHandlers(); +} + } // namespace test } // namespace jami diff --git a/test/unitTest/syncHistory/syncHistory.cpp b/test/unitTest/syncHistory/syncHistory.cpp index 1d5c8251f9b427aafa65346e851abf0067a02120..3f13dc284380867b72494f314b1ad9db4d04aac3 100644 --- a/test/unitTest/syncHistory/syncHistory.cpp +++ b/test/unitTest/syncHistory/syncHistory.cpp @@ -52,17 +52,20 @@ public: void tearDown(); std::string aliceId; + std::string bobId; std::string alice2Id; private: void testCreateConversationThenSync(); void testCreateConversationWithOnlineDevice(); void testCreateConversationWithMessagesThenAddDevice(); + void testReceivesInviteThenAddDevice(); CPPUNIT_TEST_SUITE(SyncHistoryTest); CPPUNIT_TEST(testCreateConversationThenSync); CPPUNIT_TEST(testCreateConversationWithOnlineDevice); CPPUNIT_TEST(testCreateConversationWithMessagesThenAddDevice); + CPPUNIT_TEST(testReceivesInviteThenAddDevice); CPPUNIT_TEST_SUITE_END(); }; @@ -81,8 +84,19 @@ SyncHistoryTest::setUp() details[ConfProperties::ARCHIVE_PATH] = ""; aliceId = Manager::instance().addAccount(details); + details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "BOB"; + details[ConfProperties::ALIAS] = "BOB"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = ""; + bobId = Manager::instance().addAccount(details); + JAMI_INFO("Initialize account..."); auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; std::mutex mtx; std::unique_lock<std::mutex> lk {mtx}; @@ -94,33 +108,43 @@ SyncHistoryTest::setUp() auto details = aliceAccount->getVolatileAccountDetails(); auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; ready = (daemonStatus == "REGISTERED"); + details = bobAccount->getVolatileAccountDetails(); + daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + ready &= (daemonStatus == "REGISTERED"); if (ready) cv.notify_one(); })); DRing::registerSignalHandlers(confHandlers); cv.wait_for(lk, std::chrono::seconds(30)); DRing::unregisterSignalHandlers(); + alice2Id = ""; } void SyncHistoryTest::tearDown() { JAMI_INFO("Remove created accounts..."); + auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz"; + std::remove(aliceArchive.c_str()); std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; std::mutex mtx; std::unique_lock<std::mutex> lk {mtx}; std::condition_variable cv; auto currentAccSize = Manager::instance().getAccountList().size(); + auto toRemove = alice2Id.empty() ? 2 : 3; confHandlers.insert( DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() { - if (Manager::instance().getAccountList().size() <= currentAccSize - 1) { + if (Manager::instance().getAccountList().size() <= currentAccSize - toRemove) { cv.notify_one(); } })); DRing::registerSignalHandlers(confHandlers); Manager::instance().removeAccount(aliceId, true); + if (!alice2Id.empty()) + Manager::instance().removeAccount(alice2Id, true); + Manager::instance().removeAccount(bobId, true); // Because cppunit is not linked with dbus, just poll if removed cv.wait_for(lk, std::chrono::seconds(30)); @@ -180,25 +204,6 @@ SyncHistoryTest::testCreateConversationThenSync() DRing::unregisterSignalHandlers(); confHandlers.clear(); - // Remove alice 2 and alice.gz - std::remove(aliceArchive.c_str()); - - auto currentAccSize = Manager::instance().getAccountList().size(); - confHandlers.insert( - DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() { - if (Manager::instance().getAccountList().size() <= currentAccSize - 1) { - cv.notify_one(); - } - })); - DRing::registerSignalHandlers(confHandlers); - - Manager::instance().removeAccount(alice2Id, true); - // Because cppunit is not linked with dbus, just poll if removed - cv.wait_for(lk, std::chrono::seconds(30)); - - DRing::unregisterSignalHandlers(); - confHandlers.clear(); - // Check if conversation is ready CPPUNIT_ASSERT(conversationReady); } @@ -256,25 +261,6 @@ SyncHistoryTest::testCreateConversationWithOnlineDevice() DRing::unregisterSignalHandlers(); confHandlers.clear(); - // Remove alice 2 and alice.gz - std::remove(aliceArchive.c_str()); - - auto currentAccSize = Manager::instance().getAccountList().size(); - confHandlers.insert( - DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() { - if (Manager::instance().getAccountList().size() <= currentAccSize - 1) { - cv.notify_one(); - } - })); - DRing::registerSignalHandlers(confHandlers); - - Manager::instance().removeAccount(alice2Id, true); - // Because cppunit is not linked with dbus, just poll if removed - cv.wait_for(lk, std::chrono::seconds(30)); - - DRing::unregisterSignalHandlers(); - confHandlers.clear(); - // Check if conversation is ready CPPUNIT_ASSERT(conversationReady); } @@ -353,25 +339,76 @@ SyncHistoryTest::testCreateConversationWithMessagesThenAddDevice() CPPUNIT_ASSERT(messages[0]["body"] == "Message 3"); CPPUNIT_ASSERT(messages[1]["body"] == "Message 2"); CPPUNIT_ASSERT(messages[2]["body"] == "Message 1"); +} + +void +SyncHistoryTest::testReceivesInviteThenAddDevice() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - // Remove alice 2 and alice.gz + // Export alice + auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz"; std::remove(aliceArchive.c_str()); + aliceAccount->exportArchive(aliceArchive); - auto currentAccSize = Manager::instance().getAccountList().size(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto uri = aliceAccount->getUsername(); + + // Start conversation for Alice + auto convId = bobAccount->startConversation(); + bobAccount->addConversationMember(convId, uri); + + // Check that alice receives the request + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto requestReceived = false; confHandlers.insert( - DRing::exportable_callback<DRing::ConfigurationSignal::AccountsChanged>([&]() { - if (Manager::instance().getAccountList().size() <= currentAccSize - 1) { - cv.notify_one(); - } - })); + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> /*metadatas*/) { + if (accountId == aliceId && conversationId == convId) { + requestReceived = true; + cv.notify_one(); + } + })); DRing::registerSignalHandlers(confHandlers); - Manager::instance().removeAccount(alice2Id, true); - // Because cppunit is not linked with dbus, just poll if removed cv.wait_for(lk, std::chrono::seconds(30)); + DRing::unregisterSignalHandlers(); + confHandlers.clear(); + CPPUNIT_ASSERT(requestReceived); + + // Now create alice2 + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE2"; + details[ConfProperties::ALIAS] = "ALICE2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = aliceArchive; + alice2Id = Manager::instance().addAccount(details); + + requestReceived = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> /*metadatas*/) { + if (accountId == alice2Id && conversationId == convId) { + requestReceived = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + cv.wait_for(lk, std::chrono::seconds(30)); DRing::unregisterSignalHandlers(); confHandlers.clear(); + CPPUNIT_ASSERT(requestReceived); } } // namespace test